组件库之按需加载

8,992

组件库之按需加载

方式

目前按需加载有两种方式实现。

  • 使用babel-plugin-import插件来自动按需引入
  • 提供es module版本,开启tree shaking

babel-plugin-import

babel-plugin-important-design团队出的一个babel插件,主要用于模块的按需加载。其原理就是将直接引入的方式通过babel转化成按需引入的方式。如果css也需要按需加载,也会注入css引用代码。

例如:

import { Button } from 'antd';

转换成:

import Button from 'antd/es/button';
import 'antd/es/button/style';

babel-plugin-import默认js路径是 [libraryName]/[moduleType]/[componentName],默认样式路径是 [libraryName]/[moduleType]/[componentName]/style,如果所用的ui组件库不符合babel-plugin-import的转换规则,可以通过babel-plugin-import提供的customName字段来自定义转换后的路径。通过style字段,来进一步自定义转换后的样式路径。

具体使用文档可以参考babel-plugin-import

tree shaking

如果组件库提供了es module版本,并开启了tree shaking,那么不需要babel-plugin-import,也可以达到按需加载的目的,这个方法只针对于js, 对于样式的按需加载仍需要手动引入。 当然babel-plugin-importtree shaking 也可以并存使用。但大部分情况并存使用与单独使用体积差距不是很大。

例如:

import { Button } from 'antd';
import 'antd/es/button/style';

webpack可以通过在package.json设置sideEffects: false,开启tree shaking

ssr应用使用哪一种?

对于csrspa项目,使用以上两种办法达到的效果都是差不多的。但是如果我们的项目是ssr应用,可能会有一点差异,因为ssr应用在server端,我们是通过设置webpack.server.config.jsexternal,将第三方模块不交给webpack打包处理,直接require引入即可,这样做的目的是不希望server端由webpack打包的bundle文件体积过大。

// webpack.server.config.js
module.exports = {
    ...
    externals: nodeExternals({
    whitelist: [/\.(css|scss|less)$/, /\?vue&type=style/] //除去样式
  }),
}

如果我们使用tree shaking方式, 由于server端的webpack不处理这些第三方模块,那么就没办法做tree shaking,而且server端是node环境, 目前nodeesm支持的并不是很好,所以还是普遍使用cjs。那么在server端,组件库实际还是会整个引入进来。

所以如果需要在server端做按需加载,还是建议用babel-plugin-import

下面我会以bootstrap-vue举例,因为bootstrap-vue使用babel-plugin-import并不是很平滑。

bootstrap-vue

bootstrap-vue提供了es module版本,并且开启了tree shaking,可以不需要babel-plugin-import。直接引入即可。

//...引入样式
import { ButtonPlugin, LayoutPlugin, TabsPlugin } from 'bootstrap-vue'
Vue.use(ButtonPlugin)
Vue.use(LayoutPlugin)
Vue.use(TabsPlugin)

但如果是在ssr应用里,根据上面提到的原因,server端其实引入的是dist/bootstrap-vue.common.js,我们可以通过alinode堆快照分析工具来查看下这个文件占了多大的内存。

可以看到bootstrap-vueRetained Heap达到了1.18MB,稳居第一位。 所以如果希望server端的bootstrap-vue也做到按需加载,可以使用babel-plugin-import

使用babel-plugin-import

bootstrap-vue分成components,directives,两个组成部分,也就是组件和指令部分,并提供了两种引入方式,一个是批量注册组件或者指令的plugin,另一个是单独引入组件或者指令。 例如:

//批量注册组件
import { LayoutPlugin } from 'bootstrap-vue'
Vue.use(LayoutPlugin)
//批量注册指令
import { VBModalPlugin } from 'bootstrap-vue'
Vue.use(VBModalPlugin)
//单独注册组件
import { BModal } from 'bootstrap-vue'
Vue.compoents('b-modal', BModal)
//单独注册插件
import { VBModal } from 'bootstrap-vue'
Vue.directives('b-modal', VBModal)

bootstrap-vue的目录并不符合babel-plugin-import的转换规则,因为bootstrap-vue不仅提供了两个主文件夹components,directives, 而且还有plugin组件,以及非plugin组件。另外,一个组件目录还包括了多个组件 如carousel这个目录,就包含了carouselcarousel-slide两个组件。

