Chrome插件vue-devtools是如何把VSCode中对应的组件文件打开的

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

本文属于源码共读第1期 | vue-devtools 组件可以打开编辑器,点击了解本期详情一起参与

前言

通过本文可以学到哪些知识?

  1. 如何在Chrome中安装vue-devtools插件
  2. 创建vue3 demo项目,并调试一键打开对应组件文件
  3. 通过关联图可清晰整个流程的大体脉络
  4. 通过源码解剖原理
  5. vite项目如何进行调试使用
  6. 我为什么看源码?以及看源码能学到什么呢?

1、安装Chrome浏览器扩展插件vue-devtools

1、打开Chrome浏览器,点击最右侧`三个点`
2、点击:更多工具
3、继续点击:扩展程序
4、然后点击:页面左上角`三横杠`
5、继续点击:打开Chrome 网上应用店
6、然后搜索框输入:`vue`
7、出现列表:选择'Vue.js devtools (https://vuejs.org)',点击此项进入
8、然后点击`添加至Chrome`
9、出现弹框后,点击`添加扩展程序`,执行完,便添加完毕

注意了,这个插件也就是在开发的时候可以使用哟。

2、创建项目并测试

// 更新脚手架
npm update -g @vue/cli

//查看版本
vue -V // 5.0.8

// 创建项目
vue create vue3-ts-demo

// 选择一系列的模版组合,创建完毕,并安装依赖

// 直接通过VSCode打开此项目

// 直接运行
npm run serve

浏览器打开页面

image.png

会发现浏览器调试右侧出现一个标签页vue

image.png

点击如图所示的图标,就是打开对应的组件文件。可能初次使用,点击好几次都没有响应,然后返回到VSCode编辑器后发现,如下图所示的错误,该怎么办呢?

image.png

我现在使用的mac电脑,在VSCode中尝试快捷键command + shift + p,如果是window电脑可能是 Ctrl + Shift + p,然后输入shell,选择安装code。如下图:

image.png

选中安装Code命令后,重新运行项目,然后再次点击如图箭头所示位置

image.png

就会跳转到VSCode并打开对应的组件文件

image.png

原理:其实就是通过code命令,进行打开对应的文件,你可以在项目根目录进行测试code package.json或者其他文件,稍等片刻便会打开该文件。

3、一图胜千言

其实调试完上面的代码以后,我才开始想这其中的原理到底是怎么实现的,心里还在嘀咕有那么神奇吗?

其实作为菜如鸡的我一开始还真想不明白,于是我不断的阅读源码,一步一步的揭开了神秘的面纱,后来才发现也不过尔尔。

image.png

这里需要搞清楚几个小问题,也是我在梳理的过程中花费时间比较长的疑惑。

  1. vue-devTools也算是Chrome插件,是Chrome DevTools扩展,开发者工具里面的Tab扩展页

  2. 点击按钮发送的请求,然后通过webpack里的devServer进行请求拦截

  3. 请求拦截后,调用尤大大写的小插件 launch-editor

  4. 而launch-editor最终调用的child_process.spawn来执行命令

  5. 最终执行vscode中的code命令(我上面写过如何去安装)

4、从源码的角度解剖原理

4.1、准备工作

4.2、vue-devTools 插件中点击按钮

查看的代码当然就在devtools

按钮的模版代码

<VueButton
  v-if="fileIsPath"
  v-tooltip="{
    content: $t('ComponentInspector.openInEditor.tooltip', { file: data.file }),
    html: true
  }"
  icon-left="launch"
  class="flat icon-button"
  @click="openFile()"
/>
     
// tooltip显示的字符串
ComponentInspector: {
  openInEditor: {
    tooltip: 'Open <mono><<insert_drive_file>>{{file}}</mono> in editor',
  },
},

通过代码可以发现按钮事件openFile

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

可以看到里面又调用了openInEditor

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

其中主要的代码便是src变量的赋值,以及通过eval或者target.chrome.devtools.inspectedWindow.eval来执行src字符串。

src变量中的代码,不看错误处理的代码,其实也非常简单 就是一个fetch请求,其中变量SharedData.openInEditorHost就是一个/

  fetch(`/__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'
    }
  })

这里最重要的其实就是这个请求路径__open-in-editor

4.3、再来看我们创建的demo项目vue3-ts-demo

通过一个截图来看看代码位置

image.png

代码其实是在node_modules/@vue/cli-service../serve.js,也就是运行一下命令后执行的脚本代码

"serve": "vue-cli-service serve",

执行完上述命令,会将__open-in-editor请求地址注册到devServer,后续如果有请求过来,地址刚好一样,便会调用下面的launchEditorMiddleware,并传递了一个匿名函数。

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

// .....省略很多暂时用不到的代码

devServer.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`
)))

