Vue3 launch-editor源码解读

653 阅读3分钟

若川的解读文章
据说 99% 的人不知道 vue-devtools 还能直接打开对应组件文件?本文原理揭秘: juejin.cn/post/695934…
尤雨溪开发的 vue-devtools 如何安装,为何打开文件的功能鲜有人知?:juejin.cn/post/699428…

川哥最近组织了一次源码共读活动。每周读 200 行左右的源码。感兴趣可以关注他的公众号若川视野。

准备

  1. 把川哥的文章过一遍,明确目标
  2. 学习目标:了解vue-devtools打开组件实现原理

具体操作

使用的组件

主要launch-editor-middleware和launch-editor两个库的使用

launch-editor-middleware

launch-editor-middleware中间件的主要作用调用launch-editor

源码调用

// vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js
// 46行
const launchEditorMiddleware = require('launch-editor-middleware')
// 192行
before (app, server) {
    // launch editor support.
    // 调用launchEditorMiddleware
    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`
    )))
    // 省略若干代码...
}
const url = require('url')
const path = require('path')
const launch = require('launch-editor')

// 根据上面的代码看出specifiedEditor传入的是个返回值为console.log()的函数
module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor// 赋值给onErrorCallback
    specifiedEditor = undefined
  }

  if (typeof srcRoot === 'function') {
    onErrorCallback = srcRoot
    srcRoot = undefined
  }

  // srcRoot 是传递过来的参数,或者当前node进程的目录
  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打开
      launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
      res.end()
    }
  }
}

打断点调试,点击左边红点,当点击文件进来时就停在断点处

launch-editor

但resolve解析完路径就会进入launchEditor函数

// vue3-project/node_modules/launch-editor/index.js
function launchEditor (file, specifiedEditor, onErrorCallback) {
  // 解析file路径
  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)

  // editor主要是获取编译器的参数
  const [editor, ...args] = guessEditor(specifiedEditor)
  if (!editor) {
    onErrorCallback(fileName, null)
    return
  }
  // ...
  // child_process 是 Node.js 的一个模块,它提供了衍生子进程的能力,默认情况下,会在父 Node.js 进程和衍生的子进程之间建立 stdin、stdout 和 stderr 的管道。
  if (process.platform === 'win32') {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    // 使用editor也就是code命令打开文件
    _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)
  })

}

guessEditor

这个函数主要做了如下四件事情:

  1. 如果具体指明了编辑器,则解析下返回。
  2. 找出当前进程中哪一个编辑器正在运行。macOS 和 Linux 用 ps x 命令
    windows 则用 Get-Process 命令
  1. 如果都没找到就用 process.env.VISUAL或者process.env.EDITOR。这就是为啥开头错误提示可以使用环境变量指定编辑器的原因。
  2. 最后还是没有找到就返回[null],则会报错。

guessEditor解构赋值完的参数显示

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]
}

总结

  1. vue-devtool能够打开文件的原理是通过code命令执行的
code /path/file
  1. 使用的是node的childProcess来运行命令,学会了一些childProcess的应用
  2. 主要步骤分为两步:
 1.  点击打开,进入launch中间件
 2.  解析相关路径
 3.  使用childProcess开启子进程,使用cmd执行code命令打开路径文件