Vue组件库工程探索与实践之按需加载

《Vue组件库工程探索与实践》系列文章第二篇,聊一聊组件库按需加载功能。javascript

一个组件库一般有数十个组件,随着版本迭代组件数量还可能进一步增长。组件库文件的体积也随之膨胀,动辄几百KB。而咱们的业务项目中,有可能只用到了这个组件库的少数几个组件,这时把整个组件库打包进去,非但没有必要,还会徒增项目构建文件的体积,这与应用性能优化的方向是背道而驰的。所以,组件库有必要提供一种更灵活的组件引用方式,容许应用只引用指定的组件。事实上,主流的组件库基本都具有“按需加载组件”功能。css

最简单的“按需加载组件”实现方式,就是在应用中直接引用所需组件的源文件,在应用的构建工具中跟应用一块儿构建。说它简单,是由于这种方式几乎不须要组件库作什么工做,应用直接引用组件源码,并不须要通过组件库的构建过程。vue

这种方式的局限性也大都与“组件未经组件库构建”有关。在应用中构建这些组件,就意味着应用的构建工具必需要具有构建这些组件的能力。好比须要有编译Vue模板、编译ES6+语法、编译Scss/Less语法、支持postcss等的能力,若是说上面这些功能很基础,大多数应用的构建工具都能支持,那么组件可能还有一些不太常见或者组件库特有的功能,好比处理SVG、定制主题、国际化等等,一般应用构建工具不具有或者依赖于组件库配置文件,这就给直接在用户的应用中编译组件源码带来了困难。另外一方面,未经构建的组件模块化接口单一,没法直接在其余模块化场景和非模块化场景使用。还有,若是组件库支持直接引用组件源码,则须要把全部组件源码随NPM包一块儿发布,可能会致使npm包过大,看起来并非一个好主意。java

好吧,咱们换个思路,不直接引用组件源码,而是让组件库对这些用户指定的组件(而非所有组件)进行构建,生成一个自定义版本的组件库给用户应用使用。这就须要组件库与用户进行交互,收集用户所须要的组件信息,而后将指定组件编译成一个自定义版本的库文件。这种自定义构建方案常见的状况有两种,一种是经过网页收集信息,在服务端进行构建。遥想当年jQuery时代,jQuery-UI库提供的自定义构建下载方式[1],让用户在线选择所需组件,而后在服务端进行编译,完成后提供给用户下载(固然,服务端也可能存在已经提早编译完的各类组合的构建包)。那个时代已然远去,现在下载安装组件库“政治正确”的姿式是经过npm/Yarn。node

另外一种方案是经过命令行界面(CLI)收集信息并在客户端构建。好比jQuery的“不一样父异母”的小兄弟Zepto.js,官方标准包里只包含部分模块,若是须要增长或移除模块就须要进行自定义构建了:在Zepto.js项目目录下安装依赖,在MODULES中指定须要的模块,而后执行npm run-script dist进行构建,完事儿后dist目录下zepto.js和zepto.min.js就是自定义构建出来的包,拿到项目里使用便可。这种方式节约服务器资源,甚至不须要本身的服务器。jquery

# do a custom build
$ MODULES="zepto event data" npm run-script dist

# on Windows
c:\zepto> SET MODULES=zepto event data
c:\zepto> npm run-script dist
复制代码

NutUI 1.x 时期的按需加载方案,相似上述第二种方案,较之还有一些改进。用户在NutUI 1.x项目中安装依赖,而后执行npm run custom命令,这时命令行界面会列出全部组件名,用户选择须要的组件后回车,组件库的构建工具会将所选组件进行构建,获得与完整组件库文件同名的构建文件nutui.js,正常使用便可。webpack

只看这种方案自身,彷佛没什么问题,确实实现了按需构建,并且并不繁琐,只是几行命令而已,也不须要架设服务器。可是若是结合用户使用场景来看,问题仍是很多:git

  • 用户一般是经过npm/Yarn方式安装的组件库,须要进node_modules目录找到组件库项目目录安装依赖
  • 自定义构建以后的文件在组件库项目目录的dist目录下,因组件库目录位于node_modules目录中,而node_modules目录一般不被提交到代码仓库,所以在换电脑或多人合做的时候每每还须要再次构建才能在本地拿到自定义构建后的组件库文件,若是版本有差别,还可能会增长风险
  • 为了支持用户进行自定义构建,须要把几乎整个组件库的源码都发布到npm包中

因而NutUI 2.0时,咱们决定对按需加载功能进行从新设计。咱们参考了业界优秀组件库的实现方案。在组件库构建时,除了构建完整的组件库包之外,还把每一个组件单独构建了一个包,这样就能够独立引用每个组件了。github

// 加载构建后的组件JS
import Button from '@nutui/nutui/dist/packages/button/button.js';

//加载构建后的组件CSS
import '@nutui/nutui/dist/packages/button/button.css';
复制代码

webpack的中如何实现构建多个bundle呢?主要是entry选项的配置,entry的值一般是一个字符串,其实它还能够是一个对象。咱们新增一个webpack配置文件,基于组件库的组件配置文件生成一个对象,key是组件名,value是组件的入口js文件,将此对象做为该配置文件的entry选项值便可,其余配置与完整版的组件库webpack配置文件一致(输出目录可根据须要自行配置)。构建时执行这两个配置文件,便可构建出一个完整版的组件库包和每一个组件独立的包。web

