從 VSCode 看大型 IDE 技術(shù)架構(gòu)
點(diǎn)擊上方 程序員成長指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí) Node 進(jìn)階交流群
作者:paranoidjk https://zhuanlan.zhihu.com/p/96041706
談起 Web IDE,沒人能繞開 VSCode,它非常流行,同時(shí)又完全開源,總共 350000 行 TypeScript 代碼的巨大工程,使用了 142 個(gè)開源庫。市面上選擇基于 VSCode 去修改定制的 IDE 比比皆是:Weex Studio、白鷺Egret Wing、快應(yīng)用IDE...
我希望從 VSCode 身上看到什么?
-
大型復(fù)雜 GUI 軟件(如 IDE 類)如何組織功能模塊代碼 -
如何使用 Electron 技術(shù)將 Web 軟件桌面化 -
如何在打造插件化開放生態(tài)的同時(shí)保證軟件整體質(zhì)量與性能 -
如何打造一款好用的、流行的工具軟件
一、VSCode 是什么
官方定義
https://code.visualstudio.com/
關(guān)鍵詞:
-
代碼編輯(工具屬性)
-
跨平臺(tái)運(yùn)行、開源
-
核心功能:
-
-
IntelliSense(代碼提示)、 debugging(代碼調(diào)試)、 git(代碼管理) 都是圍繞代碼編輯的核心鏈路 -
extensions (插件)則肩負(fù)著打造開放生態(tài)的責(zé)任
點(diǎn)評:
對于工具軟件而言,需要內(nèi)心能想清楚邊界。
哪些是自己應(yīng)該專注去做的,哪些可以外溢到交給第三方擴(kuò)展來滿足。
發(fā)展歷程
-
2015-04 (4年前) 發(fā)布 -
2015-11 (發(fā)布之后半年)開源 -
2019-05 發(fā)布 VSCode Remote Development
團(tuán)隊(duì)負(fù)責(zé)人:Erich Gamma . JUnit 作者之一,《設(shè)計(jì)模式》作者之一, Eclipse 架構(gòu)師。2011 加入微軟,在瑞士蘇黎世組建團(tuán)隊(duì)開發(fā)基于 web 技術(shù)的編輯器,也就是后來的 monaco-editor。VSCode 開發(fā)團(tuán)隊(duì)從 10 來個(gè)人開始,早期成員大多有 Eclipse 開發(fā)團(tuán)隊(duì)的背景。
Visual Studio Code有哪些工程方面的亮點(diǎn) 維護(hù)一個(gè)大型開源項(xiàng)目是怎樣的體驗(yàn)? 「Shape Up」 適合中小團(tuán)隊(duì)的一種工作方式
Erich Gamma 在 GOTO 2016 發(fā)表了主題為 《The journey of visual studio code: Building an App Using JS/TypeScript, Node, Electron & 100 OSS Components》的演講,詳細(xì)講解了這個(gè)項(xiàng)目的發(fā)展歷程:
沒時(shí)間觀看視頻的同學(xué)可以選擇下載完整 PDF
PPT 的第一頁,就是 Erich Gamma 截取自己正式加入微軟之后收到的工作內(nèi)容描述的郵件:
”探索一種全新的和桌面 IDE 一樣成功的在線開發(fā)工具模式“
整個(gè)團(tuán)隊(duì)從大致 10 個(gè)人開始,混合老中新三代不同水平的程序員,在微軟這個(gè)巨無霸的商業(yè)公司里面想要落地這樣一個(gè)宏大的愿景是不容易的,團(tuán)隊(duì)一開始定下的思路就是像 start up 一樣工作,每月每年都要 ship 東西。
同時(shí)他也提出早期會(huì)瘋狂的在公司內(nèi)部尋找落地場景,比如 Visual Studio Online 的在線 Code DIff 頁面,TypeScript 的官網(wǎng)的 Playground 編輯器,OneDrive 代碼文件,Edge 瀏覽器 Dev Tool 的代碼瀏覽等。
一個(gè)重要轉(zhuǎn)折點(diǎn)是微軟本身發(fā)生的巨大變化:
伴隨微軟整個(gè)的開放開源跨平臺(tái)風(fēng)潮,Erich Gamma 敏銳的決定將產(chǎn)品從 Browser Based IDE 轉(zhuǎn)向跨平臺(tái)的 Desktop IDE,但仍然使用 Web 技術(shù),于是 electron 完美契合,VSCode 團(tuán)隊(duì)花了六個(gè)月使用 Electron 將 Web 編輯器桌面化,又花了六個(gè)月將整個(gè) IDE 插件化,最終 VSCode 成為一個(gè)流行的產(chǎn)品同時(shí)也成為一個(gè)典型的 Electron 客戶端開源項(xiàng)目。
產(chǎn)品定位
Erich Gamma 在 2017 SpringOne Platform 上有一個(gè) 關(guān)于 VSCode 的分享,講解了在他開發(fā) Eclipse 的過往經(jīng)驗(yàn)基礎(chǔ)上,對 VSCode 進(jìn)行頂層設(shè)計(jì)時(shí)的諸多思路與決策,其中提到過對于 VSCode 的產(chǎn)品定位:
從圖中可以看出 VSCode 定位是處于編輯器和 IDE 的中間并且偏向輕量編輯器一側(cè)的。VSCode 的核心是“編輯器 + 代碼理解 + 調(diào)試“,圍繞這個(gè)關(guān)鍵路徑做深做透,其他東西非??酥?,產(chǎn)品保持輕量與高性能。
點(diǎn)評:
生產(chǎn)力工具類的軟件一定要守住主線,否則很可能會(huì)變成不收門票的游樂園
牛逼的產(chǎn)品背后一定有牛逼的團(tuán)隊(duì),比如微軟挖到 Anders Hejlsberg,接連創(chuàng)造了 C# 和 TypeScript
挖到 Erich Gamma,接連誕生了 monaco 和 vscode 這些明珠
二、Electron 是什么
上文提到 VSCode 有一個(gè)特性是跨平臺(tái),它的跨平臺(tái)實(shí)質(zhì)是通過 electron 實(shí)現(xiàn)的。所以我們需要先簡單了解下 electron
官方定義
核心技術(shù)
-
使用 Web 技術(shù)來編寫 UI,用 chrome 瀏覽器內(nèi)核來運(yùn)行 -
使用 NodeJS 來操作文件系統(tǒng)和發(fā)起網(wǎng)絡(luò)請求 -
使用 NodeJS C++ Addon 去調(diào)用操作系統(tǒng)的 native API
應(yīng)用架構(gòu)
https://electronjs.org/docs/tutorial/application-architecture
-
1 個(gè)主進(jìn)程:一個(gè) Electron App 只會(huì)啟動(dòng)一個(gè)主進(jìn)程,它會(huì)運(yùn)行 package.json 的 main 字段指定的腳本 -
N 個(gè)渲染進(jìn)程:主進(jìn)程代碼可以調(diào)用 Chromium API 創(chuàng)建任意多個(gè) web 頁面,而 Chromium 本身是多進(jìn)程架構(gòu),每個(gè) web 頁面都運(yùn)行在屬于它自己的渲染進(jìn)程中
進(jìn)程間通訊:
-
Render 進(jìn)程之間的通訊本質(zhì)上和多個(gè) Web 頁面之間通訊沒有差別,可以使用各種瀏覽器能力如 localStorage -
Render 進(jìn)程與 Main 進(jìn)程之間也可以通過 API 互相通訊 (ipcRenderer/ipcMain)
點(diǎn)評:
普通 web 頁面無法調(diào)用 native api,因此缺少一些能力
electron 的 web 頁面所處的 Render 進(jìn)程可以將任務(wù)轉(zhuǎn)發(fā)至運(yùn)行在 NodeJS 環(huán)境的 Main 進(jìn)程,從而實(shí)現(xiàn) native API
這套架構(gòu)大大擴(kuò)展了 electron app 相比 web app 的能力豐富度
但同時(shí)又保留了 web 快捷流暢的開發(fā)體驗(yàn),再加上 web 本身的跨平臺(tái)優(yōu)勢
結(jié)合起來讓 electron 成為性價(jià)比非常高的方案
三、VSCode 技術(shù)架構(gòu)
多進(jìn)程架構(gòu)
-
主進(jìn)程:VSCode 的入口進(jìn)程,負(fù)責(zé)一些類似窗口管理、進(jìn)程間通信、自動(dòng)更新等全局任務(wù) -
渲染進(jìn)程:負(fù)責(zé)一個(gè) Web 頁面的渲染 -
插件宿主進(jìn)程:每個(gè)插件的代碼都會(huì)運(yùn)行在一個(gè)獨(dú)屬于自己的 NodeJS 環(huán)境的宿主進(jìn)程中,插件不允許訪問 UI -
Debug 進(jìn)程:Debugger 相比普通插件做了特殊化 -
Search 進(jìn)程:搜索是一類計(jì)算密集型的任務(wù),單開進(jìn)程保證軟件整體體驗(yàn)與性能
開發(fā)流程
-
開發(fā)文檔:https://github.com/Microsoft/vscode/wiki/How-to-Contribute -
主倉庫:https://github.com/microsoft/vscode -
其它關(guān)聯(lián)項(xiàng)目:https://github.com/Microsoft/vscode/wiki/Related-Projects
# 檢出代碼
git clone [email protected]:microsoft/vscode.git
cd vscode
# 安裝依賴
yarn
# 啟動(dòng) web 版
yarn watch && yarn web
# 啟動(dòng) 桌面 版
yarn watch && ./scripts/code.sh
# 打包
yarn run gulp vscode-[platform]
yarn run gulp vscode-[platform]-min
# platforms: win32-ia32 | win32-x64 | darwin | linux-ia32 | linux-x64 | linux-arm
源碼組織
https://github.com/microsoft/vscode/wiki/Source-Code-Organization
下面是整個(gè) VSCode project 的一些頂級(jí)的重點(diǎn)文件夾,后文會(huì)重點(diǎn)關(guān)注 src 與 extensions:
├── build # 構(gòu)建腳本
├── extensions # 內(nèi)置插件
├── scripts # 工具腳本
├── out # 產(chǎn)物目錄
├── src # 源碼目錄
├── test # 測試代碼
VSCode 的代碼架構(gòu)也是隨著產(chǎn)品階段演進(jìn)而不斷更迭的:
下文會(huì)分享一些整個(gè) VScode 源碼組織的一些亮點(diǎn)與特色:
1. 隔離內(nèi)核 (src) 與插件 (extensions),內(nèi)核分層模塊化
-
/src/vs:分層和模塊化的 core -
/src/vs/base: 通用的公共方法和公共視圖組件 -
/src/vs/code: VSCode 應(yīng)用主入口 -
/src/vs/platform:可被依賴注入的各種純服務(wù) -
/src/vs/editor: 文本編輯器 -
/src/vs/workbench:整體視圖框架 -
/src/typings: 公共基礎(chǔ)類型 -
/extensions:內(nèi)置插件
2. 每層按環(huán)境隔離
內(nèi)核里面每一層代碼都會(huì)遵守 electron 規(guī)范,按不同環(huán)境細(xì)分文件夾:
-
common: 公共的 js 方法,在哪里都可以運(yùn)行的 -
browser: 只使用瀏覽器 API 的代碼,可以調(diào)用 common -
node: 只使用 NodeJS API 的代碼,可以調(diào)用 common -
electron-browser: 使用 electron 渲染線程和瀏覽器 API 的代碼,可以調(diào)用 common,browser,node -
electron-main: 使用 electron 主線程和 NodeJS API 的代碼,可以調(diào)用 common, node -
test: 測試代碼
點(diǎn)評:
云鳳蝶也遇到了類似問題,作為一個(gè) 低代碼 + 可視化 的研發(fā)平臺(tái),許多功能模塊的實(shí)現(xiàn)都需要橫跨編輯態(tài)和運(yùn)行態(tài)
如果代碼不加以限制和區(qū)分,很容易導(dǎo)致錯(cuò)誤的依賴關(guān)系和預(yù)期之外的 bug
因此云鳳蝶最終也決定采用 (editor/runtime/common) 類似的隔離架構(gòu)
3. 內(nèi)核代碼本身也采用擴(kuò)展機(jī)制: Contrib
可以看到 /src/vs/workbench/contrib 這個(gè)目錄下存放著非常多的 VSCode 的小的功能單元:
├── backup
├── callHierarchy
├── cli
├── codeActions
├── codeEditor
├── comments
├── configExporter
├── customEditor
├── debug
├── emmet
├──....中間省略無數(shù)....
├── watermark
├── webview
└── welcome
Contrib 有一些特點(diǎn):
-
Contrib 目錄下的所有代碼不允許依賴任何本文件夾之外的文件 -
Contrib 主要是使用 Core 暴露的一些擴(kuò)展點(diǎn)來做事情 -
每一個(gè) Contrib 如果要對外暴露,將API 在一個(gè)出口文件里面導(dǎo)出 eg: contrib/search/common/search.ts -
一個(gè) Contrib 如果要和另一個(gè) Contrib 發(fā)生調(diào)用,不允許使用除了出口 API 文件之外的其它文件 -
接上一條,即使 Contrib 事實(shí)上可以調(diào)用另一個(gè) Contrib 的出口 API,也要審慎的考慮并盡量避免兩個(gè) Contrib 互相依賴
VSCode 開發(fā)團(tuán)隊(duì)做這個(gè)設(shè)計(jì)的目的我猜大概是因?yàn)橹匦偷墓ぞ哕浖δ茳c(diǎn)實(shí)在太多,而且非常多的地方都是采用相似的模式去橫向擴(kuò)展,如果這些功能代碼直接采用原始的模塊引用的方式在 core 里面硬編碼聚合拼裝起來,是一個(gè)自頂向下的架構(gòu),對維護(hù)性的挑戰(zhàn)比較大。
而采用暴露擴(kuò)展點(diǎn)的方式,可以將依賴關(guān)系反轉(zhuǎn),依附于擴(kuò)展點(diǎn)協(xié)議,獨(dú)立的小功能的代碼實(shí)現(xiàn)可以單獨(dú)聚合,核心模塊無需硬編碼和集成所有判斷,整體是一個(gè)松散式的架構(gòu),降低了代碼信息密度與提升維護(hù)性,也更好擴(kuò)展。
但是 VSCode Contrib 的具體業(yè)務(wù)代碼組織其實(shí)看起來沒有太多范式,而且這個(gè)內(nèi)核代碼的擴(kuò)展機(jī)制 Contrib 和 VSCode 開放給外界的插件化機(jī)制 extension 是有差異的,讀起來十分頭疼。
通過和兄弟團(tuán)隊(duì) CloudIDE 開發(fā)組的專家交流,我得到兩條主要差異性:
-
extension 每一個(gè)都是運(yùn)行在歸宿于自己的獨(dú)立宿主進(jìn)程,而 contrib 的功能基本是要運(yùn)行在主進(jìn)程的 -
extension 只能依附于 core 開放的擴(kuò)展點(diǎn)而活,但是 contrib 可以通過依賴注入拿到所有 core 內(nèi)部實(shí)現(xiàn)的 class (雖然官方不推薦)
4. 依賴注入
上一小節(jié)提到了 VSCode 的代碼大量使用了依賴注入,這項(xiàng)技術(shù)的具體實(shí)現(xiàn)細(xì)節(jié)本文不會(huì)展開細(xì)講,感興趣的可以閱讀一些好的實(shí)現(xiàn):
-
云鳳蝶團(tuán)隊(duì)同學(xué)寫的 power-di -
社區(qū)開源的強(qiáng)大的 http://inversify.io
TS 依賴注入常見的實(shí)現(xiàn)原理是使用 reflect-metadata 設(shè)置與獲取元信息,從而可以實(shí)現(xiàn)在運(yùn)行時(shí)拿到本來屬于編輯態(tài)的 TypeScript 類型相關(guān)元信息,具體來說就是下面這些 API:
-
Reflect.getMetadata("design:type", , target, key): 獲取 class 屬性類型元信息 -
Reflect.getMetadata("design:paramtypes", target, key): 獲取 class 方法參數(shù)元信息 -
Reflect.getMetadata("design:returntype", target, key):獲取 class 方法返回值元信息 -
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey):設(shè)置元信息 -
Reflect.getMetadata(metadataKey, target, propertyKey): 獲取元信息
不過具體到 VSCode 的依賴注入,它沒有使用 reflect-metadata 這一套,而是基于 decorator 去標(biāo)注元信息,整個(gè)實(shí)現(xiàn)了一套自己的依賴注入方式,具體可以參考vscode 源碼解析-依賴注入 這篇文章,大致包含如下幾類角色:
-
Service:服務(wù)的實(shí)現(xiàn)邏輯 -
Interface:服務(wù)的接口描述 -
Client:服務(wù)使用方 -
Manger:服務(wù)管理器
舉個(gè)例子來看,在 /src/core/platform 里面定義了大量 service,其他地方消費(fèi)者 Client 都可以用依賴注入的方式使用到,偽代碼如下:
class Client {
// 構(gòu)造函數(shù)參數(shù)注入(依賴注入方式的一種)
constructor(
// 必選
@IModelService modelService: IModelService,
// 可選
@optional(IEditorService) editorService: IEditorService
) {
// use services
}
}
// 實(shí)例化
instantiationService.createInstance(Client);
5. 絕對路徑 import
點(diǎn)評:
絕對路徑 import 是一個(gè)非常值得學(xué)習(xí)的技巧,具體的方式是配置 TypeScript compilerOptions.paths
相對路徑 import 對閱讀者的大腦負(fù)擔(dān)高,依賴當(dāng)前文件位置上下文信息才能理解
假設(shè)修改代碼的時(shí)候移動(dòng)文件位置,相對路徑需要修改本文件的所有 import,絕對路徑不需要
6. 命令系統(tǒng)
VSCode 和 monaco-editor 都有自己的命令系統(tǒng),螞蟻 CloudIDE 團(tuán)隊(duì)的同學(xué)也曾經(jīng)對命令系統(tǒng)的優(yōu)勢做過總結(jié):
傳統(tǒng)的模塊調(diào)用是個(gè)網(wǎng)狀,不太好找到一個(gè)切面來理解或治理:
而命令系統(tǒng)是中心化的,各功能末端變成了扁平化的結(jié)構(gòu):
點(diǎn)評:
云鳳蝶編輯器也遇到這類問題,云鳳蝶的自由畫布編輯器有非常多的功能,參見 《打造媲美 Sketch 的自由畫布》
而且很多情況下一個(gè)功能的觸發(fā)方式是多種多樣的,比如大綱樹右鍵菜單觸發(fā),頂部工具欄觸發(fā),畫布右鍵菜單觸發(fā),鍵盤快捷鍵觸發(fā)等
這就要求該功能的實(shí)現(xiàn)函數(shù)能跨區(qū)域在多個(gè)位置調(diào)用,這很容易導(dǎo)致代碼依賴關(guān)系異?;靵y。
而命令系統(tǒng)就是一種解決這個(gè)問題的很好思路
7. TypeScript
啟動(dòng)流程 (TLDR)
上文初步了解了 vscode 的技術(shù)架構(gòu)與源碼組織,手癢的同學(xué)估計(jì)有點(diǎn)等不及嘗試走一遍 vscode 的啟動(dòng)流程了。
然后在正式發(fā)車之前,我需要給大家一點(diǎn)友情提醒,如果你沒耐心看完下面的 VSCode 的啟動(dòng)流程,應(yīng)該知道,人生得過且過 o(╥﹏╥)o
總體來看,VSCode 的啟動(dòng)代碼真正 show 給我們看了一個(gè)復(fù)雜的客戶端軟件的代碼會(huì)工程化到什么地步,這其中摻雜了大量的基于 TypeScript 的 OOP 式的代碼組織,各種對邊界,宿主環(huán)境,上下文的處理,本來簡單的啟動(dòng) APP 渲染一個(gè)頁面流程變得極其復(fù)雜。
下面精簡抽取核心啟動(dòng)鏈路的文件和方法看一看:
-
入口:package.json { "main": "./out/main" } : electron 的標(biāo)準(zhǔn)啟動(dòng)入口 -
'/out/main.js': 是構(gòu)建產(chǎn)物的入口文件,它對應(yīng)源碼 '/src/main.js'
// /src/main.js 的精簡核心鏈路
const { app, protocol } = require('electron');
app.once('ready', function () {
// electron 啟動(dòng)好之后,調(diào)用 vscode 的入口
onReady();
});
async function onReady() {
// 獲取緩存文件目錄地址和語言配置,執(zhí)行啟動(dòng)
const [cachedDataDir, nlsConfig] = await Promise.all([nodeCachedDataDir.ensureExists(), resolveNlsConfiguration()]);
startup(cachedDataDir, nlsConfig);
}
function startup(cachedDataDir, nlsConfig) {
// 先加載 vscode 自己開源的 AMD Loader https://github.com/Microsoft/vscode-loader/
// 再使用這個(gè) loader 去加載 VSCode 的主入口文件
require('./bootstrap-amd').load('vs/code/electron-main/main');
}
// /src/vs/code/electron-main/main.ts 精簡核心鏈路
// 初始化主類
const code = new CodeMain();
// 執(zhí)行主入口函數(shù)
code.main();
class CodeMain {
main() {
// vscode 的 class public 入口一般只是空殼,真正的都在 private 邏輯里面
this.startUp();
}
private async startup() {
// 先創(chuàng)建依賴的初始化 service
const [instantiationService, instanceEnvironment] = this.createServices();
// 創(chuàng)建編輯器實(shí)例并調(diào)用 startUp 方法
return instantiationService.createInstance(CodeApplication).startup();
}
}
// /src/vs/code/electron-main/app.ts
export class CodeApplication extends Disposable {
async startup(): Promise<void> {
// IPC Server
const electronIpcServer = new ElectronIPCServer();
// SharedProcess
const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
// 創(chuàng)建一大堆依賴的 service
// IUpdateService IWindowsMainService IDialogMainService IMenubarService IStorageMainService......
const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient);
// 打開一個(gè)窗口
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
}
private openFirstWindow() {
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
return windowsMainService.open();
}
}
// /src/vs/platform/windows/electron-main/windowsMainService.ts
export class WindowsMainService extends Disposable implements IWindowsMainService {
open() {
// 執(zhí)行 open
this.doOpen();
}
private doOpen() {
// 打開瀏覽器窗口
this.openInBrowserWindow();
}
private openInBrowserWindow() {
// 創(chuàng)建窗口
const createdWindow = window = this.instantiationService.createInstance(CodeWindow, {
state,
extensionDevelopmentPath: configuration.extensionDevelopmentPath,
isExtensionTestHost: !!configuration.extensionTestsPath
});
}
private doOpenInBrowserWindow() {
// 加載頁面
window.load(configuration);
}
}
// /src/vs/code/electron-main/window.ts
export class CodeWindow extends Disposable implements ICodeWindow {
load() {
// 調(diào)用 electron 的 api 加載一個(gè) url 的 html 頁面
this._win.loadURL(this.getUrl(configuration));
}
private getUrl() {
// 獲取要打開的 url
let configUrl = this.doGetUrl(config);
return configUrl;
}
private doGetUrl(config: object): string {
// 終于看到 html 了??!淚流滿面〒▽〒
// 打開 VSCode 的工作臺(tái),也就是 workbench
return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
}
}
代碼編輯器技術(shù)
因?yàn)楸疚年P(guān)注的重點(diǎn)并不在真正的代碼編輯器技術(shù)而是在調(diào)研一下大型軟件的工程化,因此本文只會(huì)簡要介紹一下代碼編輯相關(guān)的的一些核心技術(shù):
-
monaco-editor 文本編輯器,非常精深的領(lǐng)域,不展開聊,可以看一些有意思的細(xì)節(jié):
-
-
Text Buffer 性能優(yōu)化 -
MVVM 架構(gòu) -
language server protocol 語言提示, 也是 vscode 的一大創(chuàng)舉:
-
-
不再關(guān)注 AST 和 Parser,轉(zhuǎn)而關(guān)注 Document 和 Position,從而實(shí)現(xiàn)語言無關(guān)。 -
將語言提示變成 CS 架構(gòu),核心抽象成當(dāng)點(diǎn)擊了文檔的第幾行第幾列位置需要 server 作出什么響應(yīng)的一個(gè)簡單模型,基于 JSON RPC 協(xié)議傳輸,每個(gè)語言都可以基于協(xié)議實(shí)現(xiàn)通用后端
-
Debug Adaptor Prototal: 調(diào)試協(xié)議
點(diǎn)評:
monaco-editor 可以看出專家級(jí)人物的領(lǐng)域積累,屬于 VSCode 的核心競爭力
language server protocol 和 Debug Adaptor Prototal 的設(shè)計(jì)就屬于高屋建瓴
可以看出思想層次,把自己的東西做成標(biāo)準(zhǔn),做出生態(tài),開放共贏
四、VSCode 插件系統(tǒng)
理念差異
對比幾大 IDE:
-
Visual Studio / IntelliJ:不需要插件,all in one (不夠開放) -
Eclipse: 一切皆插件 (臃腫、慢、不穩(wěn)定、體驗(yàn)差) -
VSCode:中庸之道
VSCode 插件的強(qiáng)隔離
-
獨(dú)立進(jìn)程:VSCode plugin 代碼運(yùn)行在只屬于自己的獨(dú)立 Extension Host 宿主進(jìn)程里 -
邏輯與視圖隔離:插件完全無法訪問 DOM 以及操作 UI,插件只能響應(yīng) VSCode Core 暴露的擴(kuò)展點(diǎn) -
視圖擴(kuò)展能力非常弱:VSCode 有非常穩(wěn)定的交互與視覺設(shè)計(jì),提供給插件的 UI 上的洞(component slot)非常少且穩(wěn)定 -
只能使用限制的組件來擴(kuò)展:VSCode 對視圖擴(kuò)展的能力限制非常強(qiáng),洞里面的 UI 是并不能隨意繪制的,只能使用一些官方提供的內(nèi)置組件,比如 TreeView 之類
vscode 有哪些擴(kuò)展能力?https://code.visualstudio.com/api/extension-capabilities/overview
點(diǎn)評:
視圖擴(kuò)展的克制帶來統(tǒng)一的視覺與交互風(fēng)格,帶來好的用戶體驗(yàn),便于建立穩(wěn)定的用戶心智
插件獨(dú)立進(jìn)程,與視圖隔離,保證整體軟件的質(zhì)量、性能、安全性
Workbench 視圖結(jié)構(gòu)
https://code.visualstudio.com/docs/getstarted/userinterface
-
標(biāo)題欄: Title Bar -
活動(dòng)欄: Activity Bar -
側(cè)邊欄: Side Bar -
面板: Panal -
編輯器: Editor -
狀態(tài)欄: Status Bar
在這個(gè)視圖結(jié)構(gòu)里面有哪些可擴(kuò)展呢?詳見 extending workbench:
插件 API 注入
插件開發(fā)者調(diào)用 core 能力時(shí)需要引入名為 vscode 的 npm 模塊
import * as vscode from 'vscode';
而實(shí)際上這只是一個(gè) vscode.d.ts 類型聲明文件,它聲明了所有插件可用的 API 類型。這些 API 的具體實(shí)現(xiàn)在 src/vs/workbench/api/common/extHost.api.impl.ts createApiFactoryAndRegisterActors
那么具體這些 API 在 plugin 執(zhí)行上下文是何時(shí)注入的呢?其實(shí)是在插件 import 語句執(zhí)行的時(shí)候動(dòng)了手腳。
// /src/vs/workbench/api/common/extHostRequireInterceptor.ts
class VSCodeNodeModuleFactory implements INodeModuleFactory {
public load(_request: string, parent: URI): any {
// get extension id from filename and api for extension
const ext = this._extensionPaths.findSubstr(parent.fsPath);
if (ext) {
let apiImpl = this._extApiImpl.get(ExtensionIdentifier.toKey(ext.identifier));
if (!apiImpl) {
apiImpl = this._apiFactory(ext, this._extensionRegistry, this._configProvider);
this._extApiImpl.set(ExtensionIdentifier.toKey(ext.identifier), apiImpl);
}
return apiImpl;
}
// fall back to a default implementation
if (!this._defaultApiImpl) {
let extensionPathsPretty = '';
this._extensionPaths.forEach((value, index) => extensionPathsPretty += `\t${index} -> ${value.identifier.value}\n`);
this._logService.warn(`Could not identify extension for 'vscode' require call from ${parent.fsPath}. These are the extension path mappings: \n${extensionPathsPretty}`);
this._defaultApiImpl = this._apiFactory(nullExtensionDescription, this._extensionRegistry, this._configProvider);
}
return this._defaultApiImpl;
}
}
vscode plugin 的 require 全部被 Microsoft/vscode-loader 劫持了,通過對 require 的 hack 將插件 API 注入到了運(yùn)行環(huán)境。
插件開發(fā)與配置
腳手架:https://github.com/Microsoft/vscode-generator-code 官方 demo: https://github.com/Microsoft/vscode-eslint
npm install -g yo generator-code
yo code
一個(gè)插件核心就是一個(gè)配置文件:Extension Manifest JSON (package.json 里面的一個(gè)字段)
https://code.visualstudio.com/api/references/extension-manifest
一些關(guān)鍵配置如下:
main:主文件入口,比如導(dǎo)出一個(gè) activate 方法,可以接受 ctx 做一些事情
"main": "./src/extension.js",
// extension.js
const vscode = require("vscode");
function activate(context) {
console.log('Congratulations, your extension "helloworld" is now active!');
let disposable = vscode.commands.registerCommand(
"extension.helloWorld",
function() {
vscode.window.showInformationMessage("Hello World!");
}
);
context.subscriptions.push(disposable);
}
exports.activate = activate;
Activation Events 激活時(shí)機(jī)
-
onLanguage:包含該語言類型的文件被打開 -
onLanguage:json -
onCommand:某個(gè)命令 -
onCommand:extension.sayHello -
onDebug:開始調(diào)試 -
onDebugInitialConfigurations -
onDebugResolve -
workspaceContains:有匹配規(guī)則的文件被打開 -
workspaceContains:**/.editorconfig -
onFileSystem:打開某個(gè)特殊協(xié)議的文件 -
onFileSystem:sftp -
onView:某個(gè) id 的視圖被顯示 -
onView:nodeDependencies -
onUri:向操作系統(tǒng)注冊的 schema -
vscode://vscode.git/init -
onWebviewPanel:某種 viewType 的 webview 打開時(shí) -
onWebviewPanel:catCoding -
*:啟動(dòng)就立即打開
Contribution Points 擴(kuò)展點(diǎn)
-
contributes.configuration:本插件有哪些可供用戶配置的選項(xiàng) -
contributes.configurationDefaults:覆蓋 vscode 默認(rèn)的一些編輯器配置 -
contributes.commands:向 vscode 的命令系統(tǒng)注冊一些可供用戶調(diào)用的命令 -
contributes.menus:擴(kuò)展菜單 -
...
五、感想
Eric Raymond 有一本非常知名的著作 《大教堂與集市》,其中提到過一些有意思的觀點(diǎn):
-
健壯的結(jié)構(gòu)遠(yuǎn)比精巧的設(shè)計(jì)來得重要。換句話說,結(jié)構(gòu)是第一位的,功能是第二位的。 -
保持項(xiàng)目的簡單性。設(shè)計(jì)達(dá)到完美的時(shí)候,不是無法再增加?xùn)|西了,而是無法再減少東西了。
VSCode 的一些工程上的優(yōu)秀設(shè)計(jì),比如依賴注入、絕對路徑引用、命令系統(tǒng)對于云鳳蝶來說是可以馬上學(xué)以致用的,而 contrib 與 extension 的擴(kuò)展系統(tǒng),則非一日之功,也并不宜盲目下手。
而事實(shí)上在嘗試打造每一個(gè)開發(fā)者都?jí)粝氲娜f物皆 plugin 式的工具軟件之前,有一些通用的問題需要先冷靜下來思考:
-
用戶核心在操作的資源是什么? -
用戶的關(guān)鍵路徑是什么? -
這個(gè)軟件的整體功能形態(tài),交互與視覺設(shè)計(jì)已經(jīng)穩(wěn)定了嗎? -
內(nèi)核功能區(qū)和第三方擴(kuò)展的功能域之間的界限在哪里? -
哪些環(huán)節(jié)可能會(huì)出現(xiàn)外溢需求需要第三方擴(kuò)展才能被滿足,不適宜官方動(dòng)手做嗎?
對 VSCode 而言:
-
核心操作的資源是文件 -
關(guān)鍵路徑是:打開文件 - 編輯文件 - 保存文件 -
整體功能設(shè)計(jì),交互與視覺設(shè)計(jì)非常穩(wěn)定 -
內(nèi)核是文件管理與代碼編輯,多樣性的編程語言生態(tài),CICD 等衍生研發(fā)鏈路等可能會(huì)出現(xiàn)擴(kuò)展需求
對云鳳蝶而言:
-
核心操作的資源是頁面:(分為視圖和代碼) -
關(guān)鍵路徑是:打開頁面 - 編輯頁面(拖拽視圖,配置屬性,編寫代碼)- 保存頁面 -
整體功能設(shè)計(jì)還在快速迭代中 -
內(nèi)核是頁面的制作,多樣性的資產(chǎn)生態(tài),CI CD 等衍生研發(fā)鏈路等可能會(huì)出現(xiàn)擴(kuò)展需求
圍繞這個(gè)思考,云鳳蝶將持續(xù)吸納優(yōu)秀的思想與架構(gòu),持續(xù)將編輯器的核心功能鏈路打磨通透,底層架構(gòu)搭建穩(wěn)定。
六、相關(guān)資料
-
https://www.youtube.com/watch?v=uLrnQtAq5Ec -
https://www.youtube.com/watch?v=Vs3AGfeuNKU -
https://github.com/JChehe/blog/issues/5 -
https://electronjs.org/docs/tutorial/application-architecture -
https://zhuanlan.zhihu.com/p/54289476 -
http://phosphorjs.github.io/ -
https://developer.egret.com/cn/docs/page/778 -
https://developer.chrome.com/extensions/getstarted -
https://developer.alipay.com/article/8708 -
https://zhaomenghuan.js.org/blog/vscode-custom-development-basic-preparation.html
最后

“分享、點(diǎn)贊、在看” 支持一波
