解析 ui组件库的 按需引入 原理

背景

由于自己也写了 公司内部的组件库,然后希望实现 按需引入 的功能。达到缩小文件体积的目的。提升项目性能。

此文章作为一个整理,分享思考的过程和思路给大家

首先寻找实现案例

先看element-ui的按需引入

查看官网

  • 依赖一个babel插件 babel-plugin-component
  • 亲测不使用这个插件,按需引入无效(查看run build后的文件大小)
    结论
  • 看不出什么特别之处,核心可能跟 babel-plugin-component 插件有关系

在看lodash的按需引入

  1. // 按需引入
  2. import cloneDeep from 'lodash/cloneDeep'; // 注意看路径'lodash/cloneDeep'
  3. var objects = [{ 'a': 1 }, { 'b': 2 }];
  4. var deep = cloneDeep(objects);
  5. console.log(deep[0] === objects[0]); // false
  6. // 非按需引入(全部引入)
  7. import _ from 'lodash';
  8. var objects = [{ 'a': 1 }, { 'b': 2 }];
  9. var deep = _.cloneDeep(objects);
  10. console.log(deep[0] === objects[0]); // false

分2次打包,一次按需引入,一次全部引入。最终webpack打包后的结果:(确实是生效的)

lodash.png

翻看lodash源码,目录结构如下:

  1. lodash
  2. ├── ...
  3. ├── cloneDeep.js
  4. ├── ...

合理猜想:按需引入的原理 和 按文件路径引入有关

  • 在进一步猜想,lodash模块的导出应该是ES6 Module(查看源码,确实如此)
    • 为什么一定要是ES6 Module。因为只有ES6 Module可以做tree shaking,因为ES6 Module是静态的,在编译时可以分析出依赖关系。(详细版 可以看我的另一篇https://juejin.cn/post/6959360326299025445)

验证猜想:按需引入的原理 是 按文件路径引入

正好拿element-ui尝试,我不引入他的babel插件(babel-plugin-component)

我用文件路径的引入的方式写

  1. // 按需引入(文件路径的引入)
  2. import Button from "element-ui/lib/button" // 把node_modules的源码翻出来,找到对应的目录结构
  3. // 也可以写成 import Button from 'element-ui/packages/button'; 区别就是,上面的是打包过的。这个是没打包的文件,有更好的source map方便调试
  4. import 'element-ui/lib/theme-chalk/button.css' // 样式文件也可以按需引入
  5. Vue.component(Button.name, Button);
  6. // 全局引入
  7. import ElementUI from 'element-ui';
  8. import 'element-ui/lib/theme-chalk/index.css';
  9. Vue.use(ElementUI);

分2次打包,一次按需引入,一次全部引入。打包后,对比: 确实生效了! js和css体积都减少了很多

  • js:从809kb 减小到 101kb
  • css:从236kb 减小到 11kb
    eleme.png

最后猜想

element-ui 的 babel-plugin-component 插件的作用

element-ui官网的按需加载写法:

  1. // main.js
  2. import { Button } from 'element-ui';
  3. Vue.component(Button.name, Button);
  4. // 安装npm install babel-plugin-component -D
  5. // .babelrc 修改为: (摘自官网)
  6. {
  7. "presets": [["es2015", { "modules": false }]], // 此处有坑,如果用了babel 7版本以上。 此次要写成 [["@babel/preset-env", { "modules": false }]],
  8. "plugins": [
  9. [
  10. "component",
  11. {
  12. "libraryName": "element-ui",
  13. "styleLibraryName": "theme-chalk"
  14. }
  15. ]
  16. ]
  17. }

猜想:babel-plugin-component的作用是 把简单语法import { Button } from 'element-ui'; 转成按文件路径引入import Button from 'element-ui/lib/button';

验证猜想:写一个loader,放在babel-loader的前面

  1. {
  2. test: /.js$/,
  3. loader: './my-loader', // 写一个自己的loader,放在babel-loader的前面,可以得到babel解析之后的结果(因为loader的解析顺序是从下到上,从后到前的)
  4. },
  5. {
  6. test: /.js$/,
  7. loader: 'babel-loader',
  8. include: /src/,
  9. options: {
  10. cacheDirectory: true
  11. }
  12. },

my-loader.js(和webpack.config.js 同目录下)

  1. module.exports = function (source) {
  2. console.log(source)
  3. debugger
  4. return source
  5. }

可以得到 babel-plugin-component 转换后的 js代码如下:

  1. // 转换前
  2. import Vue from 'vue';
  3. import App from './App.vue';
  4. import { Button } from 'element-ui';
  5. Vue.component(Button.name, Button);
  6. new Vue({
  7. el: '#app',
  8. render: h => h(App)
  9. });
  10. // babel-plugin-component 转换后
  11. import Vue from 'vue';
  12. import App from './App.vue';
  13. import _Button2 from "element-ui/lib/theme-chalk/button.css"; // 此处和猜想是一致
  14. import "element-ui/lib/theme-chalk/base.css"; // 多增加了一个base配置,翻了一下源码,是一些icon的class和动画配置。个人觉得看需求,可以不引入
  15. import _Button from "element-ui/lib/button"; // 此处和猜想是一致
  16. Vue.component(_Button.name, _Button);
  17. new Vue({
  18. el: '#app',
  19. render: function render(h) {
  20. return h(App);
  21. }
  22. });

猜想是对的!

结论:

按需引入的原理:就是 按 资源的路径引入,前提是要用ES6 Module

结论是否适用所有组件库?是否适用于其他的第三方资源?

合理猜想,只要使用ES6 Module,并且把子模块都拆出来,应该是适用的

  • 比如ui组件库(el-ui,iview),函数工具库(lodash),都是适用的

码字不易,点赞鼓励