源码学习目录

1. 前言

1.1 环境

  1. 操作系统: macOS 11.5.2
  2. 浏览器: Chrome 94.0.4606.81
  3. shell: zsh (如何安装zsh,可以自行百度 ohmyzsh,windows系统需要使用WSL子系统安装)
  4. @vue/cli 4.5.15
  5. vue 3.2.21
  6. webpack 4.46.0

    1.2 阅读该文章可以get以下知识点

  7. 使用Vue.js devtools浏览器插件打开vue文件所在位置

  8. launch-editor-middleware、launch-editor 实现原理

    2. 准备工作

    2.1 创建vue3项目

    1. npm install -g @vue/cli
    2. vue create vue3-project

    image.png
    image.png

    2.2 安装Vue Devtools插件

    Vue Devtools: 官方文档
    由于使用vue3,需要安装bata版本的vue.js devtools,不需要任何配置开箱即用
    注意: 旧版本的插件不支持vue3,因此会出现Vue.js not detected
    image.png

    2.3 使用Vue Devtools插件

  9. 打开一个vue项目,在浏览器中打开,正常开启如图下所示

image.png

  1. 点击下图中的红框位置,就可以在vscode打开文件所在位置

image.png

  1. 如果发现点击后没有任何反应,可以在vscode命令行看到报错

image.png

  1. 上述问题解决方案:

mac 电脑在 VSCode command + shift + p,Windows 则是 ctrl + shift + p。然后输入shell,选择安装code。如下图:
image.png

  1. 如果需要调试node_modules,需要开启auto attach

image.png

3. 开始

3.1 查阅Vue Devtools文档

  1. 通过插件安装详情的说明或者直接在github上搜索可以找到该插件的GitHub地址

image.png
image.png

  1. GitHub上有该插件的使用文档,找到open in editor,里面有webpack相关配置

image.png

  1. 通过上述图片,可以定位到代码,devServer使用了launch-editor-middleware这个中间件

    3.2 定位launch-editor-middleware (在vue-cli中)

  2. 使用vscode全局搜索, vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js

image.png
由于中间件的包在node_modules下面,需要将忽略的文件夹放开,

  1. // 文件位置 vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js
  2. const launchEditorMiddleware = require('launch-editor-middleware')
  3. const server = new WebpackDevServer(compiler, Object.assign(
  4. // 省略...
  5. {
  6. before (app, server) {
  7. // launch editor support.
  8. // this works with vue-devtools & @vue/cli-overlay
  9. // 在webpack devServer添加中间件,当匹配路由/__open-in-editor时,调用launchEditorMiddleware函数
  10. app.use('/__open-in-editor', launchEditorMiddleware(() => console.log(
  11. `To specify an editor, specify the EDITOR env variable or ` +
  12. `add "editor" field to your Vue project config.\n`
  13. )))
  14. }
  15. }))

上述代码是webpack devServer的配置,相关文档: WebpackDevServer

  1. devServer.before
  2. Provides the ability to execute custom middleware prior to all other middleware internally within the server. This could be used to define custom handlers, for example:
  3. // 大致意思 在server中提供自定义中间件的能力优先于其他的中间件运行

3.3 launch-editor-middleware源码

  1. const url = require('url')
  2. const path = require('path')
  3. const launch = require('launch-editor')
  4. // 从上一步传入了一个回调函数做console输出
  5. module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  6. // 如果第一个参数是函数,将specifiedEditor 赋值给onErrorCallback
  7. if (typeof specifiedEditor === 'function') {
  8. onErrorCallback = specifiedEditor
  9. specifiedEditor = undefined
  10. }
  11. // 如果第二个参数是函数,srcRoot赋值给onErrorCallback
  12. if (typeof srcRoot === 'function') {
  13. onErrorCallback = srcRoot
  14. srcRoot = undefined
  15. }
  16. // 上述两步操作,相当于改写了函数入参,参数不定时,保证了函数赋值给onErrorCallback.
  17. // 如果srcRoot是传入的非函数参数,直接使用,否则使用node进程的目录
  18. srcRoot = srcRoot || process.cwd()
  19. // 返回一个函数,是一个express中间件,
  20. return function launchEditorMiddleware (req, res, next) {
  21. // 获取url的query参数file
  22. const { file } = url.parse(req.url, true).query || {}
  23. // 不存在,说明没有这个文件
  24. if (!file) {
  25. res.statusCode = 500
  26. res.end(`launch-editor-middleware: required query param "file" is missing.`)
  27. } else {
  28. // 调用launch函数
  29. launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
  30. res.end()
  31. }
  32. }
  33. }

