Vue Devtools 的『Open component in editor』功能是如何实现的?

1,422 阅读5分钟

我正在参与掘金会员专属活动-源码共读第一期,点击参与

前言

学习尤大写的 launch-editor 源码,了解 Vue Devtools 面板中 Open component in editor 功能的实现原理。

vue-devtools 功能:打开组件文件在编辑器中的位置

过程原理简述

  1. 当我们点击 Vue Devtools 面板上的 open in editor 按钮时image.png,会发起一个 get 请求 /_open-in-editor?file=/path/to/xxx.vue

    image.png
  2. 我们本地开发启动一个项目时,相应的工具(例如 vue-cli-service \ vite)会创建一个开发服务器(http server),开发服务器会挂载一个路径为 /__open-in-editor的中间件 launch-editor-middleware;所以当我们请求 /_open-in-editor?file=/path/to/file 时,就会去执行中间件 launch-editor-middleware

    QQ20221224-133247@2x.png
  3. 中间件 launch-editor-middleware 内部实际调用 launch-editor 打开编辑器及对应文件。

    image.png

launch-editor-middleware 源码

launch-editor-middleware 导出的是一个工厂函数,执行它会返回一个中间件函数 launchEditorMiddleware,所以在使用的时候需要先执行下它,让它返回一个中间件,例如:

const launchMiddleware = require('launch-editor-middleware')

app.use('/__open-in-editor', launchMiddleware())

launchEditorMiddleware 函数内部调用 launch-editor 函数打开文件。 在调用 launch-editor 前,对参数 specifiedEditorsrcRoot 判断是否为函数,为函数就将值赋给 onErrorCallback并将自身置为 undefined,这样做的原因是三个参数都是可选的,让onErrorCallback 回调参数可以放在任意位置,只要它是作为最后一个参数。

launch-editor-middleware 包源码:

const path = require('path')
const launch = require('launch-editor')

/**
 * 中间件工厂函数,三个参数都是可选的,onErrorCallback 回调参数可以放在任意位置,只要它是作为最后一个参数
 * @param {*} specifiedEditor 指定打开的编辑器,值见 https://github.com/yyx990803/launch-editor#supported-editors
 * @param {*} srcRoot 源文件根目录
 * @param {*} onErrorCallback 启动编辑器失败时的回调
 */
module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  if (typeof specifiedEditor === "function") {
    onErrorCallback = specifiedEditor;
    specifiedEditor = undefined;
  }
  if (typeof srcRoot === "function") {
    onErrorCallback = srcRoot;
    srcRoot = undefined;
  }
  // 源文件根目录,未指定则默认为当前 node 进程目录
  srcRoot = srcRoot || process.cwd();
  // 返回一个 express 中间件函数
  return function launchEditorMiddleware(req, res, next) {
    // 解析请求路径,获取查询参数 file; 例如请求 /__open-in-editor?file=/path/to/xxx.vue
    const { file } = url.parse(req.url, true).query || {};
    // file 值若不存在,则响应状态 500 及结果信息
    if (!file) {
      res.statusCode = 500;
      res.end(
        `launch-editor-middleware: required query param "file" is missing.`
      );
    } else {
      // 调用 launch-editor 包
      launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback);
      // 结束响应
      res.end();
    }
  };
}

launch-editor 源码

代码较多就不贴出来了,可以克隆仓库代码下来看,lauch-editor 包的入口文件为 index.jsindex.js 导出的函数是 launchEditor,从它开始看。

launchEditor 函数

该函数函数内部主要做了以下事情:

  1. 解析文件路径、行列号,判断文件是否存在,不存在则直接 return 结束;

  2. 找出用户指定或者当前进程中运行的编辑器及对应的可执行程序,若没找到则打印报错信息并 return 结束(调用了 guessEditor 函数来查找编辑器);

  3. 调用 node 子进程 child_process.spawn()方法,执行对应编辑器的可执行程序打开目标文件。比如 vscode 编辑器:

    在 Windows 系统中对应的可执行程序为 Code.exe

    image.png

    在 macOS 系统中对应的可执行程序为 code

    image.png

guessEditor 函数

该函数用于找出用户指定或者当前进程中运行的编辑器及对应的可执行程序,函数内部做了以下事情:

  1. 若指定了编辑器参数 specifiedEditor,则调用 shell-quote 包解析参数 shellQuote.parse(specifiedEditor)返回解析结果数组;

    image.png
  2. 若没有指定编辑器参数,则在当前运行的所有进程中,找出支持的编辑器中的哪一个编辑器正在运行;

    查找当前所有进程使用的命令,macOS 和 Linux 用 ps x 命令,windows 则用 Get-CimInstance命令(旧代码用的 Get-Process 命令)。

    image.png

    支持打开的编辑器见这里,代码里会维护编辑器在不同系统中对应的可执行程序。

    image.png

  3. 如果没找到相应编辑器就判断环境变量process.env.VISUAL或者 process.env.EDITOR有没有配置(所以我们也可以使用环境变量来指定编辑器);

    image.png

  4. 如果以上方式都没找到,则返回 [null]

该功能在工具中的集成实现

看完以上源码,我们知道了这个功能核心其实跟 Vue Devtools 没啥关系,主要在我们的相关工具有没有集成该功能,下面介绍在几个工具中的集成实现。

Vue CLI

在运行 vue-cli-service serve 命令启动一个开发服务器时 (基于 webpack-dev-server) ,会在内部挂载一个路径为/__open-in-editor的中间件 launch-editor-middleware,当发生请求时就会去执行相应的中间件。源码地址

image.png

Vite

在运行 vite(别名:vite devvite serve) 命令启动一个开发服务器时,也是会在内部注册挂载一个路径为/__open-in-editor的中间件 launch-editor-middleware,当发生请求时去执行相应的中间件。源码地址

image.png

总结

一句话总结 Open component in editor 功能的原理:“使用 nodejs中的子进程child_process.spawn() 方法,执行编辑器对应的可执行程序打开目标文件。”,通过了解该功能实现原理,在以后我们需要类似功能的场景中,也可以集成进去。

相关链接