很感谢若川大佬组织的源码阅读小组活动

每天下班后逼自己学习学习 以下为若川原文:https://juejin.cn/post/6959348263547830280#heading-2

什么是Vue-DevTools?

作为一个Vue开发者(不是),自然少不了Chrome中的Vue调试插件。
Vue-DevTools是一个可以在Chrome中进行Vue项目调试的工具,可以帮助开发者在使用Vue开发时,更清楚的了解目前页面中的组件、数据情况。
目前该插件有两个版本,支持Vue3的Beta版本,和支持Vue2的版本。
image.png

要了解什么?

这次主要了解在新版本DevTools中支持了一个新特性:在选择对应的组件后,点击open-in-editor的按钮后,即可在编译器中打开对应的组件。
image.png

实现原理:

主要通过launch-editor-middleware和launch-editor两个库实现了该功能,这两个库又通过调用node的process、child_process能力,创建一个node的子进程调起编译器打开选中的组件

阅读前准备:

  1. 在Chrome中准备支持Vue3的最新版本插件(目前最新版本号6.0.0 beta 15)
  2. vue create 创建一个vue-cli3项目
  3. 准备一个编译器

    开始调试:

    Open in editor在Vue3中是一个开箱即用的功能 具体如何配置使用:Open component in editor

1.寻找入口,进行调试

1.1寻找入口

根据上述文档的项目引入配置,需要在编译器中搜索'/__open-in-editor',即可在node_modules 中定位到该方法,此时在此处打个点~
image.png
再继续进入launchEditorMiddleware 发现这个中间件会调用launch-editor进行后续的打开编译器操作,此时可以在调用launch函数这行打上一个点~
image.png

1.2启动调试

以Vscode为例:
进入项目的package.json,可以看到在script属性上有一个“调试”或“debug”的按钮,点击后选择serve即可进入调试模式
image.png

在这里我踩了一个小坑(也是因为自己不够谨慎) 在npm i完成之后,先npm run serve在8080端口启动了项目,再点击调试 这会造成编译器再开启一个进程在8081端口启动项目,这也许会让你在后续调试时发现无法进入断点处 此时需要注意调试启动的项目端口是否与浏览器端口一致

接下来就进入到阅读源码部分~

开始阅读:

1.launchEditorMiddleware部分

在项目开始编译时,就会自动进入该部分代码。

个人理解在这部分代码中主要做了两件事: 1.函数重载,满足不同开发传参需求 2.通过node.js获取当前进程所在的位置,为后续打开编译器做准备

  1. // serve.js
  2. app.use('/__open-in-editor', launchEditorMiddleware(() => console.log(
  3. `To specify an editor, specify the EDITOR env variable or ` +
  4. `add "editor" field to your Vue project config.\n`
  5. )))
  6. //launch-editor-middleware/index.js
  7. module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  8. //这里对传入的第一个参数做一个判断,如果该参数为函数,则将这个参数与错误回调函数的值进行对调
  9. if (typeof specifiedEditor === 'function') {
  10. onErrorCallback = specifiedEditor
  11. specifiedEditor = undefined
  12. }
  13. //同样对传入的第二个参数也是做同样的判断
  14. if (typeof srcRoot === 'function') {
  15. onErrorCallback = srcRoot
  16. srcRoot = undefined
  17. }
  18. //第二个参数如果传入的是目录,则直接用
  19. //如果不是则调用node.js中process的能力,获取当前进程所在的位置
  20. srcRoot = srcRoot || process.cwd()
  21. return function launchEditorMiddleware (req, res, next) {
  22. //返回一个中间件
  23. }
  24. }

2 launch-editor部分

2.1执行前路径的判断

F12打开Vue-DevTools调试面板,选择一个组件,点击open-in-editor即可进入断点处
此时,如果切换到Chrome的Network栏时,会发现此时浏览器发送了一个请求:
image.png
结合编译前的app.use('/__open-in-editor', launchEditorMiddleware(...)不难知道这是一个中间件的写法,当浏览器发送请求时,就会进入到接下来的代码逻辑中

  1. module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  2. // ....省略
  3. return function launchEditorMiddleware (req, res, next) {
  4. // 首先会读取路径中的file参数
  5. const { file } = url.parse(req.url, true).query || {}
  6. if (!file) {
  7. res.statusCode = 500
  8. res.end(`launch-editor-middleware: required query param "file" is missing.`)
  9. } else {
  10. // 如果存在该路径,则会执行launch-editor逻辑
  11. launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
  12. res.end()
  13. }
  14. }
  15. }

2.2执行中最重要的一部分

进入到launchEditor函数后,也是该功能最重要的一部分

  1. function launchEditor (file, specifiedEditor, onErrorCallback) {
  2. //2.2.1通过正则匹配的方式读取文件路径、行号、列号的信息并进行返回
  3. const parsed = parseFile(file)
  4. let { fileName } = parsed
  5. const { lineNumber, columnNumber } = parsed
  6. // 2.2.2调用node.js的方法,以同步的方式检测该路径是否存在,不存在就return结束
  7. if (!fs.existsSync(fileName)) {
  8. return
  9. }
  10. // 这里同样是一个函数重载的方法
  11. if (typeof specifiedEditor === 'function') {
  12. onErrorCallback = specifiedEditor
  13. specifiedEditor = undefined
  14. }
  15. // 2.2.3这里跟错误回调调用了一个方法,比较有意思
  16. onErrorCallback = wrapErrorCallback(onErrorCallback)
  17. }

