VUE UI组件库按需引入的探索(2)

4,516 阅读9分钟

VUE UI组件库按需引入的探索
VUE UI组件库按需引入的探索(2)

上篇文章 中,我使用纯粹的tres shaking方案来实现vui ui组件库的按需引入,最终结果差强人意。经过总结反思,我决定换一种方案,尝试 babel-plugin-component。已经有一大堆的先例,我再使用这种成熟的方案造一个轮子,实在是很没有必要,但是coder的道路不造就失去了乐趣,我不仅要再造一个,还要造得和它们不一样。废话不多说,开造!

babel-plugin-component实现组件按需引入实践

这里我直接在上一篇文章的代码基础上进行修改, 搭建项目的过程和讲解大家可以看前一篇文章

VUE UI组件库按需引入的探索

注意: 相较于上一篇中的代码,我把组件文件夹的命名规范改为了大驼峰,compA -> CompA,这样做的目的大家看完本篇文章应该就能理解,文章末尾我也会做出说明。

组件样式分开打包

使用babel-plugin-component做按需引入的原理就是路径替换。

import { Button } from 'components' 

被替换成

var button = require('components/lib/button')
require('components/lib/button/style.css')

分析以上代码,项目打包后的代码需要满足两个条件

  1. 以组件为单位进行拆分
  2. 组件和样式分开

第一点可以使用多入口方式实现,第二点可以配置rollup-plugin-postcss实现。

rollup配置

先贴出rollup.config.js

import { terser } from "rollup-plugin-terser";
import babel from 'rollup-plugin-babel';
import vue from 'rollup-plugin-vue2';
import postcss from 'rollup-plugin-postcss'
import fs from 'fs'
import path from 'path'
import simplevars from 'postcss-simple-vars';
// nested插件允许css嵌套
import nested from 'postcss-nested';
// cssnext插件允许开发人员在当前的项目中使用 CSS将来版本中可能会加入的新特性。cssnext 负责把这些新特性转译成当前浏览器中可以使用的语法。
import cssnext from 'postcss-cssnext';
// cssnano插件负责压缩css
import cssnano from 'cssnano';

// 获取items下一级所有的文件和文件夹
const items = fs.readdirSync('./src/components');

// 过滤掉非文件夹,防止有人不小心在组件文件夹components下混入其他js
// 同时也是一个制约,实行组件就近维护原则
const dirs = items.filter(item => {
  return fs.statSync(path.resolve('./src/components', item)).isDirectory()
})

// rollup 多入口打包
export default dirs.map(dir => {
  return {
    // 每个组件都是一个入口
    input: `./src/components/${dir}/index.js`,
    // 打包成通用模块,按照babel-plugin-component的转化规则组织输出目录
    output: {
      file: `lib/${dir}/index.js`,
      name: `${dir}`,
      format: 'umd',
      exports: 'named'
    },
    plugins: [
      vue(),
      terser(),
      babel({
        exclude: 'node_modules/**',
        runtimeHelpers: true
      }),
      postcss({
        plugins: [
          nested(),
          cssnext({ warnForDuplicates: false, }),
          cssnano(),
        ],
        // 打包.css后缀的文件
        extensions: ['.css'],
        // 单独生成css文件而不是和js混合在一起
        extract: true
      })  
    ]
  }
})

上面的配置已经给出了详细的注释,部分基础配置的讲解可以查看上一篇文章。

打包

打包之前先把打包脚本优化一下,现在每次打包都直接使用 roll -c,而且lib文件夹也没有清空。在package.json的scripts中加入

"build": "rm -rf ./lib && rollup -c",

先移除lib文件夹,然后再打包。

npm run build

打包结果如下

到目前为止,我已经基本实现了babel-plugin-component按需引入需要的两个条件——组件拆分和css分离。

完整引入配置

有了按需引入别忘了还有完整引入,这里还需要一个包含所有组件的完整包。修改一下rollup配置文件

