聽說 99% 的人不知道 vue-devtools 還能直接打開對應組件文件?本文原理揭祕

1. 前言

你好,我是若川,微信搜索「若川視野」關注我,專一前端技術分享,一個願景是幫助5年內前端開闊視野走向前列的公衆號。歡迎加我微信ruochuan12,長期交流學習。前端

這是學習源碼總體架構系列 之 launch-editor 源碼(第九篇)。學習源碼總體架構系列文章(有哪些必看的JS庫):jQueryunderscorelodashsentryvuexaxioskoaredux。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。下一篇應該是《學習 Vuex 4 源碼總體架構,深刻理解其原理及provide/inject原理》。vue

本文倉庫地址git clone https://github.com/lxchuan12/open-in-editor.git,本文最佳閱讀方式,克隆倉庫本身動手調試,容易吸取消化。node

要是有人說到怎麼讀源碼,正在讀文章的你能推薦個人源碼系列文章,那真是無覺得報啊react

個人文章儘可能寫得讓想看源碼又不知道怎麼看的讀者能看懂。我都是推薦使用搭建環境斷點調試源碼學習哪裏不會點哪裏邊調試邊看,而不是硬看。正所謂:授人與魚不如授人予漁linux

閱讀本文後你將學到:ios

  1. 如何解決該功能報錯問題
  2. 如何調試學習源碼
  3. launch-editor-middleware、launch-editor 等實現原理

1.1 短期找不到頁面對應源文件的場景

不知道大家有沒有碰到這樣的場景,打開你本身(或者你同事)開發的頁面,卻短期難以找到對應的源文件。git

這時你可能會想要是能有點擊頁面按鈕自動用編輯器打開對應文件的功能,那該多好啊。github

vue-devtools提供了這樣的功能,也許你不知道。我以爲很大一部分人都不知道,由於感受不少人都不經常使用vue-devtoolsweb

open-in-editor

你也許會問,我不用vue,我用react有沒有相似功能啊,有啊,請看react-dev-inspector。你可能還會問,支持哪些編輯器呀,主流的 vscode、webstorm、atom、sublime 等都支持,更多能夠看這個列表 Supported editorsvuex

本文就是根據學習尤大寫的 launch-editor 源碼,本着知其然,知其因此然的宗旨,探究 vue-devtools「在編輯器中打開組件」功能實現原理。

1.2 一句話簡述其原理

code path/to/file

一句話簡述原理:利用nodejs中的child_process,執行了相似code path/to/file命令,因而對應編輯器就打開了相應的文件,而對應的編輯器則是經過在進程中執行ps xWindow則用Get-Process)命令來查找的,固然也能夠本身指定編輯器。

1.3 打開編輯器沒法打開組件的報錯解決方法

而你真正用這個功能時,你可能碰到報錯,說不能打開這個文件。

Could not open App.vue in the editor.

To specify an editor, specify the EDITOR env variable or add "editor" field to your Vue project config.

控制檯不能打開編輯器的錯誤提示

這裏說明下寫這篇文章時用的是 Windows 電腦,在 Ubuntu子系統下使用的終端工具。同時推薦個人文章 使用 ohmyzsh 打造 windows、ubuntu、mac 系統高效終端命令行工具用過的都說好

解決辦法也簡單,就是這句英文的意思。具體說明編輯器,在環境變量中說明指定編輯器。在vue項目的根目錄下,對應本文則是:vue3-project,添加.env.delelopment文件,其內容是EDITOR=code

# .env.development
# 固然,個人命令行終端已經有了code這個命令。
EDITOR=code

不用指定編輯器的對應路徑(c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code),由於會報錯。爲何會報錯,由於我看了源碼且試過。由於會被根據空格截斷,變成c/Users/lxchu/AppData/Local/Programs/Microsoft,固然就報錯了。

接下來咱們從源碼角度探究「在編輯器中打開組件」功能的實現原理。

2. vue-devtools Open component in editor 文檔

探究原理以前,先來看看vue-devtools官方文檔。

vuejs/vue-devtools
文檔

Open component in editor

To enable this feature, follow this guide.

這篇指南中寫了在Vue CLI 3中是開箱即用

Vue CLI 3 supports this feature out-of-the-box when running vue-cli-service serve.

也詳細寫了如何在Webpack下使用。

# 1. Import the package:
var openInEditor = require('launch-editor-middleware')
# 2. In the devServer option, register the /__open-in-editor HTTP route:
devServer: {
  before (app) {
    app.use('/__open-in-editor', openInEditor())
  }
}
# 3. The editor to launch is guessed. You can also specify the editor app with the editor option. See the supported editors list.
# 用哪一個編輯器打開會自動猜想。你也能夠具體指明編輯器。這裏顯示更多的支持編輯器列表
openInEditor('code')
# 4. You can now click on the name of the component in the Component inspector pane (if the devtools knows about its file source, a tooltip will appear).
# 若是`vue-devtools`開發者工具備提示點擊的組件的顯示具體路徑,那麼你能夠在編輯器打開。