点击 插件的open in editor 按钮,会发送一个http get请求, http://localhost:8080/__open-in-editor?file=src/App.vue, 上面的file变量,就是这里的src/App.vue,path.resolve(srcRoot, file),获取了完整的路径

3.4 launch-editor源码

  1. const fs = require('fs')
  2. const os = require('os')
  3. const path = require('path')
  4. // 命令行日志打印颜色处理
  5. const chalk = require('chalk')
  6. // 子进程
  7. const childProcess = require('child_process')
  8. // 猜测编译器
  9. const guessEditor = require('./guess')
  10. const getArgumentsForPosition = require('./get-args')
  11. function wrapErrorCallback (cb) {
  12. return (fileName, errorMessage) => {
  13. console.log()
  14. console.log(
  15. chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
  16. )
  17. if (errorMessage) {
  18. if (errorMessage[errorMessage.length - 1] !== '.') {
  19. errorMessage += '.'
  20. }
  21. console.log(
  22. chalk.red('The editor process exited with an error: ' + errorMessage)
  23. )
  24. }
  25. console.log()
  26. if (cb) cb(fileName, errorMessage)
  27. }
  28. }
  29. function isTerminalEditor (editor) {
  30. switch (editor) {
  31. case 'vim':
  32. case 'emacs':
  33. case 'nano':
  34. return true
  35. }
  36. return false
  37. }
  38. // 行数和列数清除
  39. const positionRE = /:(\d+)(:(\d+))?$/
  40. function parseFile (file) {
  41. const fileName = file.replace(positionRE, '')
  42. const match = file.match(positionRE)
  43. const lineNumber = match && match[1]
  44. const columnNumber = match && match[3]
  45. return {
  46. fileName,
  47. lineNumber,
  48. columnNumber
  49. }
  50. }
  51. let _childProcess = null
  52. // 主函数
  53. function launchEditor (file, specifiedEditor, onErrorCallback) {
  54. // 解析文件路径,拿到文件名,行数,列数
  55. const parsed = parseFile(file)
  56. let { fileName } = parsed
  57. const { lineNumber, columnNumber } = parsed
  58. if (!fs.existsSync(fileName)) {
  59. return
  60. }
  61. // 这里做了和launch-editor-middleware中间件同样的事情
  62. if (typeof specifiedEditor === 'function') {
  63. onErrorCallback = specifiedEditor
  64. specifiedEditor = undefined
  65. }
  66. // 包装了一层函数处理
  67. onErrorCallback = wrapErrorCallback(onErrorCallback)
  68. // 猜测当前进程运行的是哪个编译器,具体内容看下面3.5的内容
  69. const [editor, ...args] = guessEditor(specifiedEditor)
  70. if (!editor) {
  71. onErrorCallback(fileName, null)
  72. return
  73. }
  74. // 判断操作系统,
  75. if (
  76. process.platform === 'linux' &&
  77. fileName.startsWith('/mnt/') &&
  78. /Microsoft/i.test(os.release())
  79. ) {
  80. // Assume WSL / "Bash on Ubuntu on Windows" is being used, and
  81. // that the file exists on the Windows file system.
  82. // `os.release()` is "4.4.0-43-Microsoft" in the current release
  83. // build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
  84. // When a Windows editor is specified, interop functionality can
  85. // handle the path translation, but only if a relative path is used.
  86. // 返回的值是 从a位置,到b位置 http://nodejs.cn/api/path/path_relative_from_to.html
  87. fileName = path.relative('', fileName)
  88. }
  89. // 如果有行列数,添加额外的参数
  90. if (lineNumber) {
  91. const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
  92. args.push.apply(args, extraArgs)
  93. } else {
  94. args.push(fileName)
  95. }
  96. // 如果子进程存在,删除子进程
  97. if (_childProcess && isTerminalEditor(editor)) {
  98. // There's an existing editor process already and it's attached
  99. // to the terminal, so go kill it. Otherwise two separate editor
  100. // instances attach to the stdin/stdout which gets confusing.
  101. _childProcess.kill('SIGKILL')
  102. }
  103. // windows系统
  104. if (process.platform === 'win32') {
  105. // On Windows, launch the editor in a shell because spawn can only
  106. // launch .exe files.
  107. // 运行子进程的编译器
  108. _childProcess = childProcess.spawn(
  109. 'cmd.exe',
  110. ['/C', editor].concat(args),
  111. { stdio: 'inherit' }
  112. )
  113. } else {
  114. _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
  115. }
  116. // 监听进程结束
  117. _childProcess.on('exit', function (errorCode) {
  118. _childProcess = null
  119. if (errorCode) {
  120. onErrorCallback(fileName, '(code ' + errorCode + ')')
  121. }
  122. })
  123. // 监听进程错误
  124. _childProcess.on('error', function (error) {
  125. onErrorCallback(fileName, error.message)
  126. })
  127. }
  128. module.exports = launchEditor