// ...........
// 前面的配置保持一样
// ...........
export default dirs.map(dir => {
  return {
    input: `./src/components/${dir}/index.js`,
    output: {
      file: `lib/${dir}/index.js`,
      name: `${dir}`,
      format: 'umd',
      exports: 'named'
    },
    plugins: [
      vue(),
      terser(),
      babel({
        exclude: 'node_modules/**',
        runtimeHelpers: true
      }),
      postcss({
        plugins: [
          nested(),
          cssnext(),
          cssnano(),
        ],
        extensions: ['.css'],
        extract: true
      })  
    ]
  }
}).concat([
  {
    input: `./src/index.js`,
    output: [
      {
        file: `lib/v-ui.umd.js`,
        name: `v-ui`,
        format: 'umd',
        exports: 'named'
      }
    ],
    plugins: [
      vue(),
      terser(),
      babel({
        exclude: 'node_modules/**',
        runtimeHelpers: true
      }),
      postcss({
        plugins: [
          nested(),
          cssnext(),
          cssnano(),
        ],
        extensions: ['.css'],
        extract: 'lib/index.css'
      })  
    ]
  }
])

配置里我增加了一段代码,以./src/index.js为入口,单独打包一个包含所有组件的js文件,css部分全部合并成一个文件lib/index.css

npm run build

打包结果如下

v-ui.umd.js中包含了所组件的js代码,index.css中包含了所有组件的css代码。

试用

还是一样,先把VUI链接到全局

npm link

用@vue/cli初始化一个vue项目

vue create testvui

引入VUI

npm link VUI

完整引入

先尝试一下完整引入

import VUI from 'VUI'
// 现在css打包成了独立文件,需要单独引入
import 'VUI/lib/index.css'
Vue.use(VUI)

修改一下HellloWorld.vue

启动testvui

npm run serve

结果如下

按需引入

按需引入需要借助babel-plugin-component插件。在testvui中安装一下

npm i babel-plugin-component -D

配置babel

plugins: [
  [
    "component",
    {
      // 库的名字为VUI
      "libraryName": "VUI",
      // 存放库文件的文件夹为lib
      "libDir": "lib",
      // 样式文件名为index.css
      "style": "index.css",
      // 组件名称是否由驼峰转化为破折号格式,默认为true
      "camel2Dash": false
    }
  ]
]

每一个配置的具体含义大家可以参考babel-plugin-component的文档。


说明:大家是否还记得文章开头我把组件文件夹名由小驼峰格式改成了大驼峰格式,compA -> CompA。现在对此做出解释,如果保持compA,打包之后lib中组件文件夹的名字依然会是compA,不配置camel2Dash时

import { CompA } from 'VUI'

会被转换成

var comp-a = quire('VUI/lib/comp-a/')
require('VUI/lib/comp-a/index.css')

很明显lib中并不存在comp-a文件夹,代码会直接报错。 配置"camel2Dash"为false时

import { CompA } from 'VUI'

会被转换成

var CompA = quire('VUI/lib/CompA/')
require('VUI/lib/CompA/index.css')

此时lib中的文件夹名为compA,也找不到CompA。所以我更改了src中的组件文件夹命名, 一步错、步步错...... 惨惨惨


配置好babel-plugin-component后,开始尝试按需引入。

启动testvui

npm run serve

成功的引入了CompA的组件内容和样式,但是是否真的按需引入了,得打包之后才知道。打包testvui

npm run build

打包结果如下

js看起来没问题了,按需引入成功。再看看css

WTF!!! 我的css呢???

成功的路上总不会一帆风顺,但是我没想到会是如此的命途多舛......

成也tree shaking 败也tree shaking

研究了好久,我终于发现css之所被干掉是因为VUI中我设置了

"sideEffects": false

这个当初让我充满幻想的小情人,今天又实实在在坑了我一把。设置"sideEffects": false后,css因为没有副作用,被tree shaking优化掉了。知道原因就好办了,重新设置一下sideEffects

"sideEffects": ["*.css"]

VUI重新打包一下

npm run build

testvui也重新打包一下

npm run build

打包结果如下

结果很不错,该有的都有,不该有的都没有。至此,babel-plugin-component版本的按需引入基本实现。

tree shaking的执念

我终于借助babel-plugin-component实现了按需引入,高兴之余心里又空荡荡的,似乎总是缺了什么,我忘了自己的初心 —— tree shaking,一个无数次让我失望又坑过我的东西。上一篇文章中,我实现了阉割版的tree shaking按需引入,样式问题让我无能为力。但我想再尝试一次,样式打包成独立css文件,单独引入,js部分还是使用tree shaking来做按需引入。

