上一章 我们详细介绍了为什么需要一个自动化的密钥管理方案,以及如何利用虚拟模块机制设计并实现一个适用于 Rollup 的密钥管理插件。
我们从需求出发,将核心的密钥拆分还原算法独立为 crypto-splitter
模块,再通过 rollup-plugin-crypto-key
模块将其接入 Rollup / Vite 的构建流程。用户只需简单配置明文密钥,就能实现在业务代码中像导入普通模块一样获取密钥,同时保证构建产物中不暴露密钥明文。
方案探索
回到上一章末尾提出的问题,我们需要让插件提供对 TypeScript 的支持,以提升用户的开发体验。
那么如何为一个模块提供类型定义呢?
查阅 TypeScript 文档 可以知道,如果需要为一个 JavaScript 模块提供类型定义,应该使用 .d.ts
文件让 TypeScript 的类型系统知道这个模块里有什么内容。例如:
typescript
declare module "virtual:crypto-key" {
export const KEY1: string;
export const KEY2: string;
}
大家可以试试手动创建这样一个 crypto-key.d.ts
文件,并且让 tsconfig.json 包含它,不出意外的话 TypeScript 的报错信息就消失了,并且 IDE 也能正确提示模块的内容。
真是一个完美的解决方案,那么本章的内容就到此结束吧!
让用户自己手动创建 .d.ts
文件固然能够解决问题,但是大家都知道手动操作既低效又容易出错,也违背了我们这个插件的初衷。
所以,我们的插件需要能够根据密钥配置自动生成 .d.ts
文件,接下来就一起实现这个功能吧!
编码实现
既然要生成文件,就需要使用到文件相关的 API,大家第一时间想到的应该是 node:fs 模块,但是今天我们使用 fs-extra 插件来完成。
先简单介绍一下 fs-extra
插件:
fs-extra
adds file system methods that aren't included in the nativefs
module and adds promise support to thefs
methods. It also uses graceful-fs to preventEMFILE
errors. It should be a drop in replacement forfs
.
fs-extra
是 node:fs
模块的增强版,它在完全兼容 node:fs
的基础上,提供了更多常用且方便的文件系统操作方法,并且它的所有方法同时支持 callback
和 Promise
。
然后是我们稍后会使用到的几个方法:
-
ensureFile(file: string)
确保目标文件存在。如果目标文件不存在,则会自动创建文件及其父目录。如果目标文件已经存在,则不会做任何修改。
-
outputFile(file: string, data: string | NodeJS.ArrayBufferView)
几乎与
fs.writeFile
相同(即会覆盖文件)。不同之处在于,如果父目录不存在,则会自动创建。
准备好了前置知识,就可以开始正式编码啦!
diff
import type { Plugin } from "rollup";
import { getCode } from "./code";
+ import { writeDeclaration } from "./declaration";
export interface Options {
+ /**
+ * 生成类型声明文件,支持布尔值或者文件路径
+ */
+ dts?: boolean | string;
keys?: Record<string, string>;
}
const VIRTUAL_MODULE_ID = "virtual:crypto-key";
const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
export default function cryptoKey(options: Options = {}): Plugin {
const {
keys = {},
+ dts = false
} = options;
+ if (dts) {
+ writeDeclaration(keys, {
+ moduleId: VIRTUAL_MODULE_ID,
+ dts
+ });
+ }
return {
name: "crypto-key",
resolveId(source) {
if (source !== VIRTUAL_MODULE_ID) {
return null;
}
return RESOLVED_VIRTUAL_MODULE_ID;
},
load(id) {
if (id !== RESOLVED_VIRTUAL_MODULE_ID) {
return null;
}
return getCode(keys);
}
};
}
我们添加了 dts
配置项用于控制类型声明文件的生成,支持传入布尔值或者文件路径。如果 dts
为 true
或者字符串路径,插件就会调用 writeDeclaration
方法生成 .d.ts
文件到指定路径。
接下来我们将着手 writeDeclaration
方法的实现:
typescript
// declaration.ts
import { ensureFile, outputFile } from "fs-extra";
interface DeclarationOptions {
/**
* 模块名
*/
moduleId: string;
}
interface WriteDeclarationOptions extends DeclarationOptions {
/**
* 声明文件生成路径
*
* - true:默认文件路径
* - 字符串:自定义文件路径
*/
dts: true | string;
}
/**
* 根据 dts 参数获取声明文件生成路径
*/
function getDeclarationPath(dts: true | string): string {
if (dts === true) {
// 使用默认文件路径
return "crypto-key.d.ts";
}
// 使用传入的自定义文件路径
return dts;
}
/**
* 根据密钥表生成声明文件的内容
*/
function getDeclarationCode(keys: Record<string, string>, options: DeclarationOptions): string {
return `declare module "${options.moduleId}" {
${
Object.keys(keys)
.map((it) => {
return ` export const ${it}: string;`;
})
.join("\n")
}
}`;
}
/**
* 根据密钥表生成类型声明文件到磁盘
*/
export async function writeDeclaration(
keys: Record<string, string>,
options: WriteDeclarationOptions
): Promise<void> {
// 获取声明文件路径
const path = getDeclarationPath(options.dts);
// 确保声明文件存在
await ensureFile(path);
// 写入声明文件内容
await outputFile(path, getDeclarationCode(keys, {
moduleId: options.moduleId
}));
}
首先,根据传入的 dts
参数获取文件生成路径,如果为 true
就在根目录生成 crypto-key.d.ts
文件,否则根据传入的自定义路径生成。然后,遍历密钥映射表逐一生成 export const
语句到 declare module
块中并合成类型声明文件内容。最后,将类型声明文件实际写入到磁盘,也就完成了我们期望的目标。
插件使用
又到了激动人心的时刻,开始体验我们为插件添加的新功能吧!
diff
// vite.config.(js|ts)
import CryptoKey from "rollup-plugin-crypto-key";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
CryptoKey({
+ dts: true,
keys: {
DEMO_KEY1: "iamxiaohe",
DEMO_KEY2: "ilovexiaohe"
}
})
]
});
只需要在配置项中添加上 dts
的配置,插件就可以自动完成类型声明文件的生成工作。然后让 tsconfig.json 包含声明文件,TypeScript 的类型系统就能准确知道 virtual:crypto-key
模块中的内容,IDE 也能顺利完成代码提示功能啦!
下面以默认路径为例:
diff
// tsconfig.json
{
"include": [
// ...
+ "crypto-key.d.ts"
]
}
至此,我们成功为插件实现了对 TypeScript 的支持,让用户的开发体验得到了保障!让我们一起为自己点个赞 👍 吧!
源码
插件的完整代码可以在 virtual-crypto-key 仓库中查看。赠人玫瑰,手留余香,如果对你有帮助可以给我一个 ⭐️ 鼓励,这将是我继续前进的动力,谢谢大家 🙏!
下一步
现在我们的插件已经可以在兼容 Rollup 的环境(Rollup、Rolldown、Vite)中顺利使用,虽然 Vite 现在是大势所趋,越来越多的项目都基于 Vite 开发,但是仍然有大量的项目使用其他的构建工具(Webpack、Rspack、Esbuild 等),那么我们能不能同时支持更多的构建工具呢?
可是,如果为每种构建工具都单独去写一个插件,这样会在一定程度上增加工作量和维护成本,所以有没有一种工具可以让我们的插件使用一套代码就能够适用于多种构建工具的插件系统呢?
Unjs 团队的 Unplugin 项目就实现了这样一个统一的插件系统,能够同时支持 Vite、Rollup、Webpack、Esbuild 等构建工具。
为了让我们的插件能够被更多的用户使用,所以下一章我们将会一起将插件迁移到 Unplugin 以支持更多的构建工具!