关于lerna:基于-monorepo-的-vscode-插件及其相关-packages-开发的架构实践总结

34次阅读

共计 19474 个字符,预计需要花费 49 分钟才能阅读完成。

前言

GithubBlog:https://github.com/Nealyang/PersonalBlog/issues/99

背景如是:

  • pmlci 源码脚手架:https://mp.weixin.qq.com/s/JRF4GjYqXw1f6jGqcYofnQ

随着脚手架的提供,以及新增页面和模块的性能封装。

毕竟 多提供一层标准,就多了一层束缚。 而架构的实质是为了让开发者可能将精力更加的 focus 到业务的开发中,无需关怀其余。比方上述脚手架初始化进去的一些模块配置、异步加载甚至一些已定义并且保留在初始化架构中的一些业务 hooks 等。

如上起因,我心愿可能提供一套可视化的操作(创立我的项目、抉择依赖、增加页面,抉择所需物料、配置物料属性等),一言以蔽之就是让用户对于源码开发而言,只须要编写对应的业务模块组件,而不需治理架构是如何组织模块和状态散发的,除了业务模块编码,其余都是可视化操作

因为团队里 100% 的同学都是以 vscode 作为饭碗,所以自然而然的 vscode extinction 就是我的第一抉择了。打算中会提供创立我的项目、新增页面、模块配置、页面配置、新增模块等一系列插件。后续阶段性停顿,再发文总结。咳咳,是的,这将是一个 源码工作台 的赶脚~

截止目前,曾经将我的项目的脚手架根本搭建了个 90%,此处作为第一阶段性总结。

成绩展现

extensions 文件夹为 vscode 插件的文件夹、packages 文件夹是寄存公共的组件、scripts 为公布、构建、开发的脚本,其余就是一些工程配置。

当然,这里最次要不是产品性能的展现,嘎嘎~

packages.json scripts


  "scripts": {
    "publish": "lerna list && publish:package",
    "publish-beta": "lerna list && npm run publish-beta:package",
    "start":"tnpm run start:packages && tnpm run start:extensions",
    "start:packages": "tnpm run setup:packages && tnpm run packages:watch",
    "start:extensions":"tnpm run extensions:link",
    "commit": "git-cz",
    "env": "node ./scripts/env.js",
    "packages:link": "lerna link",
    "packages:install": "rm -rf node_modules && rm -rf ./packages/*/node_modules && rm -rf ./packages/*/package-lock.json && SASS_BINARY_SITE=https://npm.taobao.org/mirrors/node-sass/ yarn install --registry=http://registry.npm.taobao.org",
    "packages:clean": "rm -rf ./packages/*/lib",
    "packages:watch": "ts-node ./scripts/watch.ts",
    "packages:build": "npm run packages:clean && ts-node ./scripts/build.ts",
    "setup:packages": "npm run packages:install && lerna clean --yes && npm run packages:build && npm run packages:link",
    "publish-beta:package": "ts-node ./scripts/publish-beta-package.ts",
    "publish:package": "ts-node ./scripts/publish-package.ts",
    "extensions:install": "rm -rf ./extensions/*/node_modules && rm -rf ./extensions/*/package-lock.json && rm -rf ./extensions/*/web/node_modules && rm -rf ./extensions/*/web/package-lock.json && ts-node ./scripts/extension-deps-install.ts",
    "extensions:link": "ts-node ./scripts/extension-link-package.ts"
  }

scripts 没有增加齐全,目前开发间接 npm start 公布 packages 别离为 npm run publish-beta:packagenpm run publish:package , 下面也有 publish 的命令汇总。

架构选型

目前是为了将 pmCli 性能全副封装成插件,而后通过可视化代替掉编码过程中对于架构配置的相干操作。所以插件必然不会只有一个,而是一个基于源码架构的一个 操作集:多 extensions。插件中有十分多的类似性能封装。比方从 gitlab 上读取根底文件、vscodeWebView 的通信、AST 的根本封装等,所以必然须要依赖十分多的 packages,为了开发提效和汇合的对立治理,必然想到基于 lernamonorepo 的我的项目构造。

