一 AST 是什么?css
1 AST:Abstract Syntax Tree - 抽象语法树html
当咱们查看目前主流的项目中的 devDependencies,会发现各类各样的模块工具。概括一下有:JavaScript转译、css预处理器、elint、pretiier 等等。这些模块咱们不会在生产环境用到,但它们在咱们的开发过程当中充当着重要的角色,而全部的上述工具,都创建在 AST 的基础上。前端
2 AST 工做流程node
3 AST 树预览react
AST 辅助开发工具:https://astexplorer.net/git
二 从一个简单需求上手github
代码压缩的伪需求:将 square 函数参数与引用进行简化,变量由 num 转换为 n:算法
解法1:使用 replace 暴力转换express
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...
**
技术公开课**
《React 入门与实战》
React是一个用于构建用户界面的JavaScript库。本课程共54课时,带你全面深刻学习React的基础知识,并经过案例掌握相关应用。
点击“阅读原文”开始学习吧~