最近在做一个使用Taro框架的业务,最初因为只用到了H5,图片/svg等静态资源都是硬编码到代码中的,现在要编译成小程序,就需要将这些静态资源上传到oss。如果手动替换就是费时费力,所以需要使用webpack来做自动化上传,在此记录下。
解决思路
解决思路还是比较明确的,首先是loader部分,就是要webpack在解析图片和svg时执行上传任务,然后返回上传后的地址。
然后为了每次编译时不要重复上传已经上传过的内容,就需要记录下哪些资源已经上传过了,使用plugin来生成一个map文件,将该文件作为参数传到loader中,loader解析时判断如果已经存在在map文件中就直接返回地址,否则才执行上传任务。
loader部分
ini
function webpackUploadLoader(source, map, meta) {
const callback = this.async();
const options = getOptions(this) || {};
validate(schema as any, options, {
name: 'Upload Loader',
baseDataPath: 'options',
});
const { upload, mapJson } = options;
const { resourcePath, rootContext } = this;
const defaultCallback = url => {
callback(null, url, map, meta);
};
let data;
const relativePath = resourcePath.replace(rootContext, '.').replace(/\\/g, '/');
try {
data = this.fs.readFileSync(mapJson, 'utf8');
data = JSON.parse(data);
} catch (err) {}
if (data && data[relativePath]) {
defaultCallback(data[relativePath]);
return;
}
const mimetype = getMimetype(options.mimetype, resourcePath);
const fileName = interpolateName(this, '[contenthash].[ext][query]', {
context: resourcePath,
content: source,
});
const spinner = ora(
`uploading file: ${`${fileName}`} local: ${resourcePath.replace(rootContext, '')}`,
).start();
upload
.call(this, {
fileName,
resourcePath,
source,
mimetype,
})
.then(url => {
spinner.succeed(`uploaded to: ${url}------${resourcePath}`);
callback(null, url, map, meta);
})
.catch(err => {
spinner.clear();
this.emitError(
`${resourcePath} upload error: ${err === null || err === void 0 ? void 0 : err.toString()}`,
);
});
}
使用
javascript
{
type:'javascript/auto',
test: /.(png|jpe?g|gif|bpm|svg|webp)(?.*)?$/,
include: path.resolve(__dirname, './src'),
use:[
{
loader: 'url-loader',
options:{
esModule: false,
generator: (content) => content.toString('utf8'),
}
},
{
loader: 'webpack-assets-upload',
options:{
mapJson: path.resolve(__dirname, 'ossMap.json'),
// eslint-disable-next-line consistent-return
upload: async ({ fileName, resourcePath }) => {
// Promise.resolve('https://www.baidu.com')
try {
const { url, res } = await put(fileName, resourcePath);
if (res.status === 200) {
return url;
}
throw Error('上传失败');
} catch (e) {
console.log(e);
}
}
,
}
}
]
}
接受两个配置项,一个就是上传方法upload,该方法应该返回一个图片地址string。然后进入下一个loader(url-loader)将内容插入到结果中。其中url-loader需要注意将esModule
设置为false,主要是为了处理css中的图片/svg资源,需要用commonjs的格式。
另一个就是通过plugin生成的json格式的映射文件。以此来判断资源是否上传过。
plugin部分
typescript
const fs = require("fs");
const path = require("path");
class AssetsUploadPlugin {
static defaultOptions = {
outputFile: "ossMap.json",
};
options: any;
// 需要传入自定义插件构造函数的任意选项
//(这是自定义插件的公开API)
constructor(options = {}) {
// 在应用默认选项前,先应用用户指定选项
// 合并后的选项暴露给插件方法
// 记得在这里校验所有选项
this.options = { ...AssetsUploadPlugin.defaultOptions, ...options };
}
apply(compiler) {
const pluginName = AssetsUploadPlugin.name;
compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
const imageUrls = compilation.modules.filter((m) =>
/\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/.test(m.resource)
); // 这里可以获取到所有的模块源(包括图片文件和其它模块)
const imageUrlMap = {}; // 用于存储图片地址和原始文件地址的映射
imageUrls.forEach((source) => {
let moduleName = source.resourceResolveData.relativePath; // 文件相对路径
moduleName = moduleName.startsWith("./")
? moduleName
: `./${moduleName}`;
try {
const sourcePath = JSON.parse(
source._source?._value.split("module.exports =")[1]
); // 处理后的文件路径(URL)
imageUrlMap[moduleName] = sourcePath; // 将图片地址和原始文件地址存储到映射对象中
} catch {
console.log(source._source?._value.split("module.exports =")[1]);
}
});
const content = JSON.stringify(imageUrlMap); // 将映射对象转换为 JSON 字符串
fs.writeFileSync(this.options.outputFile, content); //在这里生成一个新的文件,内容为图片地址和原始文件地址的映射
callback(); // 表示成功生成输出文件,继续下一轮构建过程
});
}
}
module.exports = { AssetsUploadPlugin };
使用相对路径来作为key表示资源唯一性,通过source._source?._value来获取到每个静态资源的最终地址。生成key-value的map文件。
最后
现在每次运行webpack编译都会自动上传没有在map文件中记录过的新的资源,重新生成map文件。同时也提高了编译速度,减小了打包体积。