其中对于 lerna 的一些采坑就不多说了,次要是我也只是看了市面上大部分的实际文章和官网文档,不足一些本人实际(毕竟感觉钻研多也解决不了多大的痛点,就不想花精力了)最终的 monorepo 是基于 yarn workspace 实现的,通过 lerna link 来软链packagelerna 的公布 package 比拟鸡肋,就参考 App works 本人写了一些打包公布到预发、线上的脚本。

我的项目工作流以及编码束缚通过 huskylint-stagedgit-czeslintprettier 等惯例配置。

编码采纳 ts 编码,所以对于 extensions 以及 packages 中都有很多公共的配置,这里能够提取进去公共局部搁置到我的项目根目录下(如上我的项目目录截图)。

实际

通过 lerna initlerna create xxx 来初始化这里就不说了。反正完事当前就是带有一个packagespackage.json 文件的一个目录构造。

我的项目架构

package 构造

以上构造阐明都在图片里了

脚本封装

在我的项目的根目录下搁置了一个 scripts 文件夹,寄存着一些公布、开发以及依赖的装置的脚本啥的。

getPakcageInfo.ts

用于从 packages 中获取相干 publish 信息。其中 shouldPublish 是将本地 version 和线上 version 比照,判断徒弟须要执行 publish

/*
 * @Author: 一凨
 * @Date: 2021-06-07 18:47:32
 * @Last Modified by: 一凨
 * @Last Modified time: 2021-06-07 19:12:28
 */
import {existsSync, readdirSync, readFileSync} from 'fs';
import {join} from 'path';
import {getLatestVersion} from 'ice-npm-utils';

const TARGET_DIRECTORY = join(__dirname, '../../packages');
// 定义须要获取到的信息结构
export interface IPackageInfo {
  name: string;
  directory: string;
  localVersion: string;
  mainFile: string; // package.json main file
  shouldPublish: boolean;
}
// 查看 package 是否 build 胜利
function checkBuildSuccess(directory: string, mainFile: string): boolean {return existsSync(join(directory, mainFile));
}
// 判断线上最新 version 是否和本地 version 雷同
function checkVersionExists(pkg: string, version: string): Promise<boolean> {return getLatestVersion(pkg)
    .then((latestVersion) => version === latestVersion)
    .catch(() => false);
}

export async function getPackageInfos ():Promise<IPackageInfo[]>{const packageInfos: IPackageInfo[] = [];
  if (!existsSync(TARGET_DIRECTORY)) {console.log(`[ERROR] Directory ${TARGET_DIRECTORY} not exist!`);
  } else {
    // 拿到所有 packages 目录,再去遍历其 package.json
    const packageFolders: string[] = readdirSync(TARGET_DIRECTORY).filter((filename) => filename[0] !== '.');
    console.log('[PUBLISH] Start check with following packages:');
    await Promise.all(packageFolders.map(async (packageFolder) => {const directory = join(TARGET_DIRECTORY, packageFolder);
        const packageInfoPath = join(directory, 'package.json');

        // Process package info.
        if (existsSync(packageInfoPath)) {const packageInfo = JSON.parse(readFileSync(packageInfoPath, 'utf8'));
          const packageName = packageInfo.name || packageFolder;

          console.log(`- ${packageName}`);
                    // 从 package.json 中取信息 返回
          try {
            packageInfos.push({
              name: packageName,
              directory,
              localVersion: packageInfo.version,
              mainFile: packageInfo.main,
              // If localVersion not exist, publish it
              shouldPublish:
                checkBuildSuccess(directory, packageInfo.main) &&
                !(await checkVersionExists(packageName, packageInfo.version)),
            });
          } catch (e) {console.log(`[ERROR] get ${packageName} information failed: `, e);
          }
        } else {console.log(`[ERROR] ${packageFolder}'s package.json not found.`);
        }
      }),
    );
  }
  return packageInfos;
}

代码的解释都在正文里了,外围做的事件就是,从 packages 中读取每一个 packagepackage.json 中的信息,而后组成须要的格局返回进来,用于公布。

publish-beta-package

/*
 * @Author: 一凨
 * @Date: 2021-06-07 18:45:51
 * @Last Modified by: 一凨
 * @Last Modified time: 2021-06-07 19:29:26
 */
import * as path from 'path';
import * as fs from 'fs-extra';
import {spawnSync} from 'child_process';
import {IPackageInfo, getPackageInfos} from './fn/getPackageInfos';

