使用 vscode-page 简化 vscode 插件的 Web
vscode 是时下最流行的开发工具之一,高逼格的 UI 和各种酷酷的插件,不仅提高了使用者的开发效率,还让他们的形象大为改善,不再是人们眼中的“死肥宅”。使用 vscode 很潮,但更潮的就是为它开发插件。作为今年的兴趣点,我也投身于 vscode 插件开发者大军之中。
vscode 的插件开发并不需要太高深的技术,就是普通的开发技术的组合。因为面向的用户就是开发者,所以一般情况下主要是对开发者工作现场(如编辑文本框)的增强,并搭配完成任务的各种命令,很少需要复杂的 UI 。但凡事总有例外,当需要 GUI 加持的时候,就轮到 Webview 出场了。
Webview 开发本身也很简单,就两个任务:
- 负责 GUI 部分的 HTML 页面
- 充当 vscode 和 HTML 页面之间桥梁的 vscode.WebviewPanel
可是,就算是简单的任务,做多了你也会发现还是存在一些值得优化的空间,避免在每个新页面开发都重复以下的事情:
- 设置 html 的 resource root 。
- createOrShow 逻辑:即有则显示,无则创建。
- 隐藏时保存之前页面的状态。
- html 与 WebviewPanel 之间交互涉及的 boilerplate code:开发者只需考虑两者之间的消息格式和处理逻辑就好了。
这样做有助于设定一个简单的开发模型,处理好消息、处理和展示三者的关系,不必每次开发新页面时“copy - paste - change”,而是采用一致的模型并促进组内协作。
于是,vscode-page 应运而生。
设计原理
简单地讲,vscode-page 是一个针对 vscode Webview 开发的轻量级页面微框架,抽象了 html 与 WebviewPanel 之间的通信交互,使得开发者只需要关心具体业务逻辑。其整个架构图如下:
image.png看到这张图,熟悉 Web 开发框架的同学应该已经秒懂整个设计:
- vscode-page 充当消息 dispatcher 。
- MessageMapping 定义消息、处理器和页面模板(采用流行的 handlebars.js )这三者的关系。
- Response 的 html 片段用于更新指定 element 的 innerHTML。
更详细的说明请参见 README 。
这里没有采用花哨的 Angular 、React 和 Vue ,主要就是为了简单和轻量。并且,在 vscode Webview 这种受限的环境下,使用传统的 jQuery + BootStrap + Handlebars 就是最佳选择,而且一般情况下插件的 GUI 也不会太复杂,前面的几个工具已经足以应对。
使用指南
vscode-page 的使用很简单:
定义页面和 WebviewPanel 之间的消息交互。
在这里定义消息请求和响应内容,其中:
- 请求:command + parameters
- 响应:command + ( contents 或 result )
编写页面
这里有几点注意,在每个页面前面添加以下两行:
<base href="{{base}}" />
<script type="text/javascript">
"{{init}}"
</script>
- 前者用于初始化资源根目录,这样后面的资源引用可以直接引用相对路径即可。
- 后者会注入一个 EventListerner 函数定义。
一般情况下,你直接调用:initEventListener() 来完成初始化就行了,它实现了:
- 若消息的 command 为 Response 结尾且有 contents 属性,则为消息中指定的 element 替换 innerHTML 。
- 对于不满足上面条件的消息,则丢弃。
假如你想处理,则可以换一种初始化方式,传入处理函数:
initEventListener(message => {...});
此时,你可以从 message.result 拿到结果。
定义 MessageMapping 。
这个过程没有什么复杂的,就是定义三个属性:
- command,与请求中的 command 对应
- handler,处理逻辑
- templates,请求完毕后要显示的模板定义
详细例子,参见:https://github.com/DTeam-Top/vscode-page/blob/master/example/src/home.ts 。
值得一提的是,MessageMapping 还支持 forward 模式,即请求处理之后,直接转到另一个 command ,以它的处理结果为最终处理结果,类似 Web 开发中的 forward 模式。
{
command: "submitRepositories",
handler: async parameters => {
……
},
forward: "ready"
},
上面的 ready 就是另一个 MessageMapping 定义的 command 名。
创建 WebviewPannel
这个最简单,就一句话:
createOrShowPage(
'name',
'ext.home',
'Sample Page',
'pages',
'home.html',
context,
messageMappings
);
定义了 title、根目录和需要加载的页面。
关于使用的完整例子请结合 README 和样例工程来看,后者是我们即将试水发布的一个 vscode 插件,用于帮助开发者或企业搭建自己的私有插件仓库。因为总有些时候会需要私有插件仓库,:)
关于 vscode 插件开发
资源根目录
虽然插件开发指南已经很详尽,但初学者总是会遗漏一些细节,在开发时才发现文档中早有提及。这里尤其值得一提的就是 Webview 的资源加载问题,因为它浪费了我一些时间。
最简单的方案,当然就是直接使用 vscode-page 插件就行了,它已经替你 cover 了一些细节,你只需照常使用就行,你可以查看这个页面。
假如不使用 vscode-page ,我也没打算藏着掖着,只需要复制下面的几行代码即可:
const rootString = path.join(context.extensionPath, base);
const localResourceRoots = vscode.Uri.file(path.join(rootString, '/')).with(
{
scheme: 'vscode-resource',
}
);
panel = vscode.window.createWebviewPanel(
viewType,
title,
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [localResourceRoots],
}
);
const pagePath = path.join(rootString, page);
panel.webview.html = fs
.readFileSync(pagePath, 'utf-8')
.replace('{{base}}', localResourceRoots.toString());
当然,你得记得在页面中在添加一行:
<base href="{{base}}" />
从代码中我相信你已经看出来一些端倪:scheme: 'vscode-resource',它并非一般 Web 开发中的 file: 。同时也要记得设置 localResourceRoots 属性。在开发 vscode-page 时发现使用 vscode-page 的插件总是没法加载自己的资源,报:ERR_ACCESS_DENIED ,后来发现是因为没有设置这个属性。
调试工具
开发 Webview 肯定需要类似 Web Dev Tools 那样的浏览器查看工具,vscode 也有。输入:Open Webview Developer Tools 就能激活。
工程相关
因为本质上 vscode 插件开发是前端工程的范畴,并且我们团队主要采用 TypeScript ,因此用到的插件和规范如下:
代码风格:
- gts,强烈推荐,一键格式化和修复 lint 问题。
- TypeScript 开发团队编码规范
风格可商量余地:
- "strict": true + "no-any": false,允许
相关插件:
- ESLint
- Bracket Pair Colorizer 2
- Prettier
- markdownlint
- indent-rainbow
- Path Intellisense
- Peacock
写在最后
作为插件发布的前奏,vscode-page 已经发布(0.0.1),工程路径:https://github.com/DTeam-Top/vscode-page ,欢迎加星或使用,😄。