QCon全球软件开发大会8折优惠倒计时最后3天,购票立减¥1760!了解详情 >>> 了解详情
写点什么

组件库构建方案演进

2020 年 1 月 17 日

组件库构建方案演进

组件库是前端开发过程中重要一环,一个足够灵活、功能强大的组件库方案将大幅度提升前端开发工作效率。在本篇文章中,笔者将从构建需求、项目结构、基于 webpack 的实现方案、基于 rollup 的实现方案、基于 rollup 的优化方案等五个部分来介绍组件库构建方案的演进过程,与读者共勉。


构建需求

  • 按需加载

  • 这里说的按需加载和浏览器前端资源的按需加载有点区别,组件库按需加载指的是项目只引入使用到的组件,没有使用到的组件不会被引入到项目中。通常这种引入方式都会依赖一些前端模块加载器来实现的,比如 webpack、rollup 等,这种引入方式带来的好处是减少项目代码体积,提高加载速度。

  • 更小的组件体积

  • 在开发组件的过程中,我们或多或少可能会使用到一些新的语法,像 es6,es7,ts 等等,最后都是需要经过编译才能保证在浏览器中正常运行,但是编译的过程中也总会加上一些额外的代码来实现一定的语法转换或者接口转换,甚至会带有一些脏代码等,这就不可避免地导致组件的源码大小会增加。因此我们这里说的更小的组件体积是指尽可能保持组件源码大小,把编译工具引入的额外代码量降到最低,并且去掉编译过程中的脏代码。


总的来说,第一个需求可以说是使用功能,第二个需求则更多的是偏向于代码量优化,下面我会通过一个例子来介绍组件库为实现这两个构建需求所使用的构建方案。


项目结构

在介绍构建方案之前,我们先来了解一下整个示例项目的项目结构,这个示例的项目结构和组件库的是保持一致的。


  • 源码目录结构

  • 组件源码都是放在 components 文件夹下,这个示例中有 a, b, c 三个组件以及 utils 公共方法集;每个组件都包含交互逻辑、样式、以及导出三个文件;其中 b 和 c 组件依赖 a 组件,a 组件依赖 utils 提供的工具函数。如下图:



  • 打包目录结构

  • 在构建需求中,我们说到组件按需加载是指只引入项目组件需要的组件,避免引入不需要的组件,实现这个需求,只要把每个组件单独打包成一个文件模块,就能实现项目引入需要的组件模块,而不引入其他组件模块。这跟 nodejs 的模块化思想是一样的,因此我们需要把组件打包成一个个 commonjs 模块。这里说明下为了保证打包后的脚本代码和样式代码是分离的,因此每个组件的样式也会单独打包成一个文件。最终我们定义的打包后的目录结构如下图:



从上图可以看出,我们可以通过以下的引入方法按需加载 A 组件:


import A from ‘component-build/lib/a.js';import ' component-build/lib/style/a.css';
复制代码


基于 webpack 的实现方案

