全面掌握JavaScript 调试技能

803 阅读9分钟

目录

很多 JavaScript 程序员都只会用console来调试代码,遇到一些问题需要写一大堆无用的console.log,调试效率非常低 。所谓工欲善其事必先利其器,本文教你完全掌握JavaScript调试技能。

Chrome DevTools 调试工具

Chrome Devtools 相信大家都不陌生,它可以说是前端开发者吃饭的工具。Chrome Devtools里的Source 面板就能调试当前网页的js。

在这里可以断点、单步调试等等,能详细地看到程序的运行状态。此外还有非常实用的 DOM断点、事件断点等功能,在定位特定问题的时候非常好用。其他功能建议去详细了解一下,本文只讲解JS断点调试的部分。

用Chrome打开一个网页,就可以直接在DevTools里面进行断点调试。

但是情况并没有这么简单。我们随便打开一个正式的网站,看到的代码通常是这样的:

浏览器中运行的 JS 代码通常情况下都是“编译”后的。这里的编译不是将源代码编译成了机器码(众所周知js是一种JIT语言,无须编译),而是打包工具对源代码进行了一些列转换、压缩、合并、混淆等操作,产出能被浏览器直接执行的js代码。源代码可以是 vueTSJSX 或者同样的是js

我们可以用Chrome Devtools里的格式化功能先格式化一下代码,然后进行断点调试。但是简单格式化后的代码依旧很难看懂。

如何才能调试源码呢?答案是SourceMap

什么是SourceMap

由于浏览器中运行的js代码通常都是经过了转换的,这给开发者调试带来了很大的困难,SourceMap就是为了解决这个问题而出现的。

SourceMap是一个信息文件,记录了转换后的代码与源代码之间的映射关系,浏览器可以通过这些映射关系还原源代码。

怎么启用SourceMap呢?

  1. 通过打包工具生成一个 source map文件

  2. 转换后的 JS 代码文件的末尾加上这样一行注释:

    //# sourceMappingURL=http://example.com/path/to/your/sourcemap.map
    

来看个实际例子。

有个简单的 TS 代码,test.ts

const btn = document.getElementById('btn');

function add(a: number, b: number): number {
  return a + b;
}

function handlerClick() {
  console.log('1+2=', add(1, 2));
}

btn?.addEventListener('click', handlerClick);

我们用tsc编译代码时,可以选择生成souce map文件。tsconfig.json配置如下:

