前言
- 第一篇讲了tapable,这篇记录ast。
- ast是成为大神必会的,曾经我也是非常非常讨厌学这个,毕竟感觉写的没啥成就感。不过这个熟练掌握之后可以写很多插件之类,比如给你的组件写个按需加载插件,所以还是得学!!!
AST
- ast是抽象语法树,webpack和很多工具核心就是通过ast对代码检查和分析。
- 现在把js转换成语法树有很多解析器,每个js引擎有自己的抽象语法树格式。
- babel步骤分为解析、转换、生成。解析步骤分为词法分析和语法分析,词法分析是转成token流,类似扁平语法片段数组。语法分析是转成ast树形式。
- 转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程 同时也是插件将要介入工作的部分。
- 生成步骤深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串,同时创建source map。
箭头函数转换插件
- 写插件前,先打开2个网址备用
- 一个是可视化ast网站,能看清结构。
- 一个是babelAPI官网,可以找到想生成的语法树api。
- 安装:
@babel/core babel-types babel-traverse
- 其中@babel/core就是转语法树的,babel-types用来生成语法树,babel-traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点。
- 下面例子的目的就是把
const sum = (a,b)=>a+b
变为
const sum = function sum(a, b) {
return a + b;
};
代码
let babel = require('@babel/core')
let t = require('babel-types')
const code = `const sum = (a,b)=>a+b`
//找出复用节点
let ArrayFunctionPlugin = {
visitor: {//访问所有节点
ArrowFunctionExpression: (path) => {//函数名是type,如果一段代码没匹配到type,代码就不会传来
let node = path.node //获得当前对应节点
let id = path.parent.id //拿到sum的节点
let params = node.params// 拿到参数节点
let body = t.blockStatement([//语句块
t.returnStatement(node.body)//返回语句 原来式子中的body
])
let functionExpression = t.functionExpression(id, params, body, false, false)//创建function函数 名字就是sum
path.replaceWith(functionExpression)//替换
}
}
}
let result = babel.transform(code, {
plugins: [ArrayFunctionPlugin]
})
console.log(result.code)
-
逻辑不难,需要自己打印下看下就明白,所有代码已注释,其实这个path是相对于你这个type做的树。所以path.parent就会有东西。
-
主要还是能复用就复用,不能复用就造,然后组装,组装完替换。
-
插件选项可以在第二个参数收到。
{
plugins: [
["my-plugin", {
"option1": true,
"option2": false
}]
]
}
转换为插件
- 刚才做了个箭头函数的插件,现在得用起来,这个找了我很长时间,很多地方都说怎么做这个,但是没说咋变成插件。
- 首先还是上面那段,改成:
module.exports = function ({ types: t }) {
return {
visitor: {
ArrowFunctionExpression: (path) => {//函数名是type,如果一段代码没匹配到type,代码就不会传来
let node = path.node //获得箭头表达式下的节点
let id = path.parent.id //拿到sum的节点
let params = node.params// 拿到参数节点
let body = t.blockStatement([//语句块
t.returnStatement(node.body)//返回语句原来式子中的body
])
let functionExpression = t.functionExpression(id, params, body, false, false)//创建function函数 名字就是sum
path.replaceWith(functionExpression)//替换
}
}
};
}
- 然后,我们需要在node_modules里面建个文件夹,叫babel-plugin-myplugin。
- 里面index.js就写成上面那样。
- 这样在.babelrc中就可以配置它了!
- 比如我们就只配我们自己的插件:
{
// "presets": [
// ["@babel/preset-env",{
// "corejs":{ "version": 3,"proposals": true },
// "useBuiltIns":"usage"
// }]
// ],
"plugins": [
[
"myplugin"
]
]
}
- 这样完全没有干扰,使用webpack编译下,然后看编译后的结果。发现已经完美转换了:
/***/ (function(module, exports) {
eval("const sum = function sum(a, b) {\n return a + b;\n};\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
- 把babelrc清空再编译下:
/***/ (function(module, exports) {
eval("const sum = (a, b) => a + b;\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
- 完美说明是我们插件的功劳。
- 注意:plugins 的插件使用顺序是顺序的,而 preset 则是逆序的。
- 除了直接放node_module,还可以用npm link 或者webpack配置resolve目录让webpack找到插件。
按需加载插件
- 以做lodash按需加载为例
- 首先试着
import { flatten, concat } from 'lodash'
这种方式引入,发现打包出的js有552k。 - 然后换成
import flatten from 'lodash/flatten'
import concat from 'lodash/concat'
- 引入发现打包后js有24k。这体积直接小了20倍。
- 所以插件目的就是把一行引入转成2行引入。
- 首先分析一行的写法:
- 一行写法里是再ImportDeclaration节点里,其中的specifiers是数组,里面的2个ImportSpecifier就是flatten与concat。
- 这个ImportSpecifier里面有个imported和local,imported代表它本来导入的名字,local代表它在这个文件里被改名后的名字。所以,我们要变成2行的时候,需要使用Imported而不是Local的名字。
- 再分析下二行的写法:
- 这个body里面就直接写成了2个ImportDeclaration。里面是ImportDefaultSpecifier,是默认导入不是普通导入了。
- 所以,我们需要把1行里普通导入拿出来,拼成2行默认导入,有别名默认导入用别名。模块名加上普通导入的名字。
代码
const t = require('babel-types')
const visitor = {
ImportDeclaration: {
enter(path, state = { opts }) {
let specifiers = path.node.specifiers//拿到数组
let source = path.node.source //拿到最后的lodash
if (!t.isImportDefaultSpecifier(specifiers[0]) && state.opts.libraryName === source.value) {//默认导入不替换,来源名要是配的名字
let importDeclaration = specifiers.map(specifier => {//遍历数组,改成导入语句
return t.importDeclaration([t.importDefaultSpecifier(specifier.local)],
t.stringLiteral(`${source.value}/${specifier.imported.name}`)
)
})
path.replaceWithMultiple(importDeclaration)
}
}
}
}
module.exports = function (babel) {
return { visitor }
}
- 注释已经加上,并不难,讲就是api名字长,还需要自己分析下语法树。
- 测试过无问题,babelrc这么配置:
"plugins": [
[
"myimport",
{
"libraryName": "lodash"
}
]
]
- 可以自己改一下代码试试输出的是啥:验证下别名是否正确。
const t = require('babel-types')
const babel = require('@babel/core')
const visitor = {
ImportDeclaration: {
enter(path, state = { opts }) {
let specifiers = path.node.specifiers//拿到数组
let source = path.node.source //拿到最后的lodash
if (!t.isImportDefaultSpecifier(specifiers[0]) && state.opts.libraryName === source.value) {//默认导入不替换,来源名要是配的名字
let importDeclaration = specifiers.map(specifier => {//遍历数组,改成导入语句
return t.importDeclaration([t.importDefaultSpecifier(specifier.local)],
t.stringLiteral(`${source.value}/${specifier.imported.name}`)
)
})
path.replaceWithMultiple(importDeclaration)
}
}
}
}
const fll = {
visitor
}
const code = `import { flatten as tt , concat } from 'lodash'`
let result = babel.transform(code, {
plugins: [[fll, {
"libraryName": "lodash"
}]]
})
console.log(result.code)
module.exports = function (babel) {
return { visitor }
}