4.4、中间件launch-editor-middleware

上面launchEditorMiddleware最终使用的便是该npm包,里面只有一个文件,代码如下所示

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

module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  
  // specifiedEditor 传递进来的cnsole.log匿名函数
  // srcRoot和onErrorCallback都为undefined
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }
 
  if (typeof srcRoot === 'function') {
    onErrorCallback = srcRoot
    srcRoot = undefined
  }

  // 获得当前执行node命令时候的文件夹目录名 
  // /Users/user/Desktop/aehyok/github/demo/vue3-ts-demo
  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(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
      res.end()
    }
  }
}

这里初始化的时候,并不会执行return function launchEditorMiddleware。等到下次有请求为__open-in-editor的时候,才会真正的调用 launchEditorMiddleware

4.5、点击open xxx path in editor

image.png

看顶部引用

const launch = require('launch-editor')

4.6、最后的执行函数 launch-editor

然后开始执行如下函数

function launchEditor (file, specifiedEditor, onErrorCallback) {
  // 最重要的是解析出文件名 
  const parsed = parseFile(file)
  let { fileName } = parsed
  const { lineNumber, columnNumber } = parsed
  
  // 判断次文件是否存在
  if (!fs.existsSync(fileName)) {
    return
  }

  // 根据guessEditor推断出当前使用的代码编辑器
  const [editor, ...args] = guessEditor(specifiedEditor)
  // 如果editor,则会报错
  if (!editor) {
    onErrorCallback(fileName, null)
    // 其中打印错误就是之前上面的截图
    // colors.red('Could not open ' + path.basename(fileName) + ' in the editor.')
    return
  }

  //... 中间忽略很多的代码
  
  // window系统下调用cmd命令行工具执行code
  if (process.platform === 'win32') {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    _childProcess = childProcess.spawn(
      'cmd.exe',
      ['/C', editor].concat(args),
      { stdio: 'inherit' }
    )
  } else {
    // 其他平台则直接使用nodejs中childProcess模块的spawn,来执行code命令行
    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
  }
}

至此整个调试的大致过程已经清晰明了了,不知道你看懂了没,如果没看懂,一定是我没表述清楚,不过整个思路我已经了然于胸了。

5、通过vite 创建测试项目

// 创建项目
pnpm create vite

//项目名称
vite3-vue3-demo

// 选择框架
vue

// 选择一个变体
vue-ts

//OK,项目创建完毕,安装依赖
pnpm i

// 运行项目
pnpm dev

这里我试过好几个办法都没有解决去给process.env.EDITOR赋值,可能是我打开方式不对,如果有看我文章有解决方案的,可以告知菜鸡一下,非常感谢。

我这里就是直接修改node_modules中launch-editor中的guess.js

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

将最后的return [null]改为 return ['code'],其实就是上面的process.env.EDITOR不知道如何赋值,我尝试了两种方式去修改,都没成功,这其中原因我还没找出来???

6、总结

通过调试源码发现,只要仔细一点稍微花点时间,原来也能看懂尤大写的代码,没有想象中的那么难,而且感觉逻辑非常清晰,阅读起来很优雅。所以大家如果有想看源码,或者参加若川源码共读活动的,一定要大胆一些,不要怂,事情真的没有那么难。

有点目的性的阅读源码似乎更高效,这样针对性很强,不会大一统所有的源码都会过一下,时间一下子就过去了,每次带着一个小问题去看源码或许也是若川大佬的精髓所指。

通过阅读源码,就是把看不懂的函数方法关键字等,不断的查漏补缺。或者在这里的用法或者写法不一样,等等各种超乎你想象的用法、场景...,收获真的是非常大,尤其是看完后再写一篇小文总结出来,真的就比读一遍别人写的收获要多好几倍的感觉。

所以如果你还在犹豫自己看不懂,自己行不行等等借口,作为一个前端还不到两年经验的人告诉你,加加油相信自己,你完全可以的。最后一定要行动起来。

写的文章不要总想着有好多好多的浏览量,又或者有几十几百上千的点赞,我的要求不高,首先是自己真的学到了,思路真的整理总结出来了,最后送给有需要的人士,哪怕只有一位同仁看了我的文章,也就证明了它的价值所在。

谢谢在看的朋友,欢迎你一起来参加源码共读活动,学习真是一件快乐的事情,尤其是真的学到了,而不是浑浑噩噩的在混日子,当然自己之前浪费过太多的时间,假以时日,我定会.......

猜你喜欢

转载自juejin.im/post/7124860117786296334