const BETA_REG = /([^-]+)-beta\.(\d+)/; // '1.0.0-beta.1'

interface IBetaPackageInfo extends IPackageInfo {betaVersion: string;}

function setBetaVersionInfo(packageInfo: IPackageInfo): IBetaPackageInfo {const { name, localVersion} = packageInfo;

  let version = localVersion;

  if (!BETA_REG.test(localVersion)) {
    // 如果 localVersion 不是 beta version,则盘他!let betaVersion = 1;
    // 获取 package 的 dist-tag 相干信息
    const childProcess = spawnSync('npm', ['show', name, 'dist-tags', '--json'], {encoding: 'utf-8',});

    const distTags = JSON.parse(childProcess.stdout || "{}") || {};
    const matched = (distTags.beta || '').match(BETA_REG);

    // 1.0.0-beta.1 -> ["1.0.0-beta.1", "1.0.0", "1"] -> 1.0.0-beta.2
    if (matched && matched[1] === localVersion && matched[2]) {
      // 盘 version,+1
      betaVersion = Number(matched[2]) + 1;
    }
    version += `-beta.${betaVersion}`;
  }

  return Object.assign({}, packageInfo, { betaVersion: version});
}

// 将改正后的 betaVersion 写到对应 package.json 中
function updatePackageJson(betaPackageInfos: IBetaPackageInfo[]): void {betaPackageInfos.forEach((betaPackageInfo: IBetaPackageInfo) => {const { directory, betaVersion} = betaPackageInfo;

    const packageFile = path.join(directory, 'package.json');
    const packageData = fs.readJsonSync(packageFile);

    packageData.version = betaVersion;

    for (let i = 0; i < betaPackageInfos.length; i++) {const dependenceName = betaPackageInfos[i].name;
      const dependenceVersion = betaPackageInfos[i].betaVersion;

      if (packageData.dependencies && packageData.dependencies[dependenceName]) {packageData.dependencies[dependenceName] = dependenceVersion;
      } else if (packageData.devDependencies && packageData.devDependencies[dependenceName]) {packageData.devDependencies[dependenceName] = dependenceVersion;
      }
    }

    fs.writeFileSync(packageFile, JSON.stringify(packageData, null, 2));
  });
}
// npm publish --tag=beta 公布
function publish(pkg: string, betaVersion: string, directory: string): void {console.log('[PUBLISH BETA]', `${pkg}@${betaVersion}`);
  spawnSync('npm', ['publish', '--tag=beta'], {
    stdio: 'inherit',
    cwd: directory,
  });
}

// 入口文件
console.log('[PUBLISH BETA] Start:');
getPackageInfos().then((packageInfos: IPackageInfo[]) => {
  const shouldPublishPackages = packageInfos
    .filter((packageInfo) => packageInfo.shouldPublish)
    .map((packageInfo) => setBetaVersionInfo(packageInfo));

  updatePackageJson(shouldPublishPackages);

  // Publish
  let publishedCount = 0;
  const publishedPackages = [];


  shouldPublishPackages.forEach((packageInfo) => {const { name, directory, betaVersion} = packageInfo;
    publishedCount++;
    // 打印此次公布的相干信息
    console.log(`--- ${name}@${betaVersion} ---`);
    publish(name, betaVersion, directory);
    publishedPackages.push(`${name}:${betaVersion}`);
  });

  console.log(`[PUBLISH PACKAGE BETA] Complete (count=${publishedCount}):`);
  console.log(`${publishedPackages.join('\n')}`);

});

基本功能都在正文里了(这句话前面不赘述了),总结次脚本作用:

  • 拿到所有的本地 packageInfo 信息
  • 比照线上(已公布)信息,纠正此次公布须要的版本信息
  • 将纠正的版本信息补充道(写入)本地对应的 package 中的 package.json 中
  • 调用脚本,执行公布

publish-package 就非常简单了,写的也比较简单,就是调用 npm publish,当然,也须要一些根本的线上校验,比方上述的 shouldPublish。不赘述了!

须要留神的是,公布的时候,须要留神登陆(npm whoami)以及如果你也是采纳 @xxx/ 的命名形式的话,留神对应 organization的权限

watch

