「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」
书接上回,在《英雄集结篇》中我们确定了这个缝合怪项目使用的技术栈,但是整体的开发流程还没有做到如丝般顺滑。
《磨刀篇》我们将对项目做一些改动,让开发更加顺手,更加智能。
代码构建
插件部分
VSCode 开发脚手架 yo
默认生成的插件开发项目(TypeScript
),在 package.json
中提供了几个命令,用于构建项目调试、发布的代码。
"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
包
yarn add -D webpack webpack-cli node-loader
新建一个 webpack.config.js
文件,内容大致如下:
"use strict";
const path = require("path");
/**@type {import('webpack').Configuration}*/
const config = {
target: "node", // vscode插件运行在Node.js环境中 📖 -> https://webpack.js.org/configuration/node/
entry: "./src/extension.ts", // 插件的入口文件 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// 打包好的文件储存在'dist'文件夹中 (请参考package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, "dist"),
filename: "extension.js",
libraryTarget: "commonjs2",
devtoolModuleFilenameTemplate: "../[resource-path]"
},
devtool: "source-map",
externals: {
vscode: "commonjs vscode" // vscode-module是热更新的临时目录,所以要排除掉。 在这里添加其他不应该被webpack打包的文件, 📖 -> https://webpack.js.org/configuration/externals/
},
resolve: {
// 支持读取TypeScript和JavaScript文件, 📖 -> https://github.com/TypeStrong/ts-loader
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
,并且修改负责构建代码的几个命令:
"scripts": {
"vscode:prepublish": "webpack --mode production", // 构建生产环境下的代码
"compile": "webpack --mode none", // 构建调试、测试时的代码
"watch": "webpack --mode none --watch", // 监听并构建
...
},
.vscode/tasks.json
也需要改动一下:
"problemMatcher": "$tsc-watch"
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
"problemMatcher": [
"$ts-webpack-watch",
"$tslint-webpack-watch"
]
至此我们已经完成了插件项目构建方面的改造,接下来看看 Rust
& Neon
代码编译方面有哪些可以优化的地方吧~
Rust & Neon 部分
在插件开发上,Rust
提供的支援是以编译为 .node
模块的形式,编译的命令如下:
. env.sh && neon build
在每次修改 Rust
代码后,都需要重新执行编译,这样插件才能得到最新的 .node
文件,可是这样真的很机车nei。
能不能让我每次修改以后都自动编译?可以!强大的 VSCode
,没有什么不可以。
就算 VSCode
本身不支持,还有各路能人异士开发各种好用的插件满足你各种奇奇怪怪的需求。
这边我们使用的插件是 “Run on Save”,可以监听指定文件修改,支持通配符。
在工作区的配置文件 .vscode/settings.json
中加入以下代码:
"emeraldwalk.runonsave": {
"commands": [
{
"match": "native/src/lib.rs", // Neon 的连接文件保存时触发
"isAsync": true,
"cmd": "tasks/rust-to-node.sh" // 执行编译脚本
},
{
"match": "native/src/public/*", // public 目录下的所有文件保存时触发
"isAsync": true,
"cmd": "tasks/rust-to-node.sh" // 执行编译脚本
},
]
}
这样在文件保存后,便会自动执行 tasks/rust-to-node.sh
编译出新的 .node
文件了,rust-to-node.sh
文件内容如下:
# 将 Neon 编译为 .node 文件
# 这时候直接执行 . env.sh && neon build 会失败
# 因为找不到 neon 这个命令,所以直接找命令的真实路径运行
. env.sh && node_modules/.bin/neon build
# 创建目录,防止移动文件因为没有文件夹失败
mkdir -p src/lib/native
# 将编译好的 .node 文件移动到插件项目下
mv native/index.node src/lib/native/index.node
是否能直接以快捷键的形式触发编译呢?答案也是可以的,请把 VSCode
牛皮打在公屏上,下面我们来进行配置。
在 .vscode/tasks.json
的 tasks
数组中新增以下任务配置:
// 执行编译任务
{
"label": "compile",
"type": "shell",
"command":"tasks/rust-to-node.sh",
"isBackground": true,
}
接着打开 设置
→ 键盘快捷方式
,切换到编辑模式
新增以下按键绑定:
{
"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
文件中加上了复制新文件的命令:
# 复制最新 .node 文件到调试目录
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
文件内容:
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
};
}
// 将解析出来的函数类型转换为ts类型
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
连接层中的读取文件函数为例
/**
* @name read_file
* @description 读取文件
* @param {string} path - 文件路径
* @returns {string}
*/
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
文件内
/**
* @name read_file
* @description 读取文件
* @param {string} path - 文件路径
* @returns {string}
*/
export function read_file(path: string): string;
将 build-type.js
工具的执行也加入 rust-to-node.sh
中
# 为 .node 文件生成 .d.ts 文件
node tasks/build-type.js
这样每次编译 .node
文件都会自动生成 d.ts
文件了,注意按格式写注释哦。
在 TypeScript
项目中使用 .node
文件时需要如下引入方式:
import * as native from './lib/native';
这样使用时就能够列出有哪些函数啦~
少传参数也会有对应的提示
变量赋值类型的检查也没有问题
因为鄙人正则造诣不够,可能注释的格式稍微有些不正确就会导致匹配失败,所以这边提供了一个代码片段,让注释书写更方便准确,嘻嘻
"neon function annotation": {
"scope": "rust",
"prefix": "na",
"body": [
"/**",
"* @name $1",
"* @description $2",
"* @param {${5:string}} $3 - $4",
"* @returns {${6:string}}",
"*/"
],
"description": "Neon 函数注释模板"
}
这个代码片段可以配置在工作区下,也能配置在全局下,看个人喜好了。
小结
《磨刀篇》到这就结束了,完成以上的配置,已经能比较舒适地开发这个缝合怪项目了,在这过程中,其实学到了很多工(tou)程(lan)化的技巧,受益匪浅。
如果有什么优化建议,可以在评论区告诉我,十分感谢。
好了朋友们,今天的分享就到这里,潮水马上要涨上来了。