前言
随着单页应用的发展和前端应用需要处理的业务复杂度越来越高,我们不得不面临的问题是前端的代码量也变得越来越大。代码量的庞大一方面造成了开发编辑等待时间加长,影响开发效率,另一方面首屏加载需要更长的时间和带宽加载更庞大体积的文件。
为了解决单页应用打包拆分的需求,webpack很早就开始支持多种拆分的方案。最早支持的两个方案就是多entry打包和静态或动态的Code Splitting,随着webpack升级到4.0、5.0版本,代码拆分的能力和稳定性也逐渐增强。本文主要是调研了目前最新的打包拆分技术的各种方案,并且以webpack为例,具体阐释了webpack打包拆分的具体原理。
概念术语
什么是entry?
entry就是webpack的打包入口或者叫做入口文件。从入口文件开始分析,webpack可以找到整个文件依赖的关系。
什么是bundle
bundle就是webpack经过编译、压缩、拆分后得到的一系列文件,这些文件可以直接在浏览器中运行。
什么是chunk
chunk就是webpack编译过程中,将某些模块聚合在一起,组件一个单独的文件,这就是一个chunk。一般来说,一个bundle分成多个chunk后,单个chunk无法在浏览器中直接运行。
代码拆分技术分类
以webpack为例,目前webpack提供了四种拆包的方式
多bundle技术或者多entry方式,这个方式本质就是基于多路由或者微前端将一个应用拆分成多个应用。
bundle init splitting,创建许多小的chunk,但是chunk之间必须同时加载才能运行。
bundle dynamic splitting,创建许多小的chunk,允许chunk按需加载
Module Federation:共享模块
下面我们分别介绍下四种技术的优缺点。
多bundle技术
除了webpack内置支持的多entry实现多bundle之外,下面列举了四种多bundle技术:
基于路由的多entry
这个就是webpack内置支持的多entry方案,迁移比较简单,只需要简单修改webpack配置和配置反向代理即可。
iframe容器化
基于iframe的方案,将需要拆分的模块独立放置于iframe中。这种方式可以兼容多种不同的前端框架,但是缺点是改造成本比较高,框架通信也比较复杂。
微件化
SLS日志应用就是采取了微件化的方案,这个方案本质就是将每个app独立的打包成一个js文件(内部包含css和js)。每个app的大小都比较小,而且app的依赖可以和主项目共用,由主项目注入。微件化的方案适用于单一的前端框架,否则很难把控单个js文件的大小。
微前端
微前端是目前比较火的说法,主要是由single-spa这个项目引发的,国内比较著名的是基于single-spa的qiankun。这类方案的特点就是能够兼容多种不同的前端框架,并且提供了统一的环境、隔离机制、通信机制、生命周期等。
bundle init splitting
bundle init splitting就是将某一些文件单独打包,但是也必须和入口文件共同加载。这个方式与配置webpack的external类似,主要适用于将一些大型的库打包成外部依赖,一方面开发的时候可以加速编译时间,另一方面也能开启外部依赖的长效缓存机制。
这个方式有一个比较大的缺点就是每次页面加载的时候,被打包出去的依赖必须同时被浏览器加载、编译、运行的,如果这个包首屏用不到,会对首屏性能有一些影响。
下面是一个简单的bundle init splitting的Demo。
bundle dynamic splitting
bundle dynamic splitting就是webpack所谓的动态加载代码。这个方式的优点是代码是按需加载的,对首屏优化有非常大的帮助,而且特别适合用于react和vue组件的动态加载。缺点是代码的改造成本是比较高的,改造的时候需要借助webpack-bundle-analyzer等技术理清楚所有模块的依赖关系。
动态加载代码目前可以分为动态加载函数和动态加载react或者vue等组件。这里区分这两种方式是因为分别要让函数和组件实现动态加载的代价是不一样的。
动态加载函数
假设我们有如下两个文件:
//index.js
import callA from 'a'
init () {
// ...other code
callA()
// ...other code
}
//a.js
export default function callA() {
console.log('a')
}
如果想要将a.js让webpack作为动态代码加载,需要将调用callA的地方改为异步执行。并且用到callA的所有其他文件也需要改造,成本很高。
//index.js
const callA import('a') //异步加载语法
const init = async () => {
// ...other code
await callA()
// ...other code
}
//a.js
export default function callA() {
console.log('a')
}
动态加载react和vue组件
动态加载组件相对于函数来说方便很多,react和vue都有对应动态加载的方式,这里以react为例,只需要如下改写代码:
// a.js
export function Acom() {
return <div>Acom</div>
}
// b.js
const Acom = lazy(() => import('./a'))
<Suspense fallback={<div />}>
<Acom />
</Suspense>
Module Federation
Module Federation是webpack5推出的最新方案,共享模块粒度自由掌控,小到一个单独组件,大到一个完整应用。既实现了组件级别的复用,又实现了微服务的基本功能。依赖自动管理,可以共享 Host 中的依赖,版本不满足要求时自动 fallback 到 Remote 中依赖。
缺点就是更新逻辑复杂,模块更新后其他应用需要同步发版更新,也缺少应用之间的隔离机制。
结语
本文主要介绍了当前流行的前端打包拆分技术的几个方案。目前SLS也在使用这些方案优化首屏性能,主要用到了微件化、bundle init splitting、bundle dynamic splitting等方案。就我们团队目前的实践来说,单纯依赖webpack的配置实现代码拆分是非常困难的,主要有以下痛点:
打包工具的代码拆分策略配置复杂,调试困难
完全依赖打包工具的算法,具有极大的不确定性,少量修改会引起巨大的包拆分的变化
循环依赖无法解决,容易引起线上事故
面对巨石应用,无法进行异步拆分的代码基数依旧庞大。
欢迎大家评论指正。