sourcemap 从入门到掌握
我的第一家公司和第二家公司做的都是to c项目,所以对于手机的兼容性就很高,甚至在第二家公司时,被要求兼容到ios 9.0,安卓4.4的系统,被1px问题,字重问题,局中问题困扰多时,不过这些都还是小问题,最起码通过safari以及chrome://inspect能够定位调试,最怕的就是用户看到手机白屏,自己又复线不了,这时候sourcemap就登场了。
什么是 sourcemap
通俗的来说, Source Map 就是一个信息文件,里面存储了代码打包转换后的位置信息,实质是一个 json 描述文件,维护了打包前后的代码映射关系。开发环境下能够快速定位代码错误位置,方便调试。在生产环境上,上传sourcemap到错误监控平台,用于监控与还原线上错误。
使用webpack打包工具,编译下面一段代码

浏览器打开效果:

点击进入报错文件之后

打开之后,一脸懵逼根本找不到报错位置,这时候就需要sourcemap了,我们把sourcemap打开,重新构建就能够看到报错位置了
const path = require("path");
module.exports = {
mode: "production",
entry: "./src/index.js",
devtool: "source-map",
output: {
path: path.resolve(__dirname, "dist"),
filename: "my-lib.js",
// 关键配置 ↓↓↓
library: {
name: "MyLib", // 浏览器全局变量
type: "umd", // 支持 CommonJS + AMD + 浏览器
},
globalObject: "this", // 解决 node / 浏览器 兼容问题
},
};sourcemap 作用
- 前面我们知道
sourcemap用来定位错误,那什么场景下我们为什么需要sourcemap呢?- js的脚本越来越复杂,做的事情越来越多,js统一天下(🐶)
- 进行压缩混淆,代码变得难以阅读,这时候
sourcemap就能够帮助我们定位到原始代码位置。 - 其它语言编译成js,例如
typescript,vue,react等,这些语言都有自己的编译工具,编译成js之后,就需要sourcemap来定位错误位置。
浏览器怎么打开 sourcemap
- 打开F12
- 打开设置 -> Sources -> 勾选 Enable JavaScript source maps,如下图


soucemap的工作原理
通过上面的案例可以看出,我们的js文件末尾会指向 sourcemap 文件,这就是我们 soucemap文件

让我们看看sourcemap文件的内容都有什么,分别代表什么意思
{
"version": 3,
"file": "my-lib.js",
"mappings": "CAAA,SAA2CA,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAe,MAAID,IAEnBD,EAAY,MAAIC,GACjB,CATD,CASGK,KAAM,I,0BCRTC,QAAQC,IAAI,gBCAVD,QAAQC,IAAIC,K",
"sources": [
"webpack://MyLib/webpack/universalModuleDefinition",
"webpack://MyLib/./src/index.js",
"webpack://MyLib/./src/c.js"
],
"sourcesContent": [
"(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"MyLib\"] = factory();\n\telse\n\t\troot[\"MyLib\"] = factory();\n})(this, () => {\nreturn ",
"import { c } from \"./c.js\";\nconsole.log(\"this is test\");\nc();\n",
"export function c() {\n console.log(ddd);\n}\n"
],
"names": [
"root",
"factory",
"exports",
"module",
"define",
"amd",
"this",
"console",
"log",
"ddd"
],
"sourceRoot": ""
}version:Source map的版本,目前为v3,目前这一块都为v3这块不用太过于关心。sources:转换前的文件。该项是一个数组,表示可能存在多个文件合并。names:转换前的所有变量名和属性名。mappings:记录位置信息的字符串,下文会介绍,每个字符段表示一个映射单元(segment)。file:转换后的文件名。sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。sourcesContent:转换前文件的原始内容。
关于mappings
mappings 是一个很长的字符串,它的作用是将转换后的代码映射回转换前的代码
- 三层结构
;表示“新的一行”
,表示“同一行里的下一个片段”
字符段表示“一个映射单元(segment)”
整串字符串
├── 按 ; 分割 → 每一段 = 生成文件的一行
├── 每一行按 , 分割 → 每一段 = 一个映射片段
├── 每个片段是 VLQ 编码的字段数组- 每个 segment到底存什么信息
[
generatedColumn,
sourceIndex,
originalLine,
originalColumn,
nameIndex (可选)
]假设解码后都是
[4, 1, 10, 2, 3];其表示的意义为
| 字段 | 含义 |
|---|---|
| 4 | 生成代码的列号 |
| 1 | 对应 sources 数组里的第 1 个文件 |
| 10 | 原始文件第 10 行 |
| 2 | 原始文件第 2 列 |
| 3 | names 数组第 3 个变量名 |
以我们的案例为例,我们第一个 segment 为CAAA,按照base64转换后为[2,0,0,0], 代表的意思为
- 2 生成列 2
- 0 对应 sources 数组里的第 1 个文件
- 0 原始文件第 0 行
- 0 原始文件第 0 列
所有第一段代码来自,是UMD的包装头,很合理webpack://MyLib/webpack/universalModuleDefinition。
定位到console.log(ddd); 逻辑如下:
console → nameIndex = 7
log → nameIndex = 8
ddd → nameIndex = 9所以你 console.log(ddd) 对应的 segment,大概率是 mappings 最后几个片段:
...,IAAI,gBCAVD,QAAQC,IAAIC,K解析逻辑:
- IAAI → 生成代码列 + sourceIndexDelta + originalLineDelta + originalColumnDelta
- gBCAVD → 下一个 segment,对应 log
- QAAQC → 下一个 segment,对应 ddd
- IAAIC → 下一个 segment
- K → 末尾,可能是 nameIndex 9 → "ddd" 所以 你的 ddd 就对应 mappings 最后 K 这个 segment。
webpack中的 sourcemap
上面介绍了 sourcemap 的工作原理,下面我们来看看在 webpack 中 sourcemap的生成规则有哪些。
source-map:外部。可以查看错误代码准确信息和源代码的错误位置。inline-source-map:内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置hidden-source-map:外部。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置。eval-source-map:内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息 和 源代码的错误位置。nosources-source-map:外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。cheap-source-map:外部。可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列。cheap-module-source-map:外部。可以错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。
source-map
- 生成.map文件

