Bigfish VSCode 插件開發實踐


原文鏈接

https://zhuanlan.zhihu.com/p/259344620

前言

Bigfish 是螞蟻集團企業級前端研發框架,基於  umi 微內核框架,Bigfish = umi +  preset-react + 內部 presets。

前天發布了 Bigfish VSCode 插件,開發過程中遇到了不少問題,除了官方文檔外,沒有一個很好的指南,索性將 VSCode 插件開發過程記錄下,讓后面的同學可以更好地開發 VSCode 插件,因為篇幅有限,講清楚得來個系列。

同時也有一些思考,可不可以用 umi 直接開發 VSCode 插件?

快速開始

讓我們從零開始開發一個插件吧,首先我們需要先安裝一個 VSCode Insiders(類似 VSCode 開發版),這樣可以在相對純凈的插件環境進行研發,同時建議用英文版,這樣在看 microsoft/vscode 源碼時,更容易定位到具體代碼。

初始化

這里直接使用官方的腳手架生成,用 npx 不用全局 -g 安裝

➜ npx --ignore-existing -p yo -p generator-code yo code

     _-----_     ╭──────────────────────────╮
    | | │ Welcome to the Visual │ |--(o)--| │ Studio Code Extension │ `---------´ │ generator! │ ( _´U`_ ) ╰──────────────────────────╯ /___A___\ / | ~ | __'.___.'__ ´ ` |° ´ Y ` ? What type of extension do you want to create? New Extension (TypeScript) ? What's the name of your extension? hello-world ? What's the identifier of your extension? hello-world ? What's the description of your extension? ? Initialize a git repository? Yes ? Which package manager to use? yarn

然后用 VSCode Insiders 打開 hello-world 項目,點擊 『Run Extension』會啟動一個 [Extension Development Host] 窗口,這個窗口會加載我們的插件

腳手架里插件默認是輸入 『Hello World』然后右下角彈窗

至此,一個 VSCode 插件初始化就完成啦 ~

目錄結構

首先我們從項目目錄結構來了解下插件開發,組織上和我們 npm 庫基本一樣

.
├── CHANGELOG.md
├── README.md
├── .vscodeignore # 類似 .npmignore,插件包里不包含的文件 ├── out # 產物 │ ├── extension.js │ ├── extension.js.map │ └── test │ ├── runTest.js │ ├── runTest.js.map │ └── suite ├── package.json # 插件配置信息 ├── src │ ├── extension.ts # 主入口文件 │ └── test # 測試 │ ├── runTest.ts │ └── suite ├── tsconfig.json └── vsc-extension-quickstart.md

package.json

{ "name": "hello-world", "displayName": "hello-world", "description": "", "version": "0.0.1", "engines": { "vscode": "^1.49.0" }, "categories": [ "Other" ], "activationEvents": [ "onCommand:hello-world.helloWorld" ], "main": "./out/extension.js", "contributes": { "commands": [ { "command": "hello-world.helloWorld", "title": "Hello World" } ] }, "scripts": { "vscode:prepublish": "yarn run compile", "compile": "tsc -p ./", "lint": "eslint src --ext ts", "watch": "tsc -watch -p ./", "pretest": "yarn run compile && yarn run lint", "test": "node ./out/test/runTest.js" }, "devDependencies": {} }

VSCode 開發配置復用了 npm 包特性,詳見 Fields,但有幾個比較重要的屬性:

  • main 就是插件入口,實際上就是 src/extension.ts 編譯出來的產物
  • contributes 可以理解成 功能聲明清單,插件有關的命令、配置、UI、snippets 等都需要這個字段

插件入口

我們來看一下 src/extension.ts

// src/extension.ts  // vscode 模塊不需要安裝,由插件運行時注入 import * as vscode from 'vscode'; // 插件加載時執行的 activate 鈎子方法 export function activate(context: vscode.ExtensionContext) { console.log('Congratulations, your extension "hello-world" is now active!'); // 注冊一個命令,返回 vscode.Disposable 對象,該對象包含 dispose 銷毀方法  let disposable = vscode.commands.registerCommand('hello-world.helloWorld', () => { // 彈出一個信息框消息  vscode.window.showInformationMessage('Hello World from hello-world!'); }); // context 訂閱注冊事件  context.subscriptions.push(disposable); } // 插件被用戶卸載時調用的鈎子 export function deactivate() {} 

我們只需要暴露 activate 和 deactivate 兩個生命周期方法,插件就能運行了。

