38- codegen 生成 插值

101 阅读2分钟

Vue官方例子

依旧是根据Vue编译器生成code string来实现

image.png

用例

import { codegen } from "../src/codegen";

test("interpolation", () => {
  const template = "{{message}}";
  const ast = baseParse(template);
  console.log("baseP", ast);
  transform(ast);
  const code = codegen(ast);
  expect(code).toMatchSnapshot();
});

实现

抽取公用常量

如果有去玩编译器不难发现,每个类型对应的一些函数引用不是一样的,为了提高代码健壮性,我们可以把对应类型的函数常量抽取出来

runtimeHelpers.ts

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-17 12:10:36
 * @LastEditTime: 2022-04-17 12:10:36
 * @LastEditors: Lin ZeFan
 * @Description: 
 * @FilePath: \mini-vue3\src\compiler-core\src\runtimeHelpers.ts
 * 
 */

// 插值
export const TO_DISPLAY_STRING = Symbol('toDisplayString')

// 映射表,解决Symbol不能当做string引用的问题
export const HelperNameMapping = {
  [TO_DISPLAY_STRING]: 'toDisplayString',
}

处理头部函数引入

上面的截图可以看到,处理插值类型的时候,会引入对应的函数

image.png

根据类型,添加头部函数

拓展context

// transform.ts

function createTransformContext(root: any, options: any) {
  const { nodeTransforms = [] } = options;
  const context = {
    root,
    nodeTransforms: nodeTransforms || [],
    helpers: new Map(),
    helper(name: string) {
      this.helpers.set(name, 1);
    },
  };
  return context;
}

根据类型添加头部引入

import { TO_DISPLAY_STRING } from "./runtimeHelpers";

function traverseNode(node, context) {
  const { nodeTransforms } = context;

  for (let index = 0; index < nodeTransforms.length; index++) {
    const transform = nodeTransforms[index];
    transform(node);
  }

  // 在这里遍历整棵树的时候,将根据不同的 node 的类型存入不同的 helper
  switch (node.type) {
    case NodeType.INTERPOLATION:
      context.helper(TO_DISPLAY_STRING);
      break;
    case NodeType.ROOT:
    case NodeType.ELEMENT:
      // 只有在 ROOT 和 ELEMENT 才会存在 children
      traverseChildren(node, context);
      break;
    default:
      break;
  }
}

引入函数

import { HelperNameMapping, TO_DISPLAY_STRING } from "./runtimeHelpers";

export function codegen(ast) {
  const context = createCodeGenContext();
  const { push } = context;

  // 处理头部函数引入
  if (ast.helpers.length) {
    genFunctionPreamble(ast, context);
  }

  // other code
}

function genFunctionPreamble(ast: any, context) {
  // 引入都来自 Vue
  const VueBinding = "Vue";
  const { push, addLine } = context;
  // 因为是Symbol,需要用映射表匹配
  const aliasHelper = (s) =>
    `${HelperNameMapping[s]} as _${HelperNameMapping[s]}`;
  // 处理头部引入
  push(`const { ${ast.helpers.map(aliasHelper).join(", ")} } = ${VueBinding}`);
  addLine();
}

function createCodeGenContext() {
  const context = {
    code: "",
    push(source: string) {
      context.code += source;
    },
    // 换行
    addLine() {
      context.code += "\n";
    },
  };
  return context;
}


根据类型处理return内容

拓展context函数,新增helper查询

function createCodeGenContext() {
  const context = {
    code: "",
    helper(key) {
      return `_${HelperNameMapping[key]}`;
    },
    push(source: string) {
      context.code += source;
    },
    addLine() {
      context.code += "\n";
    },
  };
  return context;
}

上节只处理了 text 的情况,但实际上node类型至少有3种,所以我们要拓展一下,根据type处理

function genNode(node, context) {
  // 根据 node 的类型进行不同的处理
  switch (node.type) {
    case NodeType.TEXT:
      genText(node, context);
      break;
    case NodeType.INTERPOLATION:
      genInterpolation(node, context);
      break;
    case NodeType.SIMPLE_EXPRESSION:
      genExpression(node, context);
      break;
  }
}

function genExpression(node, context) {
  // 处理 SIMPLE_EXPRESSION
  const { push } = context;
  push(`_ctx.${node.content}`)
}

function genInterpolation(node, context) {
  const { push, helper } = context;
  // 插值对应引用的函数
  push(`${helper(TO_DISPLAY_STRING)}(`);
  // 前面处理插值类型的时候,真正的值是包在content.content里的
  // { type: NodeType.INTERPOLATION, content: { type: NodeType.SIMPLE_EXPRESSION, content: 'message'} }
  genNode(node.content, context);
  push(`)`);
}

function genText(node, context) {
  const { push } = context;
  push(`'${node.content}'`);
}

处理 SIMPLE_EXPRESSION 类型返回值

为了让代码可维护性更高,我们把 SIMPLE_EXPRESSION 内部返回值抽离做一层封装,用plugin的形式,让 transfrom 帮我们处理 SIMPLE_EXPRESSION 返回的 content

修改用例

import { codegen } from "../src/codegen";
import { transformExpression } from "../src/transforms/transformExpression";

test("interpolation", () => {
  const template = "{{message}}";
  const ast = baseParse(template);
  console.log("baseP", ast);
  transform(ast, {
    // 处理插值类型content
    nodeTransforms: [transformExpression],
  });
  const code = codegen(ast);
  expect(code).toMatchSnapshot();
});

处理插值类型的plugin

transformExpression.ts

/*
 * @Author: Lin ZeFan
 * @Date: 2022-04-10 10:45:42
 * @LastEditTime: 2022-04-17 12:19:33
 * @LastEditors: Lin ZeFan
 * @Description:
 * @FilePath: \mini-vue3\src\compiler-core\src\transforms\transformExpression.ts
 *
 */
import { NodeType } from "../ast";

// 处理多层包装
export function transformExpression(node) {
  if (node.type === NodeType.INTERPOLATION) {
    node.content = processExpression(node.content);
  }
}

function processExpression(node) {
  node.content = `_ctx.${node.content}`;

  return node;
}

修改 genExpression

因为通过 transformExpression 帮我们处理了深层的content,所以我们直接取值即可,不用再手动去拼接 _ctx

// codegen.ts

function genExpression(node, context) {
  // 处理 SIMPLE_EXPRESSION
  const { push } = context;
  push(`'${node.content}'`);
}