- 可以查看错误代码准确信息和源代码的错误位置

inline-source-map
- 没有生成.map文件,以 base64的形式插入到 sourceMappingURL中

hidden-source-map
- 可以查看错误代码准确信息,但看不到源代码的错误位置

eval-source-map
- 没有生成.map文件,而是在 eval函数

nosources-source-map
- 可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。

cheap-source-map
- 可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列,生成.map文件
cheap-module-source-map
- 可以查看错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。

6.8 总结
开发环境,推荐source-map,生产环境推荐nosources-source-map
| 环境 | 目标 | Source Map 类型 | 特点 | 推荐理由 |
|---|---|---|---|---|
| 开发环境 | 调试友好、速度快 | eval-source-map | 内联 eval,映射完整,支持行列定位,构建速度快 | 完整度高,内联速度快,适合开发 |
eval-cheap-module-source-map | 内联 eval,忽略列,但包含模块信息,构建速度快 | 错误提示忽略列,但包含其他信息,内联速度快 | ||
| 生产环境 | 隐藏源码、错误调试 | source-map | 外部 map,映射完整,可调试源码 | 提供最完整调试信息,但源码可访问 |
cheap-module-source-map | 外部 map,错误提示忽略列,包含模块信息 | 错误提示仅按行显示,速度快,源码隐藏程度较高 | ||
hidden-source-map | 外部 map,只隐藏源码,但错误信息仍会提示行列 | 用于隐藏源码但仍能定位错误 | ||
nosources-source-map | 外部 map,隐藏全部源代码信息 | 完全隐藏源码,生产环境安全性高 |
生产具体使用方式
使用sentry无脑接入,优点是可以自动化部署
使用方式如下:
import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
import { BrowserTracing } from "@sentry/tracing";
import App from "./App";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
integrations: [new BrowserTracing()],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
});
ReactDOM.render(<App />, document.getElementById("root"));阿里云 ARMS(应用实时监控服务)
可以自动收集错误信息, 并提供错误定位功能
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge,chrome=1" />
<meta name="viewport" content="width=device-width" />
<title>ARMS</title>
</head>
<body>
<script>
!(function (c, b, d, a) {
c[a] || (c[a] = {})
c[a].config = {
pid: 'xxx',
appType: 'web',
imgUrl: 'https://arms-retcode.aliyuncs.com/r.png?',
sendResource: true,
enableLinkTrace: true,
behavior: true,
useFmp: true,
enableSPA: true,
}
with (b) with (body) with (insertBefore(createElement('script'), firstChild)) setAttribute('crossorigin', '', (src = d))
})(window, document, 'https://sdk.rum.aliyuncs.com/v1/bl.js', '__bl')
</script>
<div id="app"></div>
</body>
</html>手搓问题上报
自己需要进行一套代码体系维护,以 vue为例前端代码如下errorHandler.js
import Vue from "vue";
function reportError(data) {
fetch("/api/report-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
}
/_ Vue 错误 _/;
Vue.config.errorHandler = (err, vm, info) => {
reportError({
type: "vue",
message: err.message,
stack: err.stack,
info,
});
};
/_ JS 全局错误 _/;
window.onerror = (msg, url, line, column, error) => {
reportError({
type: "js",
message: msg,
file: url,
line,
column,
stack: error?.stack,
});
};
/_ Promise 未捕获 _/;
window.addEventListener("unhandledrejection", (e) => {
reportError({
type: "promise",
message: e.reason?.message,
stack: e.reason?.stack,
});
});- 项目配置如下
module.exports = {
productionSourceMap: true,
configureWebpack: (config) => {
if (process.env.NODE_ENV === "production") {
config.devtool = "hidden-source-map";
}
},
};- 服务端代码如下
const fs = require("fs");
const { SourceMapConsumer } = require("source-map");
async function parseError(line, column) {
const rawMap = JSON.parse(fs.readFileSync("./dist/js/app.js.map", "utf-8"));
const consumer = await new SourceMapConsumer(rawMap);
const original = consumer.originalPositionFor({
line,
column,
});
console.log(original);
}