Chrome插件vueDevtools如何打开vsCode组件

782 阅读5分钟

本文参加了由公众号@若川视野发起的每周源码共读活动,点击了解详情一起参与
这是源码共读的第1期 | vue-devtools 组件可以打开编辑器,点击了解本期详情一起参与

1 学习目标

如何安装 Chrome 插件并实现一个简单的插件
了解通过 vue-devtools 插件打开 vsCode 对应组件原理


2 Chrome 插件

Chrome插件是一个用Web技术开发、用来增强浏览器功能的软件,是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包

2.1 安装 vue-devtools 插件

通常情况下,使用 Chrome 插件,需要在 Chrome 应用商店 下载安装,只是在应用商店下载需要翻墙,在这里推荐使用极简插件下载安装
在 Chrome 安装 Vue Devtools插件 Chrome 应用商店 安装完成后,打开控制台即可看到 vue-devTools 插件 image.png

2.2 如何实现 Chrome 插件

可能我们也见过像掘金打开新标签页的插件,那么如何实现一个简单的打开新标签页的 Chrome 插件。 image.png 实现一个简易的 Chrome 插件,打开新的标签页显示如下 image.png 新建一个 chrome_new_tab 空文件夹,目录内容如下(贴上主要文件代码,图片就不贴了) image.png 在文件夹下新建一个 manifest.json 文件

{
  // 插件名称
  "name": "new search",
  // 插件描述
  "description": "new search",
  // 插件版本
  "version": "1.0",
  // manifest版本 chrome18开始为2
  "manifest_version": 2,
  // 浏览器右上角图标设置
  "browser_action": {
    "default_icon": "newTab.png",
    // 鼠标悬停在扩展程序时的标题,可选
    "default_title": "这是一个示例Chrome插件",
  },
  // 权限申请
  "permissions": [
    // 标签
    "tabs"
  ],
  // 注入页面的JS\CSS
  "content_scripts": [
    {
      // <all_urls> 表示匹配所有地址
      "matches": [
        "<all_urls>"
      ],
      // Js 注入,按顺序执行
      "js": [
        "jquery.min.js",
        "canvas.js",
        "newTab.js"
      ],
      // 代码注入时间
      "run_at": "document_start"
    }
  ],
  // 覆盖浏览器默认页面
  "chrome_url_overrides": {
    "newtab": "newTab.html"
  }
}

新建 newTab.html 文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>new search</title>
    <link rel="icon" href="favicon.ico">
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" defer src="canvas.js"></script>
    <script type="text/javascript" defer src="newTab.js"></script>
    <style>
      html,
      body,
      .warp {
        height: 100%;
        width: 100%;
        position: relative;
        margin: 0;
      }
      canvas {
        position: absolute;
      }
      .search {
        position: absolute;
        top: 40%;
        left: 50%;
        transform: translate(-50%, -50%);
        height: 50px;
        display: flex;
      }

      input {
        width: 550px;
        height: 40px;
        outline: none;
        border: 0;
        font-size: 18px;
        padding: 0 10px;
        border-radius: 10px 0 0 10px;
      }

      button {
        width: 100px;
        height: 40px;
        font-size: 14px;
        color: white;
        border: 2px solid rgb(97, 126, 243);
        border-radius: 0 10px 10px 0;
        background-color: rgb(78, 110, 242);
        cursor: pointer;
          }
          </style>
  </head>

  <body>
    <div class="warp">
      <canvas id="myCanvas"></canvas>
      <div class="search">
        <input id="input" type="text" />
        <button>百度一下</button>
      </div>
    </div>
  </body>
</html>

新建 newTab.js 文件

var inputValue = document.getElementById('input')
var btn = document.querySelector('button')
if (btn) {
  btn.addEventListener('click', function () {
    location.href = 'http://www.baidu.com/s?wd=' + inputValue.value
  })
}

新建 canvans.js 文件(网上摘录)

