背景
基于组件库开发业务需求已经成为前端常态。在我们使用组件库的时候,经常能看到官网文档介绍组件库默认支持按需加载;也会有快速上手让我们用 babel-plugin-import 实现按需加载。那么组件库按需加载怎么实现的?babel-plugin-import 又做了什么?
由于业务场景,我们需要搭建业务组件库提高开发效率。Arco 刚好提供了一个搭建业务组件库的模版,详情了解 arco 物料库 。
在我们搭建组件库、开发抽取组件落地到组件库的过程中遇到了很多问题,从而引发了很多思考。今天,让我们从Tree Shaking
的角度探索下基于 arco 组件库搭建业务组件库的细节
预期收获
1. 了解 tree-shaking 与 babel-plugin-import
1.1 随着 tree-shaking 的发展,了解 babel-plugin-import 现状
2. 搞清楚组件库按需加载实现细节
2.1 为什么构建产物包含不同模块化规范的包
2.2 实际项目里用到的是哪个包呢
2.3 组件库怎么实现样式按需引入
2.4 附属组件 和 组件相关hook 为什么都挂在对应组件上导出
3. 业务组件 与 arco 组件如何在项目里共存
3.1 介绍搭建业务组件库时遇到的问题、错误的使用姿势
3.2 详细分析项目工程里业务组件库 与 arco 组件库 共存的可能情况分析
Tree-Shaking
先来了解下 DCE
Dead code elimination 是一种编译优化技术,它的用途是移除对程序运行结果没有任何影响的代码。无用代码消除广泛存在于传统的静态编程语言中,编译器可以判断出某些代码根本不影响输出,将 Dead Code 从 AST (抽象语法树)中删除。
Dead Code 具有的特征:
什么是 Tree-shaking
Tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。在前端领域,这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。它依赖于 ES2015 模块语法的 静态结构 特性。
Tree-shaking 和传统的 DCE 又不太一样。传统的 DCE 消灭不可能执行的代码,而 tree-shaking 更关注于无用模块的消除,消除那些引用了但并没有被使用的模块。
Tree-Shaking 的价值
-
javascript 绝大多数情况需要通过网络进行加载。减少 web 项目中 JavaScript 的无用代码,可以减少文件体积,加载文件资源的时间也就减少了,从而通过减少用户打开页面所需的等待时间,来增强用户体验。
- 日常开发经常需要引用各种库。但大多时候仅仅使用了这些库的某些部分,并非需要全部引入。此时Tree-Shaking如果能帮助我们删除掉没有使用的代码,将会大大缩减打包后的代码量。
- 还可以避免浏览器进行无效的运算。
How to work
依赖 ES Module 静态分析
利用 ES Module 可以进行静态分析的特点来检测模块内容的导出、导入以及被使用的情况,保留 Live Code。这就是tree-shaking的基础
所谓静态分析就是不执行代码,从字面量上对代码进行分析。ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析。ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。
ES6 module 特点:
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串常量
- import binding 是 immutable的
这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能
消除 Dead Code
消除不会被执行和没有副作用(Side Effect) 的 Dead Code,即 DCE 过程
Rollup
Rollup 打包过程中的 tree-shaking 必须具备以下两个关键实现:
- ES6 的模块引入是静态分析的,可以在编译时正确判断到底加载了什么代码。
- 分析程序流,判断哪些变量被使用、引用,打包这些代码。
而 tree-shaking 的核心就包含在这个分析程序流的过程中: 基于作用域,在 AST 过程中对函数或全局对象形成对象记录,然后在整个形成的作用域链对象中进行匹配 import 导入的标识,最后只打包匹配的代码,而删除那些未被匹配使用的代码。
参考
无用代码去哪了?项目减重之 rollup 的 Tree-shaking - 掘金
webpack
webpack 负责对代码进行标记,把 import & export 标记为 3 类:
- 所有 import 标记为 /* harmony import */
- 被使用过的 export 标记为 /* harmony export ([type]) */,其中 [type] 和 webpack 内部有关,可能是 binding, immutable 等等。
- 没被使用过的 import 标记为 /* unused harmony export [FuncName] */,其中 [FuncName] 为 export 的方法名称
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
'use strict';
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__['a'] = cube;
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
});
之后 UglifyJS、terser 这类压缩代码的工具。读取 webpack 打包结果,在压缩之前移除 bundle 中未使用的代码。
参考
Webpack 原理系列九:Tree-Shaking 实现原理
不断发展完善
由于静态分析在 JavaScript 这样的动态语言中很困难,因此 tree-shaking 仍然无法像静态语言一样强大。
无论是 rollup、还是 webapck 等构建工具都在不断发展完善 tree-shaking 技术。 也许以前tree-shaking无法覆盖的场景在不断迭代中都会被解决。
- 比如,这篇文章 里介绍的关于 babel 转译 class 产生的副作用导致无法删除无用代码问题。webpack 引入 usedExports、ModuleConcatenationPlugin 、 /#PURE/、SideEffect 更好的解决副作用导致的无法删除 Dead Code 问题
- 比如,webpack 删除无用代码的压缩工具就从 UglifyJS 到 babel-minify 再到 terser
- 比如,webpack5 对 tree-shaking 进行了很多增强,参考 Webpack 5 发布 (2020-10-10) | webpack 中文文档
babel-plugin-import
babel-plugin-import 与 tree-shaking 目的一样,可以避免把引用到的外部组件或功能库全量打包,从而导致未引用部分仍被打包进去,导致性能问题
需要相应组件库将各个组件以独立文件形式导出,供项目工程使用。避免将所有组件打包进一个文件里对外提供使用
做了什么
// 配置
{ "libraryName": "@arco-design/web-react", libraryDirectory: 'es', style: true }
import { Button } from '@arco-design/web-react';
↓ ↓ ↓ ↓ ↓ ↓
import Button from '@arco-design/web-react/es/Button';
import '@arco-design/web-react/es/Button/style';
原理
总结一下,babel-plugin-import 和普遍的 babel 插件一样,会遍历代码的 ast,然后在 ast 上做了一些事情:
- 收集依赖:找到 importDeclaration,分析出包 a 和依赖 b,c,d....,假如 a 和 libraryName 一致,就将 b,c,d... 在内部收集起来
- 判断是否使用:在多种情况下判断 收集到的 b,c,d... 是否在代码中被使用,如果有使用的,就调用 importMethod 生成新的 import 语句
- 生成引入代码:根据配置项生成代码和样式的 import 语句
详情参考 【前端工程化基础 - Babel 篇】简单实现 babel-plugin-import 插件 - 掘金
babel-plugin-import VS tree-shaking
- 目前 babel-plugin-import 存在的意义在于配合组件库做样式按需引入。
实现组件按需加载基于 ES module 的 Tree-Shaking 足够用了。如果不考虑样式按需加载完全可以舍弃 babel-plugin-import
- 不支持 tree-shaking 的项目工程实现组件库按需加载
对于低版本webapck如1.x,其本身不支持 tree-shaking,因此可以通过此插件实现组件库按需加载
参考
根据文档使用babel-plugin-import按需加载antd,但是npm run build 之后的包依然有600kb · Issue #16600 · ant-design/ant-desig
[RFC] 重新 Review 大包组件 js 按需引入&样式自动且按需引入方案 · Issue #4703 · alibaba/ice
按需加载的组件库,内部引用的组件库没有按需加载 · Issue #369 · umijs/babel-plugin-import
webpack 相关
作用域提升(scope hoisting)
过去 webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,一些工具像 Closure Compiler 和 RollupJS 可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。
ModuleConcatenationPlugin 这个插件会在 webpack 中实现以上的预编译功能。
其表现如下
// main.js
export default "hello leo~";
// index.js
import str from "./main.js";
console.log(str);
[
(function (module, __webpack_exports__, __webpack_require__) {
var __WEBPACK_IMPORTED_MODULE_0__main_js__ = __webpack_require__(1);
console.log(__WEBPACK_IMPORTED_MODULE_0__main_js__["a"]);
}),
(function (module, __webpack_exports__, __webpack_require__) {
__webpack_exports__["a"] = ('hello leo~');
})
]
[
(function (module, __webpack_exports__, __webpack_require__) {
var main = ('hello leo~');
console.log(main);
})
]
Scope Hoisting 的实现原理其实很简单:分析出模块之间的依赖关系,尽可能将打散的模块合并到一个函数中,前提是不能造成代码冗余。 因此**「只有那些被引用了一次的模块才能被合并」**。
由于 Scope Hoisting 需要分析出模块之间的依赖关系,因此源码**「必须采用 ES6 模块化语句」**,不然它将无法生效。
更多参考
ModuleConcatenationPlugin | webpack 中文文档
动态导入(dynamic import)
在单页应用中,我们经常使用 webpack 的 动态导入 功能来异步加载模块。这是一种很好的优化网页或应用的方式,可以将大包应用拆分成许多小包。这样减轻了应用的初始加载体积,加快了应用的初始加载速度,因为某些代码块可能永远不会被加载。
调用 import()
的之处将被视为分割点。这意味着,被请求的模块和它引用的所有子模块,会分割到一个单独的 chunk 中。供后续条件触发时加载该模块并执行
其流程大致如下
拆包中的公共库如何取舍
假设我们根据路由拆包分为了 A、B 2个页面,打包成2个chunk。
- 如果都使用了 echarts,且都被打包进了2个chunk里
这肯定是不能接受的,可以把echarts单独打进一个chunk里,让A、B分别去加载使用
- 如果 A 页面用到了 arco 一部分组件a,B页面也用到了 arco 一部分组件b
-
最好将 Arco 单独打包进一个chunk里,让A、B分别去加载使用。但此时就跟拆包的初衷有些违背了,我们拆包是为了减少不必要的代码加载执行。但是此时加载 A 页面需要加载 A、B 页面使用的 arco 组件合集。
-
如果 A、B 页面只把自己用到的组件代码打包进各自chunk里。看起来很符合拆包的初衷。但是对于样式这种全局影响的东西可能会导致一些预期外的问题。见下分析
import() 中的表达式
不能使用完全动态的 import 语句,例如 import(foo)。是因为 foo 可能是系统或项目中任何文件的任何路径。import() 必须至少包含一些关于模块的路径信息,打包可以限定于一个特定的目录或文件集。
例如import(./locale/${language}.json
)
webapck在打包时会把 .locale 目录中的每个 .json 文件打包到新的 chunk 中。在运行时,计算完变量 language 后,就可以使用像 english.json 或 german.json 的任何文件。
tree-shaking支持
import()
允许通过 /* webpackExports: ["abc", "default"] */
该魔法注释手动 tree shake 模块。
Webpack5 更好的 tree-shaking 支持
Webpack 5 发布 (2020-10-10) | webpack 中文文档
基于 arco 组件库 实现 业务组件库 深入分析
Arco 组件库打包产物分析 (同 业务组件库
其分别对应UMD、CommonJS、ES 模块规范
commander_1.default.command('build:component:dist').action(() => {
component_1.default.buildUMD();
});
commander_1.default.command('build:component:es').action(() => {
component_1.default.buildES();
});
commander_1.default.command('build:component:cjs').action(() => {
component_1.default.buildCJS();
});
通常情况下项目工程里打包用到的是 ES 目录下的代码。
更多详情参考:package.json 中 你还不清楚的 browser,module,main 字段优先级 · Issue #8 · SunshowerC/blog
ES module (es
ES 是一个包含多个文件的目录。各个组件在对应文件中独立存在且被导出。可通过 /es/Trigger
引入某个具体组件。
在入口文件 /``es/index.js
处,将所有组件重导出
再查看某一个组件入口,发现该组件内,所有 ES6 语法 都被转换成 ES5,但是模块规范仍是 es6 module
为什么 es 目录里代码被转译为 es5,模块规范还是 esm
- ES module 模块规范是为了做 tree-shaking
如下所示,再不引入 babel-plugin-import 的情况下,项目里通过以下方式引入组件,虽然会加载 /es/index.js
,但是依赖 es module 静态分析 会消除未引用的组件 达到按需引入的效果。
import { Button } from '@arco-design/web-react';
- 代码被转译为 es5 则是考虑到代码在低版本浏览器的兼容性问题
且 babel-loader 一般转译代码时候会忽略 node_modules
CommonJS (lib
lib 下是提供一个包含多个文件的目录。各个组件在对应文件中独立存在被导出。可通过 /lib/Trigger
引入某个组件。
在入口文件 /lib``/index.js
处,将所有组件重导出
再查看某一个组件入口,发现该组件内,所有 ES6 语法 都被转换成 ES5,但是模块规范是 commonjs
lib 目录配合 babel-plugin-import 仍可以做到按需加载
当我们通过该方式引入组件时,并没有引入 commonjs 模块规范的入口文件,而是具体的某组件对应的目录,因此也可以做到按需加载
// 配置
{ "libraryName": "@arco-design/web-react", libraryDirectory: 'lib', style: true }
import { Button } from '@arco-design/web-react';
↓ ↓ ↓ ↓ ↓ ↓
import Button from '@arco-design/web-react/lib/Button';
import '@arco-design/web-react/lib/Button/style';
为什么可以用 es module 加载 commonjs 导出的文件 ?
无论你开发使用的是 CommonJS 规范还是 ES6 模块规范,webpack 会对各种模块进行语法分析,并做转换编译。打包后的文件都统一使用 webpack 自定义的模块机制来管理、加载模块。
依赖 webapck 打包,你可以使用ES6 模块语法书写代码,也可以使用CommonJS模块语法,甚至可以两者混合使用。而打包后的文件可以兼容性跑在浏览器上
UMD (dist
Arco 通过 webpack 打包组件库代码生成 umd 模块规范代码
提供 umd 模式的文件,可以直接在 html 上通过 script 标签引用你的组件库。这种所有代码都放到一个文件里,且属于 umd 模块规范,因此 无法做到按需加载
值得注意的是,如果业务组件库依赖了arco组件,也会将依赖的组件打包进 umd 产物里。除非webpack特殊设置不把arco依赖打包
怎么实现按需引入
JS
通过 tsc 生成 es module 的包,将代码编译为 es5,但是模块规范还是 es module。每个组件在 es 目录中独立存在且被导出,在根入口处重导出
同时也通过 tsc 生成 lib module 的包,其代码编译为 es5,模块规范 commonjs。每个组件在 es 目录中独立存在且被导出,在根入口处重导出
以上,对于不支持 tree-shaking 的项目工程,也能通过 babel-plugin-import 很好的支持 按需加载
同时 package.json 新增 module 字段指向 es 目录,使 webpack、rollup 等构建工具在生产浏览器环境产物时时默认使用 es 目录
样式
/style/index.less
仅包含业务组件及子组件依赖的自身样式;/style/index.ts
包含组件自身样式 + 依赖的 arco 组件样式 + 同级业务组件样式
- 提供全量样式引入入口
注意这里全量样式入口引入的是每个组件下的 /style/index.less。如果引入 /style/index.ts 会导致重复引入样式文件。
- 引用组件时不会默认引入样式,而是交给使用方去全量引入,或者配置插件按需引入
为什么附属组件 和 组件相关hook 挂在对应组件上
避免使用方通用配置 bebel-plugin-import 按需引入,而无法覆盖相关附属组件、hook,导致不可预期的问题
假设通过如上图导出了 Label,那么配置 babel-plugin-import 根据默认配置,是没法找到对应组件
// 配置
{ "libraryName": "@arco-design/web-react", libraryDirectory: 'es', style: true }
import { Label } from '@arco-design/web-react';
↓ ↓ ↓ ↓ ↓ ↓
import Button from '@arco-design/web-react/es/Label';
import '@arco-design/web-react/es/Label/style';
按需加载导致组件、样式多次引入
假设项目里仅使用 arco 组件库,且 Popover 组件依赖 Tooltip 组件及其样式
我们可能会在项目的很多地方通过如下命令引入Tooltip
组件、Popover
组件使用
import { Tooltip } from '@arco-design/web-react';
import { Popover } from '@arco-design/web-react';
JS
无论是否使用 babel-plugin-import,我们手动引入的 Tooltip、或者通过 Popover 引入的Tooltip、甚至可能在项目里在不同模块地方引用,其引入都指向同一个文件 (webapck 称之为 module)。
至于 构建工具 是否将其打包进同一个 bundle 供所有地方共享,还是由于拆包将各个按需引入的组件打包进各个 bundle 里,在这里不作深究。
样式
全量引入
一般会在项目根入口处引入全量组件样式
按需引入样式(通过babel-pulguin-import
如果按需引入 Popover 组件,为了保证 Popover 组件能独立work,会把 Popover、Tooltip 组件独立样式都引入;此时如果再按需引入 Tooltip 组件,也会引入 Tooltip 组件独立样式。
虽然多了一个 import 语句,但引入都指向同一个样式文件。
至于 构建工具 是否将其打包进同一个 bundle 供所有地方共享,还是由于拆包将各个按需引入的组件打包进各个 bundle 里,在这里不作深究。
值得注意的是,如果分别打包进不同的chunk里,加载不同chunk时加载样式文件,由于全局样式影响可能会导致一些不可预期的问题
业务组件库 和 arco 组件库如何共存
项目中一般会使用业务组件处理特殊业务场景,但是也会大量用到 arco 组件,因此通常两者是共存的
考虑项目工程只引入业务组件库 & 不引入 arco 组件库
这里只是指项目工程不会直接使用 arco组件。实际上 arco组件库npm包
是 业务组件库npm包
的前置依赖,因此在项目工程里安装业务组件库,肯定必须要安装 arco组件库npm包
的。
按照目前业务组件库样式的设计
// 用于样式的按需加载
// 如此组件依赖了 B 组件,则需要将 B 组件的样式也引用进来,如: import '../../ComponentB/style'
import '@arco-design/web-react/es/Button/style';
import '@arco-design/web-react/es/Checkbox/style';
import '@arco-design/web-react/es/Link/style';
import '@arco-design/web-react/es/Space/style';
import '@arco-design/web-react/es/Spin/style';
import '../../Text/style';
import '../../Block/style';
import '../../PopButton/style';
import '../../Title/style';
import './index.less';
仅支持按需引入样式。
不支持全量引入业务组件库样式,如果全量引入业务组件库样式会导致缺少 所依赖的 arco 组件库样式。
换一种设计(并未落地
// 用于样式的按需加载
// 如此组件依赖了 B 组件,则需要将 B 组件的样式也引用进来,如: import '../../ComponentB/style'
import '../../Text/style';
import '../../Block/style';
import '../../PopButton/style';
import '../../Title/style';
// 将其依赖的 arco 组件样式都放在该文件中
// 但是 less 文件并不能引入 @arco-design/web-react/es/Space/style/index.ts
import './index.less';
看起来,似乎支持按需加载样式也支持全量加载样式。但实际并不可行
-
假设我们的业务组件依赖
@arco-design/web-react/es/Link/style/index.ts
/ ,这里会引入 Link 组件自身样式及其依赖的同级arco组件样式。而我们在 less 文件并不能引入@arco-design/web-react/es/Space/style/index.ts
文件。 -
也许可以看看具体依赖什么样式,从而手动引入。但实在不是一个好方案。
- 或许可以需要更改组件库样式打包逻辑
- 按需引入的入口还是
/style/index.ts
其包含所有样式。包括自身样式、依赖的 arco 组件样式、同级业务组件样式 - 单独维护一个供全量引入样式入口,其会找到所有组件的
/style/full-import.ts
。包括自身样式、依赖的 arco 组件样式。
// index.ts
// 用于样式的按需加载
// 如此组件依赖了 B 组件,则需要将 B 组件的样式也引用进来,如: import '../../ComponentB/style'
import '../../Text/style';
import '../../Block/style';
import '../../PopButton/style';
import '../../Title/style';
import './full-import.ts';
// full-import.ts
import '@arco-design/web-react/es/Button/style';
import '@arco-design/web-react/es/Checkbox/style';
import '@arco-design/web-react/es/Link/style';
import '@arco-design/web-react/es/Space/style';
import '@arco-design/web-react/es/Spin/style';
import './index.less';
全量引入arco组件库样式(dist/css/inde.less
全量引入业务组件库样式(dist/css/inde.less
目前设计
不会额外引入 arco 组件样式
另一设计
如果全量入口是引入每个组件的/style/index.less
,不会额外引入 arco 组件样式
如果全量入口是引入每个组件的/style/full-import.ts
,某个组件同一 arco 组件样式会可能会多次引入,但也都指向同一个文件(module
按需引入业务组件库样式
目前设计
如果按需引入 /style/index.less
则不会额外引入
如果按需引入 /style/index.ts
会额外引入冗余的 arco 组件样式,但也都对应同一个文件(module
另一设计
如果按需引入 /style/index.less
则不会额外引入
无论按需引入 /style/full-import.ts
还是 /style/index.ts
都会额外引入冗余的 arco 组件样式,但也都对应同一个文件(module
按需引入arco组件库样式(/style/index.ts
全量引入业务组件库样式
目前设计
只会引入业务组件库独立样式、不会额外引入arco组件样式。
但是可能会有问题,如果业务组件引入了 arco 组件A,此时并没有默认引入 arco 组件A 的样式;如果项目里没有直接使用arco 组件A, 也没按需引入 A 组件样式可能会导致bug
另一设计
如果全量入口是引入每个组件的/style/index.less
,同上
如果全量入口是引入每个组件的/style/full-import.ts
,某个组件同一 arco 组件样式会可能会多次引入,但也都对应同一个文件(module
按需引入业务组件库样式
目前设计
如果按需引入 /style/index.less
,则很容易 arco 组件库样式丢失出问题
如果按需引入 /style/index.ts
。在加载业务组件的情况下会默认加载依赖的 arco 组件样式,保证能独立work。虽然此时可能会引入冗余的 arco 组件样式。
另一设计
如果按需引入 /style/index.less
,则很容易 arco 组件库样式丢失出问题
如果按需引入 /style/full-import.ts
。在加载业务组件的情况下会默认加载依赖的 arco 组件样式,保证能独立work,但不会加载依赖的业务组件样式,也可能会出问题
如果按需引入 /style/index.ts
。在加载业务组件的情况下会默认加载依赖的 arco 组件样式、依赖的业务组件样式,保证能独立work。虽然此时可能会引入冗余的 arco 组件、业务组件样式。
业务组件库搭建 错误姿势
-
在使用业务组件时自动引入业务组件样式
一开始,我们并未考虑到业务组件库样式加载问题,像写项目组件一样将其样式入口放到了组件入口处。这样每次在项目里实际引用组件时,总会按需引入组件依赖的样式。
-
附属组件 和 组件相关hook 挂载问题
当我们修复1中的问题后,业务组件库已经支持通过 babel-plugin-import 按需加载样式了。但我们遇到了这个问题,见上有解析
-
业务组件A下的 components/a 样式引入问题
SideMenu 下有个 /components/menu 子组件。其样式我们一开始都维护在 /style.ts 中。但是这种做法在 全量引入样式的时候,其入口会找到所有组件的 /style/index.less , 导致子组件样式丢失
-
业务组件库 和 arco 组件库如何共存
由于问题3,我们更深入发现了这个问题,见上解析
-
按需加载组件库样式 与 webpack分包策略 导致的bug
假设我们根据路由拆包分为 A、B 两个chunk,在项目里按需引入业务组件库样式。其中:
-
A chunk 用到了 arco_button 组件
-
B chunk 也用到了 arco_button 组件
由于动态导入拆包优先级,在 webpack4 默认情况下并不会将 A、B chunk 用到的公共依赖单独抽出,而是分别打包进对应的chunk里。这意味着:
-
arco_button 组件及其样式被打包进A chunk,
-
arco_button 组件及其样式也同样会被打包进 B chunk
这可能会导致一些预期外的样式问题,假设
-
我们在 A chunk 里通过 css 覆盖了arco_button 原生样式,其权重与arco样式一致,通过后加载方式覆盖。
-
当我们切到 B 路由,会加载 业务库_button、 arco_button 样式。由于 css 全局特性,对当前window都会生效。
-
此时,我们再切回 A 路由,发现我们覆盖的 arco_button 原生样式,被 B 路由加载的 arco_button 样式 再次覆盖。
总结
这篇文章牵扯的知识点比较多,包括 tree-shaking、babel-plugin-import、webpack模块打包、npm包等等知识。其中并没有过多深入解读这些知识点,只是浅尝辄止刚好够用。主要是为了文章的主旨服务。
文章也列举了搭建组件库、提取组件落地到组件库过程中遇到的一些问题
关于按需加载的一些观点:
- 实现组件按需加载基于 ES module 的 Tree-Shaking 足够用了。如果不考虑样式按需加载完全可以舍弃 babel-plugin-import
- 样式按需引入的价值值得考量
目前看起来当项目达到一定体量后、尤其是当使用基于组件库封装业务组件库时,遇到的问题 与 收益 不成正比
尤其当按需引入 与 webpack动态导入分包策略 碰撞时,很可能造成一些预期外的问题。导致虽然使用了组件库按需加载,但也不得不把整个组件库使用到的组件打包进一个chunk里。这时候按需加载并不符合webpack拆包的初衷,更多的是摇掉整个项目没使用到的组件及其样式
按照现有业务组件库打包逻辑设计,如果我们在项目里用到了业务组件库、arco组件库
- arco组件库、业务组件库都全量引入样式
只需要打包工具自带的 tree-shaking 实现 js 层面的按需加载就可
- 组件库按需引入样式
需要关注拆包出的chunk引用的组件及其样式是不是分别打包进各自的chunk里,这可能导致问题。
最好将2个组件库及其样式单独打包进一个独立chunk
参考
tree-shaking
www.codingame.com/playgrounds…
babel-plugin-import
【前端工程化基础 - Babel 篇】简单实现 babel-plugin-import 插件 - 掘金
模块规范
关于 CommonJS AMD CMD UMD - 前端路漫漫 - OSCHINA - 中文开源技术交流社区
ES6
组件库发包
[译]打包用于分发的 UI 库 | plusmultiply0's blog
Webpack
tree-shaking