书摘&心得

  • 【核心】借助https://astexplorer.net/查看path.node的属性,而不是硬背属性image.png

    • 写babel插件的逻辑就是一面parse查看源代码AST语法树,一面parse差看目标代码AST语法树,找到要修改调整的节点后,在@babel/traverse中遍历目标节点,利用@babel/types创建/判断节点,然后利用@babel/traverse添加/修改/删除节点。
    • 在利用项目babel.config.js时,甚至不用关注parse和generate的过程。

      实战演练

  • 需求:

    • 希望通过 babel 能够自动在 console.log 等 api 中插入文件名和行列号的参数,方便定位到代码。
  • 思路:
    • CallExrpession 节点有两个属性,callee 和 arguments,分别对应调用的函数名和参数, 所以我们要判断当 callee 是 console.xx 时,在 arguments 的数组中中插入一个 AST 节点
  • 最终代码: ```javascript const ast = parser.parse(sourceCode, { sourceType: ‘unambiguous’, plugins: [‘jsx’] });

traverse(ast, { CallExpression (path, state) { if ( types.isMemberExpression(path.node.callee) && path.node.callee.object.name === ‘console’ && [‘log’, ‘info’, ‘error’, ‘debug’].includes(path.node.callee.property.name) ) { const { line, column } = path.node.loc.start; path.node.arguments.unshift(types.stringLiteral(filename: (${line}, ${column}))) } } });

  1. 注意节点属性都是通过[https://astexplorer.net/](https://astexplorer.net/)查看path.node的属性,所以AST语法树才是babel转换的核心
  2. <a name="jqXmI"></a>
  3. ## 改造成babel插件
  4. babel 支持 transform 插件,形式是函数返回一个对象,对象有 visitor 属性。<br />第一个参数可以拿到 typestemplate 等常用包的 api,不需要单独引入这些包。(这就是随处可见但没有任何显示引用的原因!)作为插件用的时候,并不需要自己调用 parsetraversegenerate,只需要提供一个 visitor 函数,在这个函数内完成转换功能。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2841570/1646901093632-7b1f8505-0061-4e1f-bb2c-5755b100348d.png#clientId=u3ff5d825-44b3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=254&id=u91261617&margin=%5Bobject%20Object%5D&name=image.png&originHeight=283&originWidth=539&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10279&status=done&style=none&taskId=u94d0d50a-da4e-4466-8998-8ee163ccd2c&title=&width=483.13724457690574)<br />这就是babel-plugin-import的插件形式,最终的代码:
  5. ```javascript
  6. const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
  7. module.exports = function({types, template}) {
  8. return {
  9. visitor: {
  10. CallExpression(path, state) {
  11. if (path.node.isNew) {
  12. return;
  13. }
  14. const calleeName = generate(path.node.callee).code;
  15. if (targetCalleeName.includes(calleeName)) {
  16. const { line, column } = path.node.loc.start;
  17. const newNode = template.expression(`console.log("${state.filename || 'unkown filename'}: (${line}, ${column})")`)();
  18. newNode.isNew = true;
  19. if (path.findParent(path => path.isJSXElement())) {
  20. path.replaceWith(types.arrayExpression([newNode, path.node]))
  21. path.skip();
  22. } else {
  23. path.insertBefore(newNode);
  24. }
  25. }
  26. }
  27. }
  28. }
  29. }

最后一步是利用@babel/core中的 transformSync 方法来调用该插件,实际在配置项目babel插件的时候,往往这一步是内置的,核心还是插件本身,也就是@babel/traverse、@babel/types、@babel/template以及最重要的AST语法树