Skip to content

如何用代码修改代码(含vue)

发布于  at 09:24

使用场景包括最常见的工具场景,比如自动插入一些逻辑代码,或批量修改相同的代码逻辑等等。

本文介绍了一种利用 AST 修改代码的方案,阅读本文需要了解一些编译原理基础,如果你还不了解,推荐看看这个,快速了解。

什么是 AST

引用维基百科:

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

AST 能干什么

如何操作

假如有以下代码,我们想引入 store,并注入构造参数中:

import Vue from "vue";

import App from "./App";
import router from "./router";

new Vue({
  el: "#app",
  render: h => h(App),
  router,
});

得到 AST

首先我们得有一个对应语言的 parser ,下面以 js 为例,我直接选开源的 @babel/parser 了,照着文档敲:

const parser = require("@babel/parser");
const entryContent = fs.readFileSync(filepath, "utf-8");
const AST = parser.parse(entryContent, {
  sourceType: "module",
});

在调试面板中可以看到,四个顶层节点与代码一一对应: ast

修改 AST

第一步,我们想在 router 后面追加 store 的引用。遍历 AST 可以用 @babel/traverse ,也可以自己手动写循环,出于性能考虑,官方也推荐我们自己手动循环:

// 找到相对节点
let routerImportDeclarationIndex = 0;
let newVueExpression;
AST.program.body.forEach((node, i) => {
  if (node.type === "ImportDeclaration") {
    if (node.specifiers && node.specifiers[0].local.name === "router") {
      routerImportDeclarationIndex = i;
    }
  } else if (node.type === "ExpressionStatement") {
    if (node.expression.type === "NewExpression") {
      newVueExpression = node;
    }
  }
});

我们用 @babel/type 来生成节点

const t = require("@babel/types");
// 插入 `import store from './store'`
AST.program.body.splice(
  routerImportDeclarationIndex,
  0,
  t.importDeclaration([t.importDefaultSpecifier(t.identifier("store"))], t.stringLiteral("./store"))
  // 小技巧:等同于 t.identifier(`import store from './store'`)
);
// 注入构造参数
newVueExpression.expression.arguments[0].properties.push(
  t.objectProperty(t.identifier("store"), t.identifier("store"), false, true)
);

AST 转成代码

接下来就是将这个新 AST 转换成代码了:

const babel = require("@babel/core");

let { code } = babel.transformFromAstSync(AST, entryContent, {
  generatorOpts: {
    jsescOption: {
      // escapeEverything: false,
      quotes: "single",
    },
  },
  babelrc: false,
  configFile: false,
  presets: [],
});
// 中文反转义,选项里没找到相关配置,只能先手动处理一下了
code = code.replace(/\\u([\d\w]{4})/gi, (m, g) => String.fromCharCode(parseInt(g, 16)));

fs.writeFileSync(filepath, code);

整个过程到此就结束了。

vue 文件的 AST 读写

任何语言都有相应的编译器,Vue 也是。说到这里,你是不是想到了 vue-template-compiler? 先来试一下看看效果吧:

const compiler = require("vue-template-compiler");
const sfcDescriptor = compiler.parseComponent(fs.readFileSync(filePath, "utf-8"));

sfcDescriptor 长这样:

interface SFCDescriptor {
  template: SFCBlock | undefined;
  script: SFCBlock | undefined;
  styles: SFCBlock[];
  customBlocks: SFCBlock[];
}
interface SFCBlock {
  type: string;
  content: string;
  attrs: Record<string, string>;
  start?: number;
  end?: number;
  lang?: string;
  src?: string;
  scoped?: boolean;
  module?: string | boolean;
}
vast

可以看到,sfcDescriptor(single file component descriptor) 的地位就相当于 vue ast (vast) 了,只不过结构更简单了。

因为 vue 的不同区块又是不同的语言,区块内容可以根据区块的语言交给下一个 parser 处理,例如: sfcDescriptor.script.lang === void 0 || sfcDescriptor.script.lang === 'js'时,我们把 sfcDescriptor.script.content 交给 babel 处理。

默认 js,如果这里写了lang: 'js', 那么生成代码时会多出一个 lang 属性<script lang="js"></script>

我们只需要把 sfcDescriptor (vast) 再转成代码即可。(官方没找到对应的包,我随便搜了一个,vue-sfc-descriptor-stringify,目前没遇到啥问题。)

踩过的坑

最开始 vast.template.content 用的 vue-template-compiler 处理的,但是缺点太多:

  1. 官方没有提供 transform 方法
  2. 处理太复杂,要区分各种指令、修饰符…
  3. 转换出来的 AST 细节丢失严重
    1. 注释节点丢失
    2. 无法分辨缩写,比如:v-on 还是 @

前两点还能忍,第三点对于这个场景来说,完全是不能接受的。当然,vue-template-compiler 是被设计用来生成 render function 的,也不怪它。

结论:vast.templatehtml 编译器操作,方便的一批。后来想想也是,template 默认的 lang 属性就是 html,是我自己瞎折腾,官方也没必要再写一个 template compiler

分享到: