vue + element-ui 使用 babel-plugin-component 来实现按需加载组件及样式。
一、官网按需引入示例
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
首先,安装 babel-plugin-component:
npm install babel-plugin-component -D
然后,将 .babelrc 修改为:
{
...
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:
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.项目结构
目录结构
├── index.js
├── .babelrc
├── package-lock.json
└── package.json
package.json
{
...
"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 文件
{
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
index.js 文件
import Vue from 'vue'
import { Button } from 'element-ui'
Vue.component(Button.name, Button)
Vue.component(Select.name, Select)
编译生成文件
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
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 方法
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 文件。