同時也寫了如何在Node.js中使用等。

Node.js

You can use the launch-editor package to setup an HTTP route with the /__open-in-editor path. It will receive file as an URL variable.

查看更多能夠看這篇指南

3. 環境準備工做

熟悉個人讀者,都知道我都是推薦調試看源碼的,正所謂:哪裏不會點哪裏。並且調試通常都寫得很詳細,是但願能幫助到一部分人知道如何看源碼。因而我特地新建一個倉庫open-in-editor git clone https://github.com/lxchuan12/open-in-editor.git,便於你們克隆學習。

安裝vue-cli

npm install -g @vue/cli
# OR
yarn global add @vue/cli
node -V
# v14.16.0
vue -V 
# @vue/cli 4.5.12
vue create vue3-project
# 這裏選擇的是vue三、vue2也是同樣的。
# Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)
npm install
# OR
yarn install

這裏同時說明下個人vscode版本。

code -v
1.55.2

前文提到的Vue CLI 3開箱即用Webpack使用方法。

vue3-project/package.json中有一個debug按鈕。

debug示意圖

選擇第一項,serve vue-cli-service serve

咱們來搜索下'launch-editor-middleware'這個中間件,通常來講搜索不到node_modules下的文件,須要設置下。固然也有個簡單作法。就是「排除的文件」右側旁邊有個設置圖標「使用「排查設置」與「忽略文件」」,點擊下。

其餘的就不贅述了。能夠看這篇知乎回答:vscode怎麼設置能夠搜索包含node_modules中的文件?

這時就搜到了vue3-project/node_modules/@vue/cli-service/lib/commands/serve.js中有使用這個中間件。

4. vue-devtools 開箱即用具體源碼實現

接着咱們來看Vue CLI 3開箱即用具體源碼實現。

