为什么

自动化是研发过程中比不可少的环节,提高研发效率,提升工程自动化程度是研发过程中应该不断思考的问题。那么在做自动化工具的时候可能经常会涉及到一个需求:自动化修改代码。

比如自动化生成 service,自动化插入代码,自动化修改配置,等等。通常一些简单的操作用正则表达式匹配就可以了,但是设计到一些复杂的逻辑那么就需要去解析代码抽象语法树(AST),去准确的读取和修改你的代码内容。

需要用到的包

通过 babel 我们可以很方便的实现这一切,你需要用到的就是 babel 中的三个包:

  • @babel/parser 用于把代码转换为 AST
  • @babel/traverse 用于遍历 AST,在这个过程中你就可以修改你的代码了
  • @babel/generator 把 AST 生成为最终代码

顺序使用这三个包你就可以轻松的自动化的修改你现有的代码。

除了这三个包以外还有一些辅助的工具包:

  • @babel/types 对 AST 的节点做一些类型判断或者生成一些新的类型节点等

怎么做

parser

这个很简单,很强大,你可以通过插件使得它能够读取更多格式的代码,比如内置的 jsx 的插件就可以使得它可以解析 jsx。

  1. const ast = parser.parse(content, {
  2. sourceType: 'module',
  3. plugins: [
  4. 'jsx',
  5. ],
  6. });

traverse


traverse 是最难掌握的,你需要能够在复杂的语法树中找到你想要修改的那个片段并没有那么简单。比如,自动的往某个 react 组件中插入一个片段。除了语法树成千上万的节点带来的复杂度以外,本身逻辑的复杂度也很高,比如你需要考虑这个 react 组件是纯函数的还是 class,你需要准确找到 render 方法和新的代码要插入的位置等等。

但是当你理解了 traverse 本质的一个逻辑后事情也没有那么复杂。

它接收两个参数,第一个就是通过 parser 得到的语法树,第二个是遍历它的配置。

  1. traverse(ast, {
  2. ClassMethod: {
  3. enter({ node }) {
  4. if (t.isIdentifier(node.key, {
  5. name: 'render',
  6. })) {
  7. console.log(node);
  8. }
  9. },
  10. exit() {},
  11. },
  12. });

其中第二个参数是一个对象,你可以指定拦截某种类型的遍历,比如上面的代码我们就拦截了所有 ClassMethod 的遍历(代码我们只在 enter 中有操作,实际上因为遍历是深度进行的,而且进入之后会向上层返回,所以你还会有一次 exit 调用的机会可以再拦截一次),你也可以直接用 enter 来拦截所有节点类型的遍历。上面的代码代表的就是找到 class 中定义的 render 函数。

上面的代码也可以直接忽略 enter 简写为:

  1. traverse(ast, {
  2. ClassMethod({ node }) {
  3. if (t.isIdentifier(node.key, {
  4. name: 'render',
  5. })) {
  6. console.log(node);
  7. }
  8. },
  9. });

怎么知道代码的语法树是什么样子的呢?你可以通过 https://astexplorer.net/ 这个工具在线查看代码的 AST 结构,这样你就可以很方便的找到你要修改的地方了。这个过程中要用到 @babel/types 提供的一些列 isXXX 的方法来做判断。这个过程会经常要查看 @babel/types 的文档: https://babeljs.io/docs/en/babel-types

找到之后如何插入一个新的节点呢?你可以利用 @babel/types 提供的方法来生成对应的类型节点的 AST node 对象。比如下面就是在 render 的 return 中插入一个新的 jsxElement

  1. traverse(ast, {
  2. ClassMethod({ node }) {
  3. const { key, body } = node;
  4. if (
  5. t.isIdentifier(key, {
  6. name: 'render',
  7. })
  8. ) {
  9. if (t.isBlockStatement(body)) {
  10. // blockStatement: https://babeljs.io/docs/en/babel-types#blockstatement
  11. const { body } = blockStatement;
  12. const returnBlock = body.find(b => {
  13. return t.isReturnStatement(b);
  14. });
  15. if (t.isJSXElement(returnBlock.argument)) {
  16. // https://babeljs.io/docs/en/babel-types#jsxelement
  17. const newElement = t.jsxElement(
  18. t.jsxOpeningElement(t.jsxIdentifier(identifier), [], true),
  19. null,
  20. [],
  21. true,
  22. );
  23. returnBlock.argument.children.push(newElement);
  24. }
  25. }
  26. }
  27. },
  28. });

以上代码参考 umi 的区块的代码

generator

generator 就比较简单的,下面一行代码搞定:

  1. const newCode = generate(ast, {}).code;

FAQ

如何美化代码

你可以使用 prettier:

  1. const newCode = generate(ast, {}).code;
  2. return prettier.format(newCode, {
  3. // format same as ant-design-pro
  4. singleQuote: true,
  5. trailingComma: 'es5',
  6. printWidth: 100,
  7. parser: 'babylon',
  8. });

参考文档