背景
最近,新来的小组长要整活,要把项目的小程序重构。之前小程序是用 uniapp 实现的,打包工具是webpack,现在要改成用 uniapp + vite。
图片地址转换
以下小程序特指字节小程序
为什么需要转化
- 小程序的限制。小程序对包体积是有限制的:
- 未配置分包的情况下,小程序包源码大小不超过 4M
- 分包情况
- 子包 < 2M
- 主包 < 4M
- 总包 < 16M
如果不配置 url-loader 和 file-loader (webpack的2个loader) ,默认会把本地图片转成内联base64,包体积很快就会到达上限,导致无法在开发者工具上扫码预览,无法上传到小程序后台。
- 过多的内联图片导致包体积过大,会影响首次加载速度。
如何转换
目前在项目内,所有涉及到本地图片的都是写在css中,例如:
.empty-img {
width: 100px;
height: 86px;
background: url('@/static/no-content.png') no-repeat 0% 0%/100% 100%;
}
所有本地图片都是统一存在 static 目录下,方面构建时上传到 CDN。
webpack
通过在 vue.config.js
(uniapp小程序webpack配置文件) 配置 url-loader
和file-loader
,这样在编译时,可以把小于阈值的图片转成base64内联到代码,超过阈值的图片则交由 file-loader
处理,把图片地址改成本地或者CDN地址。
vite
在vite,看了vite配置的,vite也有类似 url-loader
的配置:build.assetsInlineLimit
vite也提供了静态资源路径处理的属性 —— base:
实际上,在小程序中尝试把base设置成CDN的地址,编译出来的路径还是相对路径,跟预期结果不一致,无法满足需求。
uniapp是通过vite插件来转化图片
如果是使用 vite,uniapp 默认把 小于40k的 图片转成 base64。按照 uniapp 官网文档介绍,uniapp 小程序是不支持开发者在 vite 里配置 build.assetsInlineLimit
的:
oh,那不是无法实现 url-loader 的功能?
先来看下 uniapp 使用 vite 后,css中图片路径编译后的结果:
编译前:
编译后:
编译后,基本上图片会被转成base64(超过40k的图片会被转成相对路径),类似这样:
background-image:url('@/static/no-content.png')
/** 转化后
background-image:url(../../static/no-content.png)
**/
40K这个限制是在哪里看到的?
官方文档里没写,我猜测 uniapp 针对 vite 做了一些配置。于是写了个 vite 插件查看所有配置,发现 assetsInlineLimit 是40k,也就是说大多数图片 (项目中图片一般先经过压缩,超过40K的较少) 会被转成base64内联到代码内。
编译流程图
vite:css 执行过程
uniapp 实现的一个 vite插件,主要作用是处理css中引用的图片地址:
经过 vite:css 处理后,css中的图片地址有2种情况:
- base64
- VITE_ASSET__contentHash
同时也有一个保存了 contentHash 到 fileName 的映射,举个🌰:
// cs
.test {
background-image: url('@/static/index/no-content.png');
}
// 经常 vite:css 转化后
.test {
background-image: url("__VITE_ASSET__8a6b759d__");
}
// 映射
{ '8a6b759d' => 'static/index/no-content.png' }
实际上,最终编译成的小程序.ttss文件是这样的:
.test {
background-image: url("../../static/index/no-content.png");
}
这一步从 VITE_ASSET__contentHash 转成相对路径是在 vite:css-post 实现的。
实现自定义插件
由于直接在 vite.config.js 配置 build.assetsInlineLimt 无效,可以通过自定义插件强制改变 build.assetsInlineLimt 的大小,例如改成2k:
import { Plugin } from "vite";
export function rewriteAssetsInlineLimit(opt: {
size: number
}): Plugin {
const { size } = opt || { size: 2048 };
return {
name: "rewrite-build-assetsInlineLimit",
enforce: 'post',
config: (config) => {
if (config.build) {
config.build.assetsInlineLimit = size;
}
}
};
}
使用该插件后,现在编译后的结果:
小于 2k 转成 base64,而超过2k的变成了不带hash的相对路径。我们需要的不是相对路径,而是一个http(s)资源的地址,可以由开发者直接通过配置传入。
按照上文所讲,最终生成的相对路径其实是直接根据 contenthash 得到文件路径,它的定义如下:
export function getAssetFilename(
hash: string,
config: ResolvedConfig
): string | undefined {
return assetHashToFilenameMap.get(config)?.get(hash)
}
那么就可以从设置映射的地方入手打补丁,先把不带hash 改成 带hash,同时生成新的带hash的资源目录。
改造前编译结果
改造后编译结果
改造前目录
改造后目录
接下来,还需要一个插件来把相对地址替换成 http(s)资源的地址(就是把assets目录上传到CDN后的地址)。
这里用到 transform 这个钩子来替换地址。
有几个关键点:
区分是本地编译还是生产环境编译,分别对应本地服务地址和CDN地址 (例如通过注入process.env变量判断);
基于性能考虑,dev环境 图片不需要生成hash值,也不需要生成新图片目录,只需要把图片地址替换成本地地址,同时开启一个静态服务,可以通过http访问到图片即可;
build环境 图片需要生成hash值,需要生成新图片目录,方便上传CDN;
到这里基本已经完成了,目前本地编译结果如下:
效果对比
vite 使用自定义插件前后小程序编译后的体积:
此时,图片被打包 ,到小程序内,整个小程序体积 4.4MB
图片地址改成CDN地址,通过网络请求访问,上传小程序前删除图片目录,整个小程序体积832KB
使用自定义插件后小程序的体积下降了超过80%
总结
目前已经把uniapp小程序基于webpack打包迁移到 uniapp小程序基于 vite 打包,打包工具需要的能力基本完成了,后续如果遇到坑点会继续发出来。