现在我们来介绍基于 webpack 实现的构建方案,主要会从 4 个方面说一下 webpack 方案的配置要点。


  • 每个组件都是一个入口

  • 我们前面说到需要把每个组件都单独打包成一个模块文件,在 webpack 里如果需要把一个组件打包成一个文件,那么只需要把这个组件配置成 webpack 的一个入口即可。因此 webpack 的入口配置会是下面这样:


  {        // 每个组件模块单独一个入口    entry: {                a: 'components/a/index',                b: 'components/b/index',                c: 'components/c/index',                utils: 'components/utils/index'        }}
复制代码


这个配置可以保证每个组件打包出来之后都是单独一个文件,满足按需加载的需求。


  • 模块的构建目标

  • 我们的目标是打包出来的文件应该是满足 commonjs 模块规范的文件,可以通过设置 webpack 的 libraryTarget 来指明要把模块打包成符合 commonjs 规范。配置如下:


  {        // 确保打包出来的模块使用 module.exports 导致组件    libraryTarget: ‘commonjs2’}
复制代码


  • 提取样式文件

  • 组件通常会有样式代码,需要把样式代码单独提取成为一个样式文件,为了能满足我们打包后的目录结构,需要通过设置 mini-css-extract-plugin 插件的参数,把样式代码打包到 style 文件夹里。配置如下:


{       module: {        rules: [            {                test: /\.(s)css$/,                use: [                    MiniCssExtractPlugin.loader,                    {                        loader: 'css-loader',                        options: {                            importLoaders: 1                        }                    },                    'postcss-loader',                    'sass-loader'                ]            }        ]    },    // 提取组件样式,并统一放置到 style 文件夹下    plugins: [                 new MiniCssExtractPlugin('style/[name].css')        ]}
复制代码


  • 组件的依赖处理

  • 通常在开发组件的时候不可避免地可能会依赖另外一些组件,如果我们不处理当前组件对其他组件的依赖,那么 webpack 打包出来的当前组件代码中会包含其他组件的源码,这在按需加载中会产生冗余代码。由于每个组件都是 commonjs 的一个模块,我们只需把每个组件都配置成外部引用模块,webpack 就能把打包到当前组件的其他组件源码替换成通过 require 外部模块的方式引入。具体 webpack 配置如下:


{        // 所有组件在被其他组件所依赖时都替换成 commonjs2 的方式引入         externals: {        // name 变量为组件库名称        'components/a': { commonjs2: `${name}lib/a.js` },                   'components/b': { commonjs2: `${name}lib/b.js`  },                'components/c': { commonjs2: `${name}lib/c.js`  },                'components/utils': { commonjs2: `${name}lib/utils.js` }        }}
复制代码


  • webpack 方案的构建结果



lib 目录下的文件就是构建输出目录,输出的文件结构和前面定义的输出目录结构一致。取其中一个组件看看打包后的结果是否跟我们预期的一致,以 A 组件为示例:


  • style 下包含 A 组件的样式文件 a.css

  • 组件 A 使用 module.exports 导出

  • 组件 a 依赖的 utils 工具是以模块化的方式引入,而不是 utils 的代码直接打包在组件 a 里面


从上图打包出来的结果看,现在的方案已经是能实现按需加载了。


  • 基于 webpack 方案的缺点


我们来看看打包出来的 utils 模块,utils 模块中需要导出的是 add 和 remove 函数,但是从代码看却多了很多额外的代码,其中 webpack 的启动函数就占了大部分代码量,那么 webpack 的启动函数是否能删除掉?另外,我们组件只用到了 add 方法,remove 方法没有使用,是否能在项目打包的时候通过 tree shaking 去掉?基于这个分析我们可以看到目前方案有以下两个缺点:


  • webpack 自带的启动函数增加代码量

  • 打包的模块语法不支持 es6 的导入导出语法,没法通过 tree shaking 减少没有使用的代码


基于 rollup 的实现方案

为了解决基于 webpack 实现方案的这两个缺点,我们调研了其他的一些打包工具,发现使用 rollup 打包出来的代码不会带有工具的启动函数,而且 rollup 支持打包出 es6 模块,那么我们现在介绍一下基于 rollup 的实现方案,rollup 方案配置的内容其实和 webpack 是比较像的,rollup 方案的核心配置也是前面那 4 部分。


  • 一个配置对应一个入口组件

  • 每个组件都单独使用一份配置来进行打包(即调用多次 ruollup 打包),组件的配置只有入口文件以及输出文件不一样,其他配置内容都是一样,如下:


{        // 每个组件模块是一个入口,并且单独一份配置;A组件作为示例    input: 'components/a/index',    output: {        file: 'lib/a.js',    } }
复制代码


rollup 支持 es 模块语法的打包,即打包出来的模块使用的导入导出语法是 es6 的模块语法,所以在 rollup 的配置里设置 output.format: ‘es’ 属性值,表明打包出来的是 es6 模块,即使用 es6 的导入导出语法。如下:


{        // 声明rollup打包出来的是es模块    output: {        format: 'es'    }}
复制代码


 提取样式文件


使用 rollup-plugin-postcss 插件提取组件的样式代码,避免样式代码打包到脚本代码中。配置如下:


// 提取组件样式,并统一放置到 style 文件夹下,A 组件的配置如下:


{        plugins: [                 postcss({ extract: `lib/style/a.css` })    ]}
复制代码


  • 组件依赖处理

  • rollup 使用 external 属性来标记外部模块,并通过 output.paths 来匹配引用路径并替换成对应的值;external 的配置指定了 components 开头引入的模块都会标记为外部模块,然后通过在 paths 里面找到对应的值进行替换。配置如下:


{        output: {        paths: {      // name 变量为组件库名称            'components/a': { commonjs2: `${name} /lib/a.js` },                       'components/b': { commonjs2: `${name} /lib/b.js` },                    'components/c': { commonjs2: `${name}/lib/c.js`  },                    'components/utils': {  commonjs2: `${name}/lib/utils.js` }            }    },    // 匹配出要被标记为外部引入的模块    externals: (id, parent) => {                return /^components/i.test(id) && !!parent;        }}
复制代码


经过上面的配置,我们可以实现以下组件依赖的替换:


import 'component/a' => import 'component-build/lib/a.js'
复制代码


  • 基于 rollup 方案的打包结果



改成 rollup 的方案后,打包出来的 utils 模块是这样的:


  • 只保留了 utils 模块的 add 和 remove 方案,没有启动函数

  • 导出语法是 es6 语法,支持 tree shaking


相比 webpack 的方案,看起来代码简洁了许多,而且基本只保留了我们必须的代码。但是这个方案依然不完美。


  • 基于 rollup 方案的缺点

  • B 组件打包出来的结果图:


C 组件打包出来的结果图:



我们来看看基于 rollup 方案打包出来的 B 和 C 组件的代码:


  • 组件的依赖模块

  • 组件的 helper 函数(babel 编译自动添加的)

  • 编译后的源码

  • VUE 的序列化函数

  • 组件的导出


从 B 和 C 组件的一些相同点我们可以看出:


  • 打包后的文件可能会包含相同的 helper 函数

  • 打包后的文件包含相同的 vue 序列化函数代码


基于 rollup 的优化方案


针对前面那两个缺点,我们需要对 rollup 的打包方案做一些调整,以便可以复用多个组件都使用到的相同的 helper 函数,以及去掉各个组件相同的 vue 序列化函数。


  • 模块化引入 helper 函数和 vue 的序列化函数

  • 标记 helper 函数为外部模块,转换成模块化引入,实现不同组件使用相同 helper 函数时引用的都是同一个模块,而不是把 helper 函数的源码直接打包到组件代码中,这样通过模块化引入的方式就能解决多个组件可能包含相同 helper 函数源码的问题。externals 属性的配置修改成以下:


{        // 标记出 helper 函数的模块    externals: (id, parent) => {                return /^components/i.test(id) && !!parent ||   /^vue-runtime-helpers/.test(id) ||            /^@babel\/runtime/.test(id) ||                        /^@vue\/babel-helper-vue-jsx-merge-props/.test(id);        }}
复制代码


另外一种去掉 vue 的序列化函数方法是:vue 组件格式化函数主要是为 vue 组件绑定 render 函数以及添加样式;目前我们样式是单独一个 scss 文件的,可以说是跟 vue 组件分离的,那么组件格式化函数也是可以去掉的。目前我们使用 rollup-plugin-vue v3.0.0 版本,不使用最新 v4.x.x 版本,3.0.0 版本不会生成序列化函数。使用旧版本的配置和新版本的配置是一样的。


 指明依赖 helper 函数的 npm 包


通过前面这一步的配置,打包出来的组件代码会依赖 helper 函数的类库,因此我们需要在 package.json 中指明 helper 函数对应的 npm 包为我们的依赖包。如下:


{        "dependencies": {"@babel/runtime": "^7.8.3","vue": "^2.6.11","vue-runtime-helpers": "^v1.1.2"    }}
复制代码


  • 基于 rollup 优化方案的打包结果

  • B 组件打包后的结果:

  • C 组件打包后的结果:



从这两个图中,我们可以看出优化后的 rollup 方案,打包出来的 B 和 C 组件的代码:


  • Helper 函数变成依赖引入

  • Vue 序列化函数已变成依赖引入


最后,整个示例的代码仓库信息如下:


代码仓库地址:https://github.com/maiwenan/component-build


2020 年 1 月 17 日 18:091242

评论

发布
暂无评论
发现更多内容

产品训练营第三周作业

innovator琳

产品

深入汇编指令理解Java关键字volatile

AI乔治

Java 架构 volatile Java内存模型

掉坑了!GROUP_CONCAT函数引发的线上问题

AI乔治

Java MySQL 架构 GROUPING运算符

Java最前沿技术——ZGC

AI乔治

Java 架构 jdk ZGC JVM

高效获取信息的几点经验

彭宏豪95

效率 信息 阅读 4月日更

DAPP系统开发运营版,DAPP系统开发案例源码

系统开发咨询1357O98O718

Filecion云算力挖矿系统开发|FIL算力分发搭建

薇電13O25249123

区块链 分布式存储

介绍一款能取代 Scrapy 的爬虫框架 - feapder

星安果

Python 爬虫 Scrapy feapder

Redis-Cluster集群

Sakura

四月日更

太厉害了,终于有人能把Ansible讲的明明白白了,建议收藏

互联网老辛

ansible

HZFE 快报002 / 比特币7年来首次跌破50天均线

HZFEStudio

前端 金融科技 科技互联网 资讯

内容平台与热点挖掘思考

程序员架构进阶

28天写作 4月日更 领域思考 内容平台

打通本地部署和公有云,混合云架构让“鱼”和“熊掌”兼得(一)

UCloud技术

混合云

ceph-csi源码分析(2)-组件启动参数分析

良凯尔

Kubernetes 源码分析 Ceph CSI

读书有用吗

孙苏勇

读书

差点跳起来了!阿里首推22w字Java面试复盘宝典成功助我入职美团

程序员小毕

Java spring 架构 面试 设计模式

基于NIO高性能、可扩展网络应用库:xSocket

风翱

4月日更 xSocket

大数据-数据处理分类篇

进击的梦清

大数据 大数据处理 批处理 流式计算框架

Go 的 UTF-8 实现

Rayjun

go utf-8

思维训练

Ryan Zheng

ARST-日常打卡3

pjw

浅析 Linux 中的 I/O 管理

赖猫

Linux

关于读书的随想

小天同学

读书 4月日更

Golang 反射

escray

go 极客时间 学习笔记 4月日更 Go 语言从入门到实践

Python OOP-2

若尘

面向对象 oop 面向对象编程 Python编程

微信读书又更新,吃灰已久的Pencil又能拿出来遛一遛了。

彭宏豪95

效率工具 读书 阅读 4月日更 微信读书

封神总结!蚂蚁金服+滴滴+美团+拼多多+腾讯15万字Java面试题

Java架构追梦

Java 阿里巴巴 架构 面试题总结 金三银四

一次用户故事拆分分享

Bruce Talk

敏捷 Agile 用户故事 User Story

如何利用 Google 开源工具 Ko 在 kubernetes 建并部署 Go 应用

Chumper

Kubernetes 云原生

ceph-csi源码分析(1)-组件介绍与部署yaml分析

良凯尔

Kubernetes 源码分析 Ceph CSI

真的有那么丝滑吗?面试阿里(Java岗)从投简历到面试再到入职

互联网架构师小马

Java 面试 求职 阿里 找工作

移动应用开发的下一站

移动应用开发的下一站

组件库构建方案演进-InfoQ