功能

作為插件,提供哪些功能呢?這里整理了一個思維導圖,同時也可以對照官方文檔來看:

 

這里我們以一個點擊『打開頁面』 彈出 webview 的例子,來串一下所用到的 VSCode 功能

 

插件清單聲明

插件清單聲明(Contribution Points)是我們需要首先關注的,位於 package.json 的 contributes 屬性,這里面可以聲明 VSCode 大部分配置、UI 擴展、快捷鍵、菜單等。

為了找到我們對應配置項,VSCode 編輯器布局圖會更直觀的感受

根據例子,我們需要在 Editor Groups 里添加一個按鈕,同時需要注冊一個命令,也就是如下配置:

{ "contributes": { "commands": [ { "command": "hello-world.helloWorld", "title": "Hello World" }, + { + "command": "hello-webview.helloWorld", + "title": "打開頁面" + } ], + "menus": { + "editor/title": [ + { + "command": "hello-webview.helloWorld", + "group": "navigation@0" + } + ] + } } }

其中 命令 和 菜單 的類型如下,可以根據需求增加更多個性化配置,配置類型見 menusExtensionPoint.ts#L451-L485

注冊命令(commands)

一個命令可以理解一個功能點,比如打開 webview 就是一個功能,那么我們使用 vscode.commands.registerCommand 注冊 打開 webview 這個功能:

// src/extension.ts  export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('hello-webview.helloWorld', () => { }) ) } 

我們可以看下registerCommand 方法定義:

/**  * Registers a command that can be invoked via a keyboard shortcut,  * a menu item, an action, or directly.  *  * Registering a command with an existing command identifier twice  * will cause an error.  *  * @param command A unique identifier for the command.  * @param callback A command handler function.  * @param thisArg The `this` context used when invoking the handler function.  * @return Disposable which unregisters this command on disposal.  */ export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable; 

其中 command 要與我們前面 package.json 聲明的命令要一致, callback 就是調用后做什么事,返回的是一個 Disposable 類型,這個對象很有意思,可在插件退出時執行銷毀 dispose 方法。

打開 webview

這里需要用到 Webview API,因為有 webview,擴展了 VSCode UI 和交互,提供了更多的想象力

const panel = vscode.window.createWebviewPanel('helloWorld', 'Hello World', vscode.ViewColumn.One, { enableScripts: true, }); panel.webview.html = `  <!DOCTYPE html>  <html lang="en">  <head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Hello World</title>  </head>  <body>  <iframe width="100%" height="500px" src="https://www.yunfengdie.com/"></iframe>  </body>  </html> `; panel.onDidDispose(async () => { await vscode.window.showInformationMessage('關閉了 webview'); }, null, context.subscriptions); 

這里要注意的點是,html 中的本地 url 地址需要轉一道,不然無法運行,例如

- <script src="/bar.js"></script> + <script src="${panel.webview.asWebviewUri(vscode.Uri.file(path.join(__dirname, 'bar.js')))}"></script>

✈️ 進階

上面提到的功能只是 VSCode 功能的冰山一角,更多的功能遇到時查文檔就會用了,這里有幾點進階的部分。

命令系統

VSCode 的命令系統是一個很好的設計,優勢在於:中心化注冊一次,多地扁平化消費

 

 

我個人覺得更重要的一點在於:

  • 先功能后交互:VSCode 提供的 UI 和交互有限,我們可以先不用糾結交互,先把功能用命令注冊,再看交互怎么更好
  • 靈活性:比如 VSCode 增加了一種新交互形式,只需要一行配置就可以接入功能,非常方便

另外官網也內置了一些命令,可直接通過 vscode.commands.executeCommand 使用。

when 上下文

如果希望在滿足特定條件,才開啟插件某個功能/命令/界面按鈕,這時候可以借助插件清單里的 when 上下文來處理,例如檢測到是 Bigfish 應用( hello.isBigfish )時開啟:

"activationEvents": [ "*" ], "contributes": { "commands": [ { "command": "hello-world.helloWorld", "title": "Hello World", }, { "command": "hello-webview.helloWorld", "title": "打開頁面", } ], "menus": { "editor/title": [ { "command": "hello-webview.helloWorld", "group": "navigation@0", + "when": "hello.isBigfish" } ] } },

如果直接這樣寫,啟動插件時,會看到之前的『打開頁面』按鈕消失,這個值的設置我們用 VSCode 內置的 setContext 命令:

vscode.commands.executeCommand('setContext', 'hello.isBigfish', true); 

