如何编译出“真正”按需引入的组件库

256 阅读4分钟

我们平时在做项目优化的时候,第一反应是什么呢?

减小dist包的体积

尽量删除非必要引用的node_modules

Ui库按需引入

等等

我们今天就主要聊一聊按需引入问题。

我们平时是如何引入UI库的呢?是这样暴力引入?

Import h_ui from 'h_ui'
Vue.use(h_ui)

还是这样按需引入?

Import { Button } from 'h_ui'
Vue.use(Button)

我相信大部分人都是使用的第二种,那如果只是按照上面这样配置,能做到真正的按需引入吗?我们今天就来实现 从编译组件到按需引入的整个流程。

全量包

  1. 组件打包

我们准备三个组件ComponentA、ComponentB、ComponentC。下面我们来编写webpack

// 入口文件 index.js
import ComponentA from "./ComponentA.vue"
import ComponentB from "./ComponentB.vue"
import ComponentC from "./ComponentC.vue"

const components = {
  ComponentA,
  ComponentB,
  ComponentC
}

const install = function(Vue, opts = {}) {
  if (install.installed) return;
  Object.keys(components).forEach(key => {
      Vue.component(key, components[key]);
  });
};

if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

const API = {
  install
};

export default API

// webpack.config.js
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
module.exports = {
  entry: {
    'dist/index.js': path.resolve(__dirname + "/index.js")
  }, //入口文件
  output: {
    path: `${__dirname}/../lib`, //当前目录
    libraryTarget: "umd",
    filename: "[name].js"
  },
  module: {
    rules: [
      {
        test: /.vue$/,
        use: "vue-loader"
      },
      {
        test: /.js$/,
        use: "babel-loader"
      },
      {
        test: /.less$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"]
      }
    ]
  },
  resolve: {
    extensions: [".vue", ".js", ".less"]
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: "dist/style.css",
    })
  ]
};

相信大家对webpack已经非常熟了,就不多讲了,通过如上配置,我们就可以得到组件包:lib/dist/index.js

因为我们的组件包并没有放到node_modules中,所以我们在开发环境配置一下alias:

chainWebpack: config => {    
    config.resolve.alias
          .set("hundsun-ui", path.resolve("src/lib/dist/index.js"))
}

然后我们在main.js,再进行如下配置:

import hundsunUI from 'hundsun-ui'
Vue.use(hundsunUI)

好了,到这里一个全量的组件包已经搞定了,我们在页面直接使用组件就可以了。如下:

  1. webpack-bundle-analyzer

我们如何查看webpack编译之后的资源加载情况呢? 这里我们用到了webpack的插件:webpack-bundle-analyzer

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

configureWebpack: {
    plugins: [
      new BundleAnalyzerPlugin({
        analyzerPort: 9999
      })
    ]
}

我们在vue.config.js中进行如上配置,就可以清楚的看到资源加载情况。

  1. 分包加载

如果我们不想全量包加载,想单独引用,该如何配置呢?

// 修改 入口文件 index.js(只需要分别导出组件即可)
// 省略代码
const API = {
  install
};
export {
  ComponentA,
  ComponentB,
  ComponentC
}
export default API

这里只需要分别导出组件就可以了,我们来修改一下main.js

import { ComponentA, ComponentB, ComponentC } from 'hundsun-ui'
Vue.component(ComponentA.name, ComponentA)
Vue.component(ComponentB.name, ComponentB)
Vue.component(ComponentC.name, ComponentC)

但是这样是真正的按需引入吗?我们来看下analyzer

我们可以看到,引用的还是dist/index.js,并不会因为使用组件的数量而发生变化。

按需引入

  1. 按组件打包

既然要按需引入,那加载的时候就不能引用index.js, 应该使用哪个组件就加载哪个组件,我们来修改一下webpack

// 分别添加 ComponentA.js、Component B.js、ComponentC.js 三个文件,内容如下:
import ComponentA from "./ComponentA.vue"
export default ComponentA