let canvas = document.getElementById('myCanvas')
if (canvas) {
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight
  let width = window.innerWidth
  let height = window.innerHeight
  let ctx = canvas.getContext('2d')
  ctx.fillRect(0, 0, width, height)
  function random(min, max) {
    let num = Math.floor(Math.random() * (max - min) + min)
    if (num === 0) {
      num = 1
    }
    return num
  }
  function randomColor() {
    return `rgb(${random(0, 255)},${random(0, 255)},${random(0, 255)})`
  }
  function Ball(x, y, vx, vy, size, color, line) {
    this.x = x
    this.y = y
    this.vx = vx
    this.vy = vy
    this.size = size
    this.color = color
    this.lineColor = line
  }
  Ball.prototype.draw = function () {
    ctx.beginPath()
    ctx.fillStyle = this.color
    ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI)
    ctx.fill()
  }
  Ball.prototype.update = function () {
    if (this.x + this.size >= width || this.x - this.size <= 0) {
      this.vx = -this.vx
    }
    if (this.y + this.size >= height || this.y - this.size <= 0) {
      this.vy = -this.vy
    }
    this.x += this.vx
    this.y += this.vy
  }
  let list = []
  for (let i = 0; i <= 90; i++) {
    let circle = new Ball(
      random(0, width),
      random(0, height),
      random(-6, 6) * (1 / 3.0),
      random(-6, 6) * (1 / 3.0),
      3,
      'rgb(255,255,255)',
      `rgba(${random(0, 255)},${random(0, 255)},${random(0, 255)}`
    )
    list.push(circle)
  }
  function loopCircle () {
    ctx.fillStyle = 'rgba(0,0,0,0.6)'
    ctx.fillRect(0, 0, width, height)
    for (let i = 0; i < list.length; i++) {
      for (let j = 0; j < list.length; j++) {
        let lx = list[j].x - list[i].x
        let ly = list[j].y - list[i].y
        let LL = Math.sqrt(Math.pow(lx, 2) + Math.pow(ly, 2))
        if (LL <= 180) {
          ctx.beginPath()
          ctx.strokeStyle = `${list[i].lineColor},${(180 - LL) / 180})`
          ctx.moveTo(list[i].x, list[i].y)
          ctx.lineWidth = 1
          ctx.lineTo(list[j].x, list[j].y)
          ctx.stroke()
        }
      }
      list[i].draw()
      list[i].update()
    }
    requestAnimationFrame(loopCircle)
  }
  loopCircle()
}

完成以后,把 chrome_new_tab 文件夹添加到 Chrome 扩展程序中


3 Vue Devtools 插件

3.1 使用插件

// 先创建一个 vue3 项目,选择 vue、vue-ts
npm create vite-template
// 创建完成后,安装运行
npm i && npm run dev

浏览器打开控制台,可以看到调试界面 Vue 栏,点击下图所示图标,即可在 vscode 中打开项目中的组件 image.png 需要注意的是,首次使用的话,点击图标没有反应,我们返回 vscode 可以看到有报错提示,提示 code 指令失败 image.png 原因是打开组件是需要安装 code 指令,通过 code 指令控制 vscode 打开对应组件,那么我们安装一下 code 指令,在 mac 中,通过快捷键,command + shift + p 打开快捷栏,输入 code ,安装成功后,即可通过点击图标打开对应的组件 image.png

3.1 vue-devtools 源码分析

带着疑问出发,为什么点击图标可以跳转到 vscode 对应的组件? 我们先克隆 vue-devtools 源码,从源码中找答案

git clone https://github.com/vuejs/devtools/tree/add-remote-devtools

