您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么從VSCode看大型IDE技術架構”,在日常操作中,相信很多人在怎么從VSCode看大型IDE技術架構問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么從VSCode看大型IDE技術架構”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
https://code.visualstudio.com/
關鍵詞:
代碼編輯(工具屬性)
跨平臺運行、開源
核心功能:
IntelliSense(代碼提示)、 debugging(代碼調試)、 git(代碼管理) 都是圍繞代碼編輯的核心鏈路
extensions (插件)則肩負著打造開放生態的責任
點評:
對于工具軟件而言,需要內心能想清楚邊界。哪些是自己應該專注去做的,哪些可以外溢到交給第三方擴展來滿足。
2015-04 (4年前) 發布
2015-11 (發布之后半年)開源
2019-05 發布 VSCode Remote Development
團隊負責人:Erich Gamma . JUnit 作者之一,《設計模式》作者之一, Eclipse 架構師。2011 加入微軟,在瑞士蘇黎世組建團隊開發基于 web 技術的編輯器,也就是后來的 monaco-editor。VSCode 開發團隊從 10 來個人開始,早期成員大多有 Eclipse 開發團隊的背景。
Visual Studio Code有哪些工程方面的亮點 維護一個大型開源項目是怎樣的體驗?「Shape Up」 適合中小團隊的一種工作方式
Erich Gamma 在 GOTO 2016 發表了主題為 《The journey of visual studio code: Building an App Using JS/TypeScript, Node, Electron & 100 OSS Components》的演講,詳細講解了這個項目的發展歷程:
沒時間觀看視頻的可以下載完整 PDF
PPT 的第一頁,就是 Erich Gamma 截取自己正式加入微軟之后收到的工作內容描述的郵件:
”探索一種全新的和桌面 IDE 一樣成功的在線開發工具模式“
整個團隊從大致 10 個人開始,混合老中新三代不同水平的程序員,在微軟這個巨無霸的商業公司里面想要落地這樣一個宏大的愿景是不容易的,團隊一開始定下的思路就是像 start up 一樣工作,每月每年都要 ship 東西。
同時他也提出早期會瘋狂的在公司內部尋找落地場景,比如 Visual Studio Online 的在線 Code DIff 頁面,TypeScript 的官網的 Playground 編輯器,OneDrive 代碼文件,Edge Dev Tool 的代碼瀏覽等。
一個重要轉折點是微軟本身發生的巨大變化:
伴隨微軟整個的開放開源跨平臺風潮,Erich Gamma 敏銳的決定將產品從 Browser Based IDE 轉向跨平臺的 Desktop IDE,但仍然使用 Web 技術,于是 electron 粉墨登場,VSCode 開源這些事情接連發生,VSCode 團隊花了六個月使用 Electron 將 Web 編輯器桌面化,又花了六個月將整個 IDE 插件化。
Erich Gamma 在 2017 SpringOne Platform 上有一個 關于 VSCode 的分享,講解了在他開發 Eclipse 的過往經驗基礎上,對 VSCode 進行頂層設計時的諸多思路與決策,其中提到過對于 VSCode 的產品定位:
從圖中可以看出 VSCode 定位是處于編輯器和 IDE 的中間并且偏向輕量編輯器一側的。
VSCode 的核心是“編輯器 + 代碼理解 + 調試“,圍繞這個關鍵路徑做深做透,其他東西非常克制,產品保持輕量與高性能。
點評:
生產力工具類的軟件一定要守住主線,否則很可能會變成不收門票的游樂園牛逼的產品背后一定有牛逼的團隊,比如微軟挖到 Anders Hejlsberg,接連創造了 C# 和 TypeScript,挖到 Erich Gamma,接連誕生了 monaco 和 vscode 這些明珠
上文提到 VSCode 有一個特性是跨平臺,它的跨平臺實質是通過 electron 實現的。所以我們需要先簡單了解下 electron
使用 Web 技術來編寫 UI,用 chrome 瀏覽器內核來運行
使用 NodeJS 來操作文件系統和發起網絡請求
使用 NodeJS C++ Addon 去調用操作系統的 native API
https://electronjs.org/docs/tutorial/application-architecture
1 個主進程:一個 Electron App 只會啟動一個主進程,它會運行 package.json 的 main 字段指定的腳本
N 個渲染進程:主進程代碼可以調用 Chromium API 創建任意多個 web 頁面,而 Chromium 本身是多進程架構,每個 web 頁面都運行在屬于它自己的渲染進程中
進程間通訊:
Render 進程之間的通訊本質上和多個 Web 頁面之間通訊沒有差別,可以使用各種瀏覽器能力如 localStorage
Render 進程與 Main 進程之間也可以通過 API 互相通訊 (ipcRenderer/ipcMain)
點評:
普通 web 頁面無法調用 native api,因此缺少一些能力electron 的 web 頁面所處的 Render 進程可以將任務轉發至運行在 NodeJS 環境的 Main 進程,從而實現 native API這套架構大大擴展了 electron app 相比 web app 的能力豐富度,但同時又保留了 web 快捷流暢的開發體驗,再加上 web 本身的跨平臺優勢,結合起來讓 electron 成為性價比非常高的方案
主進程:VSCode 的入口進程,負責一些類似窗口管理、進程間通信、自動更新等全局任務
渲染進程:負責一個 Web 頁面的渲染
插件宿主進程:每個插件的代碼都會運行在一個獨屬于自己的 NodeJS 環境的宿主進程中,插件不允許訪問 UI
Debug 進程:Debugger 相比普通插件做了特殊化
Search 進程:搜索是一類計算密集型的任務,單開進程保證軟件整體體驗與性能
# 檢出代碼git clone git@github.com:microsoft/vscode.gitcd vscode# 安裝依賴yarn# 啟動 web 版yarn watch && yarn web# 啟動 桌面 版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
下面是整個 VSCode project 的一些頂級的重點文件夾,后文會重點關注 src 與 extensions:
├── build # 構建腳本├── extensions # 內置插件├── scripts # 工具腳本├── out # 產物目錄├── src # 源碼目錄├── test # 測試代碼
VSCode 的代碼架構也是隨著產品的階段演進演進不斷更迭的:
下文會分享一些整個 VScode 源碼組織的一些亮點與特色:
/src/vs:分層和模塊化的 core
/src/vs/base: 通用的公共方法和公共視圖組件
/src/vs/code: VSCode 應用主入口
/src/vs/platform:可被依賴注入的各種純服務
/src/vs/editor: 文本編輯器
/src/vs/workbench:整體視圖框架
/src/typings: 公共基礎類型
/extensions:內置插件
內核里面每一層代碼都會遵守 electron 規范,按不同環境細分文件夾:
common: 公共的 js 方法,在哪里都可以運行的
browser: 只使用瀏覽器 API 的代碼,可以調用 common
node: 只使用 NodeJS API 的代碼,可以調用 common
electron-browser: 使用 electron 渲染線程和瀏覽器 API 的代碼,可以調用 common,browser,node
electron-main: 使用 electron 主線程和 NodeJS API 的代碼,可以調用 common, node
test: 測試代碼
點評:
實際開發中也遇到了類似問題,作為一個 低代碼 + 可視化 的研發平臺,許多功能模塊的實現都需要橫跨編輯態和運行態,如果代碼不加以限制和區分,很容易導致錯誤的依賴關系和預期之外的 bug,因此最終也決定采用 (editor/runtime/common) 類似的隔離架構
可以看到 /src/vs/workbench/contrib 這個目錄下存放著非常多的 VSCode 的小的功能單元:
├── backup├── callHierarchy├── cli├── codeActions├── codeEditor├── comments├── configExporter├── customEditor├── debug├── emmet├──....中間省略無數....├── watermark├── webview└── welcome
Contrib 有一些特點:
Contrib 目錄下的所有代碼不允許依賴任何本文件夾之外的文件
Contrib 主要是使用 Core 暴露的一些擴展點來做事情
每一個 Contrib 如果要對外暴露,將API 在一個出口文件里面導出 eg: contrib/search/common/search.ts
一個 Contrib 如果要和另一個 Contrib 發生調用,不允許使用除了出口 API 文件之外的其它文件
接上一條,即使 Contrib 可以調用另一個 Contrib 的出口 API,也要審慎的考慮,應盡量避免兩個 Contrib 互相依賴
VSCode 開發團隊做這個設計的目的我猜大概是因為重型的工具軟件功能點實在太多,如果這些功能代碼直接采用原始的模塊引用的方式聚合瓶裝起來,是一個自頂向下的架構,對維護性的挑戰比較大。
而采用暴露擴展點的方式,可以將依賴關系反轉,依附于擴展點協議,將核心功能和非核心功能分割開,獨立的小功能的代碼實現可以單獨聚合,降低信息密度與提升維護性。
但是 VSCode Contrib 的具體業務代碼組織其實看起來沒有太多范式,而且這個內核代碼的擴展機制 Contrib 和 VSCode 開放給外界的插件化機制 extension 是有差異的,讀起來十分頭疼。
通過和 CloudIDE 開發同學木農的交流,我得到兩條主要差異性:
extension 每一個都是運行在歸宿于自己的獨立宿主進程,而 contrib 的功能基本是要運行在主進程的
extension 只能依附于 core 開放的擴展點而活,但是 contrib 可以通過依賴注入拿到所有 core 內部實現的 class (雖然官方不推薦)
上一小節提到了 VSCode 的代碼大量使用了依賴注入,這項技術的具體實現細節本文不會展開細講,感興趣的可以閱讀一些好的實現:
團隊揚遠同學寫的 power-di
社區開源的強大的 http://inversify.io
TS 依賴注入常見的實現原理是使用 reflect-metadata 設置與獲取元信息,從而可以實現在運行時拿到本來屬于編輯態的 TypeScript 類型相關元信息,具體來說就是下面這些 API:
Reflect.getMetadata("design:type", , target, key): 獲取 class 屬性類型元信息
Reflect.getMetadata("design:paramtypes", target, key): 獲取 class 方法參數元信息
Reflect.getMetadata("design:returntype", target, key):獲取 class 方法返回值元信息
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey):設置元信息
Reflect.getMetadata(metadataKey, target, propertyKey): 獲取元信息
不過具體到 VSCode 的依賴注入,它沒有使用 reflect-metadata 這一套,而是基于 decorator 去標注元信息,整個實現了一套自己的依賴注入方式,具體可以參考vscode 源碼解析-依賴注入 這篇文章,大致包含如下幾類角色:
Service:服務的實現邏輯
Interface:服務的接口描述
Client:服務使用方
Manger:服務管理器
舉個例子來看,在 /src/core/platform 里面定義了大量 service,其他地方消費者 Client 都可以用依賴注入的方式使用到,偽代碼如下:
class Client { // 構造函數參數注入(依賴注入方式的一種) constructor( // 必選 @IModelService modelService: IModelService, // 可選 @optional(IEditorService) editorService: IEditorService ) { // use services }}// 實例化instantiationService.createInstance(Client);
點評:絕對路徑 import 是一個非常值得學習的技巧,具體的方式是配置 TypeScript compilerOptions.paths
相對路徑 import 對閱讀者的大腦負擔高,依賴當前文件位置上下文信息才能理解重構代碼的時候移動文件位置,相對路徑需要修改本文件的所有 import,絕對路徑不需要
VSCode 和 monaco-editor 都有自己的命令系統,螞蟻 CloudIDE 團隊的同學也曾經對命令系統的優勢做過總結:
傳統的模塊調用是個網狀,不太好找到一個切面來理解或治理:
而命令系統是中心化的,各功能末端變成了扁平化的結構:
上文初步了解了 vscode 的技術架構與源碼組織,手癢的同學估計有點等不及嘗試走一遍 vscode 的啟動流程了。
然后在正式發車之前,我需要給大家一點友情提醒,如果你沒耐心看完下面的 VSCode 的啟動流程,應該知道,人生得過且過 o(╥﹏╥)o
總體來看,VSCode 的啟動代碼真正 show 給我們看了一個復雜的客戶端軟件的代碼會工程化到什么地步,這其中摻雜了大量的基于 TypeScript 的 OOP 式的代碼組織,各種對邊界,宿主環境,上下文的處理,本來簡單的啟動 APP 渲染一個頁面流程變得極其復雜。
下面精簡抽取核心啟動鏈路的文件和方法看一看:
入口:package.json { "main": "./out/main" } : electron 的標準啟動入口
'/out/main.js': 是構建產物的入口文件,它對應源碼 '/src/main.js'
// /src/main.js 的精簡核心鏈路const { app, protocol } = require('electron');app.once('ready', function () { // electron 啟動好之后,調用 vscode 的入口 onReady();});async function onReady() { // 獲取緩存文件目錄地址和語言配置,執行啟動 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/ // 再使用這個 loader 去加載 VSCode 的主入口文件 require('./bootstrap-amd').load('vs/code/electron-main/main');}
// /src/vs/code/electron-main/main.ts 精簡核心鏈路// 初始化主類const code = new CodeMain();// 執行主入口函數code.main();class CodeMain { main() { // vscode 的 class public 入口一般只是空殼,真正的都在 private 邏輯里面 this.startUp(); } private async startup() { // 先創建依賴的初始化 service const [instantiationService, instanceEnvironment] = this.createServices(); // 創建編輯器實例并調用 startUp 方法 return instantiationService.createInstance(CodeApplication).startup(); }}
// /src/vs/code/electron-main/app.tsexport class CodeApplication extends Disposable { async startup(): Promise<void> { // IPC Server const electronIpcServer = new ElectronIPCServer(); // SharedProcess const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); // 創建一大堆依賴的 service // IUpdateService IWindowsMainService IDialogMainService IMenubarService IStorageMainService...... const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient); // 打開一個窗口 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.tsexport class WindowsMainService extends Disposable implements IWindowsMainService { open() { // 執行 open this.doOpen(); } private doOpen() { // 打開瀏覽器窗口 this.openInBrowserWindow(); } private openInBrowserWindow() { // 創建窗口 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.tsexport class CodeWindow extends Disposable implements ICodeWindow { load() { // 調用 electron 的 api 加載一個 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 的工作臺,也就是 workbench return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; }}
因為本文關注的重點并不在真正的代碼編輯器技術而是在調研一下大型軟件的工程化,因此本文只會簡要介紹一下代碼編輯相關的的一些核心技術:
monaco-editor 文本編輯器,非常精深的領域,不展開聊,可以看一些有意思的細節:
Text Buffer 性能優化
MVVM 架構
language server protocol 語言提示, 也是 vscode 的一大創舉:
不再關注 AST 和 Parser,轉而關注 Document 和 Position,從而實現語言無關。
將語言提示變成 CS 架構,核心抽象成當點擊了文檔的第幾行第幾列位置需要 server 作出什么響應的一個簡單模型,基于 JSON RPC 協議傳輸,每個語言都可以基于協議實現通用后端
Debug Adaptor Prototal: 調試協議
點評:
monaco-editor 可以看出專家級人物的領域積累,屬于 VSCode 的核心競爭力language server protocol 和 Debug Adaptor Prototal 的設計就屬于高屋建瓴,可以看出思想層次,把自己的東西做成標準,做出生態,開放共贏
對比幾大 IDE:
Visual Studio / IntelliJ:不需要插件,all in one (不夠開放)
Eclipse: 一切皆插件 (臃腫、慢、不穩定、體驗差)
VSCode:中庸之道
獨立進程:VSCode plugin 代碼運行在只屬于自己的獨立 Extension Host 宿主進程里
邏輯與視圖隔離:插件完全無法訪問 DOM 以及操作 UI,插件只能響應 VSCode Core 暴露的擴展點
視圖擴展能力非常弱:VSCode 有非常穩定的交互與視覺設計,提供給插件的 UI 上的洞(component slot)非常少且穩定
只能使用限制的組件來擴展:VSCode 對視圖擴展的能力限制非常強,洞里面的 UI 是并不能隨意繪制的,只能使用一些官方提供的內置組件,比如 TreeView 之類
vscode 有哪些擴展能力?https://code.visualstudio.com/api/extension-capabilities/overview
點評:
視圖擴展的克制帶來統一的視覺與交互風格,帶來好的用戶體驗,便于建立穩定的用戶心智插件獨立進程,與視圖隔離,保證整體軟件的質量、性能、安全性
https://code.visualstudio.com/docs/getstarted/userinterface
標題欄: Title Bar
活動欄: Activity Bar
側邊欄: Side Bar
面板: Panal
編輯器: Editor
狀態欄: Status Bar
在這個視圖結構里面有哪些可擴展呢?詳見 extending workbench:
插件開發者調用 core 能力時需要引入名為 vscode 的 npm 模塊
import * as vscode from 'vscode';
而實際上這只是一個 vscode.d.ts 類型聲明文件,它聲明了所有插件可用的 API 類型。
這些 API 的具體實現在 src/vs/workbench/api/common/extHost.api.impl.ts createApiFactoryAndRegisterActors
那么具體這些 API 在 plugin 執行上下文是何時注入的呢?其實是在插件 import 語句執行的時候動了手腳。
// /src/vs/workbench/api/common/extHostRequireInterceptor.tsclass 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 注入到了運行環境。
腳手架: https://github.com/Microsoft/vscode-generator-code
官方 demo: https://github.com/Microsoft/vscode-eslint
npm install -g yo generator-codeyo code
一個插件核心就是一個配置文件:Extension Manifest JSON (package.json 里面的一個字段)
https://code.visualstudio.com/api/references/extension-manifest
一些關鍵配置如下:
"main": "./src/extension.js",
// extension.jsconst 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;
onLanguage:包含該語言類型的文件被打開
onLanguage:json
onCommand:某個命令
onCommand:extension.sayHello
onDebug:開始調試
onDebugInitialConfigurations
onDebugResolve
workspaceContains:有匹配規則的文件被打開
workspaceContains:**/.editorconfig
onFileSystem:打開某個特殊協議的文件
onFileSystem:sftp
onView:某個 id 的視圖被顯示
onView:nodeDependencies
onUri:向操作系統注冊的 schema
vscode://vscode.git/init
onWebviewPanel:某種 viewType 的 webview 打開時
onWebviewPanel:catCoding
*:啟動就立即打開
contributes.configuration:本插件有哪些可供用戶配置的選項
contributes.configurationDefaults:覆蓋 vscode 默認的一些編輯器配置
contributes.commands:向 vscode 的命令系統注冊一些可供用戶調用的命令
contributes.menus:擴展菜單
...
到此,關于“怎么從VSCode看大型IDE技術架構”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。