该小节主要知识点:
- 为什么需要调试
bin 脚本
? - 如何快速
上手
调试 bin 脚本 - 什么是
sourcemap
?调试中为什么需要 sourcemap ? - 实战:调试
Umi 源代码
- 基于调试中的问题,给 Umi 提个 pr
本小节最终效果:
(这是我手写 mini-umi 时在 Umi 源码
中打的所有 关键断点
)
1.为什么需要调试 bin 脚本?
你是否有思考过,当你在使用 VueCLI 或 create-react-app 创建的模版项目中使用 npm run dev
时,Webpack是如何 从0到1 启动这整个项目的吗?
当你在使用 Vue3+Vite 项目时,Vite dev 命令到底做了什么事情呢?Vite dev 与 Webpack 的 dev 又有什么不一样呢?
除了 Webpack,Vite,更多的还有 Nuxt
、Next
、Esbuild
等框架或工具
除了 dev,还有 build
、lint
、test
等指令
如果你想知道他们工作以及运转的原理,就需要阅读他们的源代码,而通过调试 CLI入 口的 bin脚本
,就可以看到所有指令 从0到1
是如何 执行
的
2.如何快速上手调试 bin 脚本
bin 脚本的调试其实是非常简单
的,我们来简单盘一下逻辑
还记得我们在第4小节手写CLI时学过:如果你是一个 Node 侧的命令行工具,当你去执行 npm run xxx
时,其实都是去执行了 bin 目录下的脚本文件
而 Node 环境的脚本
本质上不过是一个 Nodejs 文件
所以,我们调试 bin 脚本 和调试运行一个 Nodejs 文件又有什么区别呢?
那如何调试一个 Nodejs 文件呢?
接下来我带着大家一步一步尝试:
1.创建我们的调试项目
mkdir debugger
code debugger
2.创建调试的入口文件
// debugger.js
const yang = 'yang'
const yangyang = yang + 'yang'
console.log(yangyang+'yang');
3.尝试运行
node debugger.js
// yangyangyang
运行成功✅
接下来开始调试的部分
1.进入vscode调试面板
2.给我们的 debugger.js 文件打一个断点
也可以这样打断点,使用 debugger 关键字
+ debugger;
const yang = 'yang'
const yangyang = yang + 'yang'
console.log(yangyang + 'yang');
3.点击运行和调试按钮
4.这里直接选择 Nodejs 环境
这时候你会发现,调试模式已经启动了,成功✅
当然第4步这里其实还是要说明一下
4.这里直接选择 Nodejs 环境 你目前所在的工作文件是
debugger.js
,所以当你这里直接选择debugger.js文件之后,等同于vscode帮你自动创建
了调试的配置文件
,并把program
字段指向了我们的 debugger.js
你可以使用配置文件尝试一下,效果是等价
的
创建调试配置文件
配置文件的 program 指向谁,相当于启动调试哪个 Nodejs 文件
// .vscode/launch.json
+{
+ // 使用 IntelliSense 了解相关属性。
+ // 悬停以查看现有属性的描述。
+ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "启动程序",
+ "skipFiles": [
+ "<node_internals>/**"
+ ],
+ "program": "${workspaceFolder}/debugger.js"
+ }
+ ]
+}
问题来了
调试 Nodejs 文件我会了,但是命令行工具是启动脚本啊, bin脚本
该怎么调试呢?
我教给大家一种万能的方法,不管你的项目结构多复杂,有多少packages,这样调试 bin脚本,就非常简单
在我们的 debugger文件夹下创建bin脚本
pnpm init
// package.json
"bin": {
+ "debugger": "./bin/debugger.js"
},
// bin/debugger.js
#!/usr/bin/env node
const yang = 'yang'
const yangyang = yang + 'yang'
console.log(yangyang + 'yang');
验证bin脚本成功
npm link
debugger
// yangyangynag
进入配置文件
// .vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
- "program": "${workspaceFolder}/debugger.js"
+ "program": "${workspaceFolder}/bin/debugger.js"
}
]
}
其实就是把 NodeJS文件 的入口指定成我们的 bin脚本 的入口,是不是有点过于简单了
因为 npm run xxx
的本质
就是执行 Node环境
的bin脚本 而 Node环境的 bin脚本 就是执行Nodejs代码
3.什么是sourcemap?调试中为什么需要sourcemap?
1.通常我们写好代码之后都会在打包
的时候进行压缩
、polyfill
等一系列优化处理
2.为了让使用者得到良好的类型支持
,现代大多数类库都会选择用 TypeScript
进行开发,ts到js需要编译处理
什么是SourceMap?
SourceMap
是一个信息文件,里面存储了代码打包编译转换后的位置信息
通过开启sourcemap之后生成的xxx.map文件,你就可以知道
- 打包之后的哪一个文件对应打包之前的哪一个文件
当然,它能清晰的对应到具体的代码行数
no say, let's code 这里在前几小节已经详解过了,不过多赘述
tsc --init
// tsconfig.json
+ "declaration": true, //开启 ts声明文件 -- d.ts
// src/index.ts
export const yang: string = 'yang'
const yangyang = yang + 'yang'
console.log(yangyang + 'yang');
npm i father
// .fatherrc.ts
import { defineConfig } from 'father'
export default defineConfig({
cjs: {
output: "dist",
+ sourcemap: true // 开启编译时sourcemap
}
})
// package.json
"scripts": {
+ "dev": "father dev",
+ "build": "father build"
},
npm run dev
成功生成 产物
和 .map
文件
我们来分别看下生成的 产物
和 sourcemap文件
// 产物文件:dist/index.js
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
yang: () => yang
});
module.exports = __toCommonJS(src_exports);
var yang = "yang";
var yangyang = yang + "yang";
console.log(yangyang + "yang");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
yang
});
//# sourceMappingURL=index.js.map
// sourcemap文件:dist/index.js.map
{
"version": 3,
"sources": ["../src/index.ts"],
"sourcesContent": ["export const yang: string = 'yang'\n\nconst yangyang = yang + 'yang'\n\nconsole.log(yangyang + 'yang');\n"],
"mappings": ";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,OAAe;AAE5B,IAAM,WAAW,OAAO;AAExB,QAAQ,IAAI,WAAW,MAAM;",
"names": []
}
产物中指明了对应的 sourcemap
文件
sourceMappingURL=index.js.map sourcemap 文件中也指明了对应的
源产物
和sourcesContent
接下来,我们来利用 sourcemap打断点调试
// bin/debugger.js
#!/usr/bin/env node
+ require('../dist/index.js')
- const yang = 'yang'
- const yangyang = yang + 'yang'
- console.log(yangyang + 'yang');
在 src/index.ts
里面打断点
调试配置文件的
program
字段 还是指向 bin/debugger.js
// .vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/bin/debugger.js"
}
]
}
现在我们就可以 直接调试源代码
而不是打包压缩编译
之后看不懂
的代码了
他会在执行编译之后的产物代码的时候,自动定位到编译之前的代码,这样我们就可以调试 源码
了!
所以你可以回答我们为什么需要sourcemap吗?
如果你能看懂编译
、polyfill
、压缩
等一系列操作之后之后生成的产物代码,就不需要 sourcmap 了(嘿嘿
4.实战:调试 Umi
源代码
我们来看 Umi 的源码
Umi 这个仓库采用 pnpm 的 Monorepo
管理
我们随便进入一个其中一个 package,可以发现也是使用 father
进行编译
1. clone Umi 源码到本地
// 终端
git clone https://github.com/umijs/umi.git
code umi
2.寻找 bin 脚本入口
tips:可以在某个项目中安装 Umi,然后查看 node_modules
下 umi 中的 bin 命令
然后我们就可以在 Umi源码
中去定位入口 bin
脚本的位置了
-- packages/umi/bin/umi.js
// packages/umi/bin/umi.js
#!/usr/bin/env node
// disable since it's conflicted with typescript cjs + dynamic import
// require('v8-compile-cache');
// patch console for debug
// ref: https://remysharp.com/2014/05/23/where-is-that-console-log
if (process.env.DEBUG_CONSOLE) {
['log', 'warn', 'error'].forEach((method) => {
const old = console[method];
console[method] = function () {
let stack = new Error().stack.split(/\n/);
// Chrome includes a single "Error" line, FF doesn't.
if (stack[0].indexOf('Error') === 0) {
stack = stack.slice(1);
}
const args = [].slice.apply(arguments).concat([stack[1].trim()]);
return old.apply(console, args);
};
});
}
require('../dist/cli/cli')
.run()
.catch((e) => {
console.error(e);
process.exit(1);
});
3.安装依赖、生成 bin 脚本需要的 dist 目录
// 根目录
pnpm i
npm run build //执行了 turo run build
4.设置调试配置
注意:这里新增一个 “stopOnEntry”: true,作用是会在入口文件第一行打断点,不需要我们亲手去打,可以从入口开始调试
// .vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"program": "${workspaceFolder}/packages/umi/bin/umi.js",
"args": ["dev"],
"stopOnEntry": true
}
]
}
5.出现问题
这里我们忘记了一个关键因素,sourcemap
我这里准备去他的 .fatherrc.ts 配置文件
开启一下sourcemap
结果发现他现在father版本是3.x
,不支持
开启sourcemap
5.基于调试中的问题,给 Umi 提个pr
先提个issue
:github.com/umijs/umi/i…
pr
--升级father版本到4.x支持sourcemap
:github.com/umijs/umi/p…
到现在为止,该 pr 已经合并