次要借助 nsfw 的能力对本地文件进行监听。有变动,咱编译就完事了!

 /*
 * @Author: 一凨
 * @Date: 2021-06-07 20:16:09
 * @Last Modified by: 一凨
 * @Last Modified time: 2021-06-10 17:19:05
 */
import * as glob from 'glob';
import * as path from 'path';
import * as fs from 'fs-extra';
import {run} from './fn/shell';


// eslint-disable-next-line @typescript-eslint/no-var-requires
const nsfw = require('nsfw');

async function watchFiles(cwd, ext) {const files = glob.sync(ext, { cwd, nodir: true});

  const fileSet = new Set();
  /* eslint no-restricted-syntax:0 */
  for (const file of files) {
    /* eslint no-await-in-loop:0 */
    await copyOneFile(file, cwd);
    fileSet.add(path.join(cwd, file));
  }

  const watcher = await nsfw(cwd, (event) => {event.forEach((e) => {
      if (
        e.action === nsfw.actions.CREATED ||
        e.action === nsfw.actions.MODIFIED ||
        e.action === nsfw.actions.RENAMED
      ) {const filePath = e.newFile ? path.join(e.directory, e.newFile!) : path.join(e.directory, e.file!);
        if (fileSet.has(filePath)) {console.log('non-ts change detected:', filePath);
          copyOneFile(path.relative(cwd, filePath), cwd);
        }
      }
    });
  });
  watcher.start();}

watchFiles(path.join(__dirname, '../packages'), '*/src/**/!(*.ts|*.tsx)').catch((e) => {console.trace(e);
  process.exit(128);
});

// 在这之上的代码都是为了解决 tsc 不反对 copy 非 .ts/.tsx 文件的问题
async function tscWatcher() {await run('npx tsc --build ./tsconfig.json -w');
}

tscWatcher();

