你可能不知道的Vue-devtools打開文件的原理

這是我參與8月更文挑戰的第1天,活動詳情查看:8月更文挑戰html

首先感謝若川大佬組織的此次源碼閱讀,原文地址聽說 99% 的人不知道 vue-devtools 還能直接打開對應組件文件?本文原理揭祕vue

1.學習內容

  • vue-devtools打開對應文件的原理以及vscode的調試方式

2.準備

  • vscode
  • vue-devtools Vue3版本
  • vue3 demo項目

3.Vue-devtools是什麼

對於vue開發者來講。對這個再熟悉不過了,若是不熟悉的參考一下文檔vue-devtools。node

  1. 工具的主要功能有
  2. 查看組件
  3. 查看數據,修改數據以便模擬不一樣數據時候頁面的表現形式
  4. 查看vuex的事件和數據
  5. 打開組件對應的vue文件
  6. 查看render code
  7. 查看vue組件對應的dom

此次主要是探尋第4點的原理linux

4.學習目的

  1. 掌握vscode的調試方式
  2. 掌握vue-devtools打開組件對應的vue文件的原理

5.掌握調試和原理

5.1 原理

原理很簡單,就是使用code命令,webpack

code xxx
複製代碼

在命令後工具中輸入code,若是出現如下提示就是 本機沒有code,須要安裝git

~ % code
zsh: command not found: code
複製代碼

在mac中的vscode中 使用 command + shift + p能夠召喚出安裝窗口,輸入shell 就能夠看到,而後點擊安裝github

5.2 調試

打開準備好的vue3項目,找到package.json文件,能夠看到debugweb

  1. 切換vscode到debug 模塊
  2. 經過package.json中的debug啓動serve

vue-devtools官網能夠看到,支持 Open Components in Editor 的功能來自 launch-editor-middlewarevuex

1.vue-cli 3支持這個功能。開箱即用vue-cli

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

2.也能夠經過webpack單獨引入,具體能夠參考官方文檔 webpack中使用open-in-editor

3.經過官方文檔得知,如何喚起這個功能,是經過express

app.use('/__open-in-editor', openInEditor())
複製代碼

當觸發該功能的時候,能夠在network中看到有個以下的請求。

4.咱們在 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) => {
  if (typeof specifiedEditor === 'function') {
    onErrorCallback = specifiedEditor
    specifiedEditor = undefined
  }

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

  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()
    }
  }
}
複製代碼

從總體來看,整個方法是採用了一個閉包的方式實現一個模塊化。

4~13行,經過判斷傳入的類型,來給變量從新賦值,簡化了操做者的使用,在不想要傳入所有參數的時候傳入最後一個參數。

15行,做用是獲取當前文件的根目錄,若是沒有傳入就使用,執行serve命令的目錄

17行之後就是判斷是否有文件,若是沒有文件就返回,launch-editor-middleware: required query param "file" is missing. 不然就執行launch的方法,該方法來自 launch-editor.

此時咱們能夠在 launch 函數這裏打一個斷點。

而後,在啓動的項目上使用vue-devtools 打開文,不出意外會在此處中止。

在右上角會出現一個調試工具條,第一個是繼續,第二個是跳過當前,第三個是進入到next call中,第四個是跳出。 此時咱們能夠三個參數分別是什麼:

關注一下 onErrorCallback 他的提示