const cptConf = require('../src/config.json');
const entry = {};

cptConf.packages.map((item)=>{
    entry[cptName] = `./src/packages/${item.name.toLowerCase()}/index.js`;
});

module.exports = {
    entry
};
复制代码

若是用户项目中使用了多个组件,这种分别引用每一个组件及其样式文件的写法仍是略显繁琐,URL拼写也容易出错。代码洁癖患者的感觉也须要顾及啊~

抛开技术实现和兼容性不谈,比较理想的、面向将来的写法应该是ES6 modules风格的写法,由于一众的模块化方案中,这是亲儿子。

import { Button,Switch } from '@nutui/nutui';
复制代码

咱们考虑支持这种写法,并提供一个工具在用户应用编译阶段将代码自动转换为组件单独引用的写法:

import Button from '@nutui/nutui/dist/packages/button/button.js';
import Switch from '@nutui/nutui/dist/packages/switch/switch.js';  

import '@nutui/nutui/dist/packages/button/button.css'; 
import '@nutui/nutui/dist/packages/switch/switch.css';
复制代码

承担这种转码工做最适合的人选非Babel莫属了。大多数用户的项目脚手架都会安装Babel,用来进行ES6+语法向低版本语法的转换,咱们只须要提供一个Babel的插件,使其在转换的过程当中捎带着把咱们组件按需加载的语法也给转换了便可。咱们先来了解一下Babel的工做原理。

Babel的转码工做大体分为三个阶段:

  • 解析(parse):将代码字符串解析成AST(抽象语法树)
  • 转换(transform):对抽象语法树进行转换操做
  • 生成(generate): 将变换后的抽象语法树再生成代码字符串

抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。

咱们的Babel插件@nutui/babel-plugin-separate-import[2]的大体工做原理是在代码被解析成AST抽象语法树以后,遍历语法树找到形如import { Button,Switch } from '@nutui/nutui';的语法相关节点,转换成单独引用组件的语法,最后再生成代码字符串。

这只是基本原理,实际状况比较复杂,由于还须要考虑样式文件类型、主题换肤、国际化等因素,这里就不展开了。下面说下这个插件的基本使用。

经过npm/yarn安装@nutui/babel-plugin-separate-import 在项目的Babel配置文件(如.babelrc)中配置插件

{
  "plugins": [
    ["@nutui/babel-plugin-separate-import", {
      "style": "css"
    }]
  ]
}
复制代码

而后就可使用ES6 modules风格的语法引用所需的组件了

import Vue from 'vue';
import { Button,Switch } from '@nutui/nutui';

Vue.use(Button);
Vue.use(Switch);
复制代码

既然说到Babel与AST,咱们不妨进行一些延展(这部份内容属赠送性质)。Babel自带的AST操做相关模块能够在须要AST的场景独立使用,无需再安装其余AST工具。

  • @babel/parser模块用来把代码解析成AST抽象语法树
  • @babel/traverse模块用来对AST节点进行递归遍历
  • @babel/types模块用来对具体的AST节点进行进行增、删、改、查
  • @babel/generator模块用来将修改后的AST生成新的代码字符串

好比在NutUI 2.x项目中,咱们为新增组件提供了一个命令npm run add,可根据录入信息自动生成新组件的模板,并更新配置文件。其中一个须要更新的组件库配置文件是src目录下的nutui.js文件,这个文件很是重要,是整个项目的entry文件。添加新组件的时候,nutui.js文件有两处须要修改。

增长两个import,用于加载新组件的入口js文件和scss文件。如:

import Uploader from "./packages/uploader/index.js";
import "./packages/uploader/uploader.scss";
复制代码

向packages对象添加新组件信息。如:

const packages = {
  Cell,
  Dialog,
  Icon,
  Toast,
  ...
  Uploader
}
复制代码

第一处修改并不困难,能够经过Node.js将nutui.js文件内容读取,而后把两个新的import加在内容头部,再把新文本内容写入文件。然鹅,第二处修改就有些困难了,如何向文件中的一个js对象中追加内容呢?一个靠谱的办法就是AST,即把读取的文件内容解析成AST,而后遍历AST找到packages对象,向其中追加新组件信息,最后生成新的代码字符串,写入nutui.js文件。而这些操做能够经过Babel自带的相关模块来完成[3]。

const t = require('@babel/types');
const {parse} = require('@babel/parser');
const {default: traverse} = require('@babel/traverse');
const {default: generate} = require('@babel/generator');
复制代码

好了,这篇文章先谈到这里。留一个思考题吧,咱们知道,webpack 2+ 拥有了Tree-shaking(摇树)功能,能“摇”掉未用到的代码,那么若是咱们不借助Babel插件处理,而直接使用下面这种ES6 modules语法来引入组件,未用到的组件会被“摇”掉吗?答案固然是否认的,不然何须去开发个Babel插件,因此我真正要问的是为何不能呢?

import { Button,Switch } from '@nutui/nutui';
复制代码

连接

相关文章
相关标签/搜索