// 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.
    // this works with vue-devtools & @vue/cli-overlay
    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`
    )))
    // 省略若干代碼...
}

點擊vue-devtools中的時,會有一個請求,http://localhost:8080/__open-in-editor?file=src/App.vue,不出意外就會打開該組件啦。

open src/App.vue in editor

接着咱們在launchEditorMiddleware的具體實現。

5. launch-editor-middleware

看源碼時,先看調試截圖。

debug-launch

launch-editor-middleware中間件中做用在於最終是調用 launch-editor 打開文件。

// vue3-project/node_modules/launch-editor-middleware/index.js
const url = require('url')
const path = require('path')
const launch = require('launch-editor')

module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  // specifiedEditor => 這裏傳遞過來的則是 () => console.log() 函數
  // 因此和 onErrorCallback 切換下,把它賦值給錯誤回調函數
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }

  // 若是第二個參數是函數,一樣把它賦值給錯誤回調函數
  // 這裏傳遞過來的是undefined
  if (typeof srcRoot === 'function') {
    onErrorCallback = srcRoot
    srcRoot = undefined
  }

  // srcRoot 是傳遞過來的參數,或者當前node進程的目錄
  srcRoot = srcRoot || process.cwd()

  // 最後返回一個函數, express 中間件
  return function launchEditorMiddleware (req, res, next) {
    // 省略 ...
  }
}

上一段中,這種切換參數的寫法,在不少源碼中都很常見。爲的是方便用戶調用時傳參。雖然是多個參數,但能夠傳一個或者兩個

能夠根據狀況打上斷點。好比這裏我會在launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)打斷點。

// vue3-project/node_modules/launch-editor-middleware/index.js
module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  // 省略上半部分
  return function launchEditorMiddleware (req, res, next) {
    // 根據請求解析出file路徑
    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()
    }
  }
}

6. launch-editor

跟着斷點來看,走到了launchEditor函數。

// vue3-project/node_modules/launch-editor/index.js
function launchEditor (file, specifiedEditor, onErrorCallback) {
  // 解析出文件路徑和行號列號等信息
  const parsed = parseFile(file)
  let { fileName } = parsed
  const { lineNumber, columnNumber } = parsed

  // 判斷文件是否存在,不存在,直接返回。
  if (!fs.existsSync(fileName)) {
    return
  }
  // 因此和 onErrorCallback 切換下,把它賦值給錯誤回調函數
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }
  // 包裹一層函數
  onErrorCallback = wrapErrorCallback(onErrorCallback)

  // 猜想當前進程運行的是哪一個編輯器
  const [editor, ...args] = guessEditor(specifiedEditor)
  if (!editor) {
    onErrorCallback(fileName, null)
    return
  }
  // 省略剩餘部分,後文再講述...
}

6.1 wrapErrorCallback 包裹錯誤函數回調

onErrorCallback = wrapErrorCallback(onErrorCallback)

這段的代碼,就是傳遞錯誤回調函數,wrapErrorCallback 返回給一個新的函數,wrapErrorCallback 執行時,再去執行 onErrorCallback(cb)。

我相信讀者朋友能看懂,我單獨拿出來說述,主要是由於這種包裹函數的形式在不少源碼裏都很常見

這裏也就是文章開頭終端錯誤圖Could not open App.vue in the editor.輸出的代碼位置。

// vue3-project/node_modules/launch-editor/index.js
function wrapErrorCallback (cb) {
  return (fileName, errorMessage) => {
    console.log()
    console.log(
      chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
    )
    if (errorMessage) {
      if (errorMessage[errorMessage.length - 1] !== '.') {
        errorMessage += '.'
      }
      console.log(
        chalk.red('The editor process exited with an error: ' + errorMessage)
      )
    }
    console.log()
    if (cb) cb(fileName, errorMessage)
  }
}

6.2 guessEditor 猜想當前正在使用的編輯器

這個函數主要作了以下四件事情:

  1. 若是具體指明瞭編輯器,則解析下返回。
  2. 找出當前進程中哪個編輯器正在運行。macOSLinuxps x 命令
    windows 則用 Get-Process 命令
  3. 若是都沒找到就用 process.env.VISUAL或者process.env.EDITOR。這就是爲啥開頭錯誤提示可使用環境變量指定編輯器的緣由。
  4. 最後仍是沒有找到就返回[null],則會報錯。
const [editor, ...args] = guessEditor(specifiedEditor)
if (!editor) {
    onErrorCallback(fileName, null)
    return
}
// vue3-project/node_modules/launch-editor/guess.js
const shellQuote = require('shell-quote')

module.exports = function guessEditor (specifiedEditor) {
  // 若是指定了編輯器,則解析一下,這裏沒有傳入。若是本身指定了路徑。
  // 好比 c/Users/lxchu/AppData/Local/Programs/Microsoft VS Code/bin/code 
  //   會根據空格切割成 c/Users/lxchu/AppData/Local/Programs/Microsoft
  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 {
    //  省略...
  } 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]
}

看完了 guessEditor 函數,咱們接着來看 launch-editor 剩餘部分。

6.3 launch-editor 剩餘部分

如下這段代碼不用細看,調試的時候細看就行。

// vue3-project/node_modules/launch-editor/index.js
function launchEditor(){
  //  省略上部分...
  if (
    process.platform === 'linux' &&
    fileName.startsWith('/mnt/') &&
    /Microsoft/i.test(os.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.relative('', fileName)
  }

  if (lineNumber) {
    const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
    args.push.apply(args, extraArgs)
  } else {
    args.push(fileName)
  }

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

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

這一大段中,主要的就是如下代碼,用子進程模塊。簡單來講子進程模塊有着執行命令的能力。

const childProcess = require('child_process')

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 {
    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
}

行文至此,就基本接近尾聲了。

7. 總結

這裏總結一下:首先文章開頭經過提出「短期找不到頁面對應源文件的場景」,並針對容易碰到的報錯狀況給出瞭解決方案。
其次,配置了環境跟着調試學習了vue-devtools中使用的尤大寫的 yyx990803/launch-editor

7.1 一句話簡述其原理

咱們回顧下開頭的原理內容。

code path/to/file

一句話簡述原理:利用nodejs中的child_process,執行了相似code path/to/file命令,因而對應編輯器就打開了相應的文件,而對應的編輯器則是經過在進程中執行ps xWindow則用Get-Process)命令來查找的,固然也能夠本身指定編輯器。

最後還能作什麼呢。

能夠再看看 umijs/launch-editorreact-dev-utils/launchEditor.js 。他們的代碼幾乎相似。

也能夠利用Node.js作一些提升開發效率等工做,同時能夠學習child_process等模塊。

也不要禁錮本身的思惟,把前端禁錮在頁面中,應該把視野拓寬

Node.js是咱們前端人探索操做文件、操做網絡等的好工具

若是讀者朋友發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎評論指出。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對個人一種支持,萬分感謝。若是能關注個人前端公衆號: 「若川視野」,就更好啦。

關於

你好,我是 若川,微信搜索 「若川視野」關注我,專一前端技術分享,一個願景是幫助5年內前端開闊視野走向前列的公衆號。歡迎加我微信 ruochuan12,長期交流學習。
主要有如下系列文章: 學習源碼總體架構系列年度總結JS基礎系列

參考連接

yyx990803/launch-editor

umijs/launch-editor

vuejs/vue-devtools

vue-devtools open-in-editor.md

"Open in editor" button doesn't work in Win 10 with VSCode if installation path contains spaces

react-dev-utils/launchEditor.js

相關文章
相關標籤/搜索