Skip to content

sourcemap 从入门到掌握

我的第一家公司和第二家公司做的都是to c项目,所以对于手机的兼容性就很高,甚至在第二家公司时,被要求兼容到ios 9.0,安卓4.4的系统,被1px问题,字重问题,局中问题困扰多时,不过这些都还是小问题,最起码通过safari以及chrome://inspect能够定位调试,最怕的就是用户看到手机白屏,自己又复线不了,这时候sourcemap就登场了。

什么是 sourcemap

通俗的来说, Source Map 就是一个信息文件,里面存储了代码打包转换后的位置信息,实质是一个 json 描述文件,维护了打包前后的代码映射关系。开发环境下能够快速定位代码错误位置,方便调试。在生产环境上,上传sourcemap到错误监控平台,用于监控与还原线上错误。

  • 使用webpack打包工具,编译下面一段代码 alt text

  • 浏览器打开效果: alt text

  • 点击进入报错文件之后

    alt text

打开之后,一脸懵逼根本找不到报错位置,这时候就需要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,例如typescriptvuereact等,这些语言都有自己的编译工具,编译成js之后,就需要sourcemap来定位错误位置。

浏览器怎么打开 sourcemap

  • 打开F12
  • 打开设置 -> Sources -> 勾选 Enable JavaScript source maps,如下图

alt text

alt text

soucemap的工作原理

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

alt text

让我们看看sourcemap文件的内容都有什么,分别代表什么意思

js
{
  "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 是一个很长的字符串,它的作用是将转换后的代码映射回转换前的代码

  • 三层结构
text
;表示“新的一行”
,表示“同一行里的下一个片段”
字符段表示“一个映射单元(segment)”

整串字符串
├── 按 ; 分割 → 每一段 = 生成文件的一行
        ├── 每一行按 , 分割 → 每一段 = 一个映射片段
            ├── 每个片段是 VLQ 编码的字段数组
  • 每个 segment到底存什么信息
text
[
  generatedColumn,
  sourceIndex,
  originalLine,
  originalColumn,
  nameIndex (可选)
]

假设解码后都是

js
[4, 1, 10, 2, 3];

其表示的意义为

字段含义
4生成代码的列号
1对应 sources 数组里的第 1 个文件
10原始文件第 10 行
2原始文件第 2 列
3names 数组第 3 个变量名

以我们的案例为例,我们第一个 segmentCAAA,按照base64转换后为[2,0,0,0], 代表的意思为

  • 2 生成列 2
  • 0 对应 sources 数组里的第 1 个文件
  • 0 原始文件第 0 行
  • 0 原始文件第 0 列

所有第一段代码来自,是UMD的包装头,很合理webpack://MyLib/webpack/universalModuleDefinition

定位到console.log(ddd); 逻辑如下:

js
console → nameIndex = 7
log → nameIndex = 8
ddd → nameIndex = 9

所以你 console.log(ddd) 对应的 segment,大概率是 mappings 最后几个片段:

js
...,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文件 alt text
  • 可以查看错误代码准确信息和源代码的错误位置alt text

inline-source-map

  • 没有生成.map文件,以 base64的形式插入到 sourceMappingURL中 alt text

hidden-source-map

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

alt text

eval-source-map

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

alt text

nosources-source-map

  • 可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。 alt text

cheap-source-map

  • 可以查看错误代码准确信息和源代码的错误位置,只能把错误精确到整行,忽略列,生成.map文件

cheap-module-source-map

  • 可以查看错误代码准确信息和源代码的错误位置,module 会加入 loader 的 Source Map。

alt text

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无脑接入,优点是可以自动化部署

使用方式如下:

js
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(应用实时监控服务)

可以自动收集错误信息, 并提供错误定位功能

js
<!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

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,
  });
});
  • 项目配置如下
js
module.exports = {
  productionSourceMap: true,
  configureWebpack: (config) => {
    if (process.env.NODE_ENV === "production") {
      config.devtool = "hidden-source-map";
    }
  },
};
  • 服务端代码如下
js
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);
}

微信小程序

传送门

参考文章

base64vlq
JavaScript Source Map 详解
MDN
对应 demo

上次更新于: