当学习成为了习惯,知识也就变成了常识。感谢各位的 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github,欢迎 Watch 和 Star。
简介
了解 Babel
插件基本知识,理解按需加载的内部原理,再也不怕面试官问我按需加载的实现原理了。
import {
Button } from 'element-ui'
怎么就变成了
var Button = require('element-ui/lib/button.js')
require('element-ui/lib/theme-chalk/button.css')
为了找到答案,分两步来进行,这也是自己学习的过程:
-
babel 插件入门,编写 babel-plugin-lyn 插件
-
解读
babel-plugin-component
源码,从源码中找到答案
babel 插件入门
这一步我们去编写一个babel-plugin-lyn
插件,这一步要达到的目的是:
-
理解
babel
插件做了什么 -
学会分析
AST
语法树 -
学会使用基本的
API
-
能编写一个简单的插件,做基本的代码转换
有了以上基础我们就可以尝试去阅读babel-plugin-component
源码,从源码中找到我们想要的答案
简单介绍
Babel
是一个JavaScript
编译器,是一个从源码到源码的转换编译器,你为Babel
提供一些JavaScript
代码,Babel
按照要求更改这些代码,然后返回给你新生成的代码。
代码转换(更改)的过程中是借助AST (抽象语法树)
来完成的,通过改变AST
节点信息来达到转换代码的目的,到这里其实也就可以简单回答出我们在目标中提到的代码转化是怎么完成的 ?
,其实就是Babel
读取我们的源代码,将其转换为AST
,分析AST
,更改AST
的某些节点信息,然后生成新的代码,就完成了转换过程,而具体是怎么更改节点信息,就需要去babel-plugin-component
源码中找答案了
在Babel
的世界中,我们要更改某个节点的时候,就需要去访问(拦截)该节点,这里采用了访问者模式
,访问者
是一个用于AST
遍历的跨语言的模式,加单的说就是定义了一个对象,用于在树状结构获取具体节点的的方法,这些节点其实就是AST
节点,可以在 AST Explorer 中查看代码的AST
信息,这个我们在编写代码的时候会多次用到
babel-plugin-lyn
接下来编写一个自己的插件
初始化项目目录
mkdir babel-plugin && cd babel-plugin && npm init -y
新建插件目录
在项目的node_modules
目录新建一个文件夹,作为自己的插件目录
mkdir -p node_modules/babel-plugin-lyn
在插件目录新建 index.js
touch index.js
创建需要被处理的 JS 代码
在项目根目录下创建 index.js,编写如下代码
let a = 1
let b = 1
很简单吧,我们需要将其转换为:
const aa = 1
const bb = 1
接下来进行插件编写
babel-plugin-lyn/index.js
基本结构
// 函数会有一个 babelTypes 参数,我们结构出里面的 types
// 代码中需要用到它的一些方法,方法具体什么意思可以参考
// https://babeljs.io/docs/en/next/babel-types.html
module.exports = function ({
types: bts }) {
// 返回一个有 visitor 的对象,这是规定,然后在 visitor 中编写获取各个节点的方法
return {
visitor: {
...
}
}
}
分析源代码
有了插件的基本结构之后,接下来我们需要分析我们的代码,它在AST
中长什么样
如下图所示:
用鼠标点击需要更改的地方,比如我们要改变量名,则点击以后会看到右侧的AST tree
展开并高亮了一部分,高亮的这部分就是我们要改的变量a
的AST
节点,我们知道它是一个Identifier
类型的节点,所以我们就在visitor
中编写一个Identifier
方法
module.exports = function ({
types: bts }) {
return {
visitor: {
/**
* 负责处理所有节点类型为 Identifier 的 AST 节点
* @param {*} path AST 节点的路径信息,可以简单理解为里面放了 AST 节点的各种信息
* @param {*} state 有一个很重要的 state.opts,是 .babelrc 中的配置项
*/
Identifier (path, state) {
// 节点信息
const node = path.node
// 从节点信息中拿到 name 属性,即 a 和 b
const name = node.name
// 如果配置项中存在 name 属性,则将 path.node.name 的值替换为配置项中的值
if (state.opts[name]) {
path.node.name = state.opts[name]
}
}
}
}
}
这里我们用到了插件的配置信息,接下来我们在.babelrc
中编写插件的配置信息
.babelrc
{
"plugins": [
[
"lyn",
{
"a": "aa",
"b": "bb"
}
]
]
}
这个配置项是不是很熟悉?和babel-plugin-component
的及其相似,lyn
表示 babel 插件的名称,后面的对象就是我们的配置项
输出结果
首先安装 babel-cli
这里有一点需要注意,在安装 babel-cli 之前,把我们编写的插件备份,不然执行下面的安装时,我们的插件目录会被删除,原因没有深究,应该是我们的插件不是一个有效的 npm 包,所以会被清除掉
npm i babel-cli -D
编译
npx babel index.js
得到如下输出:
let aa = 1;
let bb = 1;
说明我们的插件已经生效,且刚才的思路是没问题的,转译代码其实就是通过更改 AST
节点的信息即可
let -> const
我们刚才已经完成了变量的转译,接下来再把let
关键字变成const
按照刚才的方法,我们需要更改关键字let
,将光标移动到let
上,发现AST Tree
高亮部分变了,可以看到let
的AST
节点类型为VariableDeclaration
,且我们要改的就是kind
属性,好了,开始写代码
module.exports = function ({
types: bts }) {
return {
visitor: {
Identifier (path, state) {
...
},
// 处理变量声明关键字
VariableDeclaration (path, state) {
// 这次就没从配置文件读了,来个简单的,直接改
path.node.kind = 'const'
}
}
}
}
编译
npx babel index.js
得到如下输出:
const aa = 1;
const bb = 1;
到这里我们第一阶段的入门就结束了,是不是感觉很简单??是的,这个入门示例真的很简单,但是真的编写一个可用于业务Babel
插件以及其中的涉及到的AST
和编译原理
是非常复杂的。但是这个入门示例已经可以支持我们去分析babel-plugin-component
插件的源码原理了。
完整代码
// 函数会有一个 babelTypes 参数,我们结构出里面的 types
// 代码中需要用到它的一些方法,方法具体什么意思可以参考
// https://babeljs.io/docs/en/next/babel-types.html
module.exports = function ({
types: bts }) {
// 返回一个有 visitor 的对象,这是规定,然后在 visitor 中编写获取各个节点的方法
return {
visitor: {
/**
* 负责处理所有节点类型为 Identifier 的 AST 节点
* @param {*} path AST 节点的路径信息,可以简单理解为里面放了 AST 节点的各种信息
* @param {*} state 有一个很重要的 state.opts,是 .babelrc 中的配置项
*/
Identifier (path, state) {
// 节点信息
const node = path.node
// 从节点信息中拿到 name 属性,即 a 和 b
const name = node.name
// 如果配置项中存在 name 属性,则将 path.node.name 的值替换为配置项中的值
if (state.opts[name]) {
path.node.name = state.opts[name]
}
},
// 处理变量声明关键字
VariableDeclaration (path, state) {
// 这次就没从配置文件读了,来个简单的,直接改
path.node.kind = 'const'
}
}
}
}
babel-plugin-component 源码分析
目标分析
在进行源码阅读之前我们先分析一下我们的目标,带着目标去阅读,效果会更好
源代码
// 全局引入
import ElementUI from 'element-ui'
Vue.use(ElementUI)
// 按需引入
import {
Button, Checkbox } from 'element-ui'
Vue.use(Button)
Vue.component(Checkbox.name, Checkbox)
上面就是我们使用element-ui
组件库的两种方式,全局引入和按需引入
目标代码
// 全局引入
var ElementUI = require('element-ui/lib')
require('element-ui/lib/theme-chalk/index.css')
Vue.use(ElementUI)
// 按需引入
var Button = require('element-ui/lib/button.js')
require('element-ui/lib/theme-chalk/button.css')
var Checkbox = require('element-ui/lib/checkbox.js')
require('element-ui/lib/theme-chalk/checkbox.css')
Vue.use(Button)
Vue.component(Checkbox.name, Checkbox)
以上就是源代码和转译后的目标代码,我们可以将他们分别复制到 AST Explorer 中查看 AST Tree
的信息,进行分析
全局引入
从上图中可以看出,这两条语句总共是由两种类型的节点组成,import
对应的ImportDeclaration
的节点,Vue.use(ElementUI)
对应于ExpressionStatement
类型的节点
可以看到import ElementUI from 'element-ui'
对应到AST
中,from
后面的element-ui
对应于source.value
,且节点类型为StringLiteral
而import ElementUI from 'element-ui'
中的ElementUI
对应于ImportDefaultSpecifier
类型的节点,是个默认导入,变量对应于Indentifier
节点的name
属性
Vue.use(ElementUI)
是个声明式的语句,对应于ExpressionStatement
的节点,可以看到参数ElementUI
放到了arguments
部分