<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于 monorepo 的架構(gòu)實(shí)踐總結(jié): vscode 插件及其相關(guān) packages 開(kāi)發(fā)

          共 37706字,需瀏覽 76分鐘

           ·

          2021-06-25 07:30

          前言

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

          背景如是:

          隨著腳手架的提供,以及新增頁(yè)面和模塊的功能封裝。

          畢竟 「多提供一層規(guī)范,就多了一層約束。」 而架構(gòu)的本質(zhì)是為了讓開(kāi)發(fā)者能夠?qū)⒕Ω拥膄ocus 到業(yè)務(wù)的開(kāi)發(fā)中,無(wú)需關(guān)心其他。比如上述腳手架初始化出來(lái)的一些模塊配置、異步加載甚至一些已定義并且保留在初始化架構(gòu)中的一些業(yè)務(wù) hooks 等。

          如上原因,我希望能夠提供一套可視化的操作(創(chuàng)建項(xiàng)目、選擇依賴、添加頁(yè)面,選擇所需物料、配置物料屬性等),一言以蔽之就是讓用戶對(duì)于源碼開(kāi)發(fā)而言,只需要編寫對(duì)應(yīng)的業(yè)務(wù)模塊組件,而不需管理架構(gòu)是如何組織模塊和狀態(tài)分發(fā)的,「除了業(yè)務(wù)模塊編碼,其他都是可視化操作」

          因?yàn)閳F(tuán)隊(duì)里 100%的同學(xué)都是以 vscode 作為飯碗,所以自然而然的 vscode extinction 就是我的第一選擇了。計(jì)劃中會(huì)提供創(chuàng)建項(xiàng)目、新增頁(yè)面、模塊配置、頁(yè)面配置、新增模塊等一系列插件。后續(xù)階段性進(jìn)展,再發(fā)文總結(jié)。咳咳,是的,這將是一個(gè)「源碼工作臺(tái)」的趕腳~

          截止目前,已經(jīng)將項(xiàng)目的腳手架基本搭建了個(gè) 90% ,此處作為第一階段性總結(jié)。

          成果展示

          demo
          項(xiàng)目目錄

          extensions 文件夾為 vscode 插件的文件夾、packages 文件夾是存放公共的組件、scripts 為發(fā)布、構(gòu)建、開(kāi)發(fā)的腳本,其他就是一些工程配置。

          ?

          當(dāng)然,這里最主要不是產(chǎn)品功能的展示,嘎嘎~

          ?

          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 沒(méi)有添加完全,目前開(kāi)發(fā)直接 npm start 發(fā)布 packages 分別為 npm run publish-beta:packagenpm run publish:package ,上面也有 publish 的命令匯總。

          架構(gòu)選型

          目前是為了將 pmCli 功能全部封裝成插件,然后通過(guò)可視化替代掉編碼過(guò)程中關(guān)于架構(gòu)配置的相關(guān)操作。所以插件必然不會(huì)只有一個(gè),而是一個(gè)基于源碼架構(gòu)的一個(gè)「操作集」:多 extensions。插件中有非常多的相似功能封裝。比如從 gitlab 上讀取基礎(chǔ)文件、vscodeWebView 的通信、AST 的基本封裝等,所以必然需要依賴非常多的 packages ,為了開(kāi)發(fā)提效和集合的統(tǒng)一管理,必然想到基于 lernamonorepo 的項(xiàng)目結(jié)構(gòu)。

          其中關(guān)于 lerna 的一些采坑就不多說(shuō)了,主要是我也只是看了市面上大部分的實(shí)踐文章和官方文檔,缺乏一些自己實(shí)踐(畢竟感覺(jué)研究多也解決不了多大的痛點(diǎn),就不想花精力了)最終的 monorepo 是基于 yarn workspace 實(shí)現(xiàn)的,通過(guò) lerna link 來(lái)軟鏈packagelerna 的發(fā)布 package 比較雞肋,就參考 App works 自己寫了一些打包發(fā)布到預(yù)發(fā)、線上的腳本。

          項(xiàng)目工作流以及編碼約束通過(guò)huskylint-stagedgit-czeslintprettier等常規(guī)配置。

          編碼采用 ts 編碼,所以對(duì)于 extensions 以及 packages 中都有很多公共的配置,這里可以提取出來(lái)公共部分放置到項(xiàng)目根目錄下(如上項(xiàng)目目錄截圖)。

          實(shí)踐

          通過(guò) lerna initlerna create xxx 來(lái)初始化這里就不說(shuō)了。反正完事以后就是帶有一個(gè)packagespackage.json 文件的一個(gè)目錄結(jié)構(gòu)。

          項(xiàng)目架構(gòu)

          項(xiàng)目結(jié)構(gòu)

          package結(jié)構(gòu)

          package
          ?

          以上結(jié)構(gòu)說(shuō)明都在圖片里了

          ?

          腳本封裝

          在項(xiàng)目的根目錄下放置了一個(gè) scripts 文件夾,存放著一些發(fā)布、開(kāi)發(fā)以及依賴的安裝的腳本啥的。

          scripts

          getPakcageInfo.ts

          ?

          用于從 packages 中獲取相關(guān) publish 信息。其中shouldPublish是將本地 version 和線上 version 對(duì)比,判斷師傅需要執(zhí)行 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');
          // 定義需要獲取到的信息結(jié)構(gòu)
          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 中讀取每一個(gè) packagepackage.json 中的信息,然后組成需要的格式返回出去,用于發(fā)布。

          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 相關(guān)信息
              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 寫到對(duì)應(yīng) 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, null2));
            });
          }
          // npm publish --tag=beta 發(fā)布
          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++;
              // 打印此次發(fā)布的相關(guān)信息
              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')}`);

          });

          基本功能都在注釋里了(這句話后面不贅述了),總結(jié)次腳本作用:

          • 拿到所有的本地 packageInfo 信息
          • 對(duì)比線上(已發(fā)布)信息,糾正此次發(fā)布需要的版本信息
          • 將糾正的版本信息補(bǔ)充道(寫入)本地對(duì)應(yīng)的 package 中的 package.json 中
          • 調(diào)用腳本,執(zhí)行發(fā)布
          ?

          publish-package 就非常簡(jiǎn)單了,寫的也比較簡(jiǎn)單,就是調(diào)用 npm publish ,當(dāng)然,也需要一些基本的線上校驗(yàn),比如上述的 shouldPublish。不贅述了!

          需要注意的是,發(fā)布的時(shí)候,需要注意登陸(npm whoami)以及如果你也是采用@xxx/的命名方式的話,注意對(duì)應(yīng) organization的權(quán)限

          ?

          watch

          主要借助 nsfw 的能力對(duì)本地文件進(jìn)行監(jiān)聽(tīng)。「有變動(dòng),咱編譯就完事了!」

           /*
           * @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 文件的問(wèn)題
          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

          因?yàn)槲覀兊?workspacepackages 目錄下,所以針對(duì)于 extensions 下的插件以及 web 頁(yè)面,我們沒(méi)有辦法通過(guò) yarn 直接install 所有依賴,隨意提供了一個(gè)插件安裝依賴的腳本。「其實(shí)就是跑到項(xiàng)目目錄下,去執(zhí)行 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) { // 拼接參數(shù)
              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 目錄,則繼續(xù)安裝 web 頁(yè)面里的依賴
              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 去執(zhí)行

          ?

          extension-link-package

          刪除本地相關(guān)的 package,讓其遞歸向上(應(yīng)用級(jí))查找到對(duì)應(yīng)軟鏈后的 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 下的插件列表,挨個(gè)遍歷執(zhí)行 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')}`);
            }
          }

          小小總結(jié)

          核心腳本目前就如上吧,其實(shí)都是比較簡(jiǎn)單直接的功能。關(guān)于 extensions 的發(fā)布啥的還沒(méi)有寫,其實(shí)也可以從 appworks 中借(抄)鑒(襲)到的。等后續(xù)發(fā)布插件了再補(bǔ)充吧。

          一個(gè)項(xiàng)目完成基建以后,基本就可以開(kāi)工了。這里我拿創(chuàng)建項(xiàng)目來(lái)舉例子吧(著重說(shuō)基建部分,對(duì)插件功能和實(shí)現(xiàn)不展開(kāi)具體的解釋,第二階段再總結(jié)吧)。

          vscode extensions(vscode-webview 封裝舉例)

          我們通過(guò) yo codeextensions 文件夾中去初始化一下我們要寫的插件。具體的基礎(chǔ)知識(shí),參考官方文檔:https://code.visualstudio.com/api

          如上以后,我們有了一個(gè)項(xiàng)目的基本架構(gòu),包的一系列管理,就已經(jīng)可以進(jìn)入到我們的開(kāi)發(fā)階段了。

          畢竟我們插件是為了可視化的一系列操作,那么vscode 的按鈕和命令必然滿足不了我們,需要一個(gè)操作界面:webView。如上圖是一個(gè)帶有 webView 插件的整體交互過(guò)程:

          • Common-xxx(utils) 是負(fù)責(zé)整個(gè)項(xiàng)目級(jí)別一些通用功能封裝
          • Extension-utils 是針對(duì)某一個(gè)插件提取的一些方法庫(kù),比如 project-utilscreateProject 初始化項(xiàng)目時(shí)候用到的方法庫(kù),類似于一個(gè) controller
          • extension-service 是承載 vscodewebView 通信的一些方法提取,顧名思義:service

          上面說(shuō)的有些繞,與傳統(tǒng) MVC 不同的是這里的 view 有兩個(gè):vscode-extensionextension-webview

          舉個(gè)栗子!這里以初始化一個(gè)項(xiàng)目教授架為例子吧~

          ?

          關(guān)于 vscode  extension with WebView相關(guān)基礎(chǔ)概念可以看這里:https://code.visualstudio.com/api/extension-guides/webview

          ?

          WebView

          WebView 其實(shí)沒(méi)有太多要準(zhǔn)備的,就是準(zhǔn)備 HTML、JavaScript 和 css 前端三大件就行了。

          這里我使用的 ice 的腳手架初始化出來(lái)的項(xiàng)目:npm init ice

          web

          然后修改 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"
            ]
          }

          碼完代碼以后得到我們的三大件即可。

          build 后的文件輸出
          ?

          更多關(guān)于 ice 的文檔,請(qǐng)移步官方文檔

          ?

          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''初始化源碼架構(gòu)', 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({ }

          這里也都是常規(guī)操作,注冊(cè)命令和相關(guān)回調(diào),初始化 WebView 。這里說(shuō)下getHtmlFroWebview


          /**
           * 給本地資源帶上安全協(xié)議
           * @param url 本地資源路徑
           * @returns 帶有 vscode-resource 協(xié)議的安全路徑
           */

          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 ,其實(shí)「就是獲取一段 html」,將本地資源添加 vscode 協(xié)議。支持 vendorextraHtml

          截止目前,我們已經(jīng)可以在 vscode 中喚起我們的 WebView 了。

          webview

          通信

          然后就是解決 vscode 和 WebView 通信的問(wèn)題了。這里的通信跟 pubSub 非常的類似:

          • 插件給 WebView 發(fā)消息
            panel.webview.postMessage({text:"你好,這里是 vscode 發(fā)送過(guò)來(lái)的消息"});
          • webview 端接受消息
            window.addEventListener('message',event=>{
             const message = event.data;
             console.log(`WebView 接受到的消息:${message}`);
            })  
          • webview 給插件發(fā)消息
          vscode.postMessage({text:"你好,這是 webView 發(fā)送過(guò)來(lái)的消息"});
          • 插件端接受
            panel.webview.onDidReceiveMessage(msg=>{
             console.log(`插件接受到的消息:${msg}`)
            },undefined,context.subscriptions);

          這種通信機(jī)制太零散了,在實(shí)際項(xiàng)目中,webView 更加的類似于我們的 view 層。所以「理論上它只要通過(guò) service 去調(diào)用 controller 接口去完成底層操作告訴我結(jié)果就可以」

          比如在創(chuàng)建項(xiàng)目的時(shí)候需要讓用戶選擇創(chuàng)建目錄,在 HTML 頁(yè)面點(diǎn)擊選擇按鈕的 click handle 應(yīng)該如下:

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

          callService的形參第一個(gè)作為 service 類、第二個(gè)作為類里面所需要調(diào)用的方法名,后續(xù)的為其對(duì)應(yīng)方法的參數(shù)。

          正對(duì)如上,我們封裝一個(gè) 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{
            // 統(tǒng)一 return promise,統(tǒng)一調(diào)用方式
            return new Promise((resolve, reject) => {
              // 生成對(duì)應(yīng)的 eventId
              const eventId = setTimeout(() => { });

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

              // 收到 vscode 發(fā)來(lái)消息,一般為處理后 webView 的需求后
              const handler = event => {
                const msg = event.data;
                console.log(`webview receive vscode message:}`, msg);
                if (msg.eventId === eventId) {// 去定時(shí)對(duì)應(yīng)的 eventID,說(shuō)明此次通信結(jié)束,可以移除(結(jié)束)此次通信了
                  window.removeEventListener('message', handler);
                  msg.errorMessage ? reject(new Error(msg.errorMessage)) : resolve(msg.result);
                }
              }

              // webview 接受 vscode 發(fā)來(lái)的消息
              window.addEventListener('message', handler);

              // WebView 向 vscode 發(fā)送消息
              vscode.postMessage({
                service,
                method,
                eventId,
                args
              });

            });
          }

          webview 層完成了對(duì)發(fā)送時(shí)間請(qǐng)求、接受時(shí)間請(qǐng)求以及接受后取消完成(removeListener)此次時(shí)間請(qǐng)求的封裝。那么我們?cè)趤?lái)給 extension 添加上對(duì)應(yīng)的webView 需要的 service.methodName 才行。

          這里我們?cè)俜庋b了一個(gè)叫做 connectService 的方法。

          connectService(projectCreatorPanel, context, { services });

          上面的projectCreatorPanel 就是create 出來(lái)的 WebviewPanel 的“實(shí)例”,而 services 可以理解為含有多個(gè)類的對(duì)象

          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);
          }

          上面的代碼也比較簡(jiǎn)單,就是「注冊(cè)監(jiān)聽(tīng)函數(shù),然后只要監(jiān)聽(tīng)到WebView post 過(guò)來(lái)的 message,就去取對(duì)應(yīng) services 下的某個(gè) servicemethod 去執(zhí)行,并且傳入 WebView 傳過(guò)來(lái)的參數(shù)」

          extension 的 services 是在這里引入的

          services

          @pmworks/project-service這個(gè) package 里面也只是封裝一些基本的方法調(diào)用。核心的處理邏輯比如下載對(duì)應(yīng)gitRpo、解析本地文件等都是在對(duì)應(yīng)的extension-utils 里面進(jìn)行。「service只管調(diào)用即可。」

          小小問(wèn)題

          如上已經(jīng)完成了基本的流程封裝,剩下就是具體邏輯的編寫了。但是在實(shí)際開(kāi)發(fā)中,web 頁(yè)面需要拿到 vscode 傳入的參數(shù)才行,而在 web 頁(yè)面開(kāi)發(fā)中,vscode 插件又沒(méi)法讀取未編譯后的代碼。如何解決呢?

          「在 webView 里面在封裝一層 callService 用于本地 web 頁(yè)面開(kāi)發(fā)所需」

          封裝 callService

          后續(xù)展望

          截止目前,基本介紹完了這兩周除業(yè)務(wù)工作外的一些開(kāi)發(fā)總結(jié)了。接下來(lái)需要惡補(bǔ)一下 vscode 插件的相關(guān) api 準(zhǔn)備開(kāi)始操刀了。當(dāng)然,在這之前,另一個(gè)非常非常緊急的任務(wù)就是還需要再升級(jí)下去年整理的源碼架構(gòu),對(duì)齊下集團(tuán)內(nèi)現(xiàn)在 rax 體系的一些能力。

          在回到這個(gè)插件體系(BeeDev源碼工作臺(tái))的開(kāi)發(fā)中,后續(xù)還需要:

          • 初始化源碼架構(gòu)
          • 創(chuàng)建頁(yè)面、拖拉拽相關(guān) H5 源碼物料(需要整個(gè)物料后臺(tái))生成初始化頁(yè)面
          • 創(chuàng)建模塊、可視化配置模塊加載類別等

          如果精力有余,其實(shí)還需要個(gè)node 后臺(tái),這樣才能打通服務(wù)端和本地的能力(就是個(gè)桌面應(yīng)用了呀~)

          好吧,不 YY,先醬紫吧~ 下一個(gè)里程碑了再總結(jié)下~~

          至于項(xiàng)目源碼。。。

          參考已開(kāi)源的 appworks

          參考文獻(xiàn)

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

          其他

          關(guān)注微信公眾號(hào)【全棧前端精選】,每天推送精選文章~

          瀏覽 76
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人在线视频免费观看 | 亚洲色婷婷精品 | 午夜操穴| 免费亚洲婷婷 | 午夜福利无码电影 |