什么是 AST?
1AST: 抽象语法树 - 比喻句子树
当我们查看当今主流项目中的 devDependencies 时,我们会发现各种模块工具。总结一下:JavaScript 转译、CSS 预处理器、elint、pretiier 等。我们不在生产中使用这些模块,但它们在我们的开发过程中发挥着重要作用,上述所有工具都是基于 AST 构建的。
2AST 工作流程
3AST 树预览
AST 辅助开发工具:
二、从简单的要求入手
代码压缩的伪要求:用引用简化平方函数参数,并将变量从 num 转换为 n:
解决方案 1:使用位置暴力转换
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'; // 转换变量名
}
}
}
}
};
通过定义标识符访问者,将遍历标识符(变量),如果标识符名称为“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 也会与上面的访问者迭代器匹配并转换,所以转换后的代码是 window.n,这会导致错误。分析要求是“简化平方函数参数和引用,将变量从 num 转换为 n”,提取的三个关键字是“平方函数、参数、引用”,进一步优化了代码。
解决方案 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运营中,要具体化“人”的判断,进行合理的改造。
三巴贝林
1接口概述
// 三剑客
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/解析器
通过 babel/parser(简单的图像)将源代码转换为 AST。
const ast = parser(rawSource, {
sourceType: 'module',
plugins: [
"jsx",
],
});
3@babel/遍历
在 AST 开发的核心,超过 95% 的代码是通过 @babel/遍历编写的。
const ast = parse(`function square(num) {
return num * num;
}`);
traverse(ast, { // 进行 ast 转换
Identifier(path) { // 遍历变量的visitor
// ...
},
// 其他的visitor遍历器
}
)
访问者的第一个参数是 path,path 不直接等于节点,path 的属性和重要路径组成如下:
4@babel/发电机
通过@babel/生成器,AST已经操作生成相应的源代码,简单直观。
const output = generate(ast, { /* options */ });
5@babel/类型
@babel/类型用于创建 AST 节点并确定 AST 节点,这些节点在实际开发中经常使用。
// is开头的用于判断节点
types.isObjectProperty(node);
types.isObjectMethod(node);
// 创建 null 节点
const nullNode = types.nullLiteral();
// 创建 square 变量节点
const squareNode = types.identifier('square');
6@babel/模板
@bable/类型可以创建 AST 节点,但过于冗长,您可以通过 @babel/模板快速创建整个 AST 节点段。下面比较了获取importReactfrom'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. 定义通用的通天塔插件
定义一个通用的 babelplugin 将有助于与 Webpack 的集成javascript数组转字符串,例如:
// 定义插件
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') 也可以为相对目录
]
};
在 Babelplugin 的开发中,可以说你是在编写 asttransformcallbackjavascript数组转字符串,没有直接接触 “@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');
四 ESLintinAST
掌握了 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 命令
有关 ESLintAPI 的更多信息,请参阅官方文档 [2]。
5. 获得您需要的 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的所有功能。让我们自己写一个 babelplugin 来获得对 JSX 的必要解释。
1JSXBabel插件我们知道HTML是描述网页的语言,
axml或vxml是描述小程序页面的语言,不同的容器是不兼容的。但相似之处在于它们都是基于 JavaScript 堆栈的,那么它们可以通过定义一组 JSX 规范在来世创建吗?
2 个进球
export default (
<view>
hello <text style={{ fontWeight: 'bold' }}>world</text>
</view>
);
<div>
hello <span style="font-weight: bold;">world</span>
</div>
<view>
hello <text style="font-weight: bold;">world</text>
</view>
目前关注的是 AST 只能用作 JavaScript 的转换,那么如何转换 HTML 和 axml 等文本标记语言呢?不妨改变思路:将上面的JSX代码转换为JS代码,在网页端和小程序上提供组件消费。这是 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 属性,如转换为 {type:'view'}
2. 标签上的属性被转换为对象的属性,例如 {}}/>转换为 {type:'view', onTap:e=>{}}
3. 将 JSX 中的子元素移植到 child 属性,这是一个字段,例如 {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 = console.log('clicked')} visible>ABC
*/
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, 如将 转化为 { 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 标签,如:
// 将 JSX 标签转化为 Object
path.replaceWith(handleJSXElement(path.node));
},
}
}
});
六、小结
我们介绍了AST和AST的工作模式,并体验了AST实现的令人震惊的能力。如今,AST 有哪些业务场景?当用户:
AST 将是你强大的装备。
注意:本文中演示的代码片段和测试方法可供感兴趣的读者使用。
紧急招聘
作者在阿里云-人工智能实验室-应用开发部工作。目前,我部门已积累近20W开发者和企业用户,为数亿台设备提供连接服务。目前,团队正在紧急招聘大型后端(后端、iOS、Android 等)、Java 开发、数据算法等全方位的工程师。方向为联通DevOps平台、移动中间件、无服务器、低代码平台、小程序云、云渲染应用平台、新零售/教育行业数字化转型等,详情请看:changwen.tcw@alibaba-inc.com
引用
[1]#L10-L14
[2]
[3]
技术公开课程
“反应与实践简介”
React 是一个用于构建用户界面的 JavaScript 库。本课程共 54 课时,带您全面深入地学习 React 的基础知识,并通过案例掌握相关应用。
用于校准非零正整数实例的 JavaScript 正则表达式
更新时间:2016年12月13日09:23:32 作者:冷战
本文分享 JavaScript 正则表达式 (^[1-9]d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^+?[1-9][ 0 -9]*$) 校准非零正整数的示例代码,代码简单易懂,有需要的同学可以阅读
话不多说javascript正则表达式javascript正则表达式,请看示例代码
function validation() { var val = document.getElementById("txtNumber").value; var regu = /^[1-9]d*$/; //var regu = /^([1-9][0-9]*){1,3}$/; 亲测可用 //var regu = /^+?[1-9][0-9]*$/; 亲测可用 if (val != "") { if (!regu.test(val)) { document.getElementById("labResult").style.color = "red"; document.getElementById("labResult").innerHTML = "验证失败!"; } else { document.getElementById("labResult").style.color = "green"; document.getElementById("labResult").innerHTML = "验证成功!"; } } } 验证结果:
以上就是本文的全部内容。 希望本文的内容能够给您的学习或者工作带来一些帮助。 同时也希望大家能够支持脚本之家!
发表评论