Bigfish 是蚂蚁集团企业级前端研发框架,基于 umi 微内核框架,Bigfish = umi + preset-react + 内部 presets。javascript
前天发布了 Bigfish VSCode 插件,开发过程当中遇到了很多问题,除了官方文档外,没有一个很好的指南,索性将 VSCode 插件开发过程记录下,让后面的同窗能够更好地开发 VSCode 插件,由于篇幅有限,讲清楚得来个系列。css
同时也有一些思考,可不能够用 umi 直接开发 VSCode 插件?html
让咱们从零开始开发一个插件吧,首先咱们须要先安装一个 VSCode Insiders(相似 VSCode 开发版),这样能够在相对纯净的插件环境进行研发,同时建议用英文版,这样在看 microsoft/vscode 源码时,更容易定位到具体代码。前端
这里直接使用官方的脚手架生成,用 npx
不用全局 -g
安装java
➜ 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 插件的初始化就完成啦 ~node
首先咱们从项目目录结构来了解下插件开发,组织上和咱们 npm 库基本同样react
.
├── 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
复制代码
{
"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,但有几个比较重要的属性:webpack
main
就是插件入口,实际上就是 src/extension.ts
编译出来的产物contributes
能够理解成 功能声明清单,插件有关的命令、配置、UI、snippets 等都须要这个字段咱们来看一下 src/extension.ts
git
// 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
两个生命周期方法,插件就能运行了。github
做为插件,提供哪些功能呢?这里整理了一个思惟导图,同时也能够对照官方文档来看:
这里咱们以一个点击『打开页面』 弹出 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。
一个命令能够理解一个功能点,好比打开 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 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.commands.executeCommand
使用。
若是但愿在知足特定条件,才开启插件某个功能/命令/界面按钮,这时候能够借助插件清单里的 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$/"
复制代码
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 兼容 VSCode API,但也有一些不兼容的 API(如 vscode.ExtensionMode
),为了保证同时兼容,用到了 CloudIDE 团队写的 @ali/ide-extension-check,可直接扫当前是否兼容 CloudIDE,这里把它作成一个 ci 流程
为了更好的体验,可使用官网内置的图标集,例如: 只须要使用
$(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
- ...
- node_modules
- package.json
复制代码
最后的包大小为: 24 files, 1.11MB ,从 16.95M
到 1.11M
,直接秒级安装。
以前一直想着把 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 ~