2.2.3部分,采用了装饰器模式(感谢同组的纪年小姐姐的总结),原理是将要执行的逻辑包裹起来,先执行其他的需要处理的代码,再执行onErrorCallback的逻辑。
继续阅读函数~

  1. function wrapErrorCallback (cb) {
  2. return (fileName, errorMessage) => {
  3. console.log()
  4. //这里先做了一个错误的输出,同时调用node.js中path的方法,提取出用"/"隔开的path最后一部分内容共
  5. //并且用了一个chalk库,可以改变控制台输出内容的颜色
  6. console.log(
  7. chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
  8. )
  9. // 此时如果有错误信息时,才会输出错误信息的提示
  10. if (errorMessage) {
  11. if (errorMessage[errorMessage.length - 1] !== '.') {
  12. errorMessage += '.'
  13. }
  14. console.log(
  15. chalk.red('The editor process exited with an error: ' + errorMessage)
  16. )
  17. }
  18. console.log()
  19. if (cb) cb(fileName, errorMessage)
  20. }
  21. }

若此时在这部分没有报错,则会继续进行接下来的流程。
2.2.4 此时会进入一个很“刺激”的猜测环节

  1. //launch-editor/index.js
  2. function launchEditor (file, specifiedEditor, onErrorCallback) {
  3. ...
  4. // 此时代码进入猜测函数
  5. const [editor, ...args] = guessEditor(specifiedEditor)
  6. }
  7. // launch-editor/guess.js
  8. module.exports = function guessEditor (specifiedEditor) {
  9. // 第一步:判断有没有传入对应的shell命令
  10. if (specifiedEditor) {
  11. // 如果传入,利用shell-quote库解析shell命令
  12. return shellQuote.parse(specifiedEditor)
  13. }
  14. // We can find out which editor is currently running by:
  15. // `ps x` on macOS and Linux
  16. // `Get-Process` on Windows
  17. // 第二步:猜测环节
  18. // 上面的三行注释也说明了可以判断当前是在哪个系统环境下运行,从而决定用何种方式启动编译器
  19. try {
  20. // 通过node.js中process中标识运行node.js进程的操作系统的方法获取当前的操作系统
  21. // 因为我的系统是MacOs,直接进入第一个猜测中
  22. if (process.platform === 'darwin') {
  23. // 此时调用了同步创建子进程的方法,这里会获取到目前的所有进程
  24. const output = childProcess.execSync('ps x').toString()
  25. // COMMON_EDITORS_OSX为一个map表,里面维护着MacOs下支持的编译器,以及对应的字段
  26. // 通过遍历的方式与当前系统中存在的编译器进行匹配
  27. const processNames = Object.keys(COMMON_EDITORS_OSX)
  28. for (let i = 0; i < processNames.length; i++) {
  29. const processName = processNames[i]
  30. if (output.indexOf(processName) !== -1) {
  31. return [COMMON_EDITORS_OSX[processName]]
  32. }
  33. }
  34. }
  35. // ... 不同平台的我就省略了,原理类似
  36. // 最后还有一个兜底的方案
  37. // Last resort, use old skool env vars
  38. if (process.env.VISUAL) {
  39. return [process.env.VISUAL]
  40. } else if (process.env.EDITOR) {
  41. return [process.env.EDITOR]
  42. }
  43. return [null]
  44. }

2.2.5 猜测完之后的操作

  1. function launchEditor (file, specifiedEditor, onErrorCallback) {
  2. // ...
  3. const [editor, ...args] = guessEditor(specifiedEditor)
  4. // 如果没有找到,就会报错
  5. if (!editor) {
  6. onErrorCallback(fileName, null)
  7. return
  8. }
  9. // 核心部分,根据不同的系统状态,打开调起不同的工具打开编译器
  10. // childProcess.spawn为异步衍生子进程,并且不会阻塞node.js的事件循环
  11. if (process.platform === 'win32') {
  12. // On Windows, launch the editor in a shell because spawn can only
  13. // launch .exe files.
  14. _childProcess = childProcess.spawn(
  15. 'cmd.exe',
  16. ['/C', editor].concat(args),
  17. { stdio: 'inherit' }
  18. )
  19. } else {
  20. // 因为是MacOs,因此调用Vscode,打开args地址(项目地址),并且子进程将使用父进程的标准输入输出。
  21. // 这块Node文档参考
  22. // http://nodejs.cn/api/child_process.html#child_process_child_process_spawn_command_args_options
  23. // 到这里,对应的组件文件就已经在编译器中被打开了
  24. _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
  25. }
  26. // 这里是对子进程结束后触发做监听,检测进程退出是否存在异常
  27. _childProcess.on('exit', function (errorCode) {
  28. _childProcess = null
  29. if (errorCode) {
  30. onErrorCallback(fileName, '(code ' + errorCode + ')')
  31. }
  32. })
  33. _childProcess.on('error', function (error) {
  34. onErrorCallback(fileName, error.message)
  35. })
  36. }

总结

首先小小的表扬一下自己,终于克服了不会读不敢读源码的问题
🎉🎉🎉🎉🎉🎉🎉
以前觉得源码都很难懂,框架也很难了解真正的原理。但是通过这次活动,小小的明白了一个工具中一个小模块的实现方法,很有意思。
也很感谢若川大佬组织这次活动,辛苦了。

这次阅读的过程同时也发现了原来Node可以做很多事情,这也是之前没有了解过的知识点。

相关文档和资料:

Vue-DevTools:https://github.com/vuejs/devtools#open-component-in-editor
尤大版本launch-editor:https://github.com/yyx990803/launch-editor
Umijs/launch-editor:https://github.com/umijs/launch-editor