打开 vue-devtools 源码项目,茫茫代码中,怎么找到点击图标对应的功能在什么位置,这时候我们注意到鼠标放到图标是有 tooltip 提示语的,而且是有规律的 open * in editor ,那么我们可以在项目中匹配正则搜索,从而快速找到源码对应的位置 image.png 接着搜索ComponentInspector.openInEditor.tooltip,这时候,我们找了 button 标签,并且是有点击事件 openFile(),"打开文件" 看起来是有这个味道了,那么我们接着查看点击事件 image.png openFile 点击事件很简单,如果存在文件路径则调用了openInEditor函数

function openFile () {
  if (!data.value) return
  openInEditor(data.value.file)
}

接着在工具函数packages/shared-utils/src/util中,找到了函数openInEditor,代码比较简单,简单分析一下,SharedData.openInEditorHost 默认为 /,也就是发送了一个携带文件路径的请求 /__open-in-editor?file=xxx

export function openInEditor (file) {
  // Console display
  const fileName = file.replace(/\\/g, '\\\\')
  const src = `fetch('${SharedData.openInEditorHost}__open-in-editor?file=${encodeURI(file)}').then(response => {
    if (response.ok) {
      console.log('File ${fileName} opened in editor')
    } else {
      const msg = 'Opening component ${fileName} failed'
      const target = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {}
      if (target.__VUE_DEVTOOLS_TOAST__) {
        target.__VUE_DEVTOOLS_TOAST__(msg, 'error')
      } else {
        console.log('%c' + msg, 'color:red')
      }
      console.log('Check the setup of your project, see https://devtools.vuejs.org/guide/open-in-editor.html')
    }
  })`
  if (isChrome) {
    target.chrome.devtools.inspectedWindow.eval(src)
  } else {
    // eslint-disable-next-line no-eval
    eval(src)
  }
}

我们可以在控制台点击图标后,在 Network 看到浏览器发送此请求 image.png 也就是说,点击图标打开 vscode 对应的组件,vue-devtools 发送了一个/__open-in-editor请求,那么可以猜测本地有监听/__open-in-editor的方法,在监听方法里面实现了打开组件

3.2 vite-template 源码分析

我们需要在工程中找到监听/__open-in-editor的位置 回到 vite-template 工程中,拓展一下知识,当我们执行 npm run dev,实质上是执行了 vite image.png 但是在命令面板中输入 vite,提示 vite 命令是不存在的,那么为何在工程中 vite 可以执行?
其实是在安装依赖npm install,npm 通过读取 vite 中的 package.json 的 bin,把文件加入到 path 中,并在node_modules/.bin下创建好 vite 的软连接,这样就可以作为命令运行工程 如果是全局安装依赖,就会把 bin 文件添加到全局命令中 image.png 扯远了,回到正题,我们在依赖包中,根据 vite 可执行文件,找到监听/__open-in-editor方法的 js 文件 image.png

3.2.1 launchEditorMiddleware 函数

在创建服务createServer函数中执行launchEditorMiddleware方法

async function createServer(inlineConfig = {}) {
  // ...
  const middlewares = connect();
  // open in editor support
  middlewares.use('/__open-in-editor', launchEditorMiddleware());
  // ...
}

// 中间件方法
var launchEditorMiddleware = (specifiedEditor, srcRoot, onErrorCallback) => {
  // 参数交换,目的是为了不限制参数的位置
  // 根据参数的类型来判断,确定参数传给指定的虚参
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor;
    specifiedEditor = undefined;
  }
  if (typeof srcRoot === 'function') {
    onErrorCallback = srcRoot;
    srcRoot = undefined;
  }
  // /Users/user/study/soundCode/vite-template
  // 获取node执行的当前工作目录
  srcRoot = srcRoot || process.cwd();
  // 返回中间件函数
  return function launchEditorMiddleware (req, res, next) {
    // url.parse 第一个参数为 Url 参数,第二个参数默认为 false ,为true 的话 query 属性会生成一个对象
    // req.url= /?file=/Users/user/study/soundCode/vite-template/src/components/HelloWorld.vue
    // file = /Users/user/study/soundCode/vite-template/src/components/HelloWorld.vue
    const { file } = url$2.parse(req.url, true).query || {};
    if (!file) {
      res.statusCode = 500;
      res.end(`launch-editor-middleware: required query param "file" is missing.`);
    } else {
      // path.resolve() 生成绝对路径
      // launch('/Users/huanghaojie/study/soundCode/vite-template/src/components/HelloWorld.vue', undefined, undefined)
      launch(path$4.resolve(srcRoot, file), specifiedEditor, onErrorCallback);
      res.end();
    }
  }
};

