Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

快速定位生产环境 javascript 异常堆栈对应的源码位置 #22

Open
whinc opened this issue Nov 20, 2020 · 0 comments
Open
Labels

Comments

@whinc
Copy link
Owner

whinc commented Nov 20, 2020

背景

前端项目发布到生产环境的最终代码一般是后缀为*.min.js的文件,这是对源码进行了一系列的转换(如babel)、压缩和混淆操作后的产物,经过这些步骤后的*.min.js代码具兼容性更好、体积更小、源码更安全的优点,但同时也导致阅读和调试目标代码更加困难了。

常见的场景之一是,生产环境上报了一个 JS 异常错误,除了从错误消息中可以获取到部分有用信息外(有时可能完全没用),无法从堆栈报错文件位置中获取有用的信息。

下面是一个JS异常错误:

TypeError: Cannot read property 'flow_id' of undefined
    at render (http://x.y.z.com/index.min.js:1:12314)
    at http://x.y.z.com/main_dep.8cba5f027cf7.min.js:1:135578
    at http://x.y.z.com/vendors/alpha.850360b98988406d8c20.min.js:119:45314

文件 index.min.js 的第1行的第12314列的附近代码:

function(e,t,n){"use strict";function i(e){return"function"==typeof e.dispose&&0===e.dispose.length}function o(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];return Array.isArray(e)?(e.forEach((function(e){return e&&e.dispose()})),[]):0===t.length?e?(e.dispose(),e):void 0:(o(e),o(t),[])}

从上面堆栈信息中离报错地方最近的位置(index.min.js:1:12314),可以看到这个位置只有一行,明显已经被压缩成了一行,通过代码格式化工具美化后会好一些,但是其中的变量名均已被混淆,依然难以阅读,更糟糕的是像babel/webpack这类工具会插入一些额外代码到最终生成的代码,这些导致难以从目标代码报错位置中解读对应源代码的信息。

从上面的堆栈信息中我们已经知道了报错的是哪个文件,以及所在文件的行号和列号,现在缺的是如何根据这些信息映射回源码中?如果能完成这一步,就可以成功定位线上问题在源码中的准确位置,进而快速分析和修复问题。

sourcemap

早期编写 Web JS 代码是没有转换、压缩和混淆这些步骤的,源码即使最终生产环境部署的代码(知道现在依然有很多站点是这样的),所以不存在上面提到的映射问题。随着前端工程化的发展,各种工具被使用,我们编写的代码与最终部署到生产环境的目标代码已经天差地别了,为了解决目标代码映射回源码的问题,诞生了 sourcemap 技术,其与目标代码一起生成,记录了目标代码中每个字符在源码中的原始位置,有了它可以将目标文件映射回源文件。

下面是 sourcemap 的例子,其中的*.min.js是目标代码,*.min.js.map是对应的 source-map 文件(约定的命名规则是在目标文件后面加上.map)。

├── editor.worker.min.js
├── editor.worker.min.js.map
├── index.min.js
├── index.min.js.map
├── index.html

sourcemap 文件打开是 JSON 格式,其字段说明如下:

{
    // source map的版本,目前(2020年)为 3
  "version" : 3,
    // 转换后的输出文件名
  "file": "out.js",
    // 转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空
  "sourceRoot" : "",
   // 转换前的文件。该项是一个数组,表示可能存在多个文件合并
  "sources": ["foo.js", "bar.js"],
   // 转换前的所有变量名和属性名
  "names": ["src", "maps", "are", "fun"],
   // 记录位置信息的字符串
   // 分号表示源码的一行
   // 逗号表示源码的一个位置
   // 逗号之间的base64字符串是以 VLQ 编码的数值,表示该位置在转换前后文件中的位置信息,编码方式及含义请阅读文末的参考链接
  "mappings": "...,SAAUvpH,QACnCiC;IAAlBjC,..."
}

要实现利用 sourcemap 将目标文件位置信息映射回源码文件,需要掌握 sourcemap 的编码规则和计算方式,幸运的是已经有现成的 mozilla/source-map 库可以方便地完成映射过程。

示例

下面是使用 nodejs 编写的工具脚本,它从命令行接收目标文件报错的位置信息(文件名、行号、列号),并输出对应的源文件的文件路径、行号、列号和该位置的符号名称。

脚本 sourcemap.js

const fs = require("fs")
const path = require("path")
const { SourceMapConsumer } = require("source-map")

const location = process.argv[2]
const matches = /([^:]+):(\d+):(\d+)/.exec(location)
if (!matches) {
  console.log(`command <location>
    location 由<filename>:<line>:<column>组成,例如 index.min.js:1:12314
`)
  process.exit(1)
}
const [, filename, line, column] = matches
const sourcemapPath = path.resolve(__dirname, `../dist/${filename}.map`)
const rawSourceMap = JSON.parse(fs.readFileSync(sourcemapPath))

// SourceMapConsumer 的第一个参数是 source-map 信息,其格式如下:
// const rawSourceMap = {
//   version: 3,
//   file: "min.js",
//   names: ["bar", "baz", "n"],
//   sources: ["one.js", "two.js"],
//   sourceRoot: "http://example.com/www/js/",
//   mappings: "CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA"
// };
SourceMapConsumer.with(rawSourceMap, null, consumer => {
  const originInfo = consumer.originalPositionFor({
    line: parseInt(line),
    column: parseInt(column)
  })
  console.log(originInfo)
})

测试脚本

$ node sourcemap.js index.min.js:1:12314

# 输出
{ source: 'webpack:///src/routes/SampleManage/SampleListPage.jsx',
  line: 224,
  column: 26,
  name: 'record' }

小结

随着前端使用的工具越来越多,源代码和目标代码已天差地别,导致目标代码报错时难以关联源代码进行分析,幸运的是 sourcemap 技术弥补了其中的鸿沟,实现了目标代码到源代码的逆向映射。sourcemap 技术不仅用于 JS 代码,理论上可以应用于任意文本之间的映射,例如 css 代码到 less/sass 等逆向映射。本文最后的小工具可以实现快速定位线上目标代码报错,不过更好的方式是将 sourcemap 融入前端工程化体系,构建时自动生成、上传、存储,当生产环境目标代码报错时实时映射成对应版本的源代码。

如果对 sourcemap 的格式和转换原理感兴趣,可进一步阅读:

@whinc whinc added the JS/TS label Nov 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant