浅析babel-plugin-component

浅析babel-plugin-component

vue + element-ui 使用 babel-plugin-component 来实现按需加载组件及样式。

一、官网按需引入示例

借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

首先,安装 babel-plugin-component:

1
npm install babel-plugin-component -D

然后,将 .babelrc 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
...

"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import { Button, Select } from 'element-ui'
import App from './App.vue'

Vue.component(Button.name, Button)
Vue.component(Select.name, Select)
/* 或写为
* Vue.use(Button)
* Vue.use(Select)
*/

new Vue({
el: '#app',
render: h => h(App)
})

二、babel 编译过程概述

使用 @babel/cli 与 @babel/core 编译文件的主要过程

  • 加载配置文件

    • 尝试加载 babel.config.js、babel.config.cjs、babel.config.mjs、babel.config.json文件
    • 加载 package.json 文件
    • 尝试加载 .bebelrc、.babelrc.js、.babelrc.cjs、.babelrc.mjs、.babelrc.json 文件
    • 合并参数
  • 加载 plugins 与 presets,分别遍历他们(plugins 每一项会调用它的回调,返回包含 visitor 的对象)

  • 解析部分
    • 以 utf8 格式读入口文件得到代码
    • 之后解析生成 ast
  • 遍历与转换部分
    • 遍历插件数组,生成最后的访问者(visitor)对象
    • 开始遍历节点,碰到感兴趣的节点就调用回调
  • 生成部分
    • 遍历 ast,将得到的代码保存在数组中,最后拼接起来

三、babel-plugin-component

通过一个简单的例子看一下 babel-plugin-component 在编译过程中做了哪些事情

1.项目结构

目录结构

1
2
3
4
├── index.js
├── .babelrc
├── package-lock.json
└── package.json

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{

...

"scripts": {
"build": "babel ./index.js --out-dir lib"
},

...

"dependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"babel-plugin-component": "^1.1.1"
}
}

.babelrc 文件

1
2
3
4
5
6
7
8
9
10
11
{
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}

index.js 文件

1
2
3
4
5
import Vue from 'vue'
import { Button } from 'element-ui'

Vue.component(Button.name, Button)
Vue.component(Select.name, Select)

编译生成文件

1
2
3
4
5
6
import _Button2 from "element-ui/lib/theme-chalk/button.css";
import "element-ui/lib/theme-chalk/base.css";
import _Button from "element-ui/lib/button";
import Vue from 'vue';
Vue.component(_Button.name, _Button);
Vue.component(Select.name, Select);

2.插件做了什么

在 babel 遍历 ast 的时候,这个插件主要关注了 ImportDeclaration 与 CallExpression 与 Program

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Program: function Program() {// 在这里做一些初始化操作,用来保存信息
specified = Object.create(null);
libraryObjs = Object.create(null);
selectedMethods = Object.create(null);
moduleArr = Object.create(null);
},
ImportDeclaration: function ImportDeclaration(path, _ref2) {
var opts = _ref2.opts;// 获得配置文件中的配置 {libraryName: "element-ui, "styleLibraryName: "theme-chalk"}

var node = path.node;// 获得当前的节点

var value = node.source.value;// 当前节点的值(import 引入 vue,这里就是 vue,引入 element-ui 这里就是element-ui)

...

var libraryName = result.libraryName || opts.libraryName || defaultLibraryName;// 拿到名称 一般是 element-ui

if (value === libraryName) {// 如果是 element-ui
node.specifiers.forEach(function (spec) {
if (types.isImportSpecifier(spec)) {
specified[spec.local.name] = spec.imported.name;// 存起来 { Button: "Button" }
moduleArr[spec.imported.name] = value;// { Button: "element-ui" }
} else {
libraryObjs[spec.local.name] = value;
}
});

if (!importAll[value]) {
path.remove();// 移除节点
}
}
},

CallExpression: function CallExpression(path, state) {
var node = path.node;
var file = path && path.hub && path.hub.file || state && state.file;
var name = node.callee.name;

if (types.isIdentifier(node.callee)) {
if (specified[name]) {
node.callee = importMethod(specified[name], file, state.opts);
}
} else {
node.arguments = node.arguments.map(function (arg) {
var argName = arg.name;

if (specified[argName]) {// 找到上面存起来的 Button
return importMethod(specified[argName], file, state.opts);
} else if (libraryObjs[argName]) {
return importMethod(argName, file, state.opts);
}

return arg;
});
}
},

