需求
在最近的开发过程中,不同的项目、不同的页面都需要用到某种UI控件,于是很自然的将这些UI控件拆出来,单独建立了一个代码库进行维护。下面是我的组件库大致的目录结构如下:
...
- lib
- components
- componentA
- index.vue
- componentB
- index.vue
- componentC
- index.vue
- index.js
...
整个组件库的出口在index.js
,里面的内容差不多是下面这样的:
import A from './lib/componentA';
import B from './lib/componentB';
import C from './lib/componentC';
export {
A,
B,
C
}
我的代码库的name为:kb-bi-vue-component
。在项目中引用这个组件库的时候,代码如下:
import { A, B } from 'kb-bi-vue-component';
....
这个时候,问题出现了,我在页面中,仅仅使用了A
、B
两个组件,但是页面打包后,整个组件库的代码都会被打进来,增加了产出的体积,包括了不少的冗余代码。很容易想到的一个解决方案是按照以下的方式引用组件。
import A from 'kb-bi-vue-component/lib/componentA';
import B from 'kb-bi-vue-component/lib/componentB';
这种方法虽然解决了问题,我想引用哪个组件,就引用哪个组件,不会有多余的代码。但是我总觉得这种写法看起来不太舒服。有没有还能像第一种写法一样引用组件库,并且只引用需要的组件呢?写一个babel-plugin好了,自动将第一种写法转换成第二种写法。
Babel的原理
本文只是简单介绍。想要深入理解代码编译,请学习<<编译原理>>这里有一个不错的Babel教程:https://github.com/jamiebuild...
Babel是Javascript编译器,更确切地说是源码到源码的编译器,通常也叫做『转换编译器』。也就是说,你给Babel提供一些Javascript代码,Babel更改这下代码,然后返回给你新生成的代码。
AST
在这整个过程中,都是围绕着抽象语法树(AST)来进行的。在Javascritp中,AST,简单来说,就是一个记录着代码语法结构的Object。比如下面的代码:
function square(n) {
return n * n;
}
转换成AST后如下,
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
params: [{
type: "Identifier",
name: "n"
}],
body: {
type: "BlockStatement",
body: [{
type: "ReturnStatement",
argument: {
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "n"
},
right: {
type: "Identifier",
name: "n"
}
}
}]
}
}
AST是分层的,由一个一个的 节点(Node) 组成。如:
{
...
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}
{
type: "Identifier",
name: ...
}
每一个节点都有一个必需的 type 字段表示节点的类型。如上面的FunctionDeclaration
、Identifier
等等。每种类型的节点都会有自己的属性。
Babel的工作过程
Babel的处理过程主要为3个:解析(parse)、转换(transform)、生成(generate)。
- 解析
解析主要包含两个过程:词法分析和语法分析,输入是代码字符串,输出是AST。
- 转换
处理AST。处理工具、插件等就是在这个过程中介入,将代码按照需求进行转换。
- 生成
遍历AST,输出代码字符串。
解析和生成过程,都有Babel都为我们处理得很好了,我们要做的就是在 转换 过程中搞事情,进行个性化的定制开发。
开发一个babel-plugin
这里有详细的介绍:https://github.com/jamiebuild...
开发方式概述
首先,需要大致了解一下babel-plugin的开发方法。
babel使用一种 访问者模式 来遍历整棵语法树,即遍历进入到每一个Node节点时,可以说我们在「访问」这个节点。访问者就是一个对象,定义了在一个树状结构中获取具体节点的方法。简单来说,我们可以在访问者中,使用Node的type来定义一个hook函数,每一次遍历到对应type的Node时,hook函数就会被触发,我们可以在这个hook函数中,修改、查看、替换、删除这个节点。说起来很抽象,直接看下面的内容吧。
开始开发吧
- 下面,根据我们的需求,来开发一个plugin。怎么配置使用自己的babel-plugin呢?我的项目中,是使用
.babelrc
来配置babel的,如下:
{
"presets": [
["es2015"],
["stage-0"]
]
}
上面的配置中,只有两个预设,并没有使用插件。首先加上插件的配置。由于是在本地开发,插件直接写的本地的相对地址。
{
"presets": [
["es2015"],
["stage-0"]
],
"plugins":["./my-import-babel-plugin"]
}
仅仅像上面这样是有问题的,因为需求是需要针对具体的library,所以肯定是需要传入参数的。改成下面这样:
{
"presets": [
["es2015"],
["stage-0"]
],
"plugins":[
["./my-import-babel-plugin", { "libraryName": "kb-bi-vue-component", "alias": "kb-bi-vue-component/lib/components"}]
]
}
我们给plugin传了一个参数,libraryName表示需要处理的library,alias表示组件在组件库内部的路径。
- 下面是插件的代码
./my-import-babel-plugin.js
module.exports = function ({ types: t }) {
return {
visitor: {
ImportDeclaration(path, source){
const { opts: { libraryName, alias } } = source;
if (!t.isStringLiteral(path.node.source, { value: libraryName })) {
return;
}
console.log(path.node);
// todo
}
}
}
}
函数的参数为babel对象,对象中的types是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。我们单独把这个types拿出来。返回的visitor就是我们上文提到的访问者对象。这次的需求是对 import 语句的修改,所以我们在visitor中定义了import
的type:ImportDeclaration
。这样,当babel处理到代码里的import
语句时,就会走到这个ImportDeclaration
函数里面来。
这里查看Babel定义的所有的AST Node: https://github.com/babel/babe...
ImportDeclaration
接受两个参数,
-
path
表示当前访问的路径,path.node
就能取到当前访问的Node. -
source
表示PluginPass
,即传递给当前plugin的其他信息,包括当前编译的文件、代码字符串以及我们在.babelrc
中传入的参数等。
在插件的代码中,我们首先取到了传入插件的参数。接着,判断如果不是我们需要处理的library,就直接返回了。
这里可以查看babel.types的使用方法:https://babeljs.io/docs/en/ba...
- 假设我们的业务代码中的代码如下:
...
import { A, B } from 'kb-bi-vue-component'
...
我们运行一下打包工具,输出一下path.node
,可以看到,当前访问的Node如下:
Node {
type: 'ImportDeclaration',
start: 9,
end: 51,
loc: SourceLocation {
start: Position {
line: 10,
column: 0
},
end: Position {
line: 10,
column: 42
}
},
specifiers: [Node {
type: 'ImportSpecifier',
start: 18,
end: 19,
loc: [Object],
imported: [Object],
local: [Object]
},
Node {
type: 'ImportSpecifier',
start: 21,
end: 22,
loc: [Object],
imported: [Object],
local: [Object]
}
],
source: Node {
type: 'StringLiteral',
start: 30,
end: 51,
loc: SourceLocation {
start: [Object],
end: [Object]
},
extra: {
rawValue: 'kb-bi-vue-component',
raw: '\'kb-bi-vue-component\''
},
value: 'kb-bi-vue-component'
}
}
稍微解释一下这个Node. specifiers
是一个数组,包含两个Node,对应的是代码import
后面的两个参数A
和B
。这两个Node的local
值都是Identifier
类型的Node。source
表示的是代码from
后面的library。
-
接下来,按照需求把这个
ImportDeclaration
类型的Node替换掉,换成我们想要的。使用path.replaceWithMultiple
这个方法来替换一个Node。此方法接受一个Node数组。所以我们首先需要构造出Node,装进一个数组里,然后扔给这个path.replaceWithMultiple
方法。查阅文档,
t.importDeclaration(specifiers, source) specifiers: Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier> (required) source: StringLiteral (required)
可以通过
t.importDeclaration
来构造import
Node,参数如上所示。构造import
Node,需要先构造其参数需要的Node。最终,修改插件的代码如下:module.exports = function ({ types: t }) { return { visitor: { ImportDeclaration(path, source) { const { opts: { libraryName, alias } } = source; if (!t.isStringLiteral(path.node.source, { value: libraryName })) { return; } const newImports = path.node.specifiers.map( item => { return t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${alias}/${item.local.name}`)) }); path.replaceWithMultiple(newImports); } } } }
开发基本结束
好了,一个babel-plugin开发完成了。我们成功的实现了以下的编译:
import { A, B } from 'kb-bi-vue-component'; ↓ ↓ ↓ ↓ ↓ ↓ import A from 'kb-bi-vue-component/lib/components/A'; import B from 'kb-bi-vue-component/lib/components/B';
babel在工作时,会优先执行
.babelrc
中的plugins
,接着才会执行presets
。我们优先将源代码进行了转换,再使用babel去转换为es5的代码,整个过程是没有问题的。当然,这是最简单的babel-plugin,还有很多其他情况没有处理,比如下面这种,转换后就不符合预期。
import { A as aaa, B } from 'kb-bi-vue-component'; ↓ ↓ ↓ ↓ ↓ ↓ import aaa from 'kb-bi-vue-component/lib/components/aaa'; import B from 'kb-bi-vue-component/lib/components/B';
要完成一个高质量的babel-plugin,还有很多的工作要做。
附:阿里已经开源了一个成熟的babel-plugin-import
参考链接:
1、https://github.com/jamiebuild...
2、https://babeljs.io/docs/en/ba...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。