原文鏈接
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,
devServer.writeToDist
:需要在 dev 時寫文件到輸出目錄,這樣保證開發階段有 js/css 文件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_modulesvsce 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 ~