乐趣区

关于javascript:用JS解释JS详解AST及其应用

一  AST 是什么?

1  AST:Abstract Syntax Tree – 形象语法树

当咱们查看目前支流的我的项目中的 devDependencies,会发现各种各样的模块工具。演绎一下有:JavaScript 转译、css 预处理器、elint、pretiier 等等。这些模块咱们不会在生产环境用到,但它们在咱们的开发过程中充当着重要的角色,而所有的上述工具,都建设在 AST 的根底上。

2  AST 工作流程

  • parse:把代码解析为 AST。
  • transform:对 AST 中的各个节点做相干操作,如新增、删除、替换、追加。业务开发 95% 的代码都在这里。
  • generator:把 AST 转换为代码。

3  AST 树预览

AST 辅助开发工具:https://astexplorer.net/

二  从一个简略需要上手

代码压缩的伪需要:将 square 函数参数与援用进行简化,变量由 num 转换为 n:

解法 1:应用 replace 暴力转换

const sourceText = `function square(num) {return num * num;}`;
sourceText.replace(/num/g, 'n');

以上操作相当的暴力,很容易引起 bug,不能投入使用。如若存在字符串 “num”,也将被转换:

// 转换前
function square(num) {return num * num;}
console.log('param 2 result num is' + square(2));

// 转换后
function square(n) {return n * n;}
console.log('param 2 result n is' + square(2));

解法 2:应用 babel 进行 AST 操作

