携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第1期,链接:www.yuque.com/ruochuan12/…。
摘要
本期源码阅读的主题是 vue-devtools 打开源码功能,这是一个非常实用的技巧,尤其是多人开发的项目,当对代码不熟悉的时候能够直接打开想要查看的组件源码,真的是非常方便,那么接下来就对相关源码进行解读。
下载
首先我们需要安装 vue-devtools,这里为不能用 chrome 直接安装插件的小伙伴提供一个非常好用的网站,可以在里面下载非常多的插件。如何安装 chrome 插件在这里就不赘述了,网上教程很多。
项目搭建
为了调试代码,我们需要搭建一个 vue 项目,这里为了保证和川哥的文章内容一致,直接使用了他搭建的项目
主要包的版本:
webpack: 4.46
vue-cli: 4.5.12
vue: 3.0.11
开始调试
项目 clone install 完毕之后,就可以开始调试源码。
- 打开 package .json
- 点击 script 上的调试
- 选择想要执行的脚本, 这里选择 serve 就好
构建完成之后点击项目地址,则会弹出一个专门用于调试的 chrome 窗口,这时候需要在这个新的浏览器界面里也安装一下 vue-devtools
F12 后选择 Vue 页签,选择想要进入的组件,在右上角侧可以看到一个跳转按钮,点击后即可在本地编辑器中打开组件代码
源码分析
这个功能相关的源码主要有三个文件,下面对其依次进行分析
\node_modules\launch-editor-middleware\index.js
launch-editor-middleware 的作用接受在 vue-devtools 点击页面打开时的网络请求
例如: //localhost:8080/__open-in-editor?file=src/components/HelloWorld.vue
如果携带了请求参数 file, 则执行关键的 lunch 函数
const url = require('url')
const path = require('path')
const launch = require('launch-editor')
module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
if (typeof specifiedEditor === 'function') {
onErrorCallback = specifiedEditor
specifiedEditor = undefined
}
if (typeof srcRoot === 'function') {
onErrorCallback = srcRoot
srcRoot = undefined
}
srcRoot = srcRoot || process.cwd()
return function launchEditorMiddleware (req, res, next) {
const { file } = url.parse(req.url, true).query || {}
if (!file) {
res.statusCode = 500
res.end(`launch-editor-middleware: required query param "file" is missing.`)
} else {
launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
res.end()
}
}
}
node_modules\launch-editor\index.js
在 launchEditor 函数里实现了如何在编辑器里打开组件文件。
首先封装了一个错误输出函数 wrapErrorCallback, 统一了报错输出格式。
关键代码是执行了 childProcess.spawn,这是 node 的内置方法,开启了一个子进程, 使用 cmd.exe 执行 C:\software\Microsoft VS Code\Code.exe 程序来打开 args 参数中的文件
const fs = require('fs')
const os = require('os')
const path = require('path')
const chalk = require('chalk')
const childProcess = require('child_process')
const guessEditor = require('./guess')
const getArgumentsForPosition = require('./get-args')
// 程序报错后的回调函数
function wrapErrorCallback (cb) {
return (fileName, errorMessage) => {
console.log()
console.log(
chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
)
if (errorMessage) {
if (errorMessage[errorMessage.length - 1] !== '.') {
errorMessage += '.'
}
console.log(
chalk.red('The editor process exited with an error: ' + errorMessage)
)
}
console.log()
if (cb) cb(fileName, errorMessage)
}
}
...
let _childProcess = null
function launchEditor (file, specifiedEditor, onErrorCallback) {
// 解析组件文件相关参数,获取文件名
const parsed = parseFile(file)
let { fileName } = parsed
const { lineNumber, columnNumber } = parsed
// 判断文件是否存在
if (!fs.existsSync(fileName)) {
return
}
if (typeof specifiedEditor === 'function') {
onErrorCallback = specifiedEditor
specifiedEditor = undefined
}
onErrorCallback = wrapErrorCallback(onErrorCallback)
//! 获取编辑器名称
const [editor, ...args] = guessEditor(specifiedEditor)
if (!editor) {
onErrorCallback(fileName, null)
return
}
...
if (process.platform === 'win32') {
// On Windows, launch the editor in a shell because spawn can only
// launch .exe files.
_childProcess = childProcess.spawn(
'cmd.exe',
['/C', editor].concat(args),
{ stdio: 'inherit' }
)
} else {
_childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
}
_childProcess.on('exit', function (errorCode) {
_childProcess = null
if (errorCode) {
onErrorCallback(fileName, '(code ' + errorCode + ')')
}
})
_childProcess.on('error', function (error) {
onErrorCallback(fileName, error.message)
})
}
module.exports = launchEditor
node_modules\launch-editor\guess.js
这块代码的功能就是来猜测你目前使用的是什么编辑器
module.exports = function guessEditor (specifiedEditor) {
if (specifiedEditor) {
return shellQuote.parse(specifiedEditor)
}
// We can find out which editor is currently running by:
// `ps x` on macOS and Linux
// `Get-Process` on Windows
try {
if (process.platform === 'darwin') {
const output = childProcess.execSync('ps x').toString()
const processNames = Object.keys(COMMON_EDITORS_OSX)
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
if (output.indexOf(processName) !== -1) {
return [COMMON_EDITORS_OSX[processName]]
}
}
} else if (process.platform === 'win32') {
const output = childProcess
.execSync('powershell -Command "Get-Process | Select-Object Path"', {
stdio: ['pipe', 'pipe', 'ignore']
})
.toString()
const runningProcesses = output.split('\r\n')
for (let i = 0; i < runningProcesses.length; i++) {
// `Get-Process` sometimes returns empty lines
if (!runningProcesses[i]) {
continue
}
const fullProcessPath = runningProcesses[i].trim()
const shortProcessName = path.basename(fullProcessPath)
if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
return [fullProcessPath]
}
}
} else if (process.platform === 'linux') {
// --no-heading No header line
// x List all processes owned by you
// -o comm Need only names column
const output = childProcess
.execSync('ps x --no-heading -o comm --sort=comm')
.toString()
const processNames = Object.keys(COMMON_EDITORS_LINUX)
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
if (output.indexOf(processName) !== -1) {
return [COMMON_EDITORS_LINUX[processName]]
}
}
}
} catch (error) {
// Ignore...
}
// Last resort, use old skool env vars
if (process.env.VISUAL) {
return [process.env.VISUAL]
} else if (process.env.EDITOR) {
return [process.env.EDITOR]
}
return [null]
}
上述代码还是比较简单的,容易理解
本人遇到的问题
我再调试的过程中遇到了两个问题,花时间研究了下,下面记录一下。
项目路径带空格时,会报错
分别创建了两个项目:
项目路径 C:\Users\zhenlv\Test 成功打开
项目路径 C:\Users\zhenlv\Test 1 报错如图
下面开启疯狂调试模式:
- 调试到 _childProcess 的 spawnargs 参数对比,也就是空格的不同
那么就应该是 childProcess.spawn 这个函数使用的问题了。而这个是 node 源码,目前的方式好像是调试不了,F11 单步调试进不去这个函数。
- 参考了node 代码调试 中的介绍, 借助 chrome 进行调试。我把关键代码拿出来,用 node --inspect-brk test.js 的方式来调试
// test.js
const childProcess = require("child_process");
_childProcess = childProcess.spawn(
"cmd.exe",
["/C", "code", "C:\Users\zhenlv\Test 1\src\components\HelloWorld.vue"],
{ stdio: "inherit" }
);
- 打开 chrome://inspect/#devices 就可以看到想要调试的代码, 点击 inspect,就打开了 test.js 文件的源码
- 继续调试,发现最终执行的是
this._handle.spawn(options);是 node 创建了个进程 Process 来执行命令。而 Process 不是 js 代码, 而是 C++ 的代码, 参见。调试到这里也没有找到问题出现的根本原因,不过可以肯定是更底层的原因导致的,如果还需要继续研究的话需要换种方式调试 node 源码,所以调试到这里就告一段落。
-
通过问题的检索和查阅, 发现这个 github 上的这个 issue —— child_process.spawn fails on Windows given a space in both the command and an argument #7367,根据大伙提供的方法我也进行了尝试,不过失败了,不知道是哪用错了还是咋滴。
5.1 尝试使用 cross-spawn
const spawn = require("cross-spawn"); _childProcess = spawn( "cmd.exe", [ "/C", "C:\software\Microsoft VS Code\Code.exe", "C:\Users\zhenlv\Test 1\src\components\HelloWorld.vue", ], { stdio: "inherit" } );5.2 添加 shell : true
const childProcess = require("child_process"); _childProcess = childProcess.spawn( "cmd.exe", [ "/C", "C:\software\Microsoft VS Code\Code.exe", "C:\Users\zhenlv\Test 1\src\components\HelloWorld.vue", ], { stdio: "inherit", shell: true } );很奇怪的是,issue 里有人说是命令,如
C:\software\Microsoft VS Code\Code.exe和参数,如C:\Users\zhenlv\Test 1\src\components\HelloWorld.vue,中都有空格会报错,而我尝试的结果是只要参数中有空格,就出问题,有些搞不懂了。所以又作出以下尝试,VsCode 的命令行指令是 code, 做出如下改变,修改执行的命令
C:\software\Microsoft VS Code\Code.exe => code, 此时无论路径参数带不带空格都能正常打开文件。const childProcess = require("child_process"); _childProcess = childProcess.spawn( "cmd.exe", ["/C", "code", "C:\Users\zhenlv\Test 1\src\components\HelloWorld.vue"], { stdio: "inherit" } );那么接下来就是第二个问题,如何将 specifiedEditor 指定为 code。
指定 specifiedEditor 时遇到的问题
想要指定 specifiedEditor, 有两个方式,配置环境变量文件、修改 webpack 配置。下面分别讲一下本人在使用两种方式遇到的问题,每个人的电脑环境不同,可能遇到的问题也不同。
配置环境变量
- 首先设置 EDITOR
- 在 guess.js 的代码里使用了该变量
- 但是在本人的调试过程中发现,我的代码都会进入到如下逻辑,获取到的就是
C:\software\Microsoft VS Code\Code.exe, 而得不到想要的 code
- 个人粗浅的考虑,是不是可以把环境变量的判断的代码块提前,这样的话如果用户使用的时候指定了,就不用猜了。而且还可以避免路径名称中存在空格带来的问题。
修改 webpack 配置
根据官网说明,修改 vue 项目的 webpack 配置就能指定编辑器。因此创建如下 vue.config.js 文件。
const openInEditor = require("launch-editor-middleware");
module.exports = {
devServer: {
before(app) {
app.use("/__open-in-editor", openInEditor("code"));
},
},
};
但是在 vue.config.js 修改了 webpack 的 devServer 配置,但是不生效, 所以又得开启调试大法
-
首先找到默认的中间件配置代码。全局搜索 launch-editor-middleware,在 node_modules 里找到对应文件
node_modules@vue\cli-service\lib\commands\serve.js
-
调试并阅读源码,从171 行开始是对 server 对象的创建。其中 projectDevServerOptions 是来自项目中对 devServer 的配置,但是从代码中我们可以发现,projectDevServerOptions 的里 before 属性被后面那个对象的 before 属性覆盖了,并且在后面对象的 before 里,最后调用了 projectDevServerOptions 里定义的 before 函数,猜测目的是想用 projectDevServerOptions 里的 before 定义的
app.use("/__open-in-editor", openInEditor("code"))覆盖掉默认的。但是经过测试发现,这里并没有覆盖掉。// create server const server = new WebpackDevServer(compiler, Object.assign({ logLevel: 'silent', clientLogLevel: 'silent', historyApiFallback: { disableDotRule: true, rewrites: genHistoryApiFallbackRewrites(options.publicPath, options.pages) }, contentBase: api.resolve('public'), watchContentBase: !isProduction, hot: !isProduction, injectClient: false, compress: isProduction, publicPath: options.publicPath, overlay: isProduction // TODO disable this ? false : { warnings: false, errors: true } //!! 下面这行,projectDevServerOptions 见名知意,是在项目里配置的 devServer 相关信息,上面配置的 before 就在这里,Object.assign 合并后被后面对象中的 before 属性覆盖掉了 }, projectDevServerOptions, { https: useHttps, proxy: proxySettings, // eslint-disable-next-line no-shadow before (app, server) { // launch editor support. // this works with vue-devtools & @vue/cli-overlay app.use('/__open-in-editor', launchEditorMiddleware(() => console.log( `To specify an editor, specify the EDITOR env variable or ` + `add "editor" field to your Vue project config.\n` ))) // allow other plugins to register middlewares, e.g. PWA api.service.devServerConfigFns.forEach(fn => fn(app, server)) // apply in project middlewares projectDevServerOptions.before && projectDevServerOptions.before(app, server) }, // avoid opening browser open: false })) -
调整了如下两行代码的顺序,那么 specifiedEditor 就能得到指定的值 code
projectDevServerOptions.before && projectDevServerOptions.before(app, server) app.use('/__open-in-editor', launchEditorMiddleware(() => console.log( `To specify an editor, specify the EDITOR env variable or ` + `add "editor" field to your Vue project config.\n` )))然后又做了另外一个测试,将自定义的 before 里的 url 地址 改为
/__open-in-editor2,在调试的时候分别访问__open-in-editor?file=src/components/HelloWorld.vue和__open-in-editor?file=src/components/HelloWorld.vue,则都能打开文件 -
综上,这里推断可能是因为在 before 里,对一个 url 的监听只能生效一个,并且是声明在前面的生效。等在学习学习 webpack 的知识后在回头研究下
总结收获
本次文章的产生是因为突然发现了 vue-devtools 这个功能,然后觉得很好用很新奇,正好川哥在源码阅读中也有相应的解读,就花了些时间好好研究下。源码其实并不是很难,但是在调试过程中遇到了不少的问题,正是这些问题给源码阅读带来了不少的乐趣。源码其实并不可怕,慢慢来,多看多练也就能掌握。调试遇到问题不要怕,通过查阅、检索、提出猜想、进行测试,才能发现问题产生原因并寻找解决办法,同时也能发现自己知识体系的缺漏。所以能够读到这篇文章的小伙伴都来参加源码阅读吧!
本次阅读有以下几点收获:
- 掌握了 vue 项目找源码的便捷方式
- 了解了 webpack 中间件
- 调试 node 源码的方式
- 阅读源码的能力
- 发现问题并调研的能力
- 项目路径不要带空格!
接下来再尝试构建基于 wepback 5 和 vite 的项目,看看这个打开源码的功能有么有什么问题。