AST : 全称为 Abstract Syntax Tree,意为抽象语法树,它是源代码语法结构的一种抽象表示。javascript
AST 是一个很是基础可是同时很是重要的知识点,咱们熟知的 TypeScript、babel、webpack、vue-cli 得都是依赖 AST 进行开发的。本文将经过 AST 与前端工程化的实战向你们展现 AST 的强大以及重要性。html
直播分享视频地址:AST 与前端工程化实战前端
第一次看见 AST 这个概念的时候仍是在《你不知道的 JavaScript》一书中看到的。咱们先看个例子vue
const a = 1
复制代码
传统编译语言中,源代码执行会先经历三个阶段java
词法分析阶段:将字符组成的字符串分解成一个个代码块(词法单元),例子中代码会被解析成 const、a、=、1 四个词法单元。node
语法分析阶段:将词法单元流转换成一个由元素逐级嵌套组成的语法结构树,即所谓的抽象语法树。例子中被解析出来的 const、a、=、1 这四个词法单元组成的词法单元流则会被转换成以下结构树webpack
咱们再来拆解一个 recast
官方的例子,相对来讲也会复杂一些ios
function add (a, b) {
return a + b
}
复制代码
function、add、(、a、,、b、)、{、return、a、+、b、}
13 个代码块上图中的 FunctionDeclaration
、Identifier
、BlockStatement
等这些代码块的类型的说明请点击连接自行查看:AST 对象文档git
因为文章中用到的 AST 相关的依赖包是 recast
,加上它自己是木有文档的,只有一个很是简短的 README.md
文件,因此这里单独开一篇对其常见的一些 API 作个介绍。开始以前,先给你们推荐一个在线查看 AST 结构的平台,很是好用github
相信对 babel
稍有了解的同窗都知道,babel
有一系列包对 AST 进行了封装,专门来处理编译这块的事宜。而 recast
也是基于 @babel/core
、@babel/parser
、@babel/types
等包进行封装开发的。
引入 recast
有两种方法,一种是 import
的形式,一种则是 CommonJs
的形式,分别以下
import
形式import { parse, print } from 'recast'
console.log(print(parse(source)).code)
import * as recast from 'recast'
console.log(recast.print(recast.parse(source)).code)
复制代码
CommonJs
形式const { parse, print } = require('recast')
console.log(print(parse(source)).code)
const recast = require('recast')
console.log(recast.print(recast.parse(source)).code)
复制代码
引入了 recast
以后,咱们一块儿来看看 recast
都能作些什么吧
咱们回到咱们例子,咱们直接对它进行 parse ,看看 parse 后的 AST 结构是如何的
// parse.js
const recast = require('recast')
const code = `function add (a, b) { return a + b }`
const ast = recast.parse(code)
// 获取代码块 ast 的第一个 body,即咱们的 add 函数
const add = ast.program.body[0]
console.log(add)
复制代码
执行 node parse.js
便可在咱们的终端查看到 add 函数的结构了
FunctionDeclaration {
type: 'FunctionDeclaration',
id: Identifier...,
params: [Identifier...],
body: BlockStatement...
}
复制代码
固然你想看更多内容直接去 AST Explorer 平台 将模式调成 recast
模式便可看到 ast 的全览了,和咱们上面分析的内容基本是一致的。
目前为止,咱们只是对其进行了拆解,若是将 ast 组装成咱们能执行的代码呢?OK,这就须要用到 recast.print
了,咱们对上面拆解好的代码原封不动的组装起来
// print.js
const recast = require('recast')
const code = `function add (a, b) { return a + b }`
const ast = recast.parse(code)
console.log(recast.print(ast).code)
复制代码
而后执行 node print.js
,能够看到,咱们打印出了
function add (a, b) {
return a + b
}
复制代码
官方给的解释就是,这就只是一个逆向处理而已,即
recast.print(recast.parse(source)).code === source
复制代码
除了咱们上面说起的 recast.print
外,recast
还提供一个代码美化的 API 叫 recast.prettyPrint
// prettyPrint.js
const recast = require('recast')
const code = `function add (a, b) { return a + b }`
const ast = recast.parse(code)
console.log(recast.prettyPrint(ast, { tabWidth: 2 }).code)
复制代码
执行 node prettyPrint.js
,会发现 code 里面的 N 多空格都能被格式化掉,输出以下
function add(a, b) {
return a + b;
}
复制代码
详细的配置请自行查看:prettyPrint
关于 builder
的 API ,别担忧,我确定是不会讲的,由于太多了。
想要具体了解每个 API 能作什么的,能够直接在 Parser API - Builders 中进行查看,或者直接查看 recast builders 定义
OK,终于进入到 recast
操做相关的核心了。咱们要想改造咱们的代码,那么 recast.types.builders
则是咱们最重要的工具了。这里咱们继续经过改造 recast
官方案例来了解 recast.types.builders
构建工具。
搞个最简单的例子,如今咱们要作一件事,那就是将 function add (a, b) {...}
改为 const add = function (a, b) {...}
。
咱们从第一章节了解到,若是咱们须要将其作成 const
声明式的话,须要先一个 VariableDeclaration
以及一个 VariableDeclarator
,而后咱们声明一个 function
则有须要建立一个 FunctionDeclaration
,剩下的则是填充表达式的参数和内容体了。具体操做以下
// builder1.js
const recast = require('recast')
const {
variableDeclaration,
variableDeclarator,
functionExpression
} = recast.types.builders
const code = `function add (a, b) { return a + b }`
const ast = recast.parse(code)
const add = ast.program.body[0]
ast.program.body[0] = variableDeclaration('const', [
variableDeclarator(add.id, functionExpression(
null, // 这里弄成匿名函数便可
add.params,
add.body
))
])
const output = recast.print(ast).code
console.log(output)
复制代码
执行 node builder1.js
,输出以下
const add = function(a, b) {
return a + b
};
复制代码
看到这,是否是以为颇有趣。真正好玩的才刚开始呢,接下来,基于此例子,咱们作个小的延伸。将其直接改为 const add = (a, b) => {...}
的格式。
这里出现了一个新的概念,那就是箭头函数,固然,recast.type.builders
提供了 arrowFunctionExpression
来容许咱们建立一个箭头函数。因此咱们第一步先来建立一个箭头函数
const arrow = arrowFunctionExpression([], blockStatement([])
复制代码
打印下 console.log(recast.print(arrow))
,输出以下
() => {}
复制代码
OK,咱们已经获取到一个空的箭头函数了。接下来咱们须要基于上面改造的基础进一步进行改造,其实只要将 functionExpression
替换成 arrowFunctionExpression
便可。
ast.program.body[0] = variableDeclaration('const', [
variableDeclarator(add.id, b.arrowFunctionExpression(
add.params,
add.body
))
])
复制代码
打印结果以下
const add = (a, b) => {
return a + b
};
复制代码
OK,到这里,咱们已经知道 recast.types.builders
能为咱们提供一系列 API,让咱们能够疯狂输出。
读取文件命令行。首先,我新建一个 read.js
,内容以下
// read.js
recast.run((ast, printSource) => {
printSource(ast)
})
复制代码
而后我再新建一个 demo.js
,内容以下
// demo.js
function add (a, b) {
return a + b
}
复制代码
而后执行 node read demo.js
,输出以下
function add (a, b) {
return a + b
}
复制代码
咱们能看出来,咱们直接在 read.js
中读出了 demo.js
里面的代码内容。那么具体是如何实现的呢?
其实,原理很是简单,无非就是直接经过 fs.readFile
进行文件读取,而后将获取到的 code
进行 parse
操做,至于咱们看到的 printSource
则提供一个默认的打印函数 process.stdout.write(output)
,具体代码以下
import fs from "fs";
export function run(transformer: Transformer, options?: RunOptions) {
return runFile(process.argv[2], transformer, options);
}
function runFile(path: any, transformer: Transformer, options?: RunOptions) {
fs.readFile(path, "utf-8", function(err, code) {
if (err) {
console.error(err);
return;
}
runString(code, transformer, options);
});
}
function defaultWriteback(output: string) {
process.stdout.write(output);
}
function runString(code: string, transformer: Transformer, options?: RunOptions) {
const writeback = options && options.writeback || defaultWriteback;
transformer(parse(code, options), function(node: any) {
writeback(print(node, options).code);
});
}
复制代码
这是一个 AST 节点遍历的 API,若是你想要遍历 AST 中的一些类型,那么你就得靠 recast.visit
了,这里能够遍历的类型与 recast.types.builders
中的能构造出来的类型一致,builders
作的事是类型构建,recast.visit
作事的事则是遍历 AST 中的类型。不过使用的时候须要注意如下几点
return false
,或者 this.traverse(path)
,不然报错。if (this.needToCallTraverse !== false) {
throw new Error(
"Must either call this.traverse or return false in " + methodName
);
}
复制代码
recast.run((ast, printSource) => {
recast.visit(ast, {
visitArrowFunctionExpression (path) {
printSource(path.node)
return false
}
})
})
复制代码
一个用来判断 AST 对象是否为指定类型的 API。
namedTypes 下有两个 API,一个是 namedTypes.Node.assert
:当类型不配置的时候,直接报错退出。另一个则是 namedTypes.Node.check
:判断类型是否一致,并输出 true 或 false。
其中 Node 为任意 AST 对象,好比我相对箭头函数作一个函数类型断定,代码以下
// namedTypes1.js
const recast = require('recast')
const t = recast.types.namedTypes
const arrowNoop = () => {}
const ast = recast.parse(arrowNoop)
recast.visit(ast, {
visitArrowFunctionExpression ({ node }) {
console.log(t.ArrowFunctionExpression.check(node))
return false
}
})
复制代码
执行 node namedTypes1.js
,能看出打印台输出结果为 true。
同理,assert 用法也差很少。
const recast = require('recast')
const t = recast.types.namedTypes
const arrowNoop = () => {}
const ast = recast.parse(arrowNoop)
recast.visit(ast, {
visitArrowFunctionExpression ({ node }) {
t.ArrowFunctionExpression.assert(node)
return false
}
})
复制代码
你想判断更多的 AST 对象类型的,直接作替换 Node 为其它 AST 对象类型便可。
如今,咱来聊聊前端工程化。
前段工程化能够分红四个块来讲,分别为
模块化:将一个文件拆分红多个相互依赖的文件,最后进行统一的打包和加载,这样可以很好的保证高效的多人协做。其中包含
组件化:不一样于模块化,模块化是对文件、对代码和资源拆分,而组件化则是对 UI 层面的拆分。
规范化:正所谓无规矩不成方圆,一些好的规范则能很好的帮助咱们对项目进行良好的开发管理。规范化指的是咱们在工程开发初期以及开发期间制定的系列规范,其中又包含了
以上这些,我以前也写过一篇文章作过一些点的详细说明,TypeScript + 大型项目实战
自动化:从最先先的 grunt、gulp 等,再到目前的 webpack、parcel。这些自动化工具在自动化合并、构建、打包都能为咱们节省不少工做。而这些前端自动化其中的一部分,前端自动化还包含了持续集成、自动化测试等方方面面。
而,处于其中任何一个块都属于前端工程化。
而本文说起的实战,则是经过 AST 改造书写一个属于咱们本身的 webpack loader,为咱们项目中的 promise 自动注入 catch 操做,避免让咱们手动书写那些通用的 catch 操做。
讲了这么多,终于进入到咱们的实战环节了。那么咱们实战要作一个啥玩意呢?
场景:平常的中台项目中,常常会有一些表单提交的需求,那么提交的时候就须要作一些限制,防止有人手抖多点了几回致使请求重复发出去。此类场景有不少解决方案,可是我的认为最佳的交互就是点击以后为提交按钮加上 loading 状态,而后将其 disabled 掉,请求成功以后再解除掉 loading 和 disabled 的状态。具体提交的操做以下
this.axiosFetch(this.formData).then(res => {
this.loading = false
this.handleClose()
}).catch(() => {
this.loading = false
})
复制代码
这样看着好像还算 OK,可是若是相似这样的操做一多,或多或少会让你项目总体的代码看起来有些重复冗余,那么如何解决这种状况呢?
很简单,咱直接使用 AST 编写一个 webpack loader,让其自动完成一些代码的注入,若咱们项目中存在下面的代码的时候,会自动加上 catch 部分的处理,并将 then 语句第一段处理主动做为 catch 的处理逻辑
this.axiosFetch(this.formData).then(res => {
this.loading = false
this.handleClose()
})
复制代码
咱们先看看,没有 catch 的这段代码它的 AST 结构是怎样的,如图
其 MemberExpression 为
this.axiosFetch(this.formData).then
复制代码
arguments 为
res => {
this.loading = false
this.handleClose()
}
复制代码
OK,咱们再来看看有 catch 处理的代码它的 AST 结构又是如何的,如图
其 MemberExpression 为
this.axiosFetch(this.formData).then(res => {
this.loading = false
this.handleClose()
}).catch
复制代码
其中有两个 ArrowFunctionExpression,分别为
// ArrowFunctionExpression 1
res => {
this.loading = false
this.handleClose()
}
// ArrowFunctionExpression 2
() => {
this.loading = false
}
复制代码
因此,咱们须要作的事情大体分为如下几步
如今,按照咱们的思路,咱们一步一步来作 AST 改造
首先,咱们须要获取到已有箭头函数中的第一个 ExpressionStatement,获取的时候咱们须要保证当前 ArrowFunctionExpression 类型的 parent 节点是一个 CallExpression 类型,而且保证其 property 为 promise 的then 函数,具体操做以下
// promise-catch.js
const recast = require('recast')
const {
identifier: id,
memberExpression,
callExpression,
blockStatement,
arrowFunctionExpression
} = recast.types.builders
const t = recast.types.namedTypes
const code = `this.axiosFetch(this.formData).then(res => { this.loading = false this.handleClose() })`
const ast = recast.parse(code)
let firstExp
recast.visit(ast, {
visitArrowFunctionExpression ({ node, parentPath }) {
const parentNode = parentPath.node
if (
t.CallExpression.check(parentNode) &&
t.Identifier.check(parentNode.callee.property) &&
parentNode.callee.property.name === 'then'
) {
firstExp = node.body.body[0]
}
return false
}
})
复制代码
紧接着,咱们须要建立一个空的箭头函数,并将 firstExp 赋值给它
const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
复制代码
随后,咱们则须要对 CallExpression 类型的 AST 对象进行遍历,并作最后的 MemberExpression 改造工做
recast.visit(ast, {
visitCallExpression (path) {
const { node } = path
const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
const originFunc = callExpression(node.callee, node.arguments)
const catchFunc = callExpression(id('catch'), [arrowFunc])
const newFunc = memberExpression(originFunc, catchFunc)
return false
}
})
复制代码
最后咱们在 CallExpression 遍历的时候将其替换
path.replace(newFunc)
复制代码
第一版的所有代码以下
// promise-catch.js
const recast = require('recast')
const {
identifier: id,
memberExpression,
callExpression,
blockStatement,
arrowFunctionExpression
} = recast.types.builders
const t = recast.types.namedTypes
const code = `this.axiosFetch(this.formData).then(res => { this.loading = false this.handleClose() })`
const ast = recast.parse(code)
let firstExp
recast.visit(ast, {
visitArrowFunctionExpression ({ node, parentPath }) {
const parentNode = parentPath.node
if (
t.CallExpression.check(parentNode) &&
t.Identifier.check(parentNode.callee.property) &&
parentNode.callee.property.name === 'then'
) {
firstExp = node.body.body[0]
}
return false
}
})
recast.visit(ast, {
visitCallExpression (path) {
const { node } = path
const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
const originFunc = callExpression(node.callee, node.arguments)
const catchFunc = callExpression(id('catch'), [arrowFunc])
const newFunc = memberExpression(originFunc, catchFunc)
path.replace(newFunc)
return false
}
})
const output = recast.print(ast).code
console.log(output)
复制代码
执行 node promise-catch.js
,打印台输出结果
this.axiosFetch(this.formData).then(res => {
this.loading = false
this.handleClose()
}).catch(() => {
this.loading = false
})
复制代码
因此能看出来,咱们已是完成了咱们想要完成的样子了
可是咱们还得对一些状况作处理,第一件就是须要在 CallExpression 遍历的时候保证其 arguments 为箭头函数。
紧接着,咱们须要断定咱们获取到的 firstExp 是否存在,由于咱们的 then 处理中能够是一个空的箭头函数。
而后防止 promise 拥有一些自定义的 catch 操做,则须要保证其 property 为 then。
最后为了防止多个 CallExpression 都须要作自动注入的状况,而后其操做又不一样,则须要在其内部进行 ArrowFunctionExpression 遍历操做
通过这些常见状况的兼容后,具体代码以下
recast.visit(ast, {
visitCallExpression (path) {
const { node } = path
const arguments = node.arguments
let firstExp
arguments.forEach(item => {
if (t.ArrowFunctionExpression.check(item)) {
firstExp = item.body.body[0]
if (
t.ExpressionStatement.check(firstExp) &&
t.Identifier.check(node.callee.property) &&
node.callee.property.name === 'then'
) {
const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
const originFunc = callExpression(node.callee, node.arguments)
const catchFunc = callExpression(id('catch'), [arrowFunc])
const newFunc = memberExpression(originFunc, catchFunc)
path.replace(newFunc)
}
}
})
return false
}
})
复制代码
而后因为以后须要作成一个 webpack-loader,用在咱们的实际项目中。因此咱们须要对 parse 的解析器作个替换,其默认的解析器为 recast/parsers/esprima
,而通常咱们项目中都会用到 babel-loader
,因此咱们这也须要将其解析器改成 recast/parsers/babel
const ast = recast.parse(code, {
parser: require('recast/parsers/babel')
})
复制代码
到这里,咱们对于代码的 AST 改造已是完成了,可是如何将其运用到咱们的实际项目中呢?
OK,这个时候咱们就须要本身写一个 webpack loader 了。
其实,关于如何开发一个 webpack loader,webpack 官方文档 已经讲的很清楚了,下面我为小伙伴们作个小总结。
首先,你须要本地新建你开发 loader 的文件,好比,咱们这将其丢到 src/index.js
下,webpack.config.js
配置则以下
const path = require('path')
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
use: [
// ... 其余你须要的 loader
{ loader: path.resolve(__dirname, 'src/index.js') }
]
}
]
}
}
复制代码
src/index.js
内容以下
const recast = require('recast')
const {
identifier: id,
memberExpression,
callExpression,
blockStatement,
arrowFunctionExpression
} = recast.types.builders
const t = recast.types.namedTypes
module.exports = function (source) {
const ast = recast.parse(source, {
parser: require('recast/parsers/babel')
})
recast.visit(ast, {
visitCallExpression (path) {
const { node } = path
const arguments = node.arguments
let firstExp
arguments.forEach(item => {
if (t.ArrowFunctionExpression.check(item)) {
firstExp = item.body.body[0]
if (
t.ExpressionStatement.check(firstExp) &&
t.Identifier.check(node.callee.property) &&
node.callee.property.name === 'then'
) {
const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
const originFunc = callExpression(node.callee, node.arguments)
const catchFunc = callExpression(id('catch'), [arrowFunc])
const newFunc = memberExpression(originFunc, catchFunc)
path.replace(newFunc)
}
}
})
return false
}
})
return recast.print(ast).code
}
复制代码
而后,搞定收工。
这里我在之前的文章中说起过,这里不谈了。若是还没搞过 npm 发包的小伙伴,能够点击下面连接自行查看
OK,到这一步,个人 promise-catch-loader
也是已经开发完毕。接下来,只要在项目中使用便可
npm i promise-catch-loader -D
复制代码
因为个人项目是基于 vue-cli3.x 构建的,因此我须要在个人 vue.config.js
中这样配置
// js 版本
module.exports = {
// ...
chainWebpack: config => {
config.module
.rule('js')
.test(/\.js$/)
.use('babel-loader').loader('babel-loader').end()
.use('promise-catch-loader').loader('promise-catch-loader').end()
}
}
// ts 版本
module.exports = {
// ...
chainWebpack: config => {
config.module
.rule('ts')
.test(/\.ts$/)
.use('cache-loader').loader('cache-loader').end()
.use('babel-loader').loader('babel-loader').end()
.use('ts-loader').loader('ts-loader').end()
.use('promise-catch-loader').loader('promise-catch-loader').end()
}
}
复制代码
而后我项目里面拥有如下 promise 操做
<script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import { Action } from 'vuex-class' @Component export default class HelloWorld extends Vue { loading: boolean = false city: string = '上海' @Action('getTodayWeather') getTodayWeather: Function getCityWeather (city: string) { this.loading = true this.getTodayWeather({ city: city }).then((res: Ajax.AjaxResponse) => { this.loading = false const { low, high, type } = res.data.forecast[0] this.$message.success(`${city}今日:${type} ${low} - ${high}`) }) } } </script>
复制代码
而后在浏览器中查看 source 能看到以下结果
关于代码,我已经托管到 GitHub 上了,promise-catch-loader
到这步,咱们的实战环节也已是结束了。固然,文章只是个初导篇,更多的类型还得小伙伴本身去探究。
AST 它的用处还很是的多,好比咱们熟知的 Vue,它的 SFC(.vue) 文件的解析也是基于 AST 去进行自动解析的,即 vue-loader,它保证咱们能正常的使用 Vue 进行业务开发。再好比咱们经常使用的 webpack 构建工具,也是基于 AST 为咱们提供了合并、打包、构建优化等很是实用的功能的。
总之,掌握好 AST,你真的能够作不少事情。
最后,但愿文章的内容可以帮助小伙伴了解到:什么是 AST?如何借助 AST 让咱们的工做更加效率?AST 又能为前端工程化作些什么?
若是以为文章不错,那么但愿你能动动你的小手,帮忙点个赞,谢谢了 ~
前端交流群:731175396
前端公众号