{
  "compilerOptions": {
    "outDir": "dist",
    "sourceMap": true,
    "preserveConstEnums": true,
    "inlineSources": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["dist/", "node_modules"]
}

执行 tsc ,得到编译产物有:test.jstest.js.map

test.js 就是转换后的代码:

var btn = document.getElementById('btn');
function add(a, b) {
    return a + b;
}
function handlerClick() {
    console.log('1+2=', add(1, 2));
}
btn === null || btn === void 0 ? void 0 : btn.addEventListener('click', handlerClick);
//# sourceMappingURL=test1.js.map

test1.js.map 是SourceMap文件:

{
  "version": 3,
  "file": "test1.js",
  "sourceRoot": "",
  "sources": [
    "../src/test1.ts"
  ],
  "names": [],
  "mappings": "AAAA,IAAM,GAAG,GAAG,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;AAE3C,SAAS,GAAG,CAAC,CAAS,EAAE,CAAS;IAC/B,OAAO,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,SAAS,YAAY;IACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC",
  "sourcesContent": [
    "const btn = document.getElementById('btn');\n\nfunction add(a: number, b: number): number {\n  return a + b;\n}\n\nfunction handlerClick() {\n  console.log('1+2=', add(1, 2));\n}\n\nbtn?.addEventListener('click', handlerClick);\n\n\n"
  ]
}

我们来看看SourceMap中的内容:

  • version:使用的source map 标准 的版本

  • sources:源文件的名字

  • mappings:代码映射关系,使用的是Base 64 VLQ 格式

  • file:转换后的文件名

  • sourceContent:源代码的内容

其中mappings 的映射逻辑非常复杂,感兴趣可以阅读这篇文章: 《How do source maps work?》

准备一个 HTML,用浏览器打开测试一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Test测试页面</title>
</head>
<body>
  <h1>测试一下</h1>
  <button id="btn">点我</button>
  <script src="./dist/test1.js"></script>
</body>
</html>

可以看到 Souces面板下的文件目录,会有一个 src/test1.ts 的文件。并且这个文件右下角声明了是“souce mapped from test1.js”。

可以直接在源码处断点调试。

不同的打包工具都有对应的sourcemap配置。值得注意的是,打包生产环境代码的时候不应该开启SourceMap,或者不应该把SourceMap文件放入服务器,避免泄漏源码。

在IDE中调试Web 应用

有了sourceMap,我们就可以在Chrome Devtools中调试源代码了。但这还不够方便,我们更希望直接在IDE中调试,这样既更方便阅读代码,同时也能边调试边修改。

Chrome DevTools工作原理

我们先了解一下Chrome DevTools的工作原理。Chrome DevTools可以分成Frontend、Backend和Protocol,以及传输信道。

Chrome DevTools Frontend 就是我们接触到的调试面板,它是一个Web应用。

Backend 可以是浏览器内核、Node.js等。

Chrome Devtools Protocol 是 Frontend 和 Backend 之间通信的协议,包含了DOM、网络、JS调试等调试能力,可以用于调试 Chrome、Chromium 和其他 Blink 内核的浏览器。

如果要使用IDE进行调试,就可以用IDE替代 Chrome Devtools Frontend的角色,解析对应的 Chrome Devtools Protocol 就行。

VS Code中调试

这里以VS Code为例,其他IDE功能类似。

首先必须用 debug 模式启动浏览器,这样才能进行调试。通过--remote-debugging-port 参数将浏览器的调试端口保留给外部程序使用。

VS Code Debugger可以分成两类,Launch和Attach。Launch是启动新的目标程序并进行调试;Attach是程序已经在运行时,“附着”上去进行调试。这两种方式没有本质的区别,只是区别在谁来启动程序而已。

用Launch方式调试的话,VS Code会自动以debug模式起一个新的Chrome进程;而用Attach方式的话,你就必须自己以debug模式启动。用 debug 模式启动 Chrome的方式为:

# Win
chrome.exe --remote-debugging-port=9222 --user-data-dir=remote-debug-profile

# MacOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=remote-debug-profile

两种调试方式的VS Code调试配置 launch.json 如下:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to Chrome",
      "port": 9222,
      "request": "attach",
      "type": "chrome",
      "webRoot": "${workspaceFolder}"
    },
    {
      "name": "Launch Chrome",
      "type": "pwa-chrome",
      "request": "launch",
      "url": "http://localhost:9080",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

调试手机端Web 应用

前面讲了如何调试电脑端的web页面,那么手机端的Web页面改怎么调试呢?

用Chrome DevTools调试Android WebView

调试Android手机里的浏览器页面是比较简单的。

默认情况下Android 手机上的Chrome和原生浏览器都支持调试。如果想调试App内嵌的Webview,则需要先在App的代码中开启调试

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
}

调试Android WebView的步骤如下:

  1. 使用USB线链接电脑和手机;

  2. 手机开启USB调试模式

  3. 电脑端Chrome打开地址:chrome://inspect#devices

  4. 手机端允许USB调试并将USB链接方式选择为传输文件

  5. 点击对应页面的inspect按钮,即可用Chrome DevTools对该页面进行调试;

DevTools 有个方便的端口转发功能,可以将手机上的请求转发到电脑上。例如我们在电脑端开启了http://localhost:9080 web服务,然后通过Port forwading 按钮设置如下:

然后在手机浏览器访问http://localhost:9080,然后在桌面端Chrome的inspect页面里打开调试。

用VS Code调试Android WebView

要使用VS Code来调试手机端的WebView,需要先将手机端WebView的调试协议端口转发到电脑端,然后再用Attach的方式进行调试。我们可以通过adb forward 命令来转发端口。

首先要安装好ADB 命令行程序。使用ADB调试手机WebView的原理为:

第一步,找到unix domain socket。通过USB链接手机后,执行命令:

adb shell cat /proc/net/unix | grep --text _devtools_remote

找到对应WebView的调试进程,例如chrome_devtools_remote或者huawei_webview_devtools_remote_31071。然后转发端口到电脑端的9222 端口(跟VS Code Attach端口一致):

adb forward tcp:9222 localabstract:chrome_devtools_remote

这个时候我们用电脑端打开地址 http://localhost:9222/json ,可以看到调试协议相关信息:

这个时候我们只需要在VS Code中Debugger面板里,选择Attach to Chrome即可进行调试。

到这里你肯定觉得这套操作下来太麻烦了。不用担心,已经有人封装好了一个VS Code插件可以直接使用。

