1. 前言
你好,我是若川[1],微信搜索「若川視野」關注我,專注前端技術分享,一個愿景是幫助5年內前端開闊視野走向前列的公眾號。歡迎加我微信
ruochuan12
,長期交流學習。
這是
學習源碼整體架構系列
之 launch-editor 源碼(第九篇)。學習源碼整體架構系列文章(有哪些必看的JS庫):jQuery、underscore、lodash、sentry、vuex、axios、koa、redux。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。下一篇應該是《學習 Vuex 4 源碼整體架構,深入理解其原理及provide/inject原理》。
本文倉庫地址[2]:
git clone https://github.com/lxchuan12/open-in-editor.git
,本文最佳閱讀方式,克隆倉庫自己動手調試,容易吸收消化。
要是有人說到怎么讀源碼,正在讀文章的你能推薦我的源碼系列文章,那真是無以為報啊。
我的文章盡量寫得讓想看源碼又不知道怎么看的讀者能看懂。我都是推薦使用搭建環境斷點調試源碼學習,哪里不會點哪里,邊調試邊看,而不是硬看。正所謂:授人與魚不如授人予漁。
閱讀本文后你將學到:
如何解決該功能報錯問題
如何調試學習源碼
launch-editor
等實現原理
1.1 短時間找不到頁面對應源文件的場景
不知道你們有沒有碰到這樣的場景,打開你自己(或者你同事)開發的頁面,卻短時間難以找到對應的源文件。
這時你可能會想要是能有點擊頁面按鈕自動用編輯器打開對應文件的功能,那該多好啊。
而vue-devtools
提供了這樣的功能,也許你不知道。我覺得很大一部分人都不知道,因為感覺很多人都不常用vue-devtools
。

你也許會問,我不用vue
,我用react
有沒有類似功能啊,有啊,請看react-dev-inspector[3]。
本文就是根據學習尤大寫的 launch-editor[4] 源碼,本著知其然,知其所以然的宗旨,探究 vue-devtools
「在編輯器中打開組件」功能實現原理。
1.2 一句話簡述其原理
code?path/to/file
一句話簡述原理:利用nodejs
中的child_process
,執行了類似code path/to/file
命令,于是對應編輯器就打開了相應的文件,而對應的編輯器則是通過在進程中執行ps x
(Window
則用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[5]文檔
Open component in editor
To enable this feature, follow this guide[6].
這篇指南中寫了在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[7] package to setup an HTTP route with the/__open-in-editor
path. It will receive file as an URL variable.
查看更多可以看這篇指南[8]。
3. 環境準備工作
熟悉我的讀者,都知道我都是推薦調試看源碼的,正所謂:哪里不會點哪里。而且調試一般都寫得很詳細,是希望能幫助到一部分人知道如何看源碼。于是我特意新建一個倉庫open-in-editor[9] 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
#?這里選擇的是vue3、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
按鈕。

選擇第一項,serve vue-cli-service serve
。
我們來搜索下'launch-editor-middleware'
這個中間件,一般來說搜索不到node_modules
下的文件,需要設置下。當然也有個簡單做法。就是「排除的文件」右側旁邊有個設置圖標「使用“排查設置”與“忽略文件”」,點擊下。
其他的就不贅述了。可以看這篇知乎回答:vscode怎么設置可以搜索包含node_modules中的文件?[10]
這時就搜到了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-overlayapp.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
,不出意外就會打開該組件啦。

接著我們在launchEditorMiddleware
的具體實現。
5. launch-editor-middleware
看源碼時,先看調試截圖。

在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?=?specifiedEditorspecifiedEditor?=?undefined}//?如果第二個參數是函數,同樣把它賦值給錯誤回調函數//?這里傳遞過來的是undefinedif?(typeof?srcRoot?===?'function')?{onErrorCallback?=?srcRootsrcRoot?=?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?=?500res.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?}?=?parsedconst?{?lineNumber,?columnNumber?}?=?parsed//?判斷文件是否存在,不存在,直接返回。if?(!fs.existsSync(fileName))?{return}//?所以和?onErrorCallback?切換下,把它賦值給錯誤回調函數if?(typeof?specifiedEditor?===?'function')?{onErrorCallback?=?specifiedEditorspecifiedEditor?=?undefined}//?包裹一層函數onErrorCallback?=?wrapErrorCallback(onErrorCallback)//?猜測當前進程運行的是哪個編輯器const?[editor,?...args]?=?guessEditor(specifiedEditor)if?(!editor)?{onErrorCallback(fileName,?null)return}//?省略剩余部分,后文再講述...
}
6.1 wrapErrorCallback 包裹錯誤函數回調
onErrorCallback?=?wrapErrorCallback(onErrorCallback)
這段的代碼,我相信讀者朋友能看懂,我單獨拿出來講述,主要是因為這種包裹函數的形式在很多源碼里都很常見。這里也就是文章開頭終端錯誤圖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 猜測當前正在使用的編輯器
這個函數主要做了如下四件事情:
如果具體指明了編輯器,則解析下返回。
找出當前進程中哪一個編輯器正在運行。
macOS
和Linux
用ps x
命令windows
則用Get-Process
命令如果都沒找到就用
process.env.VISUAL
或者process.env.EDITOR
。這就是為啥開頭錯誤提示可以使用環境變量指定編輯器的原因。最后還是沒有找到就返回
[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/Microsoftif?(specifiedEditor)?{return?shellQuote.parse(specifiedEditor)}//?We?can?find?out?which?editor?is?currently?running?by://?`ps?x`?on?macOS?and?Linux//?`Get-Process`?on?Windowstry?{//??省略...}?catch?(error)?{//?Ignore...}//?Last?resort,?use?old?skool?env?varsif?(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?=?nullif?(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[11]。
7.1 一句話簡述其原理
我們回顧下開頭的原理內容。
code?path/to/file
一句話簡述原理:利用nodejs
中的child_process
,執行了類似code path/to/file
命令,于是對應編輯器就打開了相應的文件,而對應的編輯器則是通過在進程中執行ps x
(Window
則用Get-Process
)命令來查找的,當然也可以自己指定編輯器。
最后還能做什么呢。
可以再看看 umijs/launch-editor[12] 和 react-dev-utils/launchEditor.js[13] 。他們的代碼幾乎類似。
也可以利用Node.js
做一些提高開發效率等工作,同時可以學習child_process
等模塊。
參考資料
[1]
若川: https://lxchuan12.gitee.io
[2]本文倉庫地址: https://github.com/lxchuan12/open-in-editor.git
其他引用略,具體可以點擊閱讀原文查看。
最近組建了一個江西人的前端交流群,如果你也是江西人可以加我微信 ruochuan12 拉你進群。
·················?若川出品?·················
今日話題
五一結束啦,我在假期最后一天耗時一天把這篇文章寫完,這是今年第一篇技術文,慚愧慚愧。接下來會發布第二篇《學習 Vuex 4 源碼整體架構,深入理解其原理及provide/inject原理》。歡迎在下方留言~? 歡迎分享、收藏、點贊、在看我的公眾號文章~
一個愿景是幫助5年內前端人走向前列的公眾號
可加我個人微信?ruochuan12,長期交流學習
推薦閱讀
我在阿里招前端,我該怎么幫你?(現在還能加我進模擬面試群)
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
點擊上方卡片關注我、加個星標,或者查看源碼等系列文章。
學習源碼整體架構系列、年度總結、JS基礎系列