Babel 是一个通用的多功能的 JavaScript 编译器。
浏览器编译你的js代码,需要把js转化成ast。2015年es6语法发布,但浏览器还普遍不支持es6语法。于是催生出babel模块,默认支持js到ast的转化,并通过修改ast,将es6的特性代码转化为同效用的es5代码,同时也提供了ast的操作函数。
Babel操作流程:js代码 -> 原AST -> babel处理 -> 修改后的AST -> 修改后的js代码 -> 交给浏览器编译
即: 解析(parse),转换(transform),生成(generate)
@babel/core
从名称就可以看出这是babel的核心包。首先介绍一下这个包,这个包集成了上篇文章讲述的@babel/parser,@babel/traverse,@babel/generator,@babel/types这些包。也就是说这个包具备解析(parse),转换(traverse),生成代码的能力,而且还扩展了其它功能。
npm install --save-dev @babel/cli @babel/core
AST解析示例
在线js转换到AST工具:https://astexplorer.net/#/
let a = 1
{
"type": "Program",
"start": 0,
"end": 10,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 10,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
visitor(访问者模式)
当谈及“进入”一个节点时,实际上是说我们在访问他们,之所以使用这样的术语是因为有一个访问者模式(visitor)概念
上面AST小节示例有 “type”: “VariableDeclarator” ,诸如此类的树节点在访问时,就会进入visitor对象声明的对应类型的成员方法。此时你访问到的不是节点本身,而是一个Path,所以可以追踪树的父节点等其它信息。
常用写法是取path.node,如上即取到type = VariableDeclarator 的对象
举个🌰
const babel = require("@babel/core")
const code = `a+b+c`
const obj = babel.transformSync(code, {
plugins: [
function MyPlugin(babel) {
return {
visitor: {
Identifier(path) {
console.log('visiting====>',path.node.name)
},
}
}
}
]
});
console.log(obj.code);
#生成对应的AST
{
"type": "Program",
"start": 0,
"end": 5,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 5,
"expression": {
"type": "BinaryExpression",
"start": 0,
"end": 5,
"left": {
"type": "BinaryExpression",
"start": 0,
"end": 3,
"left": {
"type": "Identifier",
"start": 0,
"end": 1,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 2,
"end": 3,
"name": "b"
}
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "c"
}
}
}
],
"sourceType": "module"
}
#代码执行后的结果
visiting====> a
visiting====> b
visiting====> c
下边修改一下代码,改为从文件读取源代码,使用babel.transformFileSync直接读取
举个🌰
//file.js
a+b+c
//index.js
const babel = require("@babel/core")
const path = require("path")
const file = path.resolve(__dirname,'./file.js')
const obj = babel.transformFileAsync(file, {
plugins: [
function MyPlugin(babel) {
return {
visitor: {
Identifier(path) {
console.log('visiting====>',path.node.name)
},
}
}
}
]
});
console.log(obj.code);
从以上讲述清楚的知道了babel插件是如何工作的。以代码的角度分析,其实就是babel/core提供的api有一个plugins配置属性,支持传入自定义的插件而已。
单独抽离插件
在前端项目当中,babel插件都是以独立模块出现。这时候babel.config.js配置文件就派上用场了。transformFileSync第二个参数对象有一个babelrc属性,默认是true。这个属性代表是否从项目根目录读取babelrc文件获取presets和plugins配置。
这时候就已经把babel插件单独抽离出来了。如果你想发布到npm上,你只需要按照发布规范和步骤发布,然后安装下来,在babel.config.js配置就可以了。
接下来 我们编译一段es6 class语法,看看编译后的结果:
code: 'class Person {\n' +
' constructor(name) {\n' +
' this.name = name;\n' +
' }\n' +
' say() {\n' +
' console.log(this.name);\n' +
' }\n' +
'}\n' +
'const person = new Person("张三");\n' +
'person.say();',
细心的小伙伴发现了,编译后的代码没有任何变化,我们之前也讲过babel/core其实只是提供了编译的流程。如果想要处理代码,还需要提供插件(在编译的第二部transform)对节点进行增删改查,才可以修改节点生成最终想要的可执行代码。
这里如果需要处理es6及以上的语法,需要使用babel/preset-env预设包(就是插件的集合),安装该包,修改.babelrc配置
npm install --save-dev @babel/preset-env
module.exports = {
"presets": [
"@babel/preset-env"
],
"plugins":["./plugin.js"]
}
//编译之后的代码
code: '"use strict";\n' +
'\n' +
'function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }\n' +
'function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n' +
'function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }\n' +
'function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }\n' +
'function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }\n' +
'function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }\n' +
'var Person = /*#__PURE__*/function () {\n' +
' function Person(name) {\n' +
' _classCallCheck(this, Person);\n' +
' this.name = name;\n' +
' }\n' +
' _createClass(Person, [{\n' +
' key: "say",\n' +
' value: function say() {\n' +
' console.log(this.name);\n' +
' }\n' +
' }]);\n' +
' return Person;\n' +
'}();\n' +
'var person = new Person("张三");\n' +
'person.say();',
map: null,
sourceType: 'script',
externalDependencies: Set(0) {}
}
由上图可知这个表达式对应CallExpression类型的节点,该节点有一个callee属性对应的是MemberExpression类型的节点,MemberExpression节点又有一个object属性为Identifier类型的节点,只有Identifier节点名字是console才可以判断当前节点是console类型的表达式。然后执行path.remove方法就可以删除当前节点。
代码如下
module.exports=function(babel){
return{
visitor:{
CallExpression(path){
if(path.node.callee&&babel.types.isIdentifier(path.node.callee.object,{name:'console'})){
path.remove()
}
}
}
}
}