在VS Code插件市场里搜索 Android WebView Debugging并安装,然后在launch.json中新增android-webview类型的调试,配置如下:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to Chrome",
      "port": 9222,
      "request": "attach",
      "type": "chrome",
      "webRoot": "${workspaceFolder}"
    },
    {
      "type": "pwa-chrome",
      "request": "launch",
      "name": "Launch Chrome",
      "url": "http://localhost:9080",
      "webRoot": "${workspaceFolder}"
    },
    {
      "type": "android-webview",
      "request": "attach",
      "name": "Attach to Android WebView",
      "application": "com.android.chrome",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

此时在VS Code Debugger面板选择 android-webview 就能直接调试了。

调试iOS 上的Web

iOS上的浏览器——Safari使用的是 Blink内核,跟Chromium一样,所以他们的调试方式也很类似,只不过调试协议有一些差别。

最直接的调试方法就是使用MacOS上的Safari对iOS端的Safari进行调试。

先在桌面端Safari打开“开发”菜单。路径为:偏好设置-高级-在菜单栏中显示“开发”菜单。用电源线链接好设备和电脑,就可以在“开发”菜单中找到设备,并进行inspect。

那么能不能用Chrome DevTools或者VS Code来调试iOS WebView呢?答案是可以的,我们需要一个协议适配程序。

可以看下 remotedebug-ios-webkit-adapterios-webkit-debug-proxy

调试 Node.js 应用

JavaScript除了运行在浏览器中,另一个重要的场景就是Node.js。接下来看看怎么调试Node.js程序。

一个简单的程序

先写个最简单的Node.js程序:

// test1.js
function sayHi(name) {
  const str = 'Hello, world.';
  console.log(name + ' says: ' + str);
}

sayHi('Bob');

测试运行一下:

> node test1.js
< Bob says: Hello, world.

用命令行调试

执行node inspect test1.js 可以进入命令行调试程序,会自动在运行的第一句代码处断点。

使用方式可以参考debugger文档

node inspect 命令保留了9229调试端口,所以我们也可以使用Chrome DevTools和VS Code Attach方式进行调试。

用Chrome DevTools调试

也可以用Chrome打开chrome://inspect/ ,可以找到该进程进行调试。

用VS Code调试

VS Code Debugger 的Attach和Launch两种方式配置如下:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach",
      "port": 9229,
      "request": "attach",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "type": "node"
    },
    {
      "type": "pwa-node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "program": "${workspaceFolder}/test1.js"
    }
  ]
}

Express.js + TS程序

写个简单的Hello-World程序。

.
├── package-lock.json
├── package.json
├── src
│   ├── app.ts
│   └── lib.ts
└── tsconfig.json
// src/app.ts

import * as express from 'express';
import { sayHI } from './lib';

const app = express()
const port = 3000


app.get('/', (req, res) => {
  const result = sayHI('Bob');
  res.send(result)
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})
// src/lib.ts

export function sayHI(name: string): string {
  const str = 'Hello World!';
  return name + ' says: ' + str;
}

先要用tsc 将TS编译成JS。npm install -D typescript ,然后执行 npx tsc。其中tsconfig.json配置如下:

{
  "compilerOptions": {
    "outDir": "dist",
    "module": "CommonJS",
    "preserveConstEnums": true,
    "sourceMap": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["dist/", "node_modules"]
}

编译后的代码为:

// dist/app.js

"use strict";
exports.__esModule = true;
var express = require("express");
var lib_1 = require("./lib");
var app = express();
var port = 3000;
app.get('/', function (req, res) {
    var result = (0, lib_1.sayHI)('Bob');
    res.send(result);
});
app.listen(port, function () {
    console.log("Example app listening on port ".concat(port));
});
//# sourceMappingURL=app.js.map

用 inspect 方式启动node程序:

node --inspect dist/app.js

然后我们可以用Chrome DevTools、VS Code调试该程序。

用Chrome DevTools调试

Chrome页面 chrome://inspect/#devices 中可以看到正在运行的程序,然后点击inspect。打开后需要手动添加代码目录。点击左侧的“+ Add folder to workspace”,找到代码目录即可。

用VS Code调试

使用VS Code调试更加方便。如果Node.js 程序是以inspect模式启动的,那么直接 Attach 就行。也可以用VS Code 的launch模式启动程序并调试。launch.json配置如下:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach",
      "port": 9229,
      "request": "attach",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "type": "node"
    },
    {
      "name": "Launch Program",
      "program": "${workspaceFolder}/dist/app.js",
      "request": "launch",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "type": "node"
    }
  ]
}

用ts-node-dev进行调试

每次修改代码都需要重新编译和运行的话,对于开发非常不方便。使用ts-node-dev可以直接运行TS程序无须编译,并且具备热重载功能。

# 安装
npm install ts-node-dev --save-dev
# 运行
npx ts-node-dev --inspect -- src/app.ts

接下来就可以愉快地用Chrome DevTools或者VS Code进行调试了。

参考