我们都经历过这样的情况:连续 10 多个小时开发新功能,一切进展顺利。构建项目并将代码推送到生产环境。突然,一个生产错误警报响起!所有人都开始寻找责任人。找到了,原来是你。但你的所有测试套件都没有报错,代码本身看起来也没有问题。
你查看日志,发现错误是:
arduino
Uncaught Error: Cannot read property 'xyz' of undefined at app.min.js:1:45678
你心想,app.min.js:1:45678
到底是什么意思?整个源代码中根本没有这样的文件?你的文件叫 app.js
,而且它有 45678 个字符长!这根本无法调试!尽管如此,你还是尝试打开文件。整个文件充满了你无法理解的乱码。你该怎么办?
现在,source map登场了。source map允许你将生产环境中的压缩代码(也就是你刚刚看到的乱码)映射到实际的源代码,从而在源代码中精确定位并有效调试。
在这篇博客中,我们将详细介绍source map是什么、为什么要创建它们以及如何创建,还会提供一些有效调试代码的技巧。让我们开始吧!
为什么源代码会被压缩?
在深入source map之前,我们先来解释源代码为何变得面目全非。
答案很简单:压缩。
压缩是将源代码转换为生产环境代码的过程,且不改变其任何功能。这通常由你使用的打包工具(如 Webpack)完成。
简单来说,打包工具通过去除空格、注释和冗余代码,来优化源代码。这使得代码更加高效,体积也小得多。
为什么会这样?
- 提高加载时间:较小的文件带来更好的网站加载时间。
- 混淆:虽然不会使代码完全无法阅读,但确实增加了普通用户理解代码的难度。
- 浏览器性能:代码被修改成浏览器引擎易于解析的形式。
以下是一个压缩后的 React 应用程序代码示例:
什么是source map?
source map是指文件名以 .map
结尾的文件,它们将压缩后的代码映射到实际的源代码。例如,这样的文件 example.min.js.map
或 styles.css.map
。它们由 Webpack、Vite、Rollup、Parcel 等构建工具生成。由于源映射仅在调试时需要,这些工具通常默认不生成源映射。例如,在 Webpack 中启用它,可以在 package.json
文件中添加以下内容:
js
// 将此添加到你的 package.json 文件中
"scripts": {
"build:dev": "webpack --mode development --devtool source-map",
}
或者在 webpack.config.js
文件中添加:
js
module.exports = {
devtool: 'source-map',
// ...其余配置
}
source map文件包含有映射的关键信息,包括实际源文件名、包含的内容、源代码中的各种变量名以及压缩后代码文件的名称等。
以下是典型源映射文件的格式:
json
{
"mappings": "AAAA,SAAQA,MAAMA,QAAQ,OAAO;AAC7B,SAAQC...",
"sources": ["src/index.js"],
"sourcesContent": [
"import React from 'react';\nimport { createRoot } from 'react-dom/..."
],
"names": ["React", "createRoot", "App", "count", "setCount", "useState", ...],
"version": 3,
"file": "bundle.js.map"
}
其中最重要的部分是 mappings
。它使用一种称为 VLQ 64 编码的特殊编码方式,将行映射到编译后的文件及其对应的原始文件。
可视化source map
"好吧,太棒了!" "这到底有什么帮助呢?我还是无法阅读source map并手动解码映射。"
这是一个很好的问题!这引出了本文的主要亮点------源映射可视化工具。这些工具允许你以查看source map,从而有效地定位和调试问题。市场上有许多source map可视化工具,但今天我们将重点介绍 Sokra & Paulirish 的source map可视化工具。你可以在他们的GitHub 存储库上找到此工具的源代码。
以下是一个代码示例的比较。然而,可视化工具的编码映射通过悬停帮助我们将这两种代码对应起来。
实践示例
让我们创建一个简单的 React 应用程序
- 创建项目目录:
js
mkdir my-project
cd my-project
- 初始化新项目:
js
npm init -y
- 将以下依赖项添加到
package.json
中:
json
{
"name": "react-counter-app",
"version": "1.0.0",
"description": "简单 React 计数器应用程序",
"main": "index.js",
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production",
"test": "echo "Error: no test specified" && exit 1"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.23.0",
"@babel/preset-env": "^7.23.0",
"@babel/preset-react": "^7.22.15",
"babel-loader": "^9.1.3",
"css-loader": "^6.8.1",
"html-webpack-plugin": "^5.5.3",
"style-loader": "^3.3.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
- 创建一个
src/index.js
文件,其中包含以下 React 代码:
js
// src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
function App() {
const [count, setCount] = React.useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div className="app">
<h1>计数器: {count}</h1>
<button onClick={increment}>增加</button>
<button onClick={decrement}>减少</button>
</div>
);
}
// React 18 新的 createRoot API
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);
- 通过添加
src/styles.css
文件来添加样式:
css
/* src/styles.css */
.app {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 0 auto;
padding: 20px;
text-align: center;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 10px;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background-color: #45a049;
}
- 现在通过在根文件夹中创建
webpack.config.js
文件来定义 Webpack 配置:
js
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
},
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3000,
open: true
},
resolve: {
extensions: ['.js', '.jsx']
}
};
- 现在你可以通过运行以下命令来启动应用程序:
js
webpack serve --mode development
应用应该如下所示(虽然很简单,但你知道的):
你可以使用浏览器的开发者工具找到source map。格式可能因浏览器而异。这里我使用 Zen,但所有浏览器的格式应该类似。
你可以通过右键单击页面上的任意位置并选择"检查元素"来找到源映射。然后,在浏览器中转到"源代码"部分并找到源文件。
现在,你可以在source-map-visualization 中加载它。它看起来会是这样的:
在右侧,你可以跳过所有 React 代码,直接转到代码的部分。将鼠标悬停在代码的各个部分上,你会看到压缩代码的哪个部分!
一开始看起来可能有点混乱,但尝试在 UI 上悬停在各个元素上,你会发现它实际上非常直观。例如,在这个示例代码中,
js
React.useState(0) ----> t().createElement("h1",null,"Counter: ",n) .... // 依此类推
将鼠标悬停在 React.useState
上,会发现它映射到压缩代码中的 createElement
。因此,我们的打包工具 Webpack 在这种情况下通过将状态直接转换为 JavaScript 元素并在后续代码中直接修改它来优化代码。
在创建示例应用程序时,你可能已经注意到我们需要在 Webpack 运行命令中显式添加 --mode development
标志。这是因为源映射仅用于调试目的,在生产环境中可能会导致安全问题,包括:
问题 | 描述 | 缓解措施 |
---|---|---|
暴露源代码 | 源映射会暴露原始代码,包括注释和逻辑 | 在生产环境中使用 hidden-source-map 或 nosources-source-map |
知识产权保护 | 完整的源映射可能会暴露知识产权 | 将源映射部署到安全、需要身份验证的位置 |
文件大小 | 源映射可能很大,影响下载性能 | 仅在开发环境中生成映射,或单独提供 |
服务器配置 | CORS 问题可能阻止源映射加载 | 配置正确的 Access-Control-Allow-Origin 标头 |
还有一些工具,如 Sentry 或 Rollbar,它们使用你的源映射来提供更好的错误报告,同时不会引发任何安全问题。
结论
source map是一个令人惊叹的功能,它允许你将源代码映射到压缩代码。我们探讨了如何使用此功能轻松调试代码,并使用源映射可视化工具辅助这一过程。