文章目录
babel开发
前置知识
学习 babel 前,必须要了解的核心概念就是 AST。
同时,希望你的项目中已经安装好了 babel 相关依赖。
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npm install --save @babel/polyfill
什么是 AST ?
来自百科的解释:
AST 长什么样?
对 AST 有个基本的理解后,那 AST 到底长什么样?
astexplorer.net 这个网站可以在线生成AST, 我们可以在里面进行尝试生成AST,用来学习一下 AST 的结构。
如下图:左边是代码,仅声明常量 const a = 1;右边是 AST 结构
babel处理过程
在了解完 AST 后,我们可以开始进入 babel 的学习了。
首先,babel 作为我们即熟悉又陌生的工具,它帮我们处理代码的时候,大致分为以下几步。
- 解析,对代码进行 AST 编译,得到代码的 AST 结构
- 转换,按我们的要求,对代码的 AST 进行处理,得到处理后的 AST 结构
- 生成,将 AST 转回成并生成我们的目标代码
解析
通过 parser 把源码转换成抽象语法书 AST 。
这个阶段的主要任务就是把代码转成 AST,其中经过两个阶段,一个是词法解析和语法解析。当 parser 阶段开始时,首先会进行文档扫描,并在此期间进行词法分析。举例:“const a = 1” 会被词法分析拆解为颗粒度最细的标记(tokens): “const”、“a”、"="、“1”。
const a = 1
AST 结构为例:
{
"type":"File","start":0,"end":11,"loc":{
"start":{
"line":1,"column":0},"end":{
"line":1,"column":11}},"errors":[],"program":{
"type":"Program","start":0,"end":11,"loc":{
"start":{
"line":1,"column":0},"end":{
"line":1,"column":11}},"sourceType":"script","interpreter":null,"body":[{
"type":"VariableDeclaration","start":0,"end":11,"loc":{
"start":{
"line":1,"column":0},"end":{
"line":1,"column":11}},"declarations":[{
"type":"VariableDeclarator","start":6,"end":11,"loc":{
"start":{
"line":1,"column":6},"end":{
"line":1,"column":11}},"id":{
"type":"Identifier","start":6,"end":7,"loc":{
"start":{
"line":1,"column":6},"end":{
"line":1,"column":7},"identifierName":"a"},"name":"a"},"init":{
"type":"NumericLiteral","start":10,"end":11,"loc":{
"start":{
"line":1,"column":10},"end":{
"line":1,"column":11}},"extra":{
"rawValue":1,"raw":"1"},"value":1}}],"kind":"const"}],"directives":[]},"comments":[]}
我截图了关键部分,如下图所示,包裹的外层 type
为 VariableDeclaration
,从单词意思可得知是声明,所使用的 kind
是 const
类型声明。而字段 declarations
描述中,还有 VariableDeclarator
声明对象,id 为声明对象的名称,同样也是个对象,它的 name 才是声明对象名称的值,init 为声明对象初始化的值(还是对象),里面的 value 才是这个初始值。
除了上面所说的字段外,还有包括第几行,第几列,值类型等详细信息。这就是我们所得到的 AST。
const parser = require('@babel/parser');
const ast = parser.parse('const a = 1'); // 转换成AST
更多信息可以访问官方文档查看 @babel/parser
转换
在 parse 解析阶段,我们已经成功得到 AST 了。babel 接受到 AST 后,会使用 @babel/traverse 对其进行深度优先遍历,插件会在这个阶段被触发,以 visitor 函数的形式,访问每种不同类型的 AST 节点。已上述为例,我们可以使用 VariableDeclaration
函数对 VariableDeclaration
节点进行回调处理,每个该类型的节点,都会触发这个函数回调:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default
const ast = parser.parse('const a = 1'); // 转换成AST
traverse(ast, {
VariableDeclaration(path, state) {
// 操作处理...
}
});
这个函数有两个形参:
path
path为当前访问的路径, 包含了节点的信息、父节点信息以及对节点操作的方法。可以利用这些方法,对 ATS 进行添加、更新、移动和删除等等操作。
state
state包含了当前plugin插件的信息和参数信息等等,并且也可以用来自定义在节点之间传递数据。
生成
最后的阶段就是 generate,把转换后的 AST 打印成目标代码,并生成 sourcemap
这个阶段就比较简单了, 在转换阶段处理 AST 结束后,将 AST 转换回 code, 在此期间会对 AST 进行深度优先遍历,根据节点所包含的信息生成对应的代码,并且会生成对应的 sourcemap。
实例操作
代码学习不能只会看,必须得亲自动手,才能深度的学习。接下来我们要写一个最简单的例子,就是把 ES6 中的 const
转变为 ES5 的 var
根据上面的步骤,我们逐步进行:
解析得到 AST
使用 @babel/parser
生成AST
比较简单,跟上面的案例是一样的,其中 ast 常量就是转换后的 AST
const parser = require('@babel/parser');
const ast = parser.parse('const a = 1'); // 转换成AST
转换处理 AST
使用 @babel/traverse
处理 AST
在这个阶段,我们通过分析所生成的 AST 结构,在 VariableDeclaration 回调中确定 kind 字段会有 const,我们只需将 kind 字段修改为 var 即可。
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default
const ast = parser.parse('const a = 1'); // 转换成AST
traverse(ast, {
// 声明变量都会触发这个VariableDeclaration函数
VariableDeclaration(path, state) {
// 通过 path.node 访问实际的 AST 节点
path.node.kind = 'var'
}
});
生成代码
最后一步,则是使用 @babel/generator
把处理好的 AST 转换回我们的代码。
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const ast = parser.parse('const a = 1'); // 转换成AST
// 转换回code代码
traverse(ast, {
// 声明变量都会触发这个VariableDeclaration函数
VariableDeclaration(path, state) {
// 通过 path.node 访问实际的 AST 节点
path.node.kind = 'var'
}
});
// 将处理好的 AST 放入 generate
const transformedCode = generate(ast).code
console.log(transformedCode,"new Code")
最后打印出来的 new Code 结果如下图:
如何开发成插件使用
从上面步骤分析可得知,我们重点关注的其实就是转换处理这个阶段。所以开发插件时,我们也只需关注这个阶段就行,另外两步webpack的插件配置已经帮我们做好了。而插件最终只需导出一个函数,该函数需要返回一个对象,而我们只需要改对象的 visitor ,这里就类似 traverse 转换处理阶段即可。
既然是函数,当然会接受几个参数:
- api, 继承了babel提供的一系列方法
- options, 是我们使用插件时所传递的参数
- dirname, 为处理时期的文件路径
module.exports = (api, options, dirname) => {
return {
visitor: {
VariableDeclaration(path, state) {
path.node.kind = 'var'
}
}
}
}
如何使用我们开发的插件
既然我们开发好了插件,那该怎么使用呢?
当然是通过 babel 和 webpack 去使用啦!
1、首先全局安装 webpack 和 局部安装 babel-loader
既然要使用 webpack ,我们的开发环境自然就需要 webpack 指令(有了就跳过)
npm install webpack -g
还需要 babel-loader 在webpack中,帮助我们加载项目文件
npm install babel-loader --save
2、编写 webpack.config.js 配置文件
通过设置 webpack 相关配置,例如:指定入口文件,使用的加载器,输出文件地址等。
代码如下:
// webpack.config.js
module.exports = {
mode: "development", // 这里使用开发模式,方便查看输出文件内容
entry: './main.js',
// output: {
// filename: 'bundle.js' // 可指定输出路径
// },
module: {
rules: [{
test: /\.js?$/,
use: ['babel-loader']
}]
}
}
3、编写 babel 插件配置
第2步已经确定 webpack 会走 babel-loader 加载后,我们就开始配置 babel-loader 的配置。babel 的配置方式有很多种,具体我不细说,这里我就用可共用的方法,通过 .babelrc 文件去设置。
// .babelrc 文件
{
"plugins":[
["./myplugin.js"] // 这里以相对路径的方式引入自己写的插件
]
}
4、执行webpack打包编译
在 webpack.config.js 目录下,输入如下指令
webpack
5、查看结果
入口文件内容:
打包后输出文件内容:
可以看到,代码是已经成功被转换了。
本次案例的代码 GitHub地址