针对以上情况,我们需要自定义转换路径,利用babel-plugin-importcustomName,style字段。我们可以利用它们来自定义转换函数,来实现下面的转换规则。bootstrap-vue没有提供style的按需加载,所以这里的按需加载,只针对于js

import { LayoutPlugin,VBModalPlugin,Carousel,CarouselSlide } from 'bootstrap-vue'
=> 
//组件插件
import LayoutPlugin from 'bootstrap-vue/esm/components/layout/index.js'
//指令插件
import VBModalPlugin from 'bootstrap-vue/esm/directives/modal/index.js'
//单独引入
import Carousel from 'bootstrap-vue/esm/components/carousel/carousel.js'
//CarouselSlide包含在carousel文件夹里
import CarouselSlide from 'bootstrap-vue/esm/components/carousel/carousel-slide.js'

由上我们可以发现:

  • VB开头的都是directive, 以B开头的都是component
  • Plugin结尾的都是plugin,非Plugin开头的都是单独注册的。
  • 如果它是一个单独引入的组件,需要获取他的组件目录。

所以我们最终的.babelrc.js应该这样写

//提供bootstrap的所有组件目录
const bootstrapComponents = [
    'alert',
    'badge',
    'breadcrumb',
    'button',
    'button-group',
    'button-toolbar',
    'card',
    'carousel',
    'collapse',
    'dropdown',
    'embed',
    'form',
    'form-checkbox',
    'form-file',
    'form-group',
    'form-input',
    'form-radio',
    'form-select',
    'form-textarea',
    'image',
    'input-group',
    'jumbotron',
    'layout',
    'link',
    'list-group',
    'media',
    'modal',
    'nav',
    'navbar',
    'pagination',
    'pagination-nav',
    'popover',
    'progress',
    'spinner',
    'table',
    'tabs',
    'toast',
    'tooltip'
  ]
module.exports = {
  ...
  'plugins': [
    [
      'import',
      {
        'libraryName': 'bootstrap-vue',
        camel2DashComponentName: false // 关闭驼峰转换
        'customName': (name) => {
          let category, cname = name, isPlugin = false
          if (/^VB/.test(cname)) { //directives like VBModalPlugin, VBModal
            category = 'directives'
            cname = cname.replace(/^VB/, '')
          } else { // components
            category = 'components'
          }
          if (/Plugin$/.test(cname)) { //plugin like ButtonPlugin,ModalPlugin
            isPlugin = true
            cname = `${cname.replace(/Plugin$/, '')}`
          } else { //Individual components like BButton, BModal
            cname = cname.replace(/^B/, '')
          }
          //FormCheckbox -> form-checkbox
          cname = cname.replace(/\B([A-Z])/, (m) => {
            return `-${m}`
          }).toLowerCase()
          // 这里需要处理下当一个组件文件夹里包含多个组件的时候,如: carousel-slide -> /carousel/carousel-slide 
           if (!isPlugin && category === 'components') {
            let dir = bootstrapComponents.filter(c => {
              return cname.startsWith(c)
            })[0]
            return `bootstrap-vue/${process.env.VUE_ENV === 'server' ? 'es' : 'esm'}/${category}/${dir}/${cname}`
          }
          return `bootstrap-vue/${process.env.VUE_ENV === 'server' ? 'es' : 'esm'}/${category}/${cname}`
        }
      }
    ]
  ]
}

process.env.VUE_ENV是注入的一个环境变量,用于区分是server端,还是client端。这里如果是server端,则使用cjsbootstrap-vuees其实是cjs,如果是client,则使用esm

总结

常用的按需加载方式

  • 使用babel-plugin-import插件
  • 提供es module版本,开启tree shaking

babel-plugin-import插件可以实现js,css的按需加载,本质上就是将按需引入的方式变更为直接引入的方式。 如果配置了style字段,同时也会注入style的直接引入代码。

tree shaking只针对于jscss如果需要按需加载,需要手动直接引入。

相比之下,由于tree shaking只针对于jsbabel-plugin-import会更方便。babel-plugin-importtree shaking也可以并存使用。并存使用在有些情况,体积会相对小一点,但与单独使用体积差距不大。

ssr应用中,由于server端的webpack不会处理第三方模块,所以没办法tree shaking,如果server端也需要考虑按需加载,可以使用babel-plugin-import

如果所用的ui组件库不符合babel-plugin-import的转换规则,可以通过babel-plugin-import提供的customName字段来自定义转换后的路径。通过style字段,来进一步自定义转换后的style路径。