這時候我們打開就有按鈕了,關於狀態什么時候設置,不同插件有自己的業務邏輯,這里不再贅述。

這里的 when 可以有簡單的表達式組合,但是有個坑點是不能用 () ,例如:

- "when": "bigfish.isBigfish && (editorLangId == typescriptreact || editorLangId == typescriptreact)" + "when": "bigfish.isBigfish && editorLangId =~ /^typescriptreact$|^javascriptreact$/" 

結合 umi

webview 的部分,如果單寫 HTML 明顯回到了 jQuery 時代,能不能將 umi 聯系起來呢?實際上是可以的,只是我們需要改一些配置。

首先對 umi,

  1. devServer.writeToDist :需要在 dev 時寫文件到輸出目錄,這樣保證開發階段有 js/css 文件
  2. history.type :使用內存路由 MemoryRouter,webview 里是沒有 url 的,這時候瀏覽器路由基本是掛的。
import { defineConfig } from 'umi'; export default defineConfig({ publicPath: './', outputPath: '../dist', runtimePublicPath: true, history: { type: 'memory', }, devServer: { writeToDisk: filePath => ['umi.js', 'umi.css'].some(name => filePath.endsWith(name)), }, }); 

加載 webview,這時候就是把 umi.css 和 umi.js 轉下路徑:

this.panel.webview.html = `  <!DOCTYPE html>  <html>  <head>  <meta charset="utf-8" />  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />  <link rel="stylesheet" href="${this.panel.webview.asWebviewUri( vscode.Uri.file(path.join(distPath, 'umi.css')), )}" />  <script>window.routerBase = "/";</script>  <script>//! umi version: 3.2.14</script>  </head>  <body>  <div id="root"></div>  <script src="${this.panel.webview.asWebviewUri(vscode.Uri.file(path.join(distPath, 'umi.js')))}"></script>  </body>  </html>`; 

然后就可以用我們的 umi 開發 webview 了

 

調試

這里的調試分兩個:插件調試、webview 調試。

插件調試直接用 VSCode 內置的斷點,非常方便

 

webview 的調試我們通過 command + shift + p 調用 Open Webview Developer Tools 來調試 webview

 

支持 CloudIDE

CloudIDE 兼容 VSCode API,但也有一些不兼容的 API(如 vscode.ExtensionMode ),為了保證同時兼容,用到了 CloudIDE 團隊寫的 @ali/ide-extension-check,可直接掃當前是否兼容 CloudIDE,這里把它做成一個 CI 流程,自動化發布、文檔同步

 

Icon 圖標

為了更好的體驗,可以使用官網內置的圖標集,例如:

只需要使用 $(iconIdentifier) 格式來表示具體 icon

{ "contributes": { "commands": [ { "command": "hello-world.helloWorld", "title": "Hello World" }, { "command": "hello-webview.helloWorld", "title": "打開頁面", + "icon": "$(browser)", } ], } }

但是在 CloudIDE 中,內置的不是 VSCode icon,而是 antd Icon。為了同時兼容 CloudIDE 和 VSCode,直接下載 vscode-icons,以本地資源形式展現。

{ "contributes": { "commands": [ { "command": "hello-world.helloWorld", "title": "Hello World" }, { "command": "hello-webview.helloWorld", "title": "打開頁面", + "icon": { + "dark": "static/dark/symbol-variable.svg", + "light": "static/light/symbol-variable.svg" + }, } ], } }

打包、發布

部署上線前需要注冊 Azure 賬號,具體步驟可以按官方文檔操作。

包體積優化

腳手架默認的是 tsc 只做編譯不做打包,這樣從源文件發布到插件市場包含的文件就有:

- out
  - extension.js
  - a.js
  - b.js
  - ...
- dist
  - umi.js
  - umi.css
  - index.html
- node_modules # 這里的 node_modules,vsce package --yarn 只提取 dependencies 相關包 - ... - package.json

那邊 Bigfish 插件第一次打包是多大呢? 11709 files, 16.95MB

為了繞過這個 node_modules ,思路是通過 webpack 將不進行 postinstall 編譯的依賴全打進 extension.js 里,webpack 配置如下:

