在最近的开发过程当中,不一样的项目、不一样的页面都须要用到某种UI控件,因而很天然的将这些UI控件拆出来,单独创建了一个代码库进行维护。下面是个人组件库大体的目录结构以下:javascript
... - lib - components - componentA - index.vue - componentB - index.vue - componentC - index.vue - index.js ...
整个组件库的出口在index.js
,里面的内容差很少是下面这样的:vue
import A from './lib/componentA'; import B from './lib/componentB'; import C from './lib/componentC'; export { A, B, C }
个人代码库的name为:kb-bi-vue-component
。在项目中引用这个组件库的时候,代码以下:java
import { A, B } from 'kb-bi-vue-component'; ....
这个时候,问题出现了,我在页面中,仅仅使用了A
、B
两个组件,可是页面打包后,整个组件库的代码都会被打进来,增长了产出的体积,包括了很多的冗余代码。很容易想到的一个解决方案是按照如下的方式引用组件。node
import A from 'kb-bi-vue-component/lib/componentA'; import B from 'kb-bi-vue-component/lib/componentB';
这种方法虽然解决了问题,我想引用哪一个组件,就引用哪一个组件,不会有多余的代码。可是我总以为这种写法看起来不太舒服。有没有还能像第一种写法同样引用组件库,而且只引用须要的组件呢?写一个babel-plugin好了,自动将第一种写法转换成第二种写法。git
本文只是简单介绍。想要深刻理解代码编译,请学习<<编译原理>>这里有一个不错的Babel教程:https://github.com/jamiebuild...github
Babel是Javascript编译器,更确切地说是源码到源码的编译器,一般也叫作『转换编译器』。也就是说,你给Babel提供一些Javascript代码,Babel更改这下代码,而后返回给你新生成的代码。数组
在这整个过程当中,都是围绕着抽象语法树(AST)来进行的。在Javascritp中,AST,简单来讲,就是一个记录着代码语法结构的Object。好比下面的代码:babel
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的处理过程主要为3个:解析(parse)、转换(transform)、生成(generate)。
解析主要包含两个过程:词法分析和语法分析,输入是代码字符串,输出是AST。
处理AST。处理工具、插件等就是在这个过程当中介入,将代码按照需求进行转换。
遍历AST,输出代码字符串。
解析和生成过程,都有Babel都为咱们处理得很好了,咱们要作的就是在 转换 过程当中搞事情,进行个性化的定制开发。
这里有详细的介绍: https://github.com/jamiebuild...
首先,须要大体了解一下babel-plugin的开发方法。
babel使用一种 访问者模式 来遍历整棵语法树,即遍历进入到每个Node节点时,能够说咱们在「访问」这个节点。访问者就是一个对象,定义了在一个树状结构中获取具体节点的方法。简单来讲,咱们能够在访问者中,使用Node的type来定义一个hook函数,每一次遍历到对应type的Node时,hook函数就会被触发,咱们能够在这个hook函数中,修改、查看、替换、删除这个节点。提及来很抽象,直接看下面的内容吧。
.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
参考连接:
一、https://github.com/jamiebuild...
二、https://babeljs.io/docs/en/ba...