babel使用及分析
参考资料
1、ast查看链接(opens new window)
2、bable官网(opens new window)
3、AST详解与运用(opens new window)
4、babel插件说明
babel是一个js编译器,是一个工具链,用于将es2015+版本的代码转换为向后兼容的js语法,可以做:
- 语法转换
- 添加目标环境中缺少的polyfill功能
- 源代码转换
提供了插件功能,一切功能都可以以插件来实现,方便使用和弃用。
1、工具包
- @babel/parser:转化为 AST 抽象语法树;
- @babel/traverse:对 AST 节点进行递归遍历;
- @babel/generator:AST抽象语法树生成为新的代码
- @babel/core:内部核心的编译和生成代码的方法,上面三个集合,一般使用这个包
- @babel/types:判断 AST 节点类型以及创建新节点的工具类
- @babel/cli:babel命令行工具内部解析相关方法
- @babel/preset-env: babel编译结果预设值,使用can i use网站作为基设
- @babel/polyfill:es6语法的补丁,安装了所有符合规范的 polyfill之后,我们需要在组件引用这个模块,就能正常的使用规范中定义的方法了。
2、使用
- 安装@babel/core和@babel/cli即可使用命令行解析工具
- 输出编译代码compiler:babel index.js -o output.js
- 使用preset预设,配置.babelrc中presets属性,适用于语法层面范畴
- 使用polyfill,需要在代码中引入polyfill模块,给所有方法打补丁,保证运行正常,适用于方法层面,polyfill通常需要 --save,其他使用--save-dev即可
- babel执行顺序:plugins先执行、再执行预设presets
几个重要概念:
- preset:预设,是一组用于支持特定语言功能的插件,主要用于对语法进行转换
- polyfill:给方法打补丁,保证运行正常,适用于方法层面
- transform-runtime:将api进行私有化,防止引入外部库冲突,eg:_promise\
// presets预设使用
// index.js
var func = () => console.log("hello es6");
var { a, b = 1 } = { a: "this is a" }
// .babelrc配置,presets预设1
var babelrc = {
"presets": [
"@babel/preset-env"
]
}
// 输出
"use strict"
var func = function func() {
return console.log("hello es6");
}
var _a = {
a: "this is a"
},
a = _a.a,
_a$b = _a.b,
b = _a$b === void 0 ? 1 : _a$b
// .babelrc配置,presets预设2
var babelrc = {
"presets": [
["@babel/preset-env", {
"targets": ">1.5%"
}]
]
}
// 输出:箭头函数和解构未转换
"use strict"
const func = () => console.log("hello es6");
const {
a,
b = 1
} = {
a: "this is a"
}
import "@babel/polyfill";
var array = [1, 2, 3];
console.log(array.includes(2));
// 输出
"use strict"
require("@babel/polyfill") // 加载了全部polyfill
var array = [1, 2, 3]
console.log(array.includes(2));
// 按需加载
// .babelrc配置
var babelrc = {
"presets": [
["@babel/preset-env", {
"targets": ">1.5%",
"useBuiltIns": "usage", // 按需加载
"corejs": 3 // 指定corejs版本
}]
]
}
// index.js,去除import
var array = [1, 2, 3]
console.log(array.includes(2));
// 输出
"use strict"
require("core-js/modules/es.array.includes.js")
var array = [1, 2, 3]
console.log(array.includes(2));
解析:@babel/preset-env中useBuiltIns 说明
- false:此时不对 polyfill 做操作。如果引入 @babel/polyfill,则无视配置的浏览器兼容,引入所有的 polyfill,默认选项
- entry:根据配置的浏览器兼容,引入浏览器不兼容的 polyfill。需要在入口文件手动添加 import '@babel/polyfill',会自动根据 browserslist 替换成浏览器不兼容的所有 polyfill,这里需要指定 **core-js **的版本
- usage:会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加
3、babel 处理步骤
- 解析:接收代码并输出AST(抽象语法树)
- 词法分析:把字符串形式的代码转换为令牌(tokens)流,令牌看作是一个扁平的语法片段数组
- 语法分析:把 一个令牌流转换为AST,使用令牌中的 信息把它们转换成一个 AST 的表述解构
- 转换:接收 AST 并对其 遍历,在此过程中对节点进行添加、更新和移除等操作。这是Babel或是其他编译器中最复杂的 过程,同时也是插件将要介入工作的部分
- 生成:把最终的AST转换成字符串形式的代码,同时创建源码映射(source maps)。代码 生成过程:深度优先遍历整个AST,然后构建可以表示转化后代码的字符串
4、手写babel原理
(add 2 (subtract 40 2)) 编译成 add(2, subtract(40, 2))
静态编译:字符串 -> 字符串
思路:正则匹配、状态机、编译器处理流程(解析、转换、生成)
- 分词:将表达式分词,水平状态
/*
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '2' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '40' },
{ type: 'number', value: '2' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' },
]
*/
function generateToken(str) {
let current = 0; // 下标
let tokens = []; // 记录分词列表
while (current < str.length) {
let char = str[current];
// 括号分词:记录为词语
if (char === "(") {
tokens.push({
// 末尾添加对象返回长度,pop删除数组最后一项,返回元素,栈方法FILO
type: "paren",
value: "("
});
current++;
continue;
}
// 括号分词:记录为词语
if (char === ")") {
tokens.push({
type: "paren",
value: ")"
});
current++;
continue;
}
// 空格分词:直接跳过
if (/\s/.test(char)) {
current++;
continue;
}
// 数字分词:二次遍历
if (/[0-9]/.test(char)) {
let numberValue = "";
while (/[0-9]/.test(char)) {
numberValue += char;
char = str[++current];
}
tokens.push({
type: "number",
value: numberValue
});
continue;
}
// 字符串分词
if (/[a-z]/.test(char)) {
let strValue = "";
while (/[a-z]/.test(char)) {
strValue += char;
char = str[++current];
}
tokens.push({
type: "name",
value: strValue
});
continue;
}
throw new TypeError("type error");
}
return tokens;
}
- 生成ast:垂直结构,estree规范
/*
json.cn 可查看json信息
{
"type": "Program",
"body": [
{
"type": "CallExpression",
"name": "add",
"params": [
{
"type": "NumberLiteral",
"value": 2
},
{
"type": "CallExpression",
"name": "subtract",
"params": [
{
"type": "NumberLiteral",
"value": "40"
},
{
"type": "NumberLiteral",
"value": "2"
}
]
}
]
}
]
}
*/
function generateAST(tokens) {
let current = 0;
let ast = {
type: "Program",
body: []
};
// 闭包处理
function walk() {
let token = tokens[current];
if (token.type === "number") {
current++;
return {
type: "NumberLiteral",
value: token.value
};
}
// 左括号为层级开始,为执行语句
if (token.type === "paren" && token.value === "(") {
token = tokens[++current];
let node = {
type: "CallExpression",
name: token.value,
params: []
};
token = tokens[++current];
while (
token.type !== "paren" ||
(token.type === "paren" && token.value !== ")")
) {
node.params.push(walk()); // 递归调用
token = tokens[current]; // 取当前值即可,walk()里完成指针移动
}
current++;
return node;
}
}
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
- 遍历ast,转化为新的ast
/*
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "add"
},
"arguments": [
{
"type": "NumberLiteral",
"value": "2"
},
{
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "subtract"
},
"arguments": [
{
"type": "NumberLiteral",
"value": "40"
},
{
"type": "NumberLiteral",
"value": "2"
}
]
}
]
}
}
]
}
*/
function transformer(ast) {
let newAst = {
type: "Program",
body: []
};
ast._context = newAst.body; // ast子元素挂载
// 类似于babel插件功能
DFS(ast, {
// 生命周期,enter、exit
NumberLiteral: {
enter(node, parent) {
// 父元素记录子元素值,为父元素CallExpression做准备
parent._context.push({
type: "NumberLiteral",
value: node.value
});
}
},
// NumberLiteral的父元素为CallExpression
CallExpression: {
enter(node, parent) {
let expression = {
type: "CallExpression",
callee: {
type: "Identifier",
name: node.name
},
arguments: []
};
// a.子元素值赋值到父元素的arguments去
node._context = expression.arguments;
// b.二次操作
if (parent.type !== "CallExpression") {
expression = {
type: "ExpressionStatement",
expression: expression
};
}
parent._context.push(expression);
}
}
});
return newAst;
}
function DFS(ast, visitor) {
// 遍历子元素数组
function traverseArray(children, parent) {
children.forEach(child => tranverseNode(child, parent));
}
function tranverseNode(node, parent) {
let methods = visitor[(node, type)];
if (methods && methods.enter) {
methods.enter(node, parent);
}
switch (node.type) {
case "Program": {
// 子元素body,父元素node
traverseArray(node.body, node);
break;
}
case "CallExpression": {
// 子元素params,父元素node
traverseArray(node.params, node);
break;
}
case "NumberLiteral": {
break;
}
default: {
break;
}
}
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
return tranverseNode(ast, null);
}
- 基于ast,生成代码
// add(2, subtract(40, 2))
function generate(ast) {
switch(ast.type) {
case "Identifier": return ast.name
case "NumberLiteral": return ast.value
// 每个子元素一行展示
case "Program": return ast.body.map(subAst => generate(subAst)).join('\n')
case "ExpressionStatement": return generate(ast.expression) + ";"
// 函数调用形式 add(参数, 参数, 参数)
case " CallExpression": return generate(ast.callee) + "(" + ast.arguments.map(arg => generate(arg)).join(', ') + ")"
default: break
}
}
5、插件添加及使用
1、类型
babel插件分为语法插件和转换插件:
- 语法插件:syntax plugin,在@babel/parser中加载,在parser过程中执行的插件,例如:@babel/plugin-syntax-jsx
- 转换插件:transform plugin,在@babel/transform中加载,在transform过程中执行的插件
2、插件思路
- 做什么插件:自己做什么事情以及受益
- 分析ast:比对原始数据与最终转换为的数据两个ast的不同,来找到所需操作的transform方法
- 参考手册进行开发:babel官网插件开发手册、@babel/types手册、estree规范手册
3、相关概念
babel插件为一个函数或者对象,若为函数:入参使用types对象,出参一个对象,输出对象中有visitor属性。
- types对象:每个单一类型节点的定义,包括节点的属性、遍历等信息。
- visitor:插件的主要访问者,visitor是一个对象,包含各种类型节点 的访问函数,接收 state和path参数
- path:表示两个节点之间连接的对象,这个对象包含当前节点和父节点的信息以及添加、修改、删除节点有关的方法
- 属性
- node:当前节点
- parent:父节点
- parentPath:父path
- scope:作用域
- context:上下文
- 方法:
- get:获取当前节点
- getSibling:获取兄弟节点
- findParent:向父节点搜寻节点
- replaceWith:用ast节点替换该节点
- repalceWithMultiple:用多个ast节点替换该节点
- insetBefore:在节点前插入节点
- insetAfter:在节点后插入节点
- remove:删除节点
- 属性
- state:visitor对象中每次访问节点方法时传入的第二个参数。包含当前plugin的信息、scope作用域信息、plugin传入的配置参数信息 ,当前节点的path信息。可以把babel插件处理过程中的自定义状态存储到state对象中。
- Scope:与js中作用域类似,如函数内外的同名变量需要区分开来。
- Bindings:所有引用属于特定的作用域,引用和作用域的这种关系称作为绑定。
4、实战
- 将字符串中的+转换为-操作符号:
// input.js
1 + 1;
// output.js
1 - 1;
// plugin.js
export default function({types: t}) {
return {
visitor: {
BinaryExpression(path) {
path.node.operator = "-"
}
}
}
}
// .babelrc
var babelrc = {
"plugins": [
["./plugin"]
]
}
2、去除代码中的console函数调用
const { transform } = require("@babel/core");
const test = "const a = 1; console.log('woshi');let b = 2; console.log('haha');let c=3";
const myPlugins = {
name: "myPlugins",
visitor: {
CallExpression(path) {
if(path.get('cellee').isMemberExpression()) {
if(path.get('callee').get('object').isIndentifier()) {
if(path.get('callee').get('object').get('name') == 'console') {
path.remove
}
}
}
}
}
};
var newCode = transform(test, {
plugins: [myPlugins]
})
console.log(newCode.code);
3、扩展场景:组件按需引用,提升LCP
import { button, nav } from "elementUi";
// 转换为:import button from 具体路径
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)