'use strict'; const path = require('path'); const tsConfigPath = path.join(__dirname, 'tsconfig.json'); /** @type {import("webpack").Configuration} */ const config = { target: 'node', devtool: process.env.NODE_ENV === 'production' ? false : 'source-map', mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', entry: './src/extension.ts', externals: { vscode: 'commonjs vscode', }, module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, loader: 'ts-loader', options: { transpileOnly: true, configFile: tsConfigPath, }, }, ], }, output: { devtoolModuleFilenameTemplate: '../[resource-path]', filename: 'extension.js', libraryTarget: 'commonjs2', path: path.resolve(__dirname, 'out'), }, resolve: { alias: { '@': path.join(__dirname, 'src'), }, extensions: ['.ts', '.js'], }, optimization: { usedExports: true } }; module.exports = config; 

.vscodeignore 里加上 node_modules ,不發到市場,這樣包結構就變成了

- out  - extension.js - dist  - umi.js - umi.css - index.html - package.json 

最后的包大小為: 24 files, 1.11MB ,從 16.95M 到 1.11M ,直接秒級安裝。

Made by ChartCube

 

預編譯依賴 & 安全性

之前一直想着把 Bigfish core 包(@umijs/core)打到 插件包里,基本沒成功過,原因在於 core 依賴了 fsevents,這個包要根據不同 OS 安裝時做編譯,所以沒辦法打到包里:

- [fail] cjs (./src/extension.ts -> out/extension.js)Error: Build failed with 2 errors: node_modules/fsevents/fsevents.js:13:23: error: File extension not supported: node_modules/fsevents/fsevents.node node_modules/@alipay/bigfish-vscode/node_modules/prettier/third-party.js:9871:10: error: Transforming for-await loops to the configured target environment is not supported yet

同時像一些內部的 sdk 包(@alipay/oneapi-bigfish-sdk)如果打進包,會有一定的安全風險,畢竟包是發到外部插件市場。

解決這兩個問題,采用了動態引用依賴,直接引用戶項目已有的依賴(Bigfish 項目內置 oneapi sdk 包),這樣一是包體積小,二是包安全性高。

import resolvePkg from 'resolve-pkg'; // origin require module // https://github.com/webpack/webpack/issues/4175#issuecomment-342931035 export const cRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; // 這樣引用是為了避免內部包泄露到 外部插件市場 const OneAPISDKPath = resolvePkg('@alipay/oneapi-bigfish-sdk', { cwd: this.ctx.cwd, }); this.OneAPISDK = cRequire(OneAPISDKPath); 

發布

直接用官方的 vsce 工具:

  • vsce publish patch :發 patch 版本
  • vsce package :輸出插件包文件 .vsix

沒有打包依賴的插件:

  • vsce publish patch --yarn :發 patch 版本,包含生產依賴的 node_modules
  • vsce package --yarn :輸出插件包文件 .vsix ,包含生產依賴的 node_modules

❓ 思考

幾乎每個 VSCode 插件的開發方式都不一樣,缺少最佳實踐(commands、provider 注冊、services 的消費、webview 的開發等)

細思下來,能不能借鑒按 SSR 方案,其實僅用一個 umi 是可以編譯打包 VSCode 插件 + webview 的(名子想了下,可能是 vsue),覺得比較好的目錄結構是:

- snippets
- src
  - commands # 命令,根據文件名自動注冊 - hello-world.ts - services # 功能建模,掛載到 ctx 上,通過 ctx.services 調用 - A.ts - B.ts - providers # Provider 類,擴展 VSCode 默認交互、UI - TreeDataProvider.ts - utils # 工具類,ctx.utils.abc 調用 - constants.ts - extension.ts - static - dark - a.png - light - webview # webview 應用 - mock - src - pages - test - .umirc.ts # 同時跑 前端 和 插件 編譯和打包 - package.json

umi 配置文件可能就是:

export default defineConfig( { entry: './webview', publicPath: './', outputPath: './dist', history: { type: 'memory', }, devServer: { writeToDisk: filePath => ['umi.js', 'umi.css'].some(name => filePath.endsWith(name)), }, // VSCode 插件打包相關配置  vscode: { entry: './src', // 插件依賴這個包,沒有則提示安裝(更多功能擴展)  globalDeps: ['@alipay/bigfish'], // 全量打包  // bundled: true,  } } ) 

最終插件包結構為:

- dist
  - umi.js
  - umi.css
  - index.html
- out
  - extension.js
- package.json

開發過程只需要 umi dev 可將插件端 + webview(如果有)同時編譯,直接 VSCode 調試即可,支持熱更新(待驗證)

有興趣的同學可以勾搭一起討論,歡迎聯系 chaolin.jcl@antgroup.com ~

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM