大家好,我是若川。這是我上周組織的源碼共讀紀年小姐姐的筆記,寫得很好。所以分享給大家。歡迎加我微信 ruochuan12,進源碼共讀群。其他更多人的筆記可以閱讀原文查看。
川哥的源碼解讀文章:據說 99% 的人不知道 vue-devtools 還能直接打開對應組件文件?本文原理揭秘
???? 非常感謝川哥組織的源碼閱讀活動
1. 解讀前的準備
1.1 粗略閱讀一遍川哥的源碼解讀文章,弄清楚文章的主旨內容:探究 vue-devtools「在編輯器中打開組件」功能實現原理**,它的核心實現就是 launch-editor**。
1.2 明確自己到底要學習什么:
1)學習調試源碼的方法;
2)在調試過程中探究 launch-editor 源碼是如何實現在編輯器打開對應的文件;
目標:跟著川哥的文章完整走完一遍調試的流程,并對外輸出記錄文檔。
1.3 資源:
下載川哥的源碼:
git clone https://github.com/lxchuan12/open-in-editor.git
,進入 vue3-project 目錄,安裝依賴yarn install
安裝 vue-devtools 谷歌擴展:翻墻去應用商店下載安裝即可(下載 6.0.0 beta 版)
了解 launch-editor[1]:主要功能是在編輯器中打開帶有行號的文件
2. 開始學習,淺嘗輒止
上述的準備工作搞完之后,我們動手操作一下。
2.1 開始動手
我使用的編輯器是 VSCode。
打開 vue3-project 目錄的 package.json,點擊調試,選擇 serve。這一步操作,使得我們以 debug 的形式,運行了 vue-cli-service serve 這個命令。

跟著文章實現到這里的時候,我有點懵逼,因為我不知道接下來為什么突然要搜索【launch-editor-middleware】這個庫。
直到我再次通讀一遍文章,發現川哥前面有提到 vue-devtools 的 Open component in editor[2] 這個文檔,這個文檔里面描述了引用了【launch-editor-middleware】這個庫來實現打開文檔的功能。而我之前先入為主地以為,這期是解讀 vue-devtools 的源碼,其實這只是解讀實現打開文檔功能的源碼而已。
理解了這一層,我們可以直接搜項目里(包括 node_modules)里的【launch-editor-middleware】關鍵字,就可以找到這個庫的源碼位置了。
2.2 調試之旅
調試的流程就是打斷點,點擊調試的流程面板,經過不斷調試,觀察數據的變化。
下圖【launch-editor-middleware】的源碼,在這份源碼中我們能很輕易地分析出,最終運行的是 launch 函數,我們可以這這里打一個斷點,然后進入到【launch-editor】的源碼,實際運行的是 launchEditor 函數。


粗略看一遍 launchEditor 函數,發現它實際上是做了四件事:
獲取 fileName,lineNumber,columnNumber
異常處理:是否存在文件,onErrorCallback,是否存在 editor
猜測當前正在使用的編輯器:guessEditor
使用 child_process.spwan 異步打開一個子進程模塊,它調起了 cmd.exe 工具打開我們的編輯器,并打開了文件(args 就是文件的參數)
看完了這個函數,其實大概實現的原理也就出來了,核心代碼如下:
if?(process.platform?===?'win32')?{_childProcess?=?childProcess.spawn('cmd.exe',['/C',?editor].concat(args),{?stdio:?'inherit'?})
}?else?{_childProcess?=?childProcess.spawn(editor,?args,?{?stdio:?'inherit'?})
}
但我們肯定還有很多疑惑,比如:
在瀏覽器控制臺點擊按鈕,編輯器是怎么接收到它的請求信息呢?
用到了哪些 API/編程技巧?
這個功能實現如果讓我們來實現,是怎么實現呢(復述思路)?
3. 動手操作,深入實踐
在前面的拆解中,雖然很多地方看似看懂了,但又沒完全懂,那我們來解答一下在看源碼的時候的疑問:
3.1 編輯器如何接收到瀏覽器的請求信息
點擊 vue-devtools 的按鈕時,我們會發現它發送了一個請求:http://localhost:8080/__open-in-editor?file=src/components/HelloWorld.vue

那編輯器是如何接收到這個請求呢?搜索【launch-editor-middleware】關鍵字,我們會發現,在 @vue/cli-service 的 serve.js 文件中,使用了 app.use("/__open-in-editor"),用過 express 的小伙伴會比較熟悉,這是express 引入中間件的用法。當瀏覽器發送 http://localhost:8080/__open-in-editor?file=src/components/HelloWorld.vue 這個請求的時候,就進入到下面這個代碼了。
//?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`)))//?省略若干代碼...
}
3.2 用到了哪些 API/編程技巧
3.2.1 函數的重載
在【launch-editor-middleware】的入口函數這里,使用了函數重載的寫法,這種寫法在很多源碼中都很常見,目的是方便用戶調用時傳參,針對不定量的參數對應不同的操作內容。
//?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)?{//?省略?...}
}
3.2.2 裝飾器模式
這段代碼 wrapErrorCallback 先執行其他代碼,再去執行 onErrorCallback,這種包裹函數的形式在很多源碼里都也很常見,可以理解為一個裝飾器,把 onErrorCallback 包裝了起來,對原函數進行了增強。
這也是設計模式中的裝飾器設計模式:
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)}
}onErrorCallback?=?wrapErrorCallback(onErrorCallback)
3.2.3 apply
apply 語法:func.apply(thisArg, [argsArray]),也經常在源碼中可以看到。這里使用 apply 是把 extraArgs 作為 push 方法的 arguments 傳進去。
if?(lineNumber)?{//?getArgumentsForPosition?返回一個數組const?extraArgs?=?getArgumentsForPosition(editor,?fileName,?lineNumber,?columnNumber)//?將?extraArgs?參數?push?到?args?里args.push.apply(args,?extraArgs)
}?else?{args.push(fileName)
}
3.2.4 child_process
child_process 是 Node.js 的一個模塊,它提供了衍生子進程的能力,默認情況下,會在父 Node.js 進程和衍生的子進程之間建立 stdin、stdout 和 stderr 的管道。
3.2.5 process.platform
用于標識運行 Node.js 進程的操作系統平臺,返回字符串,目前可能的值有:?"aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32"
3.3 如何實現(復述思路)
瀏覽器與編輯器的通訊:借助 Node.js 進程,與瀏覽器發生通訊
瀏覽器將需要打開的文件路徑通過參數傳遞給編輯器
判斷操作系統平臺和所使用的編輯器(每個平臺的命令行程序不一樣,每個編輯器的環境變量也不一樣)
借助 Node 調起 cmd.exe 工具打開我們的編輯器,打開對應路徑的文件
//?偽代碼
app.use("__open-in-editor",?handleLaunchEditor)function?handleLaunchEditor(filePath)?{const?platform?=?process.platformconst?editor?=?guessEditor()childProcess.spawn(editor,?fileArgs,?{?stdio:?'inherit'?})
}
4. 感想
編碼能力:通過解讀 launch-editor 源碼,學習/重溫了【函數的重載】【裝飾器模式】【apply 使用方法】,源碼的組織結構也非常值得我們學習,比如里面很多功能代碼都單獨封裝起來,封裝成函數或者模塊,使得整個源碼的結構非常清晰,核心通俗易懂,易于解讀和維護。(這也可以理解為自頂向下的編程方法)
拓展視野:源碼中包含了很多與 Node.js 相關的方法,有很多都是我不熟悉的,在解讀源碼的過程也是我學習 Node.js 的過程。
工作中可能會用到:
開發 VSCode 插件與外界通訊可借助 Node.js 進程
裝飾器模式的應用
判斷操作系統平臺
參考資料
[1]
launch-editor: https://github.com/yyx990803/launch-editor
[2]Open component in editor: https://github.com/vuejs/devtools/blob/legacy/docs/open-in-editor.md
最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。
推薦閱讀
我在阿里招前端,該怎么幫你(可進面試群)
我讀源碼的經歷
面對 this 指向丟失,尤雨溪在 Vuex 源碼中是怎么處理的
老姚淺談:怎么學JavaScript?
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,活躍在知乎@若川,掘金@若川。致力于分享前端開發經驗,愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~