<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)總結(jié) ( vscode 插件及相關(guān) packages 開發(fā))

          共 37388字,需瀏覽 75分鐘

           ·

          2021-06-26 21:52

          關(guān)注并將「趣談前端」設(shè)為星標(biāo)

          每早08:30按時推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

          前言

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

          背景如是:

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

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

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

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

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

          成果展示

          demo
          項目目錄

          extensions文件夾為vscode插件的文件夾、packages文件夾是存放公共的組件、scripts為發(fā)布、構(gòu)建、開發(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沒有添加完全,目前開發(fā)直接npm start發(fā)布packages分別為npm run publish-beta:packagenpm run publish:package,上面也有publish的命令匯總。

          架構(gòu)選型

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

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

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

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

          實踐

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

          項目架構(gòu)

          項目結(jié)構(gòu)

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

          package
          ?

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

          ?

          腳本封裝

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

          scripts

          getPakcageInfo.ts

          ?

          用于從packages中獲取相關(guān)publish信息。其中shouldPublish是將本地version和線上version對比,判斷師傅需要執(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中讀取每一個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 寫到對應(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 信息
          • 對比線上(已發(fā)布)信息,糾正此次發(fā)布需要的版本信息
          • 將糾正的版本信息補充道(寫入)本地對應(yīng)的 package 中的 package.json 中
          • 調(diào)用腳本,執(zhí)行發(fā)布
          ?

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

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

          ?

          watch

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

           /*
           * @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所有依賴,隨意提供了一個插件安裝依賴的腳本。「其實就是跑到項目目錄下,去執(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 頁面里的依賴
              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)用級)查找到對應(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 下的插件列表,挨個遍歷執(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é)

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

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

          vscode extensions(vscode-webview 封裝舉例)

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

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

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

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

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

          舉個栗子!這里以初始化一個項目教授架為例子吧~

          ?

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

          ?

          WebView

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

          這里我使用的 ice 的腳手架初始化出來的項目: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 的文檔,請移步官方文檔

          ?

          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ī)操作,注冊命令和相關(guān)回調(diào),初始化WebView。這里說下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,其實「就是獲取一段 html」,將本地資源添加vscode協(xié)議。支持vendorextraHtml

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

          webview

          通信

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

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

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

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

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

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

          正對如上,我們封裝一個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) => {
              // 生成對應(yīng)的 eventId
              const eventId = setTimeout(() => { });

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

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

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

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

            });
          }

          webview層完成了對發(fā)送時間請求、接受時間請求以及接受后取消完成(removeListener)此次時間請求的封裝。那么我們在來給extension添加上對應(yīng)的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);
          }

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

          extension 的 services 是在這里引入的

          services

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

          小小問題

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

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

          封裝 callService

          項目源碼參考已開源的 appworks.

          參考文獻

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


          關(guān)注公眾號【趣談前端】,不定期分享 前端工程化 可視化 / 低代碼 等技術(shù)文章。



          Dooring可視化搭建平臺數(shù)據(jù)源設(shè)計剖析

          可視化搭建的一些思考和實踐

          從零使用electron搭建桌面端Dooring


          點個在看你最好看

          瀏覽 85
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  亚洲巨爆乳一区二区三区 | 7799精品视频天天看 | 免费日逼视频 | 99国产精品麻豆 | 人人色人人色 |