3.2.2 launchEditor 函数

可以看出,当调用函数的时候,进程执行childProces.spawn(code, ['-r', '-g', '/Users/user/study/soundCode/vite-template/src/components/HelloWorld.vue:13:20'], { stdio: 'inherit' }) 的方式来打开编辑器对应的组件,核心点是 vsCode 支持使用命令 code ./文件路径 打开文件夹

var launchEditor_1 = launchEditor;
const launch = launchEditor_1;
// 启动编辑器
function launchEditor (file, specifiedEditor, onErrorCallback) {
  // 解析文件,返回文件文件路径、行、列
  const parsed = parseFile(file);
  // 文件路径
  let { fileName } = parsed;
  // 文件行、文件列
  const { lineNumber, columnNumber } = parsed;
  // 判断 fileName 路径是否存在
  if (!fs$6.existsSync(fileName)) {
    return
  }
  // specifiedEditor 值为 undefined,不执行
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor;
    specifiedEditor = undefined;
  }
  // 错误信息提示函数
  onErrorCallback = wrapErrorCallback(onErrorCallback);
  // 获取当前编辑器环境函数 specifiedEditor 值为 undefined
  // 返回['code'] editor = 'code'
  const [editor, ...args] = guessEditor(specifiedEditor);
  if (!editor) {
    onErrorCallback(fileName, null);
    return
  }
  if (
    process.platform === 'linux' &&
    fileName.startsWith('/mnt/') &&
    /Microsoft/i.test(os$1.release())
  ) {
    // Assume WSL / "Bash on Ubuntu on Windows" is being used, and
    // that the file exists on the Windows file system.
    // `os.release()` is "4.4.0-43-Microsoft" in the current release
    // build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
    // When a Windows editor is specified, interop functionality can
    // handle the path translation, but only if a relative path is used.
    fileName = path$5.relative('', fileName);
  }
  // 如果有行数
  if (lineNumber) {
    // 获取额外的参数
    // ['-r','-g','/Users/user/soundCode/vite-template/src/components/HelloWorld.vue:13:20']
    const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber);
    args.push.apply(args, extraArgs);
  } else {
    args.push(fileName);
  }
  // 存在进程 && Linus中特定的编辑器 则 kill 掉进程
  if (_childProcess && isTerminalEditor(editor)) {
    // There's an existing editor process already and it's attached
    // to the terminal, so go kill it. Otherwise two separate editor
    // instances attach to the stdin/stdout which gets confusing.
    _childProcess.kill('SIGKILL');
  }
  // window 系统
  if (process.platform === 'win32') {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    _childProcess = childProcess$1.spawn(
      'cmd.exe',
      ['/C', editor].concat(args),
      { stdio: 'inherit' }
    );
  // 其他系统
  } else {
    console.log(editor, args, { stdio: 'inherit' })
    // 执行 code 命令打开对应的组件
    _childProcess = childProcess$1.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);
  });
}

3.2.3 parseFile 函数

parseFile方法,目的是为了解析出打开文件的行和列,我们可以把 file 文件后天剑行和列,将会发现打开组件的同事,鼠标光标也停留在了行和列对应的位置

const positionRE = /:(\d+)(:(\d+))?$/;
function parseFile (file) {
  const fileName = file.replace(positionRE, '');
  const match = file.match(positionRE);
  const lineNumber = match && match[1];
  const columnNumber = match && match[3];
  return {
    fileName,
    lineNumber,
    columnNumber
  }
}

