背景
我想要实现一个简单的基于模板生成页面的工具.
第一期的时候, 采用了简单粗暴的文件复制方案.
第二期希望加上基于模板引擎(如: handlebars/ejs)的渲染.
调研了一些实现方案:
基于模板引擎自行实现
使用介绍
以 handlebars 为例, ejs 类似.
-
从文件中读取模板内容
-
使用 compile 方法渲染并注入变量
JavaScript
import Handlebars from "handlebars";
const template = Handlebars.compile("Name: {{name}}");
console.log(template({ name: "Nils" }));
- 将渲染完成的字符串写入文件
缺点
模板引擎一般提供单个文件的渲染方式, 需要自己实现文件的遍历.
基于 codesmith 封装
使用介绍
- 使用 codeSmith 运行微生成器 generator
JavaScript
const smith = new CodeSmith({debug: false,});
await smith.forge({tasks: [{generator: <generatorPath>,config: {},},],pwd: '.',});
- 在 generator 中调用 forgeTemplate
JavaScript
import { AppAPI } from '@modern-js/codesmith-api-app';
export default async (context: GeneratorContext, generator: GeneratorCore) => {
const appApi = new AppAPI(context, generator);
await appApi.forgeTemplate(...);
};
缺点
如果多个模板, 需要封装多个微生成器, 成本较高.
结论
forgeTemplate(...) 方法已经非常满足我们的需求了, 只是不想封装微生成器.
有没有一种方案, 可以通过 glob 快速匹配文件, 通过简单处理, 生成到指定的文件夹呢.
在遥远的过去, 曾经有一个王者: Gulp.
Gulp
Gulp 被 webpack 打败了, 但它并不是 webpack 这类模块打包器(webpack 也打败了同为模块打包器的 browserify).
Gulp 当年以[高性能(基于流而无需重复读写磁盘生成文件)/轻量简单易用]打败了同为 task 任务编排的 Gruntjs.
因为前端的 task 任务编排多为文件处理, 随着前端开发模式成熟, 任务模式收敛, 编译流程逐步统一, 被 webpack 这个模块打包器降维打击了.
Gulp 的方法可以简单分为两类.
如果我只是想用文件处理, 而不需要任务编排呢.
查看 gulp 源码, 我们发现, 原来 gulp 的 src/dest 方法已经被抽成了 vinyl-fs 库.
gulp 已经很久没有更新了, 而 vinyl-fs 还在活跃.
Vinyl-fs
vinyl-fs 与 gulp 的使用完全一致.
JavaScript
vfs.src(['./js/**/*.js', '!./js/vendor/*.js'])
.pipe(...)
.pipe(vfs.dest('./output'));
如果文件在 pipe 中不通过处理, src 通过 glob 读取文件后, dest 到指定目录, 就相当于简单的 copy.
glob 介绍
实现一个简单的 pipe 插件
JavaScript
import vfs from "vinyl-fs";
import File from "vinyl";
// 将管道中的文件后缀修改为 .x
vfs.src(...)
.pipe(new Transform({
objectMode: true,
transform(file: File, enc, callback) {
if (!file.isDirectory()) {
file.extname = '.x';
}
callback(null, file);
},
}))
.pipe(vfs.dest(...))
vinyl 文件系统
vinyl 是一个非常简单的元数据对象,描述一个文件。当你想到一个文件时,你会想到两个属性: path 和 contents。这些是 vinyl 对象的主要属性。
这里结合 handlebars 来举例
JavaScript
import vfs from "vinyl-fs";
import File from "vinyl";
// 实现
vfs.src(...)
.pipe(new Transform({
objectMode: true,
transform(file: File, enc, callback) {
const tpl = file.contents.toString();
const template = Handlebars.compile(tpl);
file.contents = Buffer.from(template({ ... }));
callback(null, file);
},
}))
.pipe(vfs.dest(...))
注意事项
请注意 glob 生效的目录范围
glob 永远相对于 process.cwd(), 且无法超出目录范围, 如 vfs.src('../../xxx') 将无法生效, vfs.src('/otherDir/xxx') 使用绝对路径也无法生效.
解决方案: 可以通过指定 cwd 来解决, 如我们的 模板生成页面工具中, cli 内置模板目录没有在项目中
JavaScript
const templateDir = path.resolve(dirname(fileURLToPath(import.meta.url)), `../template-${_templateObj.name}`)
vfs
.src(["page/**/*"], {
cwd: templateDir,
})
后记
使用 vinyl-fs, 我们可以通过 glob 快速匹配文件, 通过简单处理, 生成到指定的文件夹.
实现的 transformer 也可以与主流程解偶, 代码更加清晰, 可扩展性强. 就算你只是简单的 copy, 也非常推荐.
实现上, 我们也可以参考强大的 gulp 插件库.(虽然是过去式了).
gulp 插件与 vinyl transformer 完全通用, 但由于 gulp 插件库可能很久没有更新了, 所以建议不直接使用, 而是参考实现.