本文字数:14404字
预计阅读时间:37 分钟
VSCode插件能做什么?
VSCode可扩展能力有哪些?
如何开发一个VSCode插件?
VSCode插件如何集成基建的脚手架和组件库?(FAW保姆级教程)
前端常见插件的实现原理分析?
我们程序员每天的产出大部分都是在IDE
中完成,大家在日常开发过程中,多多少少会有些自己的特殊定制需求去提升开发效率,比如写shell
脚本、浏览器插件等,在Visual Studio Code (VSCode)
中我们也能开发一些插件去满足日常工作需要。
比如现在业务要新开发一个项目,设计稿风格和之前系统类似。那我第一想法肯定是去拷贝已有项目的代码(或者使用组内抽象的模板),然后稍作修改满足当前需求。但如果是新同学往往需要经历咨询已有项目/模板相关人员->开通各种权限->复用部分代码并做个性化修改->借助组件库、工具库进入业务功能开发,这个过程有一定沟通和时间成本。
所以我期望能有一个更直观的方式让新同学了解组内有哪些基建并投入使用,比如能直接在VSCode
中罗列当前的模板项目,预览后选择特定模板进行项目初始化,并且将一些个性化基础配置通过表单形式进行填写并渲染,避免遗漏。而且在开发过程能在VSCode
中直观的展示当前有哪些组件和工具函数可以使用,然后通过点点点操作实现组件的添加和快速使用。
本文也将带着下面几个问题去讲解开发VSCode插件的过程:
VSCode
插件能做什么?
如何开发一款VSCode
插件?
VSCode
中如何嵌入webview
?
VSCode
中如何配置国际化?
VSCode
插件中如何新建项目、新建页面、组件...?
目前常用 VSCode
插件可分为下面几大类:
语言类插件
语法高亮(Vetur)
代码自动补全(TabNine)
代码片段(JS JSX Snippets)
工具类插件
可视化搭建页面(面向开发者的低代码)(AppWorks)
时间管理(WakaTime)
Git管理(Git Graph)
TODO(TODO Tree)
娱乐类插件
听音乐(VSC Netease Music)
炒股(韭菜盒子)
玩游戏(小霸王)
那VSCode
提供哪些能力去实现上一章所提到的效果?
VSCode
本身是使用Electron
开发的,那他也支持对应的能力。
支持读取本地文件
支持发送接受跨域请求
支持创建本地服务器
持久化存储本地数据
可扩展能力
使用颜色或文件图标主题更改
VSCode
外观
在
UI
中添加自定义组件和视图
创建
webview
以显示使用
HTML/CSS/JS
构建的自定义网页
支持一种新的编程语言
支持调试特定运行时
VSCode
提供了各种 API
,允许您将自己的组件添加到工作台。
活动栏(Tree View Container):Azure
应用服务扩展添加了一个视图容器
侧边栏(Tree View):内置的 NPM
扩展在 Explorer
视图中添加了一个树视图
编辑器组(Webview):内置的 Markdown
扩展在编辑器组中的其他编辑器旁边添加了一个 Webview
状态栏(Status Bar):
VSCodeVim
扩展在状态栏中添加了一个状态栏项
基于正则编辑页面中的内容
例如:删掉当前页面所有注释或
log
自定义跳转、自动补全、悬浮提示
例如:输入
rfc
自动补全代码
对特定后缀名文件的解析和编辑
例如:借助插件
vetur
解析
.vue
文件
增强
VSCode
内置的
MD
预览和
Git
工具
例如:美化预览
.md
文件
与此同时,也存在一些限制,比如插件不能访 问 VSCode UI 的 DOM 节点。(如果强行改动,VSCode 会提示自身损坏)
首先对VSCode插件能力有个大概认识,然后从HelloWorld初始化项目去入门,再去集成Webview。
由于VSCode本身是使用Electron开发的,且Electron是基于Chromium,渲染进程是使用Web页面作为 UI 显示。那在VSCode中也能集成webview。
npm i -g yo generator-code
借助官方提供的脚手架生成项目
yo code
? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? HelloWorld
## Press to choose default for all options below ###
? What's the identifier of your extension? helloworld
? What's the description of your extension? LEAVE BLANK
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm
? Do you want to open the new folder with Visual Studio Code? Open with `code`
页面关键结构如下:
.
├── package.json # 插件配置
├── src
│ ├── extension.ts # 入口文件
├── tsconfig.json
package.json
关键内容如下:
{
// 扩展的激活事件
"activationEvents": [
"onCommand:extension.sayHello"
],
// 入口文件
"main": "./src/extension",
// 贡献点,vscode插件大部分功能配置都在这里
"contributes": {
"commands": [
{
"command": "extension.sayHello",
"title": "Hello World"
}
]
}
}
src/extension.ts
关键内容如下:
const vscode = require('vscode');
// 插件被激活时触发,所有代码总入口
exports.activate = function(context) {
// 注册命令 与`package.json`中`contributes.commands`
context.subscriptions.push(vscode.commands.registerCommand('extension.sayHello', function () {
vscode.window.showInformationMessage('Hello World!');
}));
};
// 插件被释放时触发
exports.deactivate = function() {};
然后在编辑器中按F5
即可打开新的窗口在命令面板中(⌘⇧P
)运行 Hello World
命令进行调试插件:
下方示例为在VSCode
集成通过ice
生成的webview
1.创建web
目录初始化项目
mkdir web
cd web
yarn create ice
# or
yarn create @umijs/umi-app
2.配置package.json
注册激活事件
{
"activationEvents": ["onCommand:project-creator.create-project.start"],
"contributes": {
"commands": {
"command": "project-creator.create-project.start",
"title": "创建项目webview"
}
}
}
activationEvents
字段值为数组,通过onCommand
注册激活事件project-cre
ator.create-project.start
,而project-creator.create-project.start
将在contributes.commands
中定义
contributes
字段可以配置扩展VSCode
各种能力,比如commands命令
、configuration配置
...
commands
中的command
将在src/extension.ts
中进行注册事件回调
3.配置src/extension.ts创建webview的具体逻辑
注册命令project-creator.create-project.start
创建
webview
面板
projectCreatorWebviewPanel
如果有,则直接展示
如果没有,则新建
配置基本配置
标题
启用
JavaScript
脚本
隐藏时保留上下文
图标
设置 webview 面板内容
提供
webview
和
vscode
交互
VSCode
中的Webview
本质就是一个iframe
,所以是可以在其中执行脚本,但是VSCode
默认禁用JavaScript
,所以需要配置enableScripts=true
开启此功能。
通过getHtmlForWebview
获取 webview
的内容。
由于使用icejs
进行构建项目,yarn build
后的目录结构为index.html
、css/index.css
、js/index.js
,如果开启MPA
,则还有vendor.css/js
。
其中通过getNonce
生成一个随机数,设置到script
的 nonce
属性,作用是在加密通信中使用一次随机数避免重复攻击,保证不同的消息与该秘钥加密的秘钥流不同。此代码拷贝自VSCode提供的官网示例。
一个很常见的场景,我们在webview
中通过调接口获取数据,然后渲染页面。但是在vscode webview
中是不允许发送ajax
请求,所有请求都是跨域(因为webview
本身没有host
),所以需要在VScode
中进行真实的接口请求。
此过程则变为在Webview
端使用vscode.postMessage
,然后在VScode
中使用webview.onDidReceiveMessage
接收到消息后做相应处理。
将交互过程封装成connectService和callService
进行统一注册和调用。
可以在
VSCode
端创建
Webview
时绑定
connectService
,在其中监听
webview
接收到的消息,然后调用
VSCode
中
api
能力,将执行结果返回给
Webview
端
在
Webview
中调用
callService
,然后将事件和参数传递给
connectService
处理,也将处理结果传给回调函数。
在options中提供当前页面需要使用的所有服务services的定义,然后再接收到调用事件时,通过const api = services && services[service] && services[service][method]获取到具体的方法,并将参数进行传递,一定程度去抹平API的差异,减少重复代码量。
VSCode
的国际化主要有三部分组成:
配置项国际化
VScode
代码国际化
Webview
代码国际化
我们可以在package.json中配置VSCode的配置项,这些配置项的国际化是约定在package.nls.json和package.nls.zh-cn.json这些文件中。
比如可以在package.nls.json中配置插件英文名称:
{
"projectCreator.create-project.commands.start.title": "Select Scaffold to Create Application"
}
在package.nls.zh-cn.json中配置插件中文名称:
{
"projectCreator.create-project.commands.start.title": "选择模板创建应用"
}
然后在package.json中使用:
"contributes": {
"commands": [
{
"command": "project-creator.create-project.start",
"title": "%projectCreator.create-project.commands.start.title%"
}
]
}
国际化的解决思路都一样:
在代码中进行注册,并且可以通过vscode.env.language获取VSCode当前语言环境。
import * as vscode from 'vscode';
import I18nService from './i18n';
import * as zhCNTextMap from './locales/zh-CN.json'; // { "webViewTitle": "Create Project" }
import * as enUSTextMap from './locales/en-US.json'; // { "webViewTitle": "创建项目" }
// 注册语言表
const i18n = new I18nService();
i18n.registry('zh-cn', zhCNTextMap);
i18n.registry('en', enUSTextMap);
// 设置使用的语言
i18n.setLocal(vscode.env.language);
export default i18n;
然后在代码中进行使用:
projectCreatorWebviewPanel = vscode.window.createWebviewPanel(
'project-creator', // webview 标识,只供内部使用
i18n.format('webViewTitle'), // 标题
vscode.ViewColumn.One, // 新开一个编辑器视图
{
enableScripts: true, // 启用 JavaScript 脚本
retainContextWhenHidden: true, // 隐藏时保留上下文
},
);
在Webview中我们采用icejs搭建项目,那就可以使用react-intl来配置国际化。
前端同学在开发过程中一般会经历但不限如下过程:
开发准备阶段
:
需求评审,查阅外部或组内知识库、开发规范
编码&联调阶段:
按需求场景根据外部或组内脚手架、组件库、工具库...进行编码调试
调式优化阶段
:
数据埋点、性能优化、自动化测试...
构建部署阶段:大部分企业都有自研的devops
解决方案
上线后数据采集&分析阶段:
进行性能监控、报警、数据分析...
技术沉淀:
对上述过程进行复盘、总结、抽象,进入下一轮需求开发
当我们进入一个新团队时,往往期望能对团队内部的前端研发全链路有一个基本认识,进而可以快速进入开发或投身到感兴趣的技术建设。
当我们开发一个新项目时,往往期望能参考老项目看是否能复用部分,进而减少不必要的重复性工作。
基于上面章节对VSCode
插件所提供的能力介绍,我们完全可以将前端研发全链路
的基建集成到我们日常编码IDE
中,并且提供可视化的操作界面,让我们能安心在IDE
中进行开发调试,从一定程度减少我们开发过程到处检索而分心低效的问题。
AppWorks是一款基于VSCode
插件的前端研发工具集,通过 GUI
操作、物料组装、代码辅助等功能让前端开发更加简单。
不过由于下面几个原因,我们决定基于AppWorks
做个性化改造以便满足部门内部使用。
他对icejs
、Rax
类型项目支持友好,但由于我们部门中后台项目技术选型为umijs
,在使用AppWorks
时面板内容显得有点冗余。
并且我们项目使用
微前端
架构,在slave
项目中不少配置是期望在初始化模板时就自动配置好。
物料
方面我们有自己一套组件库并且放在私有npm
,自定义物料的方式也期望能保留我们当前发包结构
物料:
分为组件(component)、区块(block)和项目(scaffold)三种类型
基于上述考虑,我们做了二次开发并产出了FAW,下面将从使用效果去揭秘他的核心逻辑实现。
下面示例为新建一个微前端子应用
的场景
1. 通过点击侧边栏激活创建项目流程
2. 选择具体模板后点击下一步
3. 输入项目名称、模板版本
4. 如果模板提供ask-for-vscode.js
文件,则根据配置生成表单
主要是配置publicPath
、basePath
、mountElementId
、id
...
5. 表单填写完毕后点击完成
6. 生成项目后在当前窗口打开新项目,即可进入开发
根据开发插件章节,可以将模板选择、填写配置
这些交互功能放在展示层webview
中实现,而将获取模板、拷贝模板并渲染
这些功能交由业务层VSCode
实现。
于此同时可以在入口AppWorks
中“捆绑”组内高频使用插件,实现安装一个插件时可以安装一系列插件。
并且将一些公共配置项、国际化、创建项目和创建物料的核心逻辑...
放入packages
中使用lerna
做管理并在插件中使用。
物料基本信息放在配置平台
中做统一配置;项目模板存放在Gitlab
做版本管理;组件库放在私有npm
做管理。
逻辑类似前端工程化-打造企业通用脚手架-focus create projectName核心流程
1. 点击“创建应用”,唤起webview
页面
2. 从配置中心
拉取所有“项目模板”列表
3. 选择“具体模板”后,拉取所有版本(版本默认约定为在Gitlab
端打的tag
4. 选择“具体版本”后,判断当前模板是否提供ask-for-vscode.js
文件
4.1 如果没提供则对本模板本版本做本地缓存,方便下次使用。则进入第6步
4.2 如果提供则根据配置项渲染为表单供开发者填写
配置项一般为publicPath
、basePath
、mountElementId
、id
...
5. 通过ncp
把代码拷贝到本地临时目录,然后根据 4.2 填写的内容渲染变量在ejs
模板,最后通过metalsmith
遍历所有文件做插入修改
6. 打开新窗口并启动当前项目
7. 完成,开始进入代码编写
核心代码实现
其中第2步定义模板物料的结构,然后在配置平台维护一个json
存放所有模板。
在第3步中选择具体模板后拉取所有版本,主要借助Gitlab
提供的开放能力 https://docs.gitlab.com/ee/api/api_resources.html。
在第4步中选择具体版本后,拉取对应代码,并判断是否存在ask-for-vscode.js
文件并解析其内容:
因为require需要以
require(/Users/${filename}.js
)的形式导入绝对路径+变量
,然而我们模板的名字以及配置都名为变量,故获取不到。
// 此方式可行 ✅
const code = require('/Users/careteen/Desktop/admin-umi-template/ask-for-vscode.js')
// 此方式不可行 ❌
const templateName = 'admin-umi-template'
const configName = 'ask-for-vscode'
const args = require(`/Users/careteen/Desktop/${templateName}/${configName}.js`)
所以此处采用readFileSync
和new Function(code)()
的方式获取js文件内容。其中内容如下:
// 需要根据用户填写修改的字段
const requiredPrompts = [
{
type: 'input',
name: 'repoNameEn',
message: 'please input repo English Name ? (e.g. `smart-phone`.focus.cn)',
},
{
type: 'input',
name: 'repoNameEnCamel',
message: 'please input repo English Camel Name ?(e.g. smart-case.focus.cn/`smartPhone`)',
},
{
type: 'input',
name: 'repoNameZh',
message: 'please input repo Chinese Name ?(e.g. `智能话机`)',
},
];
return {
requiredPrompts,
};
用这部分内容渲染成表单,然后再根据用户输入内容渲染ejs
模板,比如配置文件config/config.ts
// 模板
export default defineConfig({
title: '<%=repoNameZh%>',
manifest: {
basePath: '/<%=repoNameEnCamel%>/',
},
base: '/<%=repoNameEnCamel%>/',
outputPath,
publicPath: '/<%=repoNameEnCamel%>/',
mountElementId: '<%=repoNameEnCamel%>',
qiankun: {
slave: {},
},
});
// 渲染后
export default defineConfig({
title: '智能话机',
manifest: {
basePath: '/smartPhone/',
},
base: '/smartPhone/',
outputPath,
publicPath: '/smartPhone/',
mountElementId: 'smartPhone',
qiankun: {
slave: {},
},
});
在第5步中进行拷贝和修改插入:
最后项目生成成功后在窗口中打开新生成的项目:
1. 通过命令FocusWorks: Generate Page by Blocks
唤起新建页面
的页面
2. 在面板右侧添加组件,可以在左侧进行拖拽布局
3.点击生成页面
并输入页面名称和路由配置
4. 点击完成后生成页面
0. 下面示例为在umi
类型项目中新增一个列表页
1. 命令行唤起webview
页面
2. 判断当前工作区的项目类型,然后从配置中心
拉取所有“组件”列表
2.1 需要维护多套组件库并提供demo
示例
2.2 先从依赖项中判断是否有umi
,没有再判断是否有React
,没有再判断是否有Vue
3. 添加组件后借助react-sortable-hoc
支持拖拽布局
3.1 只支持纵向排列,因为组件粒度都较大,横向不好布局
4. 填写页面名称(PageName)和路由配置(pageName)
5. 从npm
中下载具体组件tgz
到本地临时目录并解压
5.1 然后将src/demo
内容拷贝到第4步
中所填写的页面地址的components
目录下
5.2 并在PageName/index.tsx
中插入引用组件的代码
6. 判断是否需要处理路由配置
6.1 如果没获取到config/route.ts
文件则不需要配置路由,进入第7步
6.2 如果需要配置,则会读取config/route.ts
文件内容,并插入一条路由配置
7. 删除第5步
中下载到临时目录的文件
8. 完成
在第2步中需要判断当前项目类型,好准确的获取对应的组件库列表。
页面物料的结构如下,粒度一般较大,比如中后台最常见的面包屑+筛选项+操作栏+列表+分页
页面
第3步使用react-sortable-hoc
来支持组件的拖拽布局。
第5步当点击完成时,生成页面
并配置路由:
生成页面的流程如下:
5.1 将组件下载到本地src/pages/PageName/components/
目录下
5.2 准备src/pages/PageName/index.tsx
页面入口模板,并写入组件引用代码
5.3 生成src/pages/PageName/index.tsx
文件
5.1 将组件下载到本地src/pages/PageName/components/目录下
5.1.0 准备组件库
5.1.1 先下载到本地临时目录.temp-block
5.1.2 将组件拷贝到当前项目的pages/PageName/components/
目录下
5.1.3 删除临时目录的文件
5.1.4 自动安装组件的依赖
5.1.0 准备组件@focus/pro-concise-table,
组件demo
存放在@focus/pro-concise-table/src/demos/index.tsx
其中获取组件压缩包地址主要使用package-json
实现,下载tgz
并解压内容则借助request-promise、zlib、tar
5.1.2 将组件拷贝到当前项目的pages/PageName/components/
目录下
5.1.4 自动安装组件的依赖
第6步如果需要配置路由,在创建路由时需要判断当前项目类型umi/react/vue
,下面的逻辑主要是处理umi
类型项目:
创建umi
类型项目路由核心逻辑主要是根据第第4步中填写的页面名称、路由、父级页面
做处理。
6.1 读取项目config/routes.ts
文件内容并使用@babel/parser.parse
将代码解析为AST
6.2 借助@babel/traverse
遍历
6.3 将新增的路由信息拼接到
第6.2步
的数组
末尾
6.4 对路由处理后重新覆盖config/routes.ts
文件
6.1 读取项目config/routes.ts
文件内容并使用@babel/parser.parse
将代码解析为AST
6.2 借助
遍历@babel/traverse
第6.1步
的
判断获取所有路由配置的AST
形式数组
6.4 对路由处理后重新覆盖config/routes.ts
文件,此处为对umi
类型项目处理,使用@babel/*
做代码替换演示。
1. 通过命令FocusWorks: Import Component
或在编辑器右上方标题菜单栏中点击“+”唤起新建组件
的页面
2. 将鼠标放置在期望新增组件的地方,点击组件的“添加”
3. 即可插入组件信息,并自动拷贝组件demo、安装依赖
实现的思路大部分同FAW新建物料,下面将重点介绍不一样的地方。
1. 如果当前有激活的文件,则在右侧唤起webview
2. 可以在contributes.menus.editor/title
中扩展编辑器标题菜单栏
3. 在鼠标光标处插入组件代码
1 .如果当前有激活的文件,则在右侧唤起webview
2.可以在contributes.menus.editor/title
中扩展编辑器标题菜单栏:
只在jsx
文件中提供新建组件
的功能:
3 .在鼠标光标处插入组件信息:
此章节介绍了我们部门以现有“智慧案场业务的微前端架构场景”(可插拔式的数据中台可能会接入若干子产品)为出发点,在此开发过程中前端组所高频使用和持续迭代的脚手架和组件库,为了让各个子产品线能快速和低成本接入,我们尝试在 IDE 中将他们进行了集成和实现。
目前这一套 IDE 插件支撑了我们15+个“宝宝”子产品的项目初始化工作,为各个业务线同学接入前期避免了大量繁琐的配置操作;也为大家开发过程提供可扩展能力:在使用公共页面和组件时可以拿来即用,也可以快速封装各自高频物料供所有人选择使用;
FAW 的定位主要是前期老同学贡献模板和组件,对新同学特别友好,过程中新老同学一起共建,服务于整个团队。
于此同时我们捆绑了组内都在使用的其他提效插件供大家一键安装,避免新同学和组内在开发过程表现不一致的问题。
下面简单介绍几个FAW
中捆绑的插件的核心实现原理。
作用:
实现JavaScript/React/TypeScript
代码自动补全
仓库:
VS Code ES7+ React/Redux/React-Native/JS snippets
核心实现:
// package.json
{
"contributes": {
"snippets": [
{
"language": "javascript",
"path": "./snippets/snippets.code-snippets"
}
]
}
}
// ./snippets/snippets.code-snippets.json
{
"typescriptReactFunctionalComponent": {
"key": "typescriptReactFunctionalComponent",
"prefix": "tsrfc",
"body": [
"import React from 'react'",
"",
"type Props = {}",
"",
"export default function ${1:${TM_FILENAME_BASE}}({}: Props) {",
" return (",
" ${1:first}",
" )",
"}"
],
"description": "Creates a React Functional Component with ES7 module system and TypeScript interface",
"scope": "typescript,typescriptreact,javascript,javascriptreact"
},
}
作用:实时计算.md
文件中字数
仓库:
https://github.com/microsoft/vscode-wordcount
核心实现:
import { window } from 'vscode'
const statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
let doc = window.activeTextEditor.document;
// 只处理`.md`文件
if (doc.languageId === 'markdown') {
let docContent = doc.getText();
// 将边界的空格删掉
docContent = docContent.replace(/(< ([^>]+)<)/g, '').replace(/\s+/g, ' ');
docContent = docContent.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
let wordCount = 0;
if (docContent !== '') {
// 获取单词数
wordCount = docContent.split(' ').length;
}
// 将当前文件的字数在左下角状态栏展示,其中`$(pencil)`是vscode提供的图标
statusBarItem.text = `$(pencil) ${wordCount} Words`;
// 在状态栏展示单词数
statusBarItem.show();
}
作用:实现特定文本高亮
地址:
https://github.com/Gruntfuggly/todo-tree
核心代码:
// 下方为伪代码
const documentHighlights = {}
const tag = 'todo'
// 1、使用正则全局匹配todo、fixme、hack...的坐标位置
// const regex = (//|#|