JavaScript Parser
JavaScript Parser是把JavaScript源码转化为抽象语法树的解析器
- esprima
- traceur
- acorn
- shift
可以在这些Parser提供的一系列回调中添加一些操作,来改变js源码:
//它可以把源代转成抽象语法树
let esprima = require('esprima');
//它可遍历语法权树,修改树上的语法节点
let estraverse = require('estraverse');
let escodegen = require('escodegen');
let sourceCode = 'function ast(){}';
let ast = esprima.parse(sourceCode);
let indent = 0;
const padding = () => ` `.repeat(indent);
let visitor = {
enter(node, parent) {
console.log(padding() + node.type);
if (node.type === 'FunctionDeclaration') {
node.id.name = 'newFunction';
}
indent++;
},
leave(node, parent) {
indent--;
console.log(padding() + node.type);
}
}
estraverse.traverse(ast,visitor)
//重新生成源代码
let newSourceCode = escodegen.generate(ast);
console.log(newSourceCode);
ast在webpack中的应用
babel就可以通过上述ast转换parser来实现
- 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
-
- 如 JSLint、JSHint 对代码错误或风格的检查,发现一些潜在的错误
- IDE 的错误提示、格式化、高亮、自动补全等等
- 代码混淆压缩
-
- UglifyJS2 等
- 优化变更代码,改变代码结构使达到想要的结构
-
- 代码打包工具 webpack、rollup 等等
- CommonJS、AMD、CMD、UMD 等代码规范之间的转化
- CoffeeScript、TypeScript、JSX 等转化为原生 Javascript
babel相关插件及功能
- @babel/parser 可以把源码转换成AST
- @babel/traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点
- @babel/generate 可以把AST生成源码,同时生成sourcemap
- @babel/types 用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法,对编写处理 AST 逻辑非常有用
- @babel/template可以简化AST的创建逻辑
- @babel/code-frame可以打印代码位置
- @babel/core Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能
-
- babel-core包括三部分
1.把源代码转成AST语法
2.遍历AST语法树,遍历的时候 会把语法树给插件进行处理。插件可以关注自己感兴趣的类型,进行处理
3.新的AST语法树重新生成源代码
- babylon Babel 的解析器,以前叫babel parser,是基于acorn扩展而来,扩展了很多语法,可以支持es2020、jsx、typescript等语法
- babel-types-api
- Babel 插件手册
- babeljs.io babel 可视化编译器
使用案例(以箭头函数转换为例):
//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
//let arrowFunctionPlugin = require('babel-plugin-transform-es2015-arrow-functions');
let arrowFunctionPlugin = {
visitor: {
//如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
ArrowFunctionExpression(path) {
let node = path.node;
hostFunctionEnvironment(path);
node.type = 'FunctionExpression';
}
}
}
/**
* 1.要在函数的外面声明一个_this变量,值是this
* 2.在函数的内容,换this 变成_this
* @param {*} path
*/
function hostFunctionEnvironment(path) {
//确定我的this变量在哪个环境里生成,向上查找 是普通函数或者是根节点 Program
const thisEnvFn = path.findParent(parent => {
return (parent.isFunction() && !path.isArrowFunctionExpression()) || parent.isProgram();
});
let thisBindings = '_this';
//var _this = this;
if (!thisEnvFn.scope.hasBinding(thisBindings)) {
thisEnvFn.scope.push({
id: types.identifier(thisBindings),//_this
init: types.thisExpression()//this
});
}
//替换this
let thisPaths = getScopeInfo(path);
thisPaths.forEach(thisPath => {
//把this替换成_this
thisPath.replaceWith(types.identifier(thisBindings));
})
}
function getScopeInfo(path) {
let thisPaths = [];
path.traverse({
ThisExpression(path) {
thisPaths.push(path);
}
})
return thisPaths;
}
let sourceCode = `
const sum = (a, b) => {
console.log(this);
const minus = (c,d)=>{
console.log(this);
return c-d;
}
return a + b;
}
`;
let targetSource = core.transform(sourceCode, {
plugins: [arrowFunctionPlugin]
});
console.log(targetSource.code);
上述代码中也实现了一个小型箭头函数的插件,其原理为遍历整个AST,当遇到ArrowFunctionExpression这类节点时将其转换成FunctionExpression类节点
如果要替换this,在外面存一个_this,可以继续拦截ThisExpression钩子
babel插件举例(babel-plugin-import)
babel-plugin-import的作用是做按需加载,例如某个js文件引用了lodash里的两个方法:
import { flatten, concat } from 'lodash';
console.log(flatten, concat);
但打包后整个lodash被引了进来,所以文件很大,babel-plugin-import可以将上述代码转换为
import flatten from 'lodash/flatten';
import concat from 'lodash/concat';
console.log(flatten, concat);
即只引入单个方法
类似的场景还有antd在使用时会直接引antd整个包,我们也可以通过这个插件将其优化成只引用到的组件
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
devtool: false,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
['import', {
//指定要按需加载的模块
libraryName: 'lodash',
//按需加载的目录,默认是lib
libraryDirectory: ''
}]
]
}
}
}
]
}
}
如果想要自己写一个babel插件,可以按如下方式使用:
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
[path.resolve(__dirname, 'import.js'), {
//指定要按需加载的模块
"libraryName": "lodash",
//按需加载的目录,默认是lib
"libraryDirectory": ""
}]
]
}
}
}
]
}
可以看到,在原来写插件名字的地方改成了path.resolve(__dirname, 'import.js'),作为自定义插件的目录
在对应的import.js中,插件定义格式如下:
//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
const visitor = {
ImportDeclaration(path, state) {
const { node } = path;//获取节点
const { specifiers } = node;//获取批量导入声明数组
const { libraryName, libraryDirectory = 'lib' } = state.opts;//获取选项中的支持的库的名称
//如果当前的节点的模块名称是我们需要的库的名称
if (node.source.value === libraryName
//并且导入不是默认导入才会进来
&& !types.isImportDefaultSpecifier(specifiers[0])) {
//遍历批量导入声明数组
const declarations = specifiers.map(specifier => {
//返回一个importDeclaration节点
return types.importDeclaration(
//导入声明importDefaultSpecifier flatten
[types.importDefaultSpecifier(specifier.local)],
//导入模块source lodash/flatten
types.stringLiteral(libraryDirectory ? `${libraryName}/${libraryDirectory}/${specifier.imported.name}` : `${libraryName}/${specifier.imported.name}`)
);
})
path.replaceWithMultiple(declarations);//替换当前节点
}
}
}
module.exports = function () {
return {
visitor
}
}