前言
在微信小程序开发中,包体积限制(主包 2MB,整个小程序 20MB)是开发者必须面对的挑战。为了优化加载速度和减小包体积,我们通常会将图片资源存放在 CDN(如阿里云 OSS)上,仅在代码中保留引用链接。
然而,手动上传图片并替换链接不仅繁琐,而且容易出错。在 Taro 2.2.17 项目中,我们可以利用 Webpack 的扩展能力,编写一个自定义插件,在构建阶段自动将指定目录下的图片上传至 OSS,并替换代码中的引用路径。
本文将基于三个核心文件(插件入口、Webpack Loader、工具库),详细拆解如何实现这一自动化流程。
核心设计思路
为了实现"构建即上传",我们的方案主要包含三个关键步骤:
- 拦截 Postcss 处理 :Taro 默认使用
postcss-url处理 CSS 中的图片引用(如转为 base64 或移动路径)。我们需要拦截这一过程,防止它修改我们的图片路径,确保后续 Webpack Loader 能捕获到原始文件。 - Webpack Loader 拦截 :利用 Webpack 的
pitch阶段,在文件被其他 Loader(如file-loader)处理之前,拦截符合条件的图片文件。 - 上传与缓存机制 :
- 计算 MD5,优先读取本地缓存(
.taro_upload_cache/data.json)。 - 若缓存未命中,则上传至阿里云 OSS。
- 上传前检查 OSS 是否已存在该文件,避免重复上传。
- 最终将文件路径替换为 OSS 的 CDN 链接。
- 计算 MD5,优先读取本地缓存(
代码实现详解
1. 插件入口:配置与拦截
首先,我们需要在 Taro 构建流程的早期介入。index.js 是插件的入口,主要负责配置校验和修改构建配置。
javascript
// index.js 核心逻辑片段
module.exports = function (ctx, options = {}) {
const { uploadImagesDir, oss, cacheDir } = options
// ...配置校验与路径解析
ctx.onBuildStart(() => {
// 1. 修改 postcss-url 配置
const miniConfig = ctx.initialConfig.mini || {}
const postcssConfig = miniConfig.postcss || {}
const urlOptions = postcssConfig.url || {}
// 覆盖 filter 函数
urlConfig.filter = function(asset) {
const isTargetFile = imageReg.test(asset.absolutePath) &&
asset.absolutePath.startsWith(absoluteUploadDir)
if (isTargetFile) {
// 如果有缓存或有 OSS 配置,拦截 postcss-url 的处理(返回 false)
const hasCache = !!autoUploadUtils.getUploadedFileUrl(...)
if (hasCache || (ossConfigExist && autoUploadUtils.checkSdkExist())) {
return false
}
}
// ...兼容原有逻辑
}
// 回写配置
ctx.initialConfig.mini = miniConfig
})
ctx.modifyWebpackChain(({ chain }) => {
// 2. 添加自定义 Webpack Rule
chain.module
.rule('autoUploadOSS')
.post() // 使用 post 确保在默认规则之后,但利用 pitch 拦截
.test(imageReg)
.use('autoUploadLoader')
.loader(require.resolve('./_AutoUploadWebpackLoader.js'))
.options({ uploadImagesDir: absoluteUploadDir, cacheDir: absoluteCacheDir, oss })
})
}
关键点解析:
ctx.onBuildStart: 我们在这里动态修改了postcss-url的filter。对于目标目录下的图片,如果满足上传条件,我们返回false阻止postcss-url将其转为 base64 或复制到 dist 目录,从而让资源流进入 Webpack 的 module 处理系统。ctx.modifyWebpackChain: 使用webpack-chainAPI 注册了一个新的 Rule。注意.post()和.test(imageReg),这确保了我们的 Loader 能捕获到图片文件。
2. Webpack Loader:核心上传逻辑
_AutoUploadWebpackLoader.js 是整个方案的核心。我们使用 pitch 阶段来处理文件,因为 pitch 是从右到左执行的,且在正常 Loader 之前,非常适合用来阻断后续的文件处理逻辑(如 file-loader)。
javascript
// _AutoUploadWebpackLoader.js 核心逻辑片段
exports.pitch = async function() {
const callback = this.async()
const { uploadImagesDir, cacheDir, oss } = this.query
// 1. 基础校验:路径匹配、文件存在性
if (!this.resourcePath.startsWith(uploadImagesDir) || !autoUploadUtils.fileExists(this.resourcePath)) {
return callback()
}
try {
// 2. 计算文件 MD5
const fileBuffer = autoUploadUtils.getBufferContent(autoUploadUtils.readFile(this.resourcePath))
const fileMd5 = autoUploadUtils.getFileMd5(fileBuffer)
// 3. 查找本地缓存
let fileUrl = autoUploadUtils.getUploadedFileUrl(cacheDir, fileMd5)
if (!fileUrl) {
// 4. 缓存未命中,初始化 OSS Client (单例模式)
const OSS = require('ali-oss')
const bucketController = await BucketController.getInstance(() => new OSS({ ...oss }), oss.bucket)
// 5. 检查远程文件是否存在
const objectName = autoUploadUtils.normalizeObjectName(...)
const fileInfo = await bucketController.getExistObject(ossObjectName)
if (!fileInfo) {
// 远程不存在,执行上传
fileUrl = await bucketController.putBuffer(fileBuffer, ossObjectName, {
meta: { [fileMd5MetaPropName]: fileMd5 },
headers: { 'x-oss-forbid-overwrite': 'true' }
})
} else {
// 远程存在,校验一致性
// ...比对 MD5 或 ETag 逻辑
fileUrl = fileInfo.res.requestUrls[...]
}
}
// 6. 更新本地缓存
if (fileUrl) {
autoUploadUtils.updateUploadedData(cacheDir, { [fileMd5]: fileUrl })
// 7. 返回 OSS URL,阻断后续 loader
return callback(null, `module.exports = ${JSON.stringify(fileUrl)}`)
}
} catch (e) {
console.error(`[AutoUploadLoader Error]`, e.message)
}
callback()
}
关键点解析:
- BucketController 单例 :为了避免每次上传文件都重新创建 OSS 连接,我们使用了一个单例模式的
BucketController来复用 client 实例。 - 一致性校验 :上传时,我们将文件的 MD5 写入 OSS 的
meta中。当远程文件已存在时,我们优先比对 meta 中的 MD5。如果 meta 丢失(旧文件),则回退比对 ETag。这确保了本地文件和远程文件的一致性。 - 阻断机制 :当
callback(null, module.exports = ...)被调用时,Webpack 会认为该模块已经被处理完毕,从而跳过后续的file-loader或url-loader,直接在代码中使用返回的 URL 字符串。
3. 工具库:缓存与辅助函数
_AutoUploadUtils.js 封装了所有文件系统操作和加密逻辑,保持 Loader 的简洁。
- 缓存策略 :使用
.taro_upload_cache/data.json存储{ [md5]: url }的映射。在构建开始时读取,上传成功后写入。这意味着只要文件内容没变(MD5 不变),即使清理了 dist 目录,再次构建时也能瞬间获取 URL,无需网络请求。 - 路径规范化 :
normalizeObjectName函数确保了在 Windows 开发环境下,上传到 OSS 的路径分隔符被统一为 POSIX 格式(/),避免路径错误。
使用方式
在 Taro 项目的 config/index.js 中引入并配置该插件:
javascript
const config = {
// ...
plugins: [
['你的插件路径', {
uploadImagesDir: 'src/assets/images', // 需要自动上传的目录
oss: {
region: 'oss-cn-hangzhou',
accessKeyId: 'your-access-key-id',
accessKeySecret: 'your-access-key-secret',
bucket: 'your-bucket-name'
},
cacheDir: '.taro_upload_cache' // 可选,默认为项目根目录下
}]
]
}
总结
这套方案通过 Taro 插件与 Webpack Loader 的深度结合,实现了图片资源的自动化管理。它不仅解决了小程序包体积限制的问题,还通过本地缓存和远程 MD5 校验机制,大大提升了构建效率和资源一致性。
对于 Taro 2.2.17 的微信小程序项目而言,这种"无侵入式"的开发体验(开发者只需将图片放入指定目录即可)能显著提升开发效率,让团队更专注于业务逻辑的实现。