async function copyOneFile(file, cwd) {const from = path.join(cwd, file);
  const to = path.join(cwd, file.replace(/src\//, '/lib/'));
  await fs.copy(from, to);
}

extensions-deps-install

因为咱们的 workspacepackages 目录下,所以针对于 extensions 下的插件以及 web 页面,咱们没有方法通过 yarn 间接 install 所有依赖,随便提供了一个插件装置依赖的脚本。 其实就是跑到我的项目目录下,去执行 npm i

import * as path from 'path';
import * as fse from 'fs-extra';
import * as spawn from 'cross-spawn';

export default function () {const extensionsPath = path.join(__dirname, '..', '..', 'extensions');
  const extensionFiles = fse.readdirSync(extensionsPath);
  const installCommonds = ['install'];
  if (!process.env.CI) { // 拼接参数
    installCommonds.push('--no-package-lock');
    installCommonds.push('--registry');
    installCommonds.push(process.env.REGISTRY ? process.env.REGISTRY : 'http://registry.npm.taobao.org');
  }

  for (let i = 0; i < extensionFiles.length; i++) {
    // 遍历装置,如果有 web 目录,则持续装置 web 页面里的依赖
    const cwd = path.join(extensionsPath, extensionFiles[i]);
    // eslint-disable-next-line quotes
    console.log("Installing extension's dependencies", cwd);

    spawn.sync('tnpm', installCommonds, {
      stdio: 'inherit',
      cwd,
    });
    const webviewPath = path.join(cwd, 'web');
    if (fse.existsSync(webviewPath)) {
      // eslint-disable-next-line quotes
      console.log("Installing extension webview's dependencies", webviewPath);
      spawn.sync('tnpm', installCommonds, {
        stdio: 'inherit',
        cwd: webviewPath,
      });
    }
  }
}

留神 scripts 都是 ts 编码,所以在 npmScripts 中采纳 ts-node 去执行

extension-link-package

删除本地相干的 package,让其递归向上(利用级)查找到对应软链后的 package

import * as path from 'path';
import * as fse from 'fs-extra';
import {run} from './fn/shell';

(async function () {const extensionsPath = path.join(__dirname, '../extensions');
  const extensionFiles = await fse.readdir(extensionsPath);
    // 获取 extensions 下的插件列表,挨个遍历执行 remove
  return await Promise.all(extensionFiles.map(async (extensionFile) => {const cwd = path.join(extensionsPath, extensionFile);
      if (fse.existsSync(cwd)) {
        // link packages to extension
        if (!process.env.CI) {await removePmworks(cwd);
        }
        const webviewPath = path.join(cwd, 'web');
        if (fse.existsSync(webviewPath)) {
          // link packages to extension webview
          if (!process.env.CI) {await removePmworks(webviewPath);
          }
        }
      }
    }),
  );
})().catch((e) => {console.trace(e);
  process.exit(128);
});

// 删除 @pmworks 下的依赖
async function removePmworks(cwd: string) {const cwdStat = await fse.stat(cwd);
  if (cwdStat.isDirectory()) {await run(`rm -rf ${path.join(cwd, 'node_modules', '@pmworks')}`);
  }
}

小小总结

外围脚本目前就如上吧,其实都是比较简单间接的性能。对于 extensions 的公布啥的还没有写,其实也能够从 appworks 中借(抄)鉴(袭)到的。等后续公布插件了再补充吧。

一个我的项目实现基建当前,根本就能够动工了。这里我拿创立我的项目来举例子吧(着重说基建局部,对插件性能和实现不开展具体的解释,第二阶段再总结吧)。

vscode extensions(vscode-webview 封装举例)

咱们通过 yo codeextensions 文件夹中去初始化一下咱们要写的插件。具体的基础知识,参考官网文档:https://code.visualstudio.com/api

如上当前,咱们有了一个我的项目的根本架构,包的一系列治理,就曾经能够进入到咱们的开发阶段了。

毕竟咱们插件是为了可视化的一系列操作,那么vscode 的按钮和命令必然满足不了咱们,须要一个操作界面:webView。如上图是一个带有 webView 插件的整体交互过程:

  • Common-xxx(utils) 是负责整个我的项目级别一些通用性能封装
  • Extension-utils 是针对某一个插件提取的一些办法库,比方 project-utilscreateProject 初始化我的项目时候用到的办法库,相似于一个 controller
  • extension-service 是承载 vscodewebView 通信的一些办法提取,顾名思义:service

下面说的有些绕,与传统 MVC 不同的是这里的 view 有两个:vscode-extensionextension-webview

举个栗子!这里以初始化一个我的项目传授架为例子吧~

对于 vscode extension with WebView 相干根底概念能够看这里:https://code.visualstudio.com/api/extension-guides/webview

WebView

WebView 其实没有太多要筹备的,就是筹备 HTML、JavaScript 和 css 前端三大件就行了。

这里我应用的 ice 的脚手架初始化进去的我的项目:npm init ice

而后批改 build.json 中的 outputDir 配置,以及指定为 mpa 模式

{
  "mpa": true,
  "vendor": false,
  "publicPath": "./",
  "outputDir": "../build",
  "plugins": [
    [
      "build-plugin-fusion",
      {"themePackage": "@alifd/theme-design-pro"}
    ],
    [
      "build-plugin-moment-locales",
      {
        "locales": ["zh-cn"]
      }
    ],
    "@ali/build-plugin-ice-def"
  ]
}

码完代码当前失去咱们的三大件即可。

更多对于 ice 的文档,请移步官网文档

Extensions


import * as vscode from 'vscode';
import {getHtmlFroWebview, connectService} from "@pmworks/vscode-webview";
import {DEV_WORKS_ICON} from "@pmworks/constants";
import services from './services';

export function activate(context: vscode.ExtensionContext) {const { extensionPath} = context;

    let projectCreatorPanel: vscode.WebviewPanel | undefined;

    const activeProjectCreator = () => {
        const columnToShowIn = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined;
        if (projectCreatorPanel) {projectCreatorPanel.reveal(columnToShowIn)
        } else {
            projectCreatorPanel = vscode.window.createWebviewPanel('BeeDev', '初始化源码架构', columnToShowIn || vscode.ViewColumn.One, {
                enableScripts: true,
                retainContextWhenHidden: true,
            });
        }
        projectCreatorPanel.webview.html = getHtmlFroWebview(extensionPath, 'projectcreator', false);
        projectCreatorPanel.iconPath = vscode.Uri.parse(DEV_WORKS_ICON);
        projectCreatorPanel.onDidDispose(() => {projectCreatorPanel = undefined;},
            null,
            context.subscriptions,
        );
        connectService(projectCreatorPanel, context, { services});
    }

    let disposable = vscode.commands.registerCommand('devworks-project-creator.createProject.start', activeProjectCreator);

    context.subscriptions.push(disposable);
}

export function deactivate() {}

这里也都是惯例操作,注册命令和相干回调,初始化 WebView。这里说下getHtmlFroWebview


/**
 * 给本地资源带上平安协定
 * @param url 本地资源门路
 * @returns 带有 vscode-resource 协定的平安门路
 */
function originResourceProcess(url: string) {return vscode.Uri.file(url).with({scheme: 'vscode-resource'});
}

export const getHtmlFroWebview = (
  extensionPath: string,
  entryName: string,
  needVendor?: boolean,
  cdnBasePath?: string,
  extraHtml?: string,
  resourceProcess?: (url: string) => vscode.Uri,): string => {
  resourceProcess = resourceProcess || originResourceProcess;
  const localBasePath = path.join(extensionPath, 'build');
  const rootPath = cdnBasePath || localBasePath;
  const scriptPath = path.join(rootPath, `js/${entryName}.js`);
  const scriptUri = cdnBasePath ?
    scriptPath :
    resourceProcess(scriptPath);
  const stylePath = path.join(rootPath, `css/${entryName}.css`);
  const styleUri = cdnBasePath ?
    stylePath :
    resourceProcess(stylePath);
  // vendor for MPA
  const vendorStylePath = path.join(rootPath, 'css/vendor.css');
  const vendorStyleUri = cdnBasePath
    ? vendorStylePath
    : resourceProcess(vendorStylePath);
  const vendorScriptPath = path.join(rootPath, 'js/vendor.js');
  const vendorScriptUri = cdnBasePath
    ? vendorScriptPath
    : resourceProcess(vendorScriptPath);

  // Use a nonce to whitelist which scripts can be run
  const nonce = getNonce();
  return `<!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <title>Iceworks</title>
    <link rel="stylesheet" type="text/css" href="${styleUri}">
    ${extraHtml || ''}
    ` +
    (needVendor ? `<link rel="stylesheet" type="text/css" href="${vendorStyleUri}" />` : '') +
    `
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="ice-container"></div>
    ` +
    (needVendor ? `<script nonce="${nonce}" src="${vendorScriptUri}"></script>` : '') +
    `<script nonce="${nonce}" src="${scriptUri}"></script>
  </body>
</html>`;
}

function getNonce(): string {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < 32; i++) {text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

办法位于 packages/vscode-webview/vscode.ts,其实 就是获取一段 html,将本地资源增加 vscode 协定。反对 vendorextraHtml

截止目前,咱们曾经能够在 vscode 中唤起咱们的 WebView 了。

通信

而后就是解决 vscode 和 WebView 通信的问题了。这里的通信跟 pubSub 十分的相似:

  • 插件给 WebView 发消息

    panel.webview.postMessage({text:"你好,这里是 vscode 发送过去的音讯"});
  • webview 端承受音讯

    window.addEventListener('message',event=>{
        const message = event.data;
        console.log(`WebView 承受到的音讯:${message}`);
    })
  • webview 给插件发消息

    vscode.postMessage({text:"你好,这是 webView 发送过去的音讯"});
  • 插件端承受

    panel.webview.onDidReceiveMessage(msg=>{console.log(` 插件承受到的音讯:${msg}`)
    },undefined,context.subscriptions);

这种通信机制太零散了,在理论我的项目中,webView 更加的相似于咱们的 view 层。所以 实践上它只有通过 service 去调用 controller 接口去实现底层操作通知我后果就能够

比方在创立我的项目的时候须要让用户抉择创立目录,在 HTML 页面点击抉择按钮的 click handle 应该如下:

  const getAppPath = async () => {const projectPath = await callService('project', 'getFolderPath', 'ok');
    setAppPath(projectPath);
  };

callService的形参第一个作为 service 类、第二个作为类外面所须要调用的办法名,后续的为其对应办法的参数。

正对如上,咱们封装一个 callService 办法:

// packages/vscode-webview/webview.ts

// @ts-ignore
export const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;

export const callService = function (service: string, method: string, ...args) {
  // 对立 return promise,对立调用形式
  return new Promise((resolve, reject) => {
    // 生成对应的 eventId
    const eventId = setTimeout(() => {});

    console.log(`WebView call vscode extension service:${service} ${method} ${eventId} ${args}`);

    // 收到 vscode 发来音讯,个别为解决后 webView 的需要后
    const handler = event => {
      const msg = event.data;
      console.log(`webview receive vscode message:}`, msg);
      if (msg.eventId === eventId) {// 去定时对应的 eventID,阐明此次通信完结,能够移除(完结)此次通信了
        window.removeEventListener('message', handler);
        msg.errorMessage ? reject(new Error(msg.errorMessage)) : resolve(msg.result);
      }
    }

    // webview 承受 vscode 发来的音讯
    window.addEventListener('message', handler);

    // WebView 向 vscode 发送音讯
    vscode.postMessage({
      service,
      method,
      eventId,
      args
    });

  });
}

webview 层实现了对发送工夫申请、承受工夫申请以及承受后勾销实现(removeListener)此次工夫申请的封装。那么咱们在来给 extension 增加上对应的webView 须要的 service.methodName 才行。

这里咱们再封装了一个叫做 connectService 的办法。

connectService(projectCreatorPanel, context, { services});

下面的projectCreatorPanel 就是 create 进去的 WebviewPanel 的“实例”,而 services 能够了解为含有多个类的对象

const services = {
    project:{getFolderPath(...args){//xxx},
        xxx
    },
    xxx:{}}

具体的 connectService 办法如下:

export function connectService(
  webviewPanel: vscode.WebviewPanel,
  context: vscode.ExtensionContext,
  options: IConnectServiceOptions
) {const { subscriptions} = context;
  const {webview} = webviewPanel;
  const {services} = options;
  webview.onDidReceiveMessage(async (message: IMessage) => {const { service, method, eventId, args} = message;
    const api = services && services[service] && services[service][method];
    console.log('onDidReceiveMessage', message);
    if (api) {
      try {
        const fillApiArgLength = api.length - args.length;
        const newArgs = fillApiArgLength > 0 ? args.concat(Array(fillApiArgLength).fill(undefined)) : args;
        const result = await api(...newArgs, context, webviewPanel);
        console.log('invoke service result', result);
        webview.postMessage({eventId, result});
      } catch (err) {console.error('invoke service error', err);
        webview.postMessage({eventId, errorMessage: err.message});
      }
    } else {vscode.window.showErrorMessage(`invalid command ${message}`);
    }
  }, undefined, subscriptions);
}

下面的代码也比较简单,就是 注册监听函数,而后只有监听到WebView post 过去的 message,就去取对应 services 下的某个 servicemethod 去执行,并且传入 WebView 传过来的参数

extension 的 services 是在这里引入的

@pmworks/project-service 这个 package 外面也只是封装一些根本的办法调用。外围的解决逻辑比方下载对应gitRpo、解析本地文件等都是在对应的extension-utils 外面进行。service 只管调用即可。

小小问题

如上曾经实现了根本的流程封装,剩下就是具体逻辑的编写了。然而在理论开发中,web 页面须要拿到 vscode 传入的参数才行,而在 web 页面开发中,vscode 插件又没法读取未编译后的代码。如何解决呢?

在 webView 外面在封装一层 callService 用于本地 web 页面开发所需

后续瞻望

截止目前,根本介绍完了这两周除业务工作外的一些开发总结了。接下来须要恶补一下 vscode 插件的相干 api 筹备开始操刀了。当然,在这之前,另一个十分十分紧急的工作就是还须要再降级上来年整顿的源码架构,对齐下团体内当初 rax 体系的一些能力。

在回到这个插件体系(BeeDev 源码工作台)的开发中,后续还须要:

  • 初始化源码架构
  • 创立页面、利落拽相干 H5 源码物料(须要整个物料后盾)生成初始化页面
  • 创立模块、可视化配置模块加载类别等

如果精力无余,其实还须要个node 后盾,这样能力买通服务端和本地的能力(就是个桌面利用了呀~)

好吧,不 YY,先酱紫吧~ 下一个里程碑了再总结下~~

至于我的项目源码。。。

参考文献

  • appworks:https://appworks.site/
  • vscode extension api:https://code.visualstudio.com…
  • monorepo&leran:https://github.com/lerna/lerna

其余

关注微信公众号【全栈前端精选】,每天推送精选文章~

正文完
 0