1. webpack 插件实现
技术要点分层阐述:
1. **插件机制理解**
- Webpack: 基于Tapable的插件系统,通过hooks介入构建生命周期
- Vite: 基于Rollup的插件系统,提供构建阶段的钩子函数
2. **关键时机选择**
- Webpack: 使用`afterEmit`钩子(资源已生成但未输出完成)
- Vite: 使用`writeBundle`钩子(打包文件写入完成后)
3. **核心功能模块**
- Sourcemap文件识别与收集
- 文件上传逻辑(支持重试机制)
- 错误处理与日志记录
- 可选的本地文件清理
具体实现方案(展示技术深度)
分步骤阐述:
// 可以配合伪代码说明,但不要逐行念
"首先,插件需要接收配置参数,比如上传地址、认证信息等。然后在合适的构建钩子中:"
1. **文件发现阶段**
- 遍历构建输出目录,识别`.map`后缀文件
- 支持递归查找,确保嵌套目录中的sourcemap也被发现
2. **上传处理阶段**
- 使用FormData进行多部分表单上传
- 实现指数退避的重试机制(网络波动容错)
- 添加元数据:项目版本、构建时间、环境信息等
3. **后续处理阶段**
- 成功上传后可选删除本地sourcemap(安全考虑)
- 详细的上传结果日志记录
- 错误分类处理(网络错误、认证失败、文件异常等)
基础版本
// webpack-sourcemap-upload-plugin.js
class WebpackSourcemapUploadPlugin {
constructor(options = {}) {
this.options = {
uploadUrl: options.uploadUrl,
apiKey: options.apiKey,
project: options.project,
version: options.version || '1.0.0',
...options
};
}
apply(compiler) {
// 使用 afterEmit 钩子,确保文件已经生成
compiler.hooks.afterEmit.tapPromise(
'WebpackSourcemapUploadPlugin',
async (compilation) => {
try {
await this.uploadSourcemaps(compilation);
} catch (error) {
console.error('Sourcemap upload failed:', error);
}
}
);
}
async uploadSourcemaps(compilation) {
const sourcemaps = this.findSourcemaps(compilation);
if (sourcemaps.length === 0) {
console.log('No sourcemaps found to upload');
return;
}
console.log(`Found ${sourcemaps.length} sourcemap files to upload`);
for (const sourcemap of sourcemaps) {
await this.uploadFile(sourcemap);
}
}
findSourcemaps(compilation) {
const sourcemaps = [];
const assets = compilation.assets;
for (const filename in assets) {
if (filename.endsWith('.map')) {
const sourcePath = compilation.outputOptions.path;
const fullPath = require('path').join(sourcePath, filename);
sourcemaps.push({
filename,
path: fullPath,
size: assets[filename].size()
});
}
}
return sourcemaps;
}
async uploadFile(fileInfo) {
const formData = new FormData();
const fileContent = require('fs').readFileSync(fileInfo.path);
formData.append('file', new Blob([fileContent]), fileInfo.filename);
formData.append('project', this.options.project);
formData.append('version', this.options.version);
const response = await fetch(this.options.uploadUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.options.apiKey}`,
},
body: formData,
});
if (response.ok) {
console.log(`✅ Successfully uploaded: ${fileInfo.filename}`);
} else {
throw new Error(`Upload failed for ${fileInfo.filename}: ${response.statusText}`);
}
}
}
module.exports = WebpackSourcemapUploadPlugin;
webpack.config.js 配置
const WebpackSourcemapUploadPlugin = require('./webpack-sourcemap-upload-plugin');
module.exports = {
mode: 'production',
devtool: 'source-map',
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
plugins: [
new WebpackSourcemapUploadPlugin({
uploadUrl: 'https://api.your-service.com/sourcemaps',
apiKey: process.env.SOURCEMAP_API_KEY,
project: 'your-project-name',
version: process.env.npm_package_version,
})
]
};
2. Vite 插件实现
基础版本
// vite-sourcemap-upload-plugin.js
function viteSourcemapUploadPlugin(options = {}) {
let config;
return {
name: 'vite-sourcemap-upload',
configResolved(resolvedConfig) {
config = resolvedConfig;
},
async writeBundle() {
// 只在生产构建时上传
if (config.command === 'build' && !config.build.ssr) {
try {
await this.uploadSourcemaps();
} catch (error) {
console.error('Sourcemap upload failed:', error);
}
}
},
methods: {
async uploadSourcemaps() {
const sourcemaps = this.findSourcemaps();
if (sourcemaps.length === 0) {
console.log('No sourcemaps found to upload');
return;
}
console.log(`Found ${sourcemaps.length} sourcemap files to upload`);
for (const sourcemap of sourcemaps) {
await this.uploadFile(sourcemap);
}
},
findSourcemaps() {
const fs = require('fs');
const path = require('path');
const outputDir = config.build.outDir || 'dist';
const sourcemaps = [];
function scanDirectory(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
scanDirectory(fullPath);
} else if (file.endsWith('.map')) {
sourcemaps.push({
filename: file,
path: fullPath,
relativePath: path.relative(outputDir, fullPath)
});
}
}
}
scanDirectory(outputDir);
return sourcemaps;
},
async uploadFile(fileInfo) {
const formData = new FormData();
const fs = require('fs');
const fileContent = fs.readFileSync(fileInfo.path);
formData.append('file', new Blob([fileContent]), fileInfo.filename);
formData.append('project', options.project);
formData.append('version', options.version);
formData.append('platform', 'web');
const response = await fetch(options.uploadUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${options.apiKey}`,
},
body: formData,
});
if (response.ok) {
console.log(`✅ Successfully uploaded: ${fileInfo.relativePath}`);
} else {
throw new Error(`Upload failed for ${fileInfo.filename}: ${response.statusText}`);
}
}
}
};
}
export default viteSourcemapUploadPlugin;
vite.config.js 配置
import { defineConfig } from 'vite';
import viteSourcemapUploadPlugin from './vite-sourcemap-upload-plugin';
export default defineConfig({
build: {
sourcemap: true, // 确保生成 sourcemap
},
plugins: [
viteSourcemapUploadPlugin({
uploadUrl: 'https://api.your-service.com/sourcemaps',
apiKey: process.env.SOURCEMAP_API_KEY,
project: 'your-project-name',
version: process.env.npm_package_version,
})
]
});
3. 增强功能版本
支持删除本地 sourcemap
// 增强的 uploadFile 方法
async uploadFile(fileInfo, deleteAfterUpload = true) {
try {
const fs = require('fs');
const formData = new FormData();
const fileContent = fs.readFileSync(fileInfo.path);
// 上传逻辑...
await this.performUpload(formData, fileInfo);
// 上传成功后删除本地文件
if (deleteAfterUpload) {
fs.unlinkSync(fileInfo.path);
console.log(`🗑️ Deleted local sourcemap: ${fileInfo.filename}`);
}
} catch (error) {
console.error(`Upload failed for ${fileInfo.filename}:`, error);
throw error;
}
}
支持重试机制
async uploadWithRetry(fileInfo, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.uploadFile(fileInfo);
return; // 成功则返回
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
console.log(`Retry ${attempt}/${maxRetries} for ${fileInfo.filename}`);
await this.delay(1000 * attempt); // 指数退避
}
}
}
throw lastError;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
4. 实际使用示例
package.json 脚本
{
"scripts": {
"build": "webpack --mode=production",
"build:with-upload": "SOURCEMAP_API_KEY=your-key webpack --mode=production"
}
}
环境变量配置
# .env
SOURCEMAP_API_KEY=your-api-key-here
SOURCEMAP_UPLOAD_URL=https://api.your-service.com/sourcemaps
PROJECT_NAME=your-project
主要注意事项
- 安全性: API密钥不要硬编码在配置中,使用环境变量
- 错误处理: 上传失败不应阻断构建过程
- 性能: 大文件上传考虑分片或压缩
- 清理: 上传后建议删除本地sourcemap文件避免泄露
这样的实现可以很好地集成到你的CI/CD流程中,确保每次构建后自动上传sourcemap文件。