importMethod 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
function importMethod(methodName, file, opts) {
if (!selectedMethods[methodName]) {
var options;
var path;

...

options = options || opts;// 配置文件中的参数 {libraryName: "element-ui", styleLibraryName: "theme-chalk"}
var _options = options,
_options$libDir = _options.libDir,//这是组件所在根目录下的路径element-ui/lib/
libDir = _options$libDir === void 0 ? 'lib' : _options$libDir,
_options$libraryName = _options.libraryName,//ui库的名字,就是elementui
libraryName = _options$libraryName === void 0 ? defaultLibraryName : _options$libraryName,
_options$style = _options.style,
style = _options$style === void 0 ? true : _options$style,
styleLibrary = _options.styleLibrary,//这是引入组件时,所需要引入对应组件样式的配置对象
_options$root = _options.root,
root = _options$root === void 0 ? '' : _options$root,
_options$camel2Dash = _options.camel2Dash,
camel2Dash = _options$camel2Dash === void 0 ? true : _options$camel2Dash;
var styleLibraryName = options.styleLibraryName;//这是组件所需样式的路径,配置参数拿的,就是 theme-chalk
var _root = root;
var isBaseStyle = true;
var modulePathTpl;
var styleRoot;
var mixin = false;
var ext = options.ext || '.css';//加载样式的后缀,默认css

...

if (libraryObjs[methodName]) {

...

} else {
path = "".concat(libraryName, "/").concat(libDir, "/").concat(parseName(methodName, camel2Dash));// 得到文件路径 element-ui/lib/button
}

var _path = path;
selectedMethods[methodName] = addDefault(file.path, path, {
nameHint: methodName
});// 这个方法是用来创建 ImportDeclaration 节点的,引入的是 element-ui/lib/button

...

if (styleLibraryName) {
if (!cachePath[libraryName]) {
var themeName = styleLibraryName.replace(/^~/, '');
cachePath[libraryName] = styleLibraryName.indexOf('~') === 0 ? resolve(process.cwd(), themeName) : "".concat(libraryName, "/").concat(libDir, "/").concat(themeName);//得到样式的路径
}

if (libraryObjs[methodName]) {

...

} else {
if (cache[libraryName] !== 1) {

var parsedMethodName = parseName(methodName, camel2Dash);// 驼峰转连字符

if (modulePathTpl) {
var modulePath = modulePathTpl.replace(/\[module]/ig, parsedMethodName);
path = "".concat(cachePath[libraryName], "/").concat(modulePath);
} else {
path = "".concat(cachePath[libraryName], "/").concat(parsedMethodName).concat(ext);// 得到完整的路径 element-ui/lib/theme-chalk/button.css
}

if (mixin && !isExist(path)) {
path = style === true ? "".concat(_path, "/style").concat(ext) : "".concat(_path, "/").concat(style);
}

if (isBaseStyle) {
addSideEffect(file.path, "".concat(cachePath[libraryName], "/base").concat(ext));// 添加 ImportDeclaration 节点引入 base 样式
}

cache[libraryName] = 2;
}
}

addDefault(file.path, path, {
nameHint: methodName
});// 添加 ImportDeclaration 节点引入组件样式
} else {
if (style === true) {
addSideEffect(file.path, "".concat(path, "/style").concat(ext));
} else if (style) {
addSideEffect(file.path, "".concat(path, "/").concat(style));
}
}
}
return selectedMethods[methodName];
}

babel-plugin-component 插件在遍历节点的时候做的事情

  • 找到引入 element-ui 的类型为 ImportDeclaration 节点,将感兴趣的值存在对象里(比如引入 button,就存起来),之后移除当前这个节点。
  • 在遍历到 CallExpression 类型节点的时候(假设使用了 button,就判断是否存在了上面的对象里),之后创建新的 ImportDeclaration 节点,用于之后加载对应的 js 与 css 文件。