module.exports = () => {
  return {
    visitor: {
      // 定义 visitor, 遍历 Identifier
      Identifier(path) {if (path.node.name === 'num') {path.node.name = 'n'; // 转换变量名}
      }
    }
  }
};

通过定义 Identifier visitor,对 Identifier(变量)进行遍历,如果 Identifier 名称为 “num”,进行转换。以上代码解决了 num 为字符串时也进行转换的问题,但还存在潜在问题,如代码为如下状况时,将引发谬误:

// 转换前
function square(num) {return num * num;}
console.log('global num is' + window.num);

// 转换后
function square(n) {return n * n;}
console.log('global num is' + window.n); // 出错了

因为 window.num 也会被上述的 visitor 迭代器匹配到而进行转换,转换后出代码为 window.n,进而引发谬误。剖析需要“将 square 函数参数与援用进行简化,变量由 num 转换为 n”,提炼出的 3 个关键词为“square 函数、参数、援用”,对此进一步优化代码。

解法 2 降级:找到援用关系

module.exports = () => {
  return {
    visitor: {Identifier(path,) {
        // 三个前置判断
        if (path.node.name !== 'num') { // 变量须要为 num
          return;
        }
        if (path.parent.type !== 'FunctionDeclaration') { // 父级须要为函数
          return;
        }
        if (path.parent.id.name !== 'square') { // 函数名须要为 square
          return;
        }
        const referencePaths = path.scope.bindings['num'].referencePaths; // 找到对应的援用
        referencePaths.forEach(path => path.node.name = 'n'); // 批改援用值
        path.node.name = 'n'; // 批改本身的值
      },
    }
  }
};

上述的代码,可形容流程为:

转换后果:

// 转换前
function square(num) {return num * num;}
console.log('global num is' + window.num);

// 转换后
function square(n) {return n * n;}
console.log('global num is' + window.num);

在面向业务的 AST 操作中,要形象出“人”的判断,做出正当的转换。

三  Babel in AST

1  API 总览

// 三剑客
const parser = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

// 配套包
const types = require('@babel/types');

// 模板包
const template = require('@babel/template').default;

2  @babel/parser

通过 babel/parser 将源代码转为 AST,简略形象。

const ast = parser(rawSource, {
  sourceType: 'module',
  plugins: ["jsx",],
});

3  @babel/traverse

AST 开发的外围,95% 以上的代码量都是通过 @babel/traverse 在写 visitor。

const ast = parse(`function square(num) {return num * num;}`);

traverse(ast, { // 进行 ast 转换
    Identifier(path) { // 遍历变量的 visitor
      // ...
    },
    // 其余的 visitor 遍历器
  } 
)

visitor 的第一个参数是 path,path 不间接等于 node(节点),path 的属性和重要办法组成如下:

4  @babel/generator

通过 @babel/generator 将操作过的 AST 生成对应源代码,简略形象。

const output = generate(ast, { /* options */});

5  @babel/types

@babel/types 用于创立 ast 节点,判断 ast 节点,在理论的开发中会常常用到。

// is 结尾的用于判断节点
types.isObjectProperty(node);
types.isObjectMethod(node);

// 创立 null 节点
const nullNode = types.nullLiteral();
// 创立 square 变量节点
const squareNode = types.identifier('square');

6  @babel/template

@bable/types 能够创立 ast 节点,但过于繁琐,通过 @babel/template 则能够疾速创立整段的 ast 节点。上面比照了取得 import React from ‘react’  ast 节点的两种形式:

// @babel/types
// 创立节点须要查找对应的 API,传参须要匹配办法
const types = require('@babel/types');
const ast = types.importDeclaration([ types.importDefaultSpecifier(types.identifier('React')) ], 
  types.stringLiteral('react')
);

// path.replaceWith(ast) // 节点替换
// 应用 @babel/template
// 创立节点输出源代码即可,清晰易懂
const template = require('@babel/template').default;
const ast = template.ast(`import React from 'react'`);

// path.replaceWith(ast) // 节点替换

7  定义通用的 babel plugin

定义通用的 babel plugin,将有利于被 Webpack 集成,示例如下:

// 定义插件
const {declare} = require('@babel/helper-plugin-utils');

module.exports = declare((api, options) => {
  return {
    name: 'your-plugin', // 定义插件名
    visitor: { // 编写业务 visitor
      Identifier(path,) {// ...},
    }
  }
});
// 配置 babel.config.js
module.exports = {
    presets: [require('@babel/preset-env'), // 可配合通用的 present
    ],
    plugins: [require('your-plugin'),
        // require('./your-plugin') 也能够为绝对目录
    ]
};

在 babel plugin 开发中,能够说就是在写 ast transform callback,不须要间接接触“@babel/parser、@babel/traverse、@babel/generator”等模块,这在 babel 外部调用了。

在须要用到 @babel/types 能力时,倡议间接应用 @babel/core,从源码 [1] 能够看出,@babel/core 间接透出了上述 babel 模块。

const core = require('@babel/core');
const types = core.types; // const types = require('@babel/types');

四  ESLint in AST

在把握了 AST 外围原理后,自定义 ESlint 规定也变的容易了,间接上代码:

// eslint-plugin-my-eslint-plugin
module.exports.rules = { 
  "var-length": context => ({ // 定义 var-length 规定,对变量长度进行检测
    VariableDeclarator: (node) => {if (node.id.name.length <= 1){context.report(node, '变量名长度须要大于 1');
      }
    }
  })
};
// .eslintrc.js
module.exports = {
  root: true,
  parserOptions: {ecmaVersion: 6},
  plugins: ["my-eslint-plugin"],
  rules: {"my-eslint-plugin/var-length": "warn"}
};

体验成果

IDE 正确提醒:

执行 eslint 命令的 warning:

查阅更多 ESLint API 可查看官网文档[2]。

五  取得你所须要的 JSX 解释权

第一次接触到 JSX 语法大多是在学习 React 的时候,React 将 JSX 的能力发扬光大[3]。但 JSX 不等于 React,也不是由 React 发明的。

// 应用 react 编写的源码
const name = 'John';
const element = <div>Hello, {name}</div>;
// 通过 @babel/preset-react 转换后的代码
const name = 'John';
const element = React.createElement("div", null, "Hello,", name);

JSX 作为标签语法既不是字符串也不是 HTML,是一个 JavaScript 的语法扩大,能够很好地形容 UI 应该呈现出它应有交互的实质模式。JSX 会使人联想到模版语言,它也具备 JavaScript 的全副性能。上面咱们本人写一个 babel plugin,来取得所须要对 JSX 的解释权。

1  JSX Babel Plugin

咱们晓得,HTML 是形容 Web 页面的语言,axml 或 vxml 是形容小程序页面的语言,不同的容器两者并不兼容。但相同点是,他们都基于 JavaScript 技术栈,那么是否能够通过定义一套 JSX 标准来生成出一样的页面体现?

2  指标

export default (
  <view>
    hello <text style={{fontWeight: 'bold'}}>world</text>
  </view>
);
<!-- 输入 Web HTML -->
<div>
  hello <span style="font-weight: bold;">world</span>
</div>
<!-- 输入小程序 axml -->
<view>
  hello <text style="font-weight: bold;">world</text>
</view>

目前的纳闷在于:AST 仅可用作 JavaScript 的转换,那 HTML 和 axml 等文本标记语言改怎么转换呢?无妨转换一种思路:将上述的 JSX 代码转化为 JS 的代码,在 Web 端和小程序端提供组件生产即可。这是 AST 开发的一个设计思维,AST 工具仅做代码的编译,具体的生产由上层操作,@babel/preset-react 与 react 就是这个模式。

// jsx 源码
module.exports = function () {
  return (
    <view
      visible
      onTap={e => console.log('clicked')}
    >ABC<button>login</button></view>
  );
};

// 指标:转后为更通用的 JavaScript 代码
module.exports = function () {
  return {
    "type": "view",
    "visible": true,
    "children": [
      "ABC",
      {
        "type": "button",
        "children": ["login1"]
      }
    ]
  };
};

明确了指标后,咱们要做的事为:

1. 将 jsx 标签转为 Object,标签名为 type 属性,如 <view /> 转化为 {type: ‘view’}

2. 标签上的属性平移到 Object 的属性上,如 <view onTap={e => {}} /> 转换为 {type: ‘view’, onTap: e => {} }

3. 将 jsx 内的子元素,移植到 children 属性上,children 属性为数组,如 {type: ‘view’, style, children: […] }

4. 面对子元素,反复后面 3 步的工作。

上面是实现的示例代码:

const {declare} = require('@babel/helper-plugin-utils');
const jsx = require('@babel/plugin-syntax-jsx').default;
const core = require('@babel/core');
const t = core.types;

/*
  遍历 JSX 标签,约定 node 为 JSXElement,如
  node = <view onTap={e => console.log('clicked')} visible>ABC<button>login</button></view>
*/
const handleJSXElement = (node) => {
  const tag = node.openingElement;
  const type = tag.name.name; // 取得表情名为 View
  const propertyes = []; // 贮存对象的属性
  propertyes.push( // 取得属性 type = 'ABC'
    t.objectProperty(t.identifier('type'),
      t.stringLiteral(type)
    )
  );
  const attributes = tag.attributes || []; // 标签上的属性
  attributes.forEach(jsxAttr => { // 遍历标签上的属性
    switch (jsxAttr.type) {
      case 'JSXAttribute': { // 解决 JSX 属性
        const key = t.identifier(jsxAttr.name.name); // 失去属性 onTap、visible
        const convertAttributeValue = (node) => {if (t.isJSXExpressionContainer(node)) { // 属性的值为表达式(如函数)return node.expression; // 返回表达式
          }
          // 空值转化为 true, 如将 <view visible /> 转化为 {type: 'view', visible: true}
          if (node === null) {return t.booleanLiteral(true);
          }
          return node;
        }
        const value = convertAttributeValue(jsxAttr.value);
        propertyes.push(// 取得 { type: 'view', onTap: e => console.log('clicked'), visible: true }
          t.objectProperty(key, value)
        );
        break;
      }
    }
  });
  const children = node.children.map((e) => {switch(e.type) {
      case 'JSXElement': {return handleJSXElement(e); // 如果子元素有 JSX,便当 handleJSXElement 本身
      }
      case 'JSXText': {return t.stringLiteral(e.value); // 将字符串转化为字符
      }
    }
    return e;
  });
  propertyes.push( // 将 JSX 内的子元素转化为对象的 children 属性
    t.objectProperty(t.identifier('children'), t.arrayExpression(children))
  );
  const objectNode = t.objectExpression(propertyes); // 转化为 Object Node
  /* 最终转化为
  {
    "type": "view",
    "visible": true,
    "children": [
      "ABC",
      {
        "type": "button",
        "children": ["login"]
      }
    ]
  }
  */
  return objectNode;
}

module.exports = declare((api, options) => {
  return {
    inherits: jsx, // 继承 Babel 提供的 jsx 解析根底
    visitor: {JSXElement(path) { // 遍历 JSX 标签,如:<view />
        // 将 JSX 标签转化为 Object
        path.replaceWith(handleJSXElement(path.node));
      },
    }
  }
});

六  总结

咱们介绍了什么是 AST、AST 的工作模式,也体验了利用 AST 所达成的惊艳能力。当初来想想 AST 更多的业务场景是什么?当用户:

  • 须要基于你的基础设施进行二次编程开发的时候
  • 有可视化编程操作的时候
  • 有代码标准定制的时候

AST 将是你强有力的武器。

注:本文演示的代码片段与测试方法在 https://github.com/chvin/learn\_ast,有趣味的读者可返回学习体验。

招聘

笔者任职于阿里云 - 人工智能实验室 - 利用研发部。我部门目前已累积了近 20w 的开发者和企业用户,为数亿的设施提供挪动服务。目前团队急招大前端(前端、iOS、Android 等)、Java 开发、数据算法等各方向的工程师。方向是挪动 Devops 平台、挪动中间件、Serverless、低代码平台、小程序云、云渲染利用平台、新批发 / 教育产业数字化转型等,无意详聊:changwen.tcw@alibaba-inc.com

参考资料

[1]https://github.com/babel/babe…

[2]https://cn.eslint.org/docs/de…

[3]https://reactjs.bootcss.com/d…


**
技术公开课 **

《React 入门与实战》

React 是一个用于构建用户界面的 JavaScript 库。本课程共 54 课时,带你全面深刻学习 React 的基础知识,并通过案例把握相干利用。

点击“浏览原文”开始学习吧~

退出移动版