最近在系统的整理 webpack 相关的内容,在 loader
这块想起之前公司的一个 cdn-loader
库,可以在运行的时候自动将 png、jpg
等图片文件上传到自己的 CDN
服务上,优化项目体验,提升构建效率。
话都说到这里了,就开始手撕一个简单的 cdn-loader
,具体需要实现的功能如下:
- 在项目运行、构建的时候自动匹配图片相关的静态资源,实现上传到 cdn 上,并替换源代码中的
资源路径
。 - 增加缓存的能力,避免重复上传消耗 cdn 容量。
1. 实现 cdn-loader
我这里以 NextJs 项目为例。初始化完项目,我们在根目录新建一个 loaders
文件夹,在该文件夹下新建一个 cdn-loader.js
文件,然后开始编辑 loader
的具体实现。
1.1 实现 loader 的整体框架
const fs = require('fs');
const path = require('path');
module.exports = function (source) {
const callback = this.async();
const resourcePath = this.resourcePath;
// 读取图片文件
fs.readFile(resourcePath, async (err, fileBuffer) => {
if (err) {
return callback(err);
}
try {
// ... do Something
} catch (uploadError) {
callback(uploadError);
}
});
return callback(null, source);
};
这就是这个 loader
的整体框架,需要注意的一点是 webpack
的 loader
是在 Nodejs
环境运行的,所以需要使用 CommonJS 规范。
:::tip[参考]
- callback: 这里是对后续上传 cdn 服务的异步回调
- resourcePath: 是图片文件的相对路径
- source: 图片文件的二进制信息
:::
1.2 实现具体的上传逻辑
// 上传到 CDN (你需要根据你的 CDN API 调整这个逻辑)
try {
const response = await axios.post('https://your-cdn.com/upload', fileBuffer, {
headers: {
'Content-Type': 'image/jpeg', // 根据图片类型调整
Authorization: `Bearer ${token}`
}
});
const cdnUrl = response.data.url;
console.log('上传成功,CDN URL:', cdnUrl);
// 将代码中的绝对地址替换成 cdn url
callback(null, `module.exports = ${JSON.stringify(cdnUrl)};`);
} catch (error) {}
到这里基础基础版的 cdn-loader
就基本完成了
2. cdn-loader 优化
通过第一步的实现,我们已经完成了一个基础版的 cdn-loader
了,但是有个潜在的问题,这样在每次运行、或者构建的情况下都会执行,会产生无用的重复数据,接下来我们着重来优化下重复数据的问题。
2.1 优先增加缓存能力
通过创建 .cdn-cache.json 来缓存已经上传的文件内容,每次动态的去判断是否已经上传过,已经上传就跳过就好,如果没有上传的话就走正常的上传逻辑。
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
// 缓存文件路径
const cacheFilePath = path.resolve(__dirname, '.cdn-cache.json');
// 读取缓存
function readCache() {
if (fs.existsSync(cacheFilePath)) {
return JSON.parse(fs.readFileSync(cacheFilePath, 'utf-8'));
}
return {};
}
// 写入缓存
function writeCache(cache) {
fs.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
}
// 生成文件哈希
function generateHash(buffer) {
return crypto.createHash('md5').update(buffer).digest('hex');
}
module.exports = function (source) {
const callback = this.async();
const resourcePath = this.resourcePath;
const cache = readCache();
fs.readFile(resourcePath, async (err, fileBuffer) => {
if (err) {
return callback(err);
}
// 生成文件哈希值
const fileHash = generateHash(fileBuffer);
// 检查缓存
if (cache[fileHash]) {
console.log(`图片 ${resourcePath} 已缓存,使用 CDN URL: ${cache[fileHash]}`);
return callback(null, `module.exports = ${JSON.stringify(cache[fileHash])};`);
}
// 如果没有缓存,则上传图片
try {
const response = await axios.post('https://your-cdn.com/upload', fileBuffer, {
headers: {
'Content-Type': 'image/jpeg',
Authorization: `Bearer ${token}`
}
});
const cdnUrl = response.data.url;
console.log('上传成功,CDN URL:', cdnUrl);
// 保存哈希和 URL 的映射
cache[fileHash] = cdnUrl;
writeCache(cache);
return callback(null, `module.exports = ${JSON.stringify(cdnUrl)};`);
} catch (uploadError) {
console.error('上传失败:', uploadError);
callback(uploadError);
}
});
return callback(null, source);
};
这里需要注意的是这里是根据文件内容生成的 hash 值,如果文件内容没变,图片名称变了的话,这个会被跳过。
到这里这个就具有一个缓存的效果了,只会上传缓存文件中没有的文件。
3. 配置 wenpack 的 loader
nextjs 在 next.config.mjs
在这里配置
const nextConfig = {
webpack(config) {
// ...
config.module.rules.push({
test: /\.(png|jpe?g|gif)$/,
exclude: /node_modules/,
use: [
{
loader: path.resolve(process.cwd(), 'loaders/cdn-loader.js')
}
]
});
return config;
},
images: {
disableStaticImages: true // 禁用静态图片优化
}
};
:::tip[注意]
这里需要注意,如果使用 Nextjs
提供的的 Image
组件会导致冲突格式冲突,所以这里我们把这个关掉,然后可以手动封装一个 Image
组件来优化和兼容图片的显示。
:::
这里通过 test: /\.(png|jpe?g|gif)$/
来匹配相关的图片资源,其实换成其他的静态资源 css
也一样,这里其实实现了一个静态资源的 cdn-loader
,可以根据自己的场景来配置,比如只在构建向上环境的时候运行等。