Babel的奇妙冒险 @babel/template

3,712 阅读3分钟

预备知识

API

  • template 根据解析结果返回一个语句或语句数组。
  • template.smart 这与默认templateAPI相同,根据解析结果返回单个节点或节点数组。
  • template.statement template.statement("foo;")() 返回单个语句节点,如果结果不是单个语句,则抛出异常。
  • template.statements template.statements("foo;foo;")() 返回语句节点的数组。
  • template.expression template.expression("foo")() 返回表达式节点。
  • template.program template.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 等信息
  • populate.js 占位符变量提换
  • string.js
    • stringTemplate 返回一个函数,接受一个要替换Object对象或者数组
      • 调用 options normalizeReplacements 处理替换字符
      • 调用 parse.parseAndBuildMetadata 生成结果
      • populate populatePlaceholders 占位符替换处理相关
      • formatters.unwrap 处理返回值, 并返回
  • literal.js 与string.js 类似
    • literalTemplate 返回一个函数,接受一个要替换Object对象或者数组
      • buildLiteralData 解析
        • 解析占位符
        • 调用 parse.parseAndBuildMetadata 生成结果
      • populate populatePlaceholders 占位符替换处理相关
      • formatters.unwrap 处理返回值, 并返回
  • builder.js
    • createTemplateBuilder 生成API方法的入口
  • index.js 代码入口

API设计

API 提供了 templatetemplate.* 均为 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(), 即无替换字符串信息。