修改rollup配置

上面的配置中,我已经实现了

  • 组件多入口打包,以组件为单位分割
  • 组件和样式分离
  • 包含所有组件的umd通用模块

我只差一个包含所有组件的esm模块,这个我在上一篇文章中就已经实现了。修改一下rollup.config.js

// 新增代码
{
  file: `lib/v-ui.esm.js`,
  format: 'esm'
}

打包VUI

npm run build

试用混合版本的VUI

注意:VUI中的package.json依然要设置main和module字段,不同的引入方式会寻找不同的入口文件

在testvui中,去掉babel-plugin-component

完整引入

css打包成独立css文件了,需要单独引入

启动testvui

npm run serve

打包testvui

npm run build

打包结果如下

所有的组件和样式都被打包了。

按需引入

修改main.js,只引入CompA,css被打包成了独立的/lib/CompA/index.css,需要单独引入

启动testvui

npm run serve

打包testvui

npm run build

打包结果如下

很明显,我又实现了tree shaking版的按需引入,只不过样式需要单独引入。

总结

本篇文章记录了两种实现按需引入的方式

  • 借助 babel-plugin-component

    import { CompA } from 'VUI' 
    

    被转化成

    var CompA = quire('VUI/lib/CompA/')
    require('VUI/lib/CompA/index.css')
    

    组件库以组件为单位进行多入口打包,每个组件生成一个目录。使用组件库时,按需引入只会require相应的文件夹。打包时,其他未被require的文件夹不会被打包,以此减小打包体积。

  • tree shaking
    组件库使用rollup打包出包含所有组件的esModule,样式以组件为单位生成相应的独立css文件。使用组件库时, 按需引入

    import { CompA } from 'VUI' 
    import 'VUI/lib/CompA/index.css'
    

    打包时,js部分利用tree shaking消除未被引入的组件代码。样式部分只引入了相应css文件,未被引入的不会被打包,以此减小打包体积。

番外

cdn引入

一个组件库被用户使用,需要考虑各种引入方式和使用场景,上面代码打包出的umd通用模块文件基本可以满足需求。在cdn引入组件库时,用户通常这样使用

<body>
  <div id="app">
    <comp-a></comp-a>
  </div>
</body>
<!--引入vue-->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!--引入组件库,需要把组件库发布到npm,这里只是举例,此代码不可直接运行-->
<script src="https://unpkg.com/v-ui/lib/index.js"></script>
<!--引入css-->
<link rel="stylesheet" href="https://unpkg.com/v-ui/lib/index.css">
<!--使用组件库-->
<script>
new Vue({
  el: '#app',
  data: function() {
    return {  }
  }
})
</script>

没有注册组件的相关代码

window.Vue.use(VUI)

上面的组件库代码如果发布到npm,cdn的使用方式会直接报错【组件未注册】。解决方案如下

cdn方式引入时,直接在window.Vue下注册所有组件,同时在install中做防重复注册处理。

加上以上代码后,可以解决cdn引入组件未注册问题,但是会导致组件库充满副作用(修改了window对象),从而引起tree shaking失效。目前我能想到的解决办法就是写两个入口文件,index.umd.js和index.esm.js,在index.umd.js中加入处理cdn引入的代码,在index.esm.js中不加入处理cdn引入的代码。然后修改rollup.config.js,esm和umd使用不同的入口文件。

这种入口文件两份的写法建议直接用脚本生成入口文件,复制粘贴太不优雅了。

为了tree shaking,真的心力交瘁了......

组件库文档

vue ui组件库的文档推荐使用vuepress,支持markdown中直接写vue组件。但是打包生成静态文档的时候会有样式丢失问题,原因是sideEffects我们只设置了["*.css"],但是vuepress是直接引入的 *.vue 文件,样式又被tree shaking掉了。解决办法就是修改sideEffects配置

这里还加入了*.scss,主要是考虑项目中可能还会有scss文件,顺便加上了。


终于完结了,心好累,tree shaking真磨人... 求赞