例如执行 launch('/Users/huanghaojie/study/soundCode/vite-template/src/components/HelloWorld.vue:13:20'),鼠标位置自动定位到对应的位置 image.png

3.2.4 wrapErrorCallback 函数

在未安装 code 指令时,vscode 的错误信息就是从此函数打印,可以在 3.1 使用插件中看到

function wrapErrorCallback (cb) {
  return (fileName, errorMessage) => {
    console.log();
    console.log(
      colors.red('Could not open ' + path$5.basename(fileName) + ' in the editor.')
    );
    if (errorMessage) {
      if (errorMessage[errorMessage.length - 1] !== '.') {
        errorMessage += '.';
      }
      console.log(
        colors.red('The editor process exited with an error: ' + errorMessage)
      );
    }
    console.log();
    if (cb) cb(fileName, errorMessage);
  }
}

3.2.5 guessEditor 函数

根据获取当前系统的进程,判断出具体的编译器所需的 value,例如 vscode 的命令为 code

var guess = 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 {
    // mac 系统
    if (process.platform === 'darwin') {
      // 获取当前系统所有程序的进程,ps 查看当前系统进程,x 获取所有程序
      const output = childProcess$2.execSync('ps x').toString();
      // 获取编辑器进程 key
      const processNames = Object.keys(COMMON_EDITORS_OSX);
      // 编辑器进程和系统进程匹配,匹配到编辑器环境所在的 value
      // '/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code'
      for (let i = 0; i < processNames.length; i++) {
        const processName = processNames[i];
        if (output.indexOf(processName) !== -1) {
          // 返回 code
          return [COMMON_EDITORS_OSX[processName]]
        }
      }
    // window 系统
    } else if (process.platform === 'win32') {
      const output = childProcess$2
        .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$7.basename(fullProcessPath);
        if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
          return [fullProcessPath]
        }
      }
    // linux 系统
    } 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$2
        .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]
};

const COMMON_EDITORS_OSX = osx;
const COMMON_EDITORS_LINUX = linux;
const COMMON_EDITORS_WIN = windows$1;
var osx = {
  '/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
  '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta':
    '/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
  '/Applications/Brackets.app/Contents/MacOS/Brackets': 'brackets',
  '/Applications/Sublime Text.app/Contents/MacOS/Sublime Text':
    '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
  '/Applications/Sublime Text.app/Contents/MacOS/sublime_text':
    '/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
  '/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2':
    '/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
  '/Applications/Sublime Text Dev.app/Contents/MacOS/Sublime Text':
    '/Applications/Sublime Text Dev.app/Contents/SharedSupport/bin/subl',
  '/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
  '/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Electron':
    'code-insiders',
  '/Applications/AppCode.app/Contents/MacOS/appcode':
    '/Applications/AppCode.app/Contents/MacOS/appcode',
  '/Applications/CLion.app/Contents/MacOS/clion':
    '/Applications/CLion.app/Contents/MacOS/clion',
  '/Applications/IntelliJ IDEA.app/Contents/MacOS/idea':
    '/Applications/IntelliJ IDEA.app/Contents/MacOS/idea',
  '/Applications/PhpStorm.app/Contents/MacOS/phpstorm':
    '/Applications/PhpStorm.app/Contents/MacOS/phpstorm',
  '/Applications/PyCharm.app/Contents/MacOS/pycharm':
    '/Applications/PyCharm.app/Contents/MacOS/pycharm',
  '/Applications/PyCharm CE.app/Contents/MacOS/pycharm':
    '/Applications/PyCharm CE.app/Contents/MacOS/pycharm',
  '/Applications/RubyMine.app/Contents/MacOS/rubymine':
    '/Applications/RubyMine.app/Contents/MacOS/rubymine',
  '/Applications/WebStorm.app/Contents/MacOS/webstorm':
    '/Applications/WebStorm.app/Contents/MacOS/webstorm'
};
var linux = {
  atom: 'atom',
  Brackets: 'brackets',
  code: 'code',
  emacs: 'emacs',
  'idea.sh': 'idea',
  'phpstorm.sh': 'phpstorm',
  'pycharm.sh': 'pycharm',
  'rubymine.sh': 'rubymine',
  sublime_text: 'subl',
  vim: 'vim',
  'webstorm.sh': 'webstorm'
};
var windows$1 = [
  'Brackets.exe',
  'Code.exe',
  'atom.exe',
  'sublime_text.exe',
  'notepad++.exe',
  'clion.exe',
  'clion64.exe',
  'idea.exe',
  'idea64.exe',
  'phpstorm.exe',
  'phpstorm64.exe',
  'pycharm.exe',
  'pycharm64.exe',
  'rubymine.exe',
  'rubymine64.exe',
  'webstorm.exe',
  'webstorm64.exe'
];

