解析 ui组件库的 按需引入 原理
背景
由于自己也写了 公司内部的组件库,然后希望实现 按需引入 的功能。达到缩小文件体积的目的。提升项目性能。
此文章作为一个整理,分享思考的过程和思路给大家
首先寻找实现案例
先看element-ui的按需引入
查看官网
- 依赖一个babel插件 babel-plugin-component
- 亲测不使用这个插件,按需引入无效(查看run build后的文件大小)
结论 - 看不出什么特别之处,核心可能跟 babel-plugin-component 插件有关系
在看lodash的按需引入
// 按需引入import cloneDeep from 'lodash/cloneDeep'; // 注意看路径'lodash/cloneDeep'var objects = [{ 'a': 1 }, { 'b': 2 }];var deep = cloneDeep(objects);console.log(deep[0] === objects[0]); // false// 非按需引入(全部引入)import _ from 'lodash';var objects = [{ 'a': 1 }, { 'b': 2 }];var deep = _.cloneDeep(objects);console.log(deep[0] === objects[0]); // false
分2次打包,一次按需引入,一次全部引入。最终webpack打包后的结果:(确实是生效的)

翻看lodash源码,目录结构如下:
lodash├── ...├── cloneDeep.js├── ...
合理猜想:按需引入的原理 和 按文件路径引入有关
- 在进一步猜想,lodash模块的导出应该是ES6 Module(查看源码,确实如此)
- 为什么一定要是ES6 Module。因为只有ES6 Module可以做tree shaking,因为ES6 Module是静态的,在编译时可以分析出依赖关系。(详细版 可以看我的另一篇https://juejin.cn/post/6959360326299025445)
验证猜想:按需引入的原理 是 按文件路径引入
正好拿element-ui尝试,我不引入他的babel插件(babel-plugin-component)
我用文件路径的引入的方式写
// 按需引入(文件路径的引入)import Button from "element-ui/lib/button" // 把node_modules的源码翻出来,找到对应的目录结构// 也可以写成 import Button from 'element-ui/packages/button'; 区别就是,上面的是打包过的。这个是没打包的文件,有更好的source map方便调试import 'element-ui/lib/theme-chalk/button.css' // 样式文件也可以按需引入Vue.component(Button.name, Button);// 全局引入import ElementUI from 'element-ui';import 'element-ui/lib/theme-chalk/index.css';Vue.use(ElementUI);
分2次打包,一次按需引入,一次全部引入。打包后,对比: 确实生效了! js和css体积都减少了很多
- js:从809kb 减小到 101kb
- css:从236kb 减小到 11kb

最后猜想
element-ui 的 babel-plugin-component 插件的作用
element-ui官网的按需加载写法:
// main.jsimport { Button } from 'element-ui';Vue.component(Button.name, Button);// 安装npm install babel-plugin-component -D// .babelrc 修改为: (摘自官网){"presets": [["es2015", { "modules": false }]], // 此处有坑,如果用了babel 7版本以上。 此次要写成 [["@babel/preset-env", { "modules": false }]],"plugins": [["component",{"libraryName": "element-ui","styleLibraryName": "theme-chalk"}]]}
猜想:babel-plugin-component的作用是 把简单语法import { Button } from 'element-ui'; 转成按文件路径引入import Button from 'element-ui/lib/button';
验证猜想:写一个loader,放在babel-loader的前面
{test: /.js$/,loader: './my-loader', // 写一个自己的loader,放在babel-loader的前面,可以得到babel解析之后的结果(因为loader的解析顺序是从下到上,从后到前的)},{test: /.js$/,loader: 'babel-loader',include: /src/,options: {cacheDirectory: true}},
my-loader.js(和webpack.config.js 同目录下)
module.exports = function (source) {console.log(source)debuggerreturn source}
可以得到 babel-plugin-component 转换后的 js代码如下:
// 转换前import Vue from 'vue';import App from './App.vue';import { Button } from 'element-ui';Vue.component(Button.name, Button);new Vue({el: '#app',render: h => h(App)});// babel-plugin-component 转换后import Vue from 'vue';import App from './App.vue';import _Button2 from "element-ui/lib/theme-chalk/button.css"; // 此处和猜想是一致import "element-ui/lib/theme-chalk/base.css"; // 多增加了一个base配置,翻了一下源码,是一些icon的class和动画配置。个人觉得看需求,可以不引入import _Button from "element-ui/lib/button"; // 此处和猜想是一致Vue.component(_Button.name, _Button);new Vue({el: '#app',render: function render(h) {return h(App);}});
猜想是对的!
结论:
按需引入的原理:就是 按 资源的路径引入,前提是要用ES6 Module
结论是否适用所有组件库?是否适用于其他的第三方资源?
合理猜想,只要使用ES6 Module,并且把子模块都拆出来,应该是适用的
- 比如ui组件库(el-ui,iview),函数工具库(lodash),都是适用的
码字不易,点赞鼓励
