书接上回,在《英雄集结篇》中我们确定了这个缝合怪项目使用的技术栈,但是整体的开发流程还没有做到如丝般顺滑。
《磨刀篇》我们将对项目做一些改动,让开发更加顺手,更加智能。
代码构建
插件部分
VSCode 开发脚手架 yo
默认生成的插件开发项目(TypeScript
),在 package.json
中提供了几个命令,用于构建项目调试、发布的代码。
1 2 3 4 5 6
| "scripts": { "vscode:prepublish": "npm run compile", // vsce publish 发布前执行构建 "compile": "tsc -p ./", // 构建代码 "watch": "tsc -watch -p ./", // 监听代码修改进行构建 ... },
|
目前的构建代码比较简单,直接使用了 tsc
命令将 .ts
文件都转换成 .js
输出到 out
目录下。当插件代码越来越复杂,引入的包更多后,这么做就不是很合理了。
幸运的是,市面上有很多能够解决这个问题的打包构建工具,比如 webpack
、esbuild
、rollup
等等。下面我们将使用 webpack
来改造我们的项目。
首先我们在项目引入 webpack
相关的 npm
包
1
| yarn add -D webpack webpack-cli node-loader
|
新建一个 webpack.config.js
文件,内容大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| "use strict";
const path = require("path");
const config = { target: "node", entry: "./src/extension.ts", output: { path: path.resolve(__dirname, "dist"), filename: "extension.js", libraryTarget: "commonjs2", devtoolModuleFilenameTemplate: "../[resource-path]" }, devtool: "source-map", externals: { vscode: "commonjs vscode" }, resolve: { extensions: [".ts", ".js", ".node"] }, module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, use: [ { loader: "ts-loader" } ] }, { test: /\.node$/, exclude: /node_modules/, loader: "node-loader", options:{ name: "native.node", } }, ] } }; module.exports = config;
|
将入口指定为 ./src/extension.ts
,输出目录为当前目录下的 dist
目录。
另外,我们还引入了一个 .node
文件的加载器,加载器会把引用的 .node
文件复制到 ./dist
目录下命名为 native.node
如果没有这个加载器,webpack
在解析代码时遇到 .node
文件会把它当成一个 .js
文件,读取其内容,遍历引用。很可惜,.node
文件是个二进制文件,这时候 webpack
就会报错,与我们想要的效果不符。
接着我将 package.json
中的 main
字段指定为 ./dist/extension.js
,并且修改负责构建代码的几个命令:
1 2 3 4 5 6
| "scripts": { "vscode:prepublish": "webpack --mode production", // 构建生产环境下的代码 "compile": "webpack --mode none", // 构建调试、测试时的代码 "watch": "webpack --mode none --watch", // 监听并构建 ... },
|
.vscode/tasks.json
也需要改动一下:
1 2 3 4 5 6
| "problemMatcher": "$tsc-watch" ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ "problemMatcher": [ "$ts-webpack-watch", "$tslint-webpack-watch" ]
|
至此我们已经完成了插件项目构建方面的改造,接下来看看 Rust
& Neon
代码编译方面有哪些可以优化的地方吧~
Rust & Neon 部分
在插件开发上,Rust
提供的支援是以编译为 .node
模块的形式,编译的命令如下:
在每次修改 Rust
代码后,都需要重新执行编译,这样插件才能得到最新的 .node
文件,可是这样真的很机车nei。
能不能让我每次修改以后都自动编译?可以!强大的 VSCode
,没有什么不可以。
就算 VSCode
本身不支持,还有各路能人异士开发各种好用的插件满足你各种奇奇怪怪的需求。
这边我们使用的插件是 “Run on Save”,可以监听指定文件修改,支持通配符。
在工作区的配置文件 .vscode/settings.json
中加入以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| "emeraldwalk.runonsave": { "commands": [ { "match": "native/src/lib.rs", "isAsync": true, "cmd": "tasks/rust-to-node.sh" }, { "match": "native/src/public/*", "isAsync": true, "cmd": "tasks/rust-to-node.sh" }, ] }
|
这样在文件保存后,便会自动执行 tasks/rust-to-node.sh
编译出新的 .node
文件了,rust-to-node.sh
文件内容如下:
1 2 3 4 5 6 7 8 9 10
|
. env.sh && node_modules/.bin/neon build
mkdir -p src/lib/native
mv native/index.node src/lib/native/index.node
|
是否能直接以快捷键的形式触发编译呢?答案也是可以的,请把 VSCode
牛皮打在公屏上,下面我们来进行配置。
在 .vscode/tasks.json
的 tasks
数组中新增以下任务配置:
1 2 3 4 5 6 7
| { "label": "compile", "type": "shell", "command":"tasks/rust-to-node.sh", "isBackground": true, }
|
接着打开 设置
→ 键盘快捷方式
,切换到编辑模式

新增以下按键绑定:
1 2 3 4 5
| { "key": "cmd+r", "command": "workbench.action.tasks.runTask", "args": "compile" }
|
这样在按下 Command
+ R
(windows
下为 Ctrl
+ R
) 快捷键的时候就会执行 label
为 compile
的任务执行编译了。
疑难杂症
编译后的 .node
文件貌似不会触发 webpack
的文件修改监听,再重新运行调试时会导致异常报错,经测试,只要复制新的 .node
文件到 dist
目录下便可解决,所以在 rust-to-node.sh
文件中加上了复制新文件的命令:
1 2 3 4 5
| rm -rf dist/native.node
cp -f src/lib/native/index.node dist/native.node
|
智能
前面说到想要让这个项目变得更智能,其实是想让 .node
文件在使用时有更好的体验,能够让开发者知道它里面都包含了哪些可用的方法。
TypeScript
中的 d.ts
文件可以描述一个对象的类型,并且让编辑器提供智能提示,所以我们可以为编译好的 .node
文件写一个 d.ts
文件,这样我们在使用的时候会方便不少。
但是我们每次改动都手动修改 d.ts
文件,非常繁琐,谈不上智能,智者花喵曾经说过:懒是第一生产力。
这种繁琐的事情就必须交给代码来完成,所以我写了个小工具,针对 Neon
的连接层暴露的函数来生成对应的 d.ts
文件。
tasks/build-type.js
文件内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| const fs = require('fs'); const { nodeOutput, neonLibPath } = require('./config.json');
function singleParamsParse(paramsItem) { paramsItem.match(/@param\s\{([a-zA-z_]*)\}\s([a-zA-z0-9_]*)\s-\s([\u4e00-\u9fa5a-zA-Z0-9_ -]*)$/gm); const type = RegExp.$1; const name = RegExp.$2; const description = RegExp.$3; return { name, type, description }; }
function singleFunctionTypeParse(origin) { origin.match(/@name\s([a-zA-z0-9_]*)[\S\s]*@returns\s\{([a-zA-Z_ -<>]*)\}/gm); const name = RegExp.$1; const returns = RegExp.$2;
const paramsMatch = origin.match(/@param\s\{([a-zA-z0-9_]*)\}\s([a-zA-z0-9_]*)\s-\s([\u4e00-\u9fa5a-zA-Z0-9_ -]*)$/gm) ?? []; const params = paramsMatch.map(singleParamsParse); return { origin, name, params, returns }; }
function convertType(functionInfo) { const { origin, name, returns, params } = functionInfo; const paramsString = params.map(item => `${item.name}: ${item.type}`).join(', '); return `${origin} export function ${name}(${paramsString}): ${returns};`; }
async function main() { const neonLibContent = fs.readFileSync(neonLibPath, 'utf8').toString(); const functionMatch = neonLibContent.match(/\/\*{1,2}[*\n\r\sa-zA-Z0-9@\u4e00-\u9fa5-_{}<>]*\*\//gm);
const resultContent = functionMatch.map(singleFunctionTypeParse).map(convertType).join('\n\n');
fs.writeFileSync(`${nodeOutput}/index.d.ts`, resultContent, 'utf8'); }
main();
|
以 Neon
连接层中的读取文件函数为例
1 2 3 4 5 6 7 8 9 10
|
pub fn read_file(mut cx: FunctionContext) -> JsResult<JsString> { let path = cx.argument::<JsString>(0)?.value(); Ok(cx.string(utils::public_read_file(path.as_str()))) }
|
上面的 build-type.js
工具将匹配注释内容,获取函数名称
、入参
、返回值
,生成以下内容写入 src/lib/native/index.d.ts
文件内
1 2 3 4 5 6 7
|
export function read_file(path: string): string;
|
将 build-type.js
工具的执行也加入 rust-to-node.sh
中
1 2
| node tasks/build-type.js
|
这样每次编译 .node
文件都会自动生成 d.ts
文件了,注意按格式写注释哦。
在 TypeScript
项目中使用 .node
文件时需要如下引入方式:
1
| import * as native from './lib/native';
|
这样使用时就能够列出有哪些函数啦~

少传参数也会有对应的提示

变量赋值类型的检查也没有问题

因为鄙人正则造诣不够,可能注释的格式稍微有些不正确就会导致匹配失败,所以这边提供了一个代码片段,让注释书写更方便准确,嘻嘻
1 2 3 4 5 6 7 8 9 10 11 12 13
| "neon function annotation": { "scope": "rust", "prefix": "na", "body": [ "/**", "* @name $1", "* @description $2", "* @param {${5:string}} $3 - $4", "* @returns {${6:string}}", "*/" ], "description": "Neon 函数注释模板" }
|
这个代码片段可以配置在工作区下,也能配置在全局下,看个人喜好了。
小结
《磨刀篇》到这就结束了,完成以上的配置,已经能比较舒适地开发这个缝合怪项目了,在这过程中,其实学到了很多工(tou)程(lan)化的技巧,受益匪浅。
如果有什么优化建议,可以在评论区告诉我,十分感谢。
好了朋友们,今天的分享就到这里,潮水马上要涨上来了。