3.2.6 getArgumentsForPosition 函数

将行、列转为编辑器的参数,当前例子中,返回['-r', '-g', '/Users/user/soundCode/vite-template/src/components/HelloWorld.vue:13:20']

// normalize file/line numbers into command line args for specific editors
var getArgs = function getArgumentsForPosition (
  editor,
  fileName,
  lineNumber,
  columnNumber = 1
) {
  const editorBasename = path$6.basename(editor).replace(/\.(exe|cmd|bat)$/i, '');
  switch (editorBasename) {
    case 'atom':
    case 'Atom':
    case 'Atom Beta':
    case 'subl':
    case 'sublime':
    case 'sublime_text':
    case 'wstorm':
    case 'charm':
      return [`${fileName}:${lineNumber}:${columnNumber}`]
    case 'notepad++':
      return ['-n' + lineNumber, fileName]
    case 'vim':
    case 'mvim':
      return [`+call cursor(${lineNumber}, ${columnNumber})`, fileName]
    case 'joe':
      return ['+' + `${lineNumber}`, fileName]
    case 'emacs':
    case 'emacsclient':
      return [`+${lineNumber}:${columnNumber}`, fileName]
    case 'rmate':
    case 'mate':
    case 'mine':
      return ['--line', lineNumber, fileName]
    case 'code':
    case 'code-insiders':
    case 'Code':
      return ['-r', '-g', `${fileName}:${lineNumber}:${columnNumber}`]
    case 'appcode':
    case 'clion':
    case 'clion64':
    case 'idea':
    case 'idea64':
    case 'phpstorm':
    case 'phpstorm64':
    case 'pycharm':
    case 'pycharm64':
    case 'rubymine':
    case 'rubymine64':
    case 'webstorm':
    case 'webstorm64':
      return ['--line', lineNumber, fileName]
  }

  // For all others, drop the lineNumber until we have
  // a mapping above, since providing the lineNumber incorrectly
  // can result in errors or confusing behavior.
  return [fileName]
};

3.2.7 isTerminalEditor 函数

判断是否在 Linux 中特定的编辑器中执行

function isTerminalEditor (editor) {
  switch (editor) {
    case 'vim':
    case 'emacs':
    case 'nano':
      return true
  }
  return false
}

4 总结

通过阅读 vue-devtools、vite 源码,捋顺了整个流程,带着疑问出发,感觉更有针对性,阅读源码的过程也更顺利
通过安装 Chrome 插件,延伸到实现一个简单的插件,虽然只是稍微触碰,但是也能一叶知秋
比较遗憾的是,vue-devtools 源码无法 run 起来,提示@devtools/xxx 不存在,在 add-remote-devtools 分支倒是可以 run 起来,但是代码比较旧了,缺失了点击图标打开组件能力,这也是本次学习的遗憾