整个打开编译器的核心, 利用nodejs中child_process,执行类似 code path/to/file 命令

3.5 guessEditor

  1. // 通过系统和进程来判断,如果都匹配不到,通过环境变量env.VISUAL || 通过环境变量env.EDITOR,
  2. const path = require('path')
  3. const shellQuote = require('shell-quote')
  4. const childProcess = require('child_process')
  5. // Map from full process name to binary that starts the process
  6. // We can't just re-use full process name, because it will spawn a new instance
  7. // of the app every time
  8. const COMMON_EDITORS_OSX = require('./editor-info/osx')
  9. const COMMON_EDITORS_LINUX = require('./editor-info/linux')
  10. const COMMON_EDITORS_WIN = require('./editor-info/windows')
  11. module.exports = function guessEditor (specifiedEditor) {
  12. if (specifiedEditor) {
  13. return shellQuote.parse(specifiedEditor)
  14. }
  15. // We can find out which editor is currently running by:
  16. // `ps x` on macOS and Linux
  17. // `Get-Process` on Windows
  18. // 区分操作系统,获取进程,在判断运行的程序
  19. try {
  20. if (process.platform === 'darwin') {
  21. const output = childProcess.execSync('ps x').toString()
  22. const processNames = Object.keys(COMMON_EDITORS_OSX)
  23. for (let i = 0; i < processNames.length; i++) {
  24. const processName = processNames[i]
  25. if (output.indexOf(processName) !== -1) {
  26. return [COMMON_EDITORS_OSX[processName]]
  27. }
  28. }
  29. } else if (process.platform === 'win32') {
  30. const output = childProcess
  31. .execSync('powershell -Command "Get-Process | Select-Object Path"', {
  32. stdio: ['pipe', 'pipe', 'ignore']
  33. })
  34. .toString()
  35. const runningProcesses = output.split('\r\n')
  36. for (let i = 0; i < runningProcesses.length; i++) {
  37. // `Get-Process` sometimes returns empty lines
  38. if (!runningProcesses[i]) {
  39. continue
  40. }
  41. const fullProcessPath = runningProcesses[i].trim()
  42. const shortProcessName = path.basename(fullProcessPath)
  43. if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
  44. return [fullProcessPath]
  45. }
  46. }
  47. } else if (process.platform === 'linux') {
  48. // --no-heading No header line
  49. // x List all processes owned by you
  50. // -o comm Need only names column
  51. const output = childProcess
  52. .execSync('ps x --no-heading -o comm --sort=comm')
  53. .toString()
  54. const processNames = Object.keys(COMMON_EDITORS_LINUX)
  55. for (let i = 0; i < processNames.length; i++) {
  56. const processName = processNames[i]
  57. if (output.indexOf(processName) !== -1) {
  58. return [COMMON_EDITORS_LINUX[processName]]
  59. }
  60. }
  61. }
  62. } catch (error) {
  63. // Ignore...
  64. }
  65. // 如果没有匹配到上面的,可以获取环境变量env.VISUAL || env.EDITOR
  66. // Last resort, use old skool env vars
  67. if (process.env.VISUAL) {
  68. return [process.env.VISUAL]
  69. } else if (process.env.EDITOR) {
  70. return [process.env.EDITOR]
  71. }
  72. return [null]
  73. }

4. 总结

  1. 感谢若川大佬的源码共读活动,从这次活动学到源码阅读的方法和代码断点调试
  2. 打开组件文件位置的核心代码其实很简单,就是通过node子进程执行下面的shell命令

    1. code path/filename

    参考文档

  3. 若川 据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘

  4. Vue Devtools: 官方文档
  5. webpack devServer文档: WebpackDevServer
  6. github 源码 Vue Devtools
  7. react类似插件 react-dev-inspector
  8. launch-editor
  9. nvs