背景 如果我不想每次构建 SPA 的时,把项目的依赖也一并重新构建一遍,我应该怎么做? 预打包项目依赖,当项目依赖长久没有发生变更时,不再重新 bundle node_modules。
解决方案 类似 Webpack DLL plugin,但是要更加智能。
只预打包项目中多次使用的依赖包。 同时支持 build/serve 两种模式。
实现细节——预打包部分: 用正则表达式找出项目中所有的 import 语句 用单独的 vite config 配合 preBundle 插件为第三方包生成预打包文件
实现细节——使用预打包文件: build 模式,这里涉及 rollup 相关的插件逻辑,需要让 rollup 将预打包的模块 id 解析为外部依赖,并且将预打包文件作为 prebuilt-chunk 输出。
dev 模式,这里涉及 vite 跟 esbuild 相关的逻辑。 由于我们并不会为所有的第三方依赖生成预打包文件,所以存在某些 optimizeDeps 模块引用预打包模块的情况。 vite 会使用 esbuild optimize deps,所以需要配合 esbuild 插件处理。
如何在导入方使用 commonjs 导出的 named export ? 在预打包阶段, rollup 不会为 cjs entry 生成 named export,当我们在导入预打包文件时,需要对这些被转换成 esm 的 cjs 做一些额外处理。
需要自己实现一个 transform hook:
// input
import { useState } from ' react ' ;
// ouput
import React from ' react ' ;
const useState = React.useState ;
反思 预打包多次使用的依赖会为每个依赖都创建一个 bundle 文件,这可能会让浏览器加载页面的时候需要发送更多的请求。 但是考虑到这些预打包文件可能很少更新,浏览器本地缓存可以起到很好的效果。如果浏览器跟服务器之间使用的是 http2 协议,这些请求似乎也不太算是问题? 如果要从根本上解决这个问题,还得靠 web bundle。
实现 merged exports https://github.com/tc39/proposal-module-declarations?tab=readme-ov-file
像 @ant-design/icons
中提供了很多 icons, 如果为每个 icon 都创建一个 prebundle chunk, 那么 output 目录中就会出现上千的小文件。
要避免这种情况,需要实现合并 exports 的功能。
// vite.config.mts
plugins : [
prebundleReference ( {
merge : {
' <ruleName> ' : [ ' <module-id-prefix> ' , ' @ant-design/icons ' ]
}
} )
]
以上的配置会在 transform 环节产生如下的代码:
// pre-bundle-merged-<ruleName>.mjs
import * as __at__ant_design_icons from ' @ant-design/icons ' ;
import * as __at__ant_design_icons_Foo from ' @ant-design/icons/Foo ' ;
export const __ns_at__ant_design_icons = __at__ant_design_icons ;
export const __ns_at__ant_design_icons_Foo = __at__ant_design_icons_Foo ;
对应的 manifest 部分
[
{
" moduleId " : " @ant-design/icons " ,
" moduleFilePath " : " pre-bundle-merged-<ruleName>.mjs " ,
" exports " : [
" default " ,
" __moduleExports "
],
" exportAs " : " __ns_at__ant_design_icons " ,
" isCommonJS " : false
},
{
" moduleId " : " @ant-design/icons/Foo " ,
" moduleFilePath " : " pre-bundle-merged-<ruleName>.mjs " ,
" exports " : [
" default " ,
" __moduleExports "
],
" exportAs " : " __ns_at__ant_design_icons_Foo " ,
" isCommonJS " : false
},
]
当在 reference 插件中使用时,需要转换 import 代码:
// helper
export function _prebundle_merge_get_default ( moduleLike ) {
return moduleLike . default ?? moduleLike ;
}
export function _prebundle_merge_get_named ( moduleLike , name ) {
if ( name in moduleLike ) {
return moduleLike [ name ] ;
}
if ( moduleLike . default && name in moduleLike . default ) {
return moduleLike . default [ name ] ;
}
}
// default import
import Foo from ' @ant-design/icons/Foo ' ;
// =>
import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$1 } from ' @ant-design/icons/Foo ' ;
const Foo = _prebundle_merge_get_default (__ns_at__ant_design_icons_Foo$1) ;
// named import
import { Foo } from ' @ant-design/icons ' ;
// =>
import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$2 } from ' @ant-design/icons/Foo ' ;
const Foo = _prebundle_merge_get_named (__ns_at__ant_design_icons_Foo$2 , ' Foo ' ) ;
// ns import
import * as Icons from ' @ant-design/icons ' ;
// =>
import { __ns_at__ant_design_icons_Foo as __ns_at__ant_design_icons_Foo$3 } from ' @ant-design/icons/Foo ' ;
const Icons = __ns_at__ant_design_icons_Foo$3 ;
prebundle merge transform 的逻辑应该在 commonjs 转换之后,因为 commonjs transform 假定 module 只会导出 { default: blabla }
// input
import { useCallback } from ' react ' ;
// transform commonjs
import React from ' react ' ;
const useCallback = React . useCallback ;
// transform prebundle merge
import { __ns_react as __ns_react$1 } from ' react ' ;
const React = _prebundle_merge_get_default (__ns_react$1) ;
const useCallback = React . useCallback ;
好像也可以将 commonjs transform 跟 prebundle merge transform 合并?
[
{
" moduleId " : " react " ,
" moduleFilePath " : " pre-bundle-merged-<ruleName>.mjs " ,
" exports " : [
" default "
],
" exportAs " : " default " ,
" isCommonJS " : true
}
]
import React , { useCallback } from ' react ' ;
// =>
import { default as __ns_react$1 } from ' react ' ;
const React = _prebundle_merge_get_default (__ns_react$1) ;
const useCallback = _prebundle_merge_get_named (__ns_react$1 . ' useCallback ' ) ;
接着,在 resolve 阶段,只需要将模块 id 都替换成 <path-to>/<moduleFilePath>
预打包依赖项,真的可以提高构建效率吗? 在我的项目中,第三方依赖的体积远大于实际的代码量,每次启动 dev server 或者构建 production artifacts,都需要消耗很多时间在转换 node_modules 代码上。预打包依赖就可以很好的解决这个问题。
另外,配合 nx 的共享缓存,这个项目的所有贡献者、CI 流水线,都可以享受到预打包带来的提升。
为啥不能用 UMD 格式提供 pre-bundle ? rollup 不支持为 umd bundle 生成 common shared chunk