在前端监控系统中,SourceMap 解析是定位线上代码错误的核心功能。然而,传统的手动上传 SourceMap 文件的方式存在诸多问题。因为涉及到生成,管理,上传等多个步骤,只要有一步对不上,基本上很难解析出来,所以我们增加webpack和vite自动上传sourceMap的插件,在打包的时候上传map文件,查看的时候精准锁定源文件,省时省力。
一、整体架构设计
我们设计了一套自动化的 SourceMap 管理和解析方案,包含三个核心组件:
核心优势
✅ **自动化**:集成到 Webpack 构建流程,无需人工干预
✅ **集中管理**:所有项目的 SourceMap 统一存储在 ClickHouse
✅ **高压缩**:Gzip + Base64 编码,压缩率约 70%
✅ **版本控制**:按项目和版本组织,支持多版本共存
✅ **分布式友好**:基于 ClickHouse,天然支持分布式部署
✅ **降级方案**:file_server 不可用时自动降级到本地文件
✅ **智能解析**:显示 100 行上下文,支持滚动查看
二、文件服务设计方案
传统的sourceMap解析方案,是需要用户去维护map文件版本,想办法把错误代码和源文件对齐,说实话,这一步真的十分繁琐。为了能够解决这一痛点,打造一个自动化的解析方案,我们需要在整个系统中内容一个文件系统服务,用于上报和下载map文件
2.1 数据库表设计
// file_server_clickhouse/schema/sourceMapFile.js
const Columns = {
tableName: 'SourceMapFile',
structure: {
id: {
type: DataTypes.UUID,
field: 'id'
},
project_id: {
type: DataTypes.STRING,
allowNull: false,
field: 'project_id'
},
project_name: {
type: DataTypes.STRING,
field: 'project_name'
},
file_name: {
type: DataTypes.STRING,
allowNull: false,
field: 'file_name'
},
release: {
type: DataTypes.STRING,
allowNull: false,
field: 'release'
},
file_content: {
type: DataTypes.STRING, // Base64 + Gzip 压缩
allowNull: false,
field: 'file_content'
},
file_size: {
type: DataTypes.INT(32),
field: 'file_size'
},
compressed_size: {
type: DataTypes.INT(32),
field: 'compressed_size'
},
upload_time: {
type: DataTypes.DATE_TIME,
field: "upload_time"
},
createdAt: {
type: DataTypes.DATE_TIME,
field: "createdAt"
}
},
engine: "ENGINE MergeTree()",
partition: "PARTITION BY toYYYYMM(createdAt)",
orderBy: "ORDER BY (project_id, release, file_name, createdAt)"
}
2.2 文件压缩策略
// 上传时压缩
const fileContent = fs.readFileSync(filePath, 'utf-8')
const compressedBuffer = zlib.gzipSync(Buffer.from(fileContent))
const base64Content = compressedBuffer.toString('base64')
// 存储到 ClickHouse
await SourceMapFile.create({
file_content: base64Content,
file_size: fileContent.length,
compressed_size: compressedBuffer.length
})
// 查询时解压
const compressed = Buffer.from(file.file_content, 'base64')
const decompressed = zlib.gunzipSync(compressed)
const fileContent = decompressed.toString('utf-8')
**压缩效果**:平均压缩率约 70%(例如:46KB → 13.6KB)
三、Webpack 插件实现
3.1 webfunny-webpack-plugin - 自动上传工具实现逻辑
// webfunny-webpack-plugin/src/WebfunnyWebpackPlugin.js
class WebfunnyWebpackPlugin {
constructor(options = {}) {
this.options = {
url: options.url,
projectId: options.projectId,
release: options.release,
projectName: options.projectName || '',
urlPrefix: options.urlPrefix || '',
deleteAfterUpload: options.deleteAfterUpload !== false,
silent: options.silent || false
};
}
apply(compiler) {
// 在文件输出后执行
compiler.hooks.afterEmit.tapPromise('WebfunnyWebpackPlugin', async (compilation) => {
const sourceMapFiles = this.getSourceMapFiles(compilation);
for (const file of sourceMapFiles) {
await this.uploadFile(file);
// 上传成功后删除 .map 文件(可选)
if (this.options.deleteAfterUpload) {
fs.unlinkSync(file.path);
}
}
});
}
}
3.2 webpack.config.js 插件配置示例
const WebfunnyWebpackPlugin = require('webfunny-webpack-plugin');
const pkg = require('./package.json');
module.exports = {
mode: 'production',
devtool: 'source-map', // 必须开启
plugins: [
new WebfunnyWebpackPlugin({
url: 'http://localhost:8033/wfFile/api/sourceMapFile/upload',
projectId: 'webfunny_20251209_022537_pro', // ⚠️ 必须与前端SDK的webMonitorId一致
release: `${pkg.version}-${Date.now()}`, // 自动生成版本号
projectName: pkg.name,
deleteAfterUpload: process.env.NODE_ENV === 'production' // 生产环境删除
})
]
};
3.3 自动版本号生成
// 格式:{version}-{YYYYMMDD}-{HHmm}
// 例如:1.0.0-20251209-0846
const getVersion = () => {
if (process.env.APP_VERSION) {
return process.env.APP_VERSION;
}
const now = new Date();
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
const time = now.toISOString().slice(11, 16).replace(/:/g, '');
return `${pkg.version}-${date}-${time}`;
};
三、