预备知识
API
template根据解析结果返回一个语句或语句数组。template.smart这与默认templateAPI相同,根据解析结果返回单个节点或节点数组。template.statementtemplate.statement("foo;")() 返回单个语句节点,如果结果不是单个语句,则抛出异常。template.statementstemplate.statements("foo;foo;")() 返回语句节点的数组。template.expressiontemplate.expression("foo")() 返回表达式节点。template.programtemplate.program("foo;")()返回Program模板的节点。
源码解析
官方 Demo
先看一段官方 demo 感受一下,template函数返回一个function,该function通过传入替换字符串生成 ast 对象。如果没有使用占位符,可以使用 .ast。
// demo1
import template from "@babel/template";
import generate from "@babel/generator";
import * as t from "@babel/types";
const buildRequire = template(`
var %%importName%% = require(%%source%%);
`);
const ast = buildRequire({
importName: t.identifier("myModule"),
source: t.stringLiteral("my-module"),
});
console.log(generate(ast).code);
// demo2
const name = "my-module";
const mod = "myModule";
const ast = template.ast`
var ${mod} = require("${name}");
`;
代码结构
- options.js 参数配置相关
- merge 合并配置
- validate 验证配置
- normalizeReplacements 模板占位符对应参数处理
- formatters.js 定义 smart statement 等各种类型API 格式相关
- parse.js
- parseAndBuildMetadata
- 调用 @babel/parser parse 生成 AST 语法树
- 调用 @babel/types removePropertiesDeep 移除_*开头的属性,如位置,raw等
- 调用 formatters 中对应的 validate 进行验证
- 调用 @babel/types traverse 对新生成的 ast 进行遍历
- 返回 ast 等信息
- parseAndBuildMetadata
- populate.js 占位符变量提换
- string.js
- stringTemplate 返回一个函数,接受一个要替换Object对象或者数组
- 调用 options normalizeReplacements 处理替换字符
- 调用 parse.parseAndBuildMetadata 生成结果
- populate populatePlaceholders 占位符替换处理相关
- formatters.unwrap 处理返回值, 并返回
- stringTemplate 返回一个函数,接受一个要替换Object对象或者数组
- literal.js 与string.js 类似
- literalTemplate 返回一个函数,接受一个要替换Object对象或者数组
- buildLiteralData 解析
- 解析占位符
- 调用 parse.parseAndBuildMetadata 生成结果
- populate populatePlaceholders 占位符替换处理相关
- formatters.unwrap 处理返回值, 并返回
- buildLiteralData 解析
- literalTemplate 返回一个函数,接受一个要替换Object对象或者数组
- builder.js
- createTemplateBuilder 生成API方法的入口
- index.js 代码入口
API设计
API 提供了 template 和 template.* 均为 function
// 实现方法1
template = () => {}
template.smart = () => {}
template.statement = () => {}
// 实现方法2
// babel-template
export const smart = createTemplateBuilder(formatters.smart);
export const statement = createTemplateBuilder(formatters.statement);
export const statements = createTemplateBuilder(formatters.statements);
export const expression = createTemplateBuilder(formatters.expression);
export const program = createTemplateBuilder(formatters.program);
export default Object.assign(
smart.bind(undefined),
{
smart,
statement,
statements,
expression,
program,
ast: smart.ast,
},
);
两种方式实现效果是等价的,但babel的实现确实比较优雅。
为什么 js 的function为什么可以添加属性? 我觉得简单的理解为:在 js 中,函数也是对象, 是 Function 的实例;函数的另一种写法为 let foo = new Function("return 1");
具体实现
export default function createTemplateBuilder<T>(
formatter: Formatter<T>,
defaultOpts?: TemplateOpts,
): TemplateBuilder<T> {
const templateFnCache = new WeakMap();
const templateAstCache = new WeakMap();
const cachedOpts = defaultOpts || validate(null);
return Object.assign(
((tpl, ...args) => {
// ...
}: Function),
{
ast: (tpl, ...args) => {
// ...
},
},
);
}
createTemplateBuilder 又一次使用 Object.assign 一次创建多个API,以template.smart 为例,通过createTemplateBuilder生成 template.smart(tpl, ...args) he template.smart.ast(tpl, ...args) 两个API
字符串参数
// 源码
// 当 tpl 为字符串时,允许接受一个额外的 Object 配置项
// extendedTrace(fn) 是对fn的一个简单封装,用于异常追踪
// stringTemplate 返回一个function,接收要替换内容的Object对象
if (typeof tpl === "string") {
if (args.length > 1) throw new Error("Unexpected extra params.");
return extendedTrace(
stringTemplate(formatter, tpl, merge(cachedOpts, validate(args[0]))),
);
}
export default function stringTemplate<T>(
formatter: Formatter<T>,
code: string,
opts: TemplateOpts,
): mixed => T {
code = formatter.code(code);
let metadata;
return (arg?: mixed) => {
// arg 替换的内容 Object | Array
// 对 arg 进行封装,若为数组,转为对象
const replacements = normalizeReplacements(arg);
// 生成ast语法树
if (!metadata) metadata = parseAndBuildMetadata(formatter, code, opts);
// 模板内容替换,并根据API类型做特定处理
return formatter.unwrap(populatePlaceholders(metadata, replacements));
};
}
// 场景
const buildRequire = template(`
var %%importName%% = require(%%source%%);
`, {});
数组参数
使用标签函数时,参数类型为数组
// 源码
// 与string 不同的是
// 1. 用 WeakMap 缓存生成函数 builder
// 2. literalTemplate内部多了${name}格式变量占位符处理
const templateFnCache = new WeakMap();
if (Array.isArray(tpl)) {
let builder = templateFnCache.get(tpl);
if (!builder) {
builder = literalTemplate(formatter, tpl, cachedOpts);
templateFnCache.set(tpl, builder);
}
return extendedTrace(builder(args));
}
export default function literalTemplate<T>(
formatter: Formatter<T>,
tpl: Array<string>,
opts: TemplateOpts,
): (Array<mixed>) => mixed => T {
// 生成 ast 及占位变量的名称
const { metadata, names } = buildLiteralData(formatter, tpl, opts);
return (arg: Array<mixed>) => {
// 模板中的占位变量保存至 defaultReplacements
const defaultReplacements = arg.reduce((acc, replacement, i) => {
acc[names[i]] = replacement;
return acc;
}, {});
return (arg: mixed) => {
// arg 替换的内容 Object | Array
// 对 arg 进行封装,若为数组,转为对象
const replacements = normalizeReplacements(arg);
// 验证模板中的占位符变量,和参数中的变量
if (replacements) {
Object.keys(replacements).forEach(key => {
if (Object.prototype.hasOwnProperty.call(defaultReplacements, key)) {
throw new Error("Unexpected replacement overlap.");
}
});
}
// 模板内容替换,并根据API类型做特定处理
return formatter.unwrap(
populatePlaceholders(
metadata,
replacements
? Object.assign(replacements, defaultReplacements)
: defaultReplacements,
),
);
};
};
}
// 场景
const name = "my-module";
const mod = "myModule";
const ast = template`
var ${mod} = require("${name}");
`;
template 与 template.ast 的区别在于一个返回 builder(arg) 是一个接收占位符的function, 另一个返回 builder(arg)()
小结
@babel/template 主要是将代码段字符串转为ast语法树,再进行模板替换。根据代码段中是否包含es6模板字符串占位符,@babel/template 会进行不同的处理,template.*和template.*.ast的区别在于一个前者是返回一个 builder 接受替换字符串信息,后者返回 builder(), 即无替换字符串信息。