组件库之按需加载
方式
目前按需加载有两种方式实现。
- 使用
babel-plugin-import
插件来自动按需引入 - 提供
es module
版本,开启tree shaking
babel-plugin-import
babel-plugin-import
是ant-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-import
和tree shaking
也可以并存使用。但大部分情况并存使用与单独使用体积差距不是很大。
例如:
import { Button } from 'antd';
import 'antd/es/button/style';
webpack
可以通过在package.json
设置sideEffects: false
,开启tree shaking
。
ssr应用使用哪一种?
对于csr
的spa
项目,使用以上两种办法达到的效果都是差不多的。但是如果我们的项目是ssr
应用,可能会有一点差异,因为ssr
应用在server
端,我们是通过设置webpack.server.config.js
的external
,将第三方模块不交给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
环境, 目前node
对esm
支持的并不是很好,所以还是普遍使用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-vue
的Retained 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
这个目录,就包含了carousel
和carousel-slide
两个组件。
针对以上情况,我们需要自定义转换路径,利用babel-plugin-import
的customName
,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
端,则使用cjs
,bootstrap-vue
的es
其实是cjs
,如果是client
,则使用esm
。
总结
常用的按需加载方式
- 使用
babel-plugin-import
插件 - 提供
es module
版本,开启tree shaking
babel-plugin-import
插件可以实现js
,css
的按需加载,本质上就是将按需引入
的方式变更为直接引入
的方式。 如果配置了style
字段,同时也会注入style
的直接引入代码。
tree shaking
只针对于js
,css
如果需要按需加载,需要手动直接引入。
相比之下,由于tree shaking
只针对于js
,babel-plugin-import
会更方便。babel-plugin-import
和tree shaking
也可以并存使用。并存使用在有些情况,体积会相对小一点,但与单独使用体积差距不大。
ssr
应用中,由于server
端的webpack
不会处理第三方模块,所以没办法tree shaking
,如果server
端也需要考虑按需加载,可以使用babel-plugin-import
。
如果所用的ui组件库不符合babel-plugin-import
的转换规则,可以通过babel-plugin-import
提供的customName
字段来自定义转换后的路径。通过style
字段,来进一步自定义转换后的style
路径。