`To specify an editor, specify the EDITOR env variable or ` 
`add "editor" field to your Vue project config. 複製代碼

後面咱們解釋一下是什麼意思,這裏可能有些人會遇到打開不到指定文件的問題。

5.3 launch-editor 源碼解析

先看主函數

function launchEditor (file, specifiedEditor, onErrorCallback) {
  // 首先這裏會解析文件
  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)
	// 看名字,大概是猜想編輯器的意思
  const [editor, ...args] = guessEditor(specifiedEditor)
  if (!editor) {
    onErrorCallback(fileName, null)
    return
  }
  // 使用process 來判斷當前系統
	
  // 一些兼容問題
  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)
  }
	
  // 防止二次綁定,若是已經有一個進程,而且是命令行工具的時候就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')
  }
	
  // 若是是win32 就執行 cmd
  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 {
    // 不然就xxx
    _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)
  })
}

module.exports = launchEditor
複製代碼

分析完上面的主函數,能夠看出,真正執行打開文件的 代碼 只有 這幾行:

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 {
    // 不然就xxx
    _childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
  }
複製代碼

5.4 意外出現

我屁顛屁顛的去瀏覽器上想打開對應的文件。惋惜很遺憾的是,我沒有打開,而是在控制檯把整個文件的內容輸出了。

因而我在

打了一個斷點,因而發現

臥槽,個人editor 怎麼是vi,怎麼不是code

因而我就看在哪裏獲取的 editor,原來是經過guessEditor獲取的,因而在這個函數這裏打個斷點

const [editor, ...args] = guessEditor(specifiedEditor)
複製代碼

guessEditor的源碼以下:

const path = require('path')
const shellQuote = require('shell-quote')
const childProcess = require('child_process')

// Map from full process name to binary that starts the process
// We can't just re-use full process name, because it will spawn a new instance
// of the app every time

// 這些文件裏存了 各個系統編輯器可能安裝的目錄
const COMMON_EDITORS_OSX = require('./editor-info/osx')
const COMMON_EDITORS_LINUX = require('./editor-info/linux')
const COMMON_EDITORS_WIN = require('./editor-info/windows')

module.exports = 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
  // 經過使用 子進程的 執行 ps x 來獲取全部進程,而後拿到key
  
  // 若是經過遍歷 獲取有沒有匹配的,若是有匹配的 就返回對應的編輯器執行打開文件的命令
  
  /* '/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': */
  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]
}

複製代碼

經過調試得知,沒有guess到正確的,命令。 因此執行到了獲取自定義的環境變量這裏。在 vue cli 環境變量這裏說到 如何添加環境變量,經過添加.env[mode]來添加環境變量。

我添加了一個editor = code的環境變量,這時候我又從新試了一下。發現怎麼仍是不行?仍是vi,因而我又試了一下添加VISUAL = code,此次很好。真的能夠了,成功啦!!

5.5 爲何設置EDITOR無效

因而我猜測,vue-cli中設置使用.env,那麼應該是在vue-cli中設置的環境變量。因而繼續使用 調試打開。找到 vue-cli-serve。在@vue/cli-service的bin下面,看到了vue-cli-service.js

經過上面代碼能夠看到,是使用了lib/Service 在lib/Service中,我找到了 loadEnv

loadEnv (mode) {
    const logger = debug('vue:env')
    const basePath = path.resolve(this.context, `.env${mode ? `.${mode}` : ``}`)
    const localPath = `${basePath}.local`

    const load = envPath => {
      try {
        const env = dotenv.config({ path: envPath, debug: process.env.DEBUG })
        dotenvExpand(env)
        logger(envPath, env)
      } catch (err) {
        // only ignore error if file is not found
        if (err.toString().indexOf('ENOENT') < 0) {
          error(err)
        }
      }
    }

    load(localPath)
    load(basePath)

    // by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
    // is production or test. However the value in .env files will take higher
    // priority.
    if (mode) {
      // always set NODE_ENV during tests
      // as that is necessary for tests to not be affected by each other
      const shouldForceDefaultEnv = (
        process.env.VUE_CLI_TEST &&
        !process.env.VUE_CLI_TEST_TESTING_ENV
      )
      const defaultNodeEnv = (mode === 'production' || mode === 'test')
        ? mode
        : 'development'
      if (shouldForceDefaultEnv || process.env.NODE_ENV == null) {
        process.env.NODE_ENV = defaultNodeEnv
      }
      if (shouldForceDefaultEnv || process.env.BABEL_ENV == null) {
        process.env.BABEL_ENV = defaultNodeEnv
      }
    }
  }
複製代碼

loadEnv 主要load兩種 env,一種是local一種是base

local是.env.local中配置的

base是.env.development相似的。具體能夠看官方文檔他們之間的區別。

而後經過 dotenv 這個庫去設置process.env,讀取.env文件也是dotenv提供的功能。

因而繼續打斷點進入 dotenv , 在其中看到了config函數

// Populates process.env from .env file
function config (options /*: ?DotenvConfigOptions */) /*: DotenvConfigOutput */ {
  let dotenvPath = path.resolve(process.cwd(), '.env')
  let encoding /*: string */ = 'utf8'
  let debug = false

  if (options) {
    if (options.path != null) {
      dotenvPath = options.path
    }
    if (options.encoding != null) {
      encoding = options.encoding
    }
    if (options.debug != null) {
      debug = true
    }
  }

  try {
    // specifying an encoding returns a string instead of a buffer
    const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug })

    Object.keys(parsed).forEach(function (key) {
      if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
        process.env[key] = parsed[key]
      } else if (debug) {
        log(`"${key}" is already defined in \`process.env\` and will not be overwritten`)
      }
    })

    return { parsed }
  } catch (e) {
    return { error: e }
  }
}
複製代碼

在,第27行看到了

若是參數在process.env中已經存在,將不會被覆蓋。 破案了,在控制檯輸入 process.env 能夠看到,EDITOR有個默認值,爲vi

個人node版本是v16.4.2,通過查閱官方文檔 process_process_env,得知:

EDITOR/VISUAL
              The user's preferred utility to edit text files.  Any
              string acceptable as a command_string operand to the sh -c
              command shall be valid.
複製代碼

到此終於明白了。

6.總結

  1. 使用閉包實現模塊化
  2. 對於多參數的處理,簡化使用
  3. 瞭解到process, child_process的一些做用
  4. 瞭解到了process.env是如何設置的,如何讀取的

7.思考

經過vue-cli-sever的實現,是否其實能夠將團隊的配置統一化,集成到團隊腳手架中去,以此來爲規範化提供便利,達到開箱即用的效果。

關注我共同進步

相關文章
相關標籤/搜索