// webpack.config.js
  entry: {
    'ComponentA': path.resolve(__dirname + "/ComponentA.js"),
    'ComponentB': path.resolve(__dirname + "/ComponentB.js"),
    'ComponentC': path.resolve(__dirname + "/ComponentC.js")
  }, 
  output: {
    path: `${__dirname}/../lib/components`, 
    libraryTarget: "umd",
    filename: "[name]/index.js"
  },
  plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name]/style.css",
    })
  ]
};

这样我们就可以得到这样的组件包,那如何使用呢?

两种方案:

1. 直接引用组件
// vue.config.js
config.resolve.alias
          .set("hundsun-ui", path.resolve("src/lib/components"))
// main.js      
import ComponentA from 'hundsun-ui/ComponentA'
Vue.component(ComponentA.name, ComponentA)

2. 解构引用组件
// vue.config.js
config.resolve.alias
          .set("hundsun-ui", path.resolve("src/lib/components"))
          
// lib/components 目录下创建index.js
import CompnentA from './ComponentA'
export {
    CompnentA
}
// main.js
import { ComponentA } from 'hundsun-ui'
Vue.component(ComponentA.name, ComponentA)

以上两种方式均可以做到组件级别引用,但是我们应该更倾向于第二种。

但是第二种真的是按需引入吗? 我们来看一下analyzer

我们可以看到并没有如我们所想,它把A、B、C三个组件都加载了。😭😭

那么问题出在哪里呢?就是这个 components/index.js 这个文件,这个js文件把所有的组件都引用进来了。那么我们该怎么做呢?

  1. babel-plugin-import

这里我们就不得不用到babel插件了。目前antd、iview也都用到该插件来实现按需加载功能。他是怎么做到的呢???

import { ComponentA } from 'hundsun-ui'

// babel-plugin-import 解析为:

require('hundsun-ui/components/ComponentA')
require('hundsun-ui/components/ComponentA/style.css')

哇,这不就是我们想要的结果吗??我们看一下如何配置:

// 创建babel.config.js
module.exports = {
  "plugins": [
    [
      "import",
      {
        "libraryName": "hundsun-ui",
        "libraryDirectory": "/components",
        "camel2DashComponentName": false,
        "style": (name) => {
          return `${name}/style.css`;
        }
      },
      "hundsun-ui"
    ]
  ]
}
// libraryName 组件库的名称
// libraryDirectory 组件库的目录
// camel2DashComponentName 禁止驼峰转译破折号
// style 返回访问路径

我们再来看一下analyzer:

大功告成!!!

  1. 组件间相互引用

到上一步真的大功告成了吗?现在我们来稍微修改一下组件,我们在A、B组件内引用组件C,再编译出来看一下:

我们发现A、B 两个组件的体积从2KB 变成了 3KB,好奇打开ComponentA.js,竟然发现ComponentC组件竟然在js里面,what?? 不是应该引用过来吗? 为什么给打到包里面去了??

我们发现如果组件间的相互引用使用相对路径的话,会当成该组件的一部分,并打入到该js中。那我们还需要修改两个地方

// webpack.config.js
module.export = {
    resolve: {
        alias: {
            ComponentC: path.resolve(__dirname, "src/components/ComponentC.vue")
        }
    },
    externals: {
        ComponentC: "hundsun-ui/components/ComponentC"
    }
}

// ComponentA 组件
import ComponentC from "ComponentC"

externals:防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖。

我们在 externals 中配置 ComponentC 的外部引用地址,这样编译后我们发现ComponentA.js 中就有了require("hundsun-ui/components/ComponentC")。

我们再看下analyzer:

我们可以看到,只加载了A组件与C组件,B组件并没有加载。

我们再来看一下npm run build 之后,B组件是不是在dist包中:

发现组件B并没有在dist中,喜极而泣😊😊

文章到这里也就结束了,希望对开发组件的同学有所帮助,加油打工人💪💪