作者簡介 zqlu 螞蟻金服·資料體驗技術團隊
VS Code 是一款新的工具,它將程式碼編輯器的簡潔和程式開發人員在開發-構建-除錯流程中所需要的工具結合在一起。Code 提供了全面的編輯和除錯功能支援、一個可擴充套件的模型、和與現有工具的輕量化整合。
這是 VSCode Github 倉庫上的介紹,如今,VSCode 的 Github Star 數已達 4.7 萬,VSCode 採用了 Electron,使用的程式碼編輯器名為 Monaco、Monaco 也是 Visual Studio Team Service(Visual Studio Online)使用的程式碼編輯器,在語言上,VSCode 使用了自家的 TypeScript 語言開發。
在開始 VSCode 本身原始碼的解析之前,首先來看 VSCode 依賴的 Electron,理解了 Electron 可以更好的理解 VSCode 的程式碼組織和依賴關係;其次是在 VSCode 原始碼中使用到的的依賴注入模式。
Electron
Electron 是一款可以前端使用 HTML、JavaScript 和 CSS 開發桌面應用程式的框架,關於 Electron 介紹的資料很多。我們可以看看 Electron 官網提供的快速啟動應用程式例項:
其中package.json
定義如下,注意其中的main
欄位和start
指令碼:執行npm start
即啟動這個 Electron 應用:
{
"name": "electron-quick-start",
"version": "1.0.0",
"description": "A minimal Electron application",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"repository": "https://github.com/electron/electron-quick-start",
"keywords": ["Electron", "quick", "start", "tutorial", "demo"],
"author": "GitHub",
"license": "CC0-1.0",
"devDependencies": {
"electron": "~1.7.8"
}
}
複製程式碼
然後看main.js
指令碼:
const electron = require('electron');
// Module to control application life.
const app = electron.app;
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow;
const path = require('path');
const url = require('url');
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({ width: 800, height: 600 });
// and load the index.html of the app.
mainWindow.loadURL(
url.format({
pathname: path.join(__dirname, 'index.html'),
protocol: 'file:',
slashes: true,
}),
);
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', function() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', function() {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
複製程式碼
可以看到,main
指令碼主要定義了應用對幾個事件的處理函式,其中對ready
事件的處理函式中,建立了一個BrowseWindow
物件,並且去載入index.html
頁面。
在index.html
中,又通過 script 標籤去載入了renderer.js
指令碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<!-- All of the Node.js APIs are available in this renderer process. -->
We are using Node.js <script>document.write(process.versions.node)</script>,
Chromium <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
<script>
// You can also require other files to run in this process
require('./renderer.js')
</script>
</body>
</html>
複製程式碼
到此,Electron 的快速啟動例項應用程式就完成了,執行npm start
後,就可以看到介面上展示index.html
中的內容了。
我們首先需要了解的是在上面 Electron 應用中會遇到的兩種程式型別,以及它們的區別,它們稱為主程式和渲染程式。
首先看主程式和渲染程式的定義:
在 Electron 應用中,
package.json
中的main
指令碼執行所在的程式被成為主程式,在主程式中執行的指令碼通過建立 web 頁面來展示使用者介面。一個 Electron 應用總是有且只有一個主程式。由於 Electron 使用了 Chromium 來展示 web 頁面,所以在 Chromium 的多程式架構也被使用到。每個 Electron 中的 web 頁面執行在它自己的渲染程式中。在普通的瀏覽器中,web 頁面通常在一個沙盒環境中執行,不被允許去接觸原生的資源。然而 Electron 的使用者在 Node.js 的 API 支援下可以在頁面中和作業系統進行一些底層互動。
主程式和渲染程式之間的區別:
主程式使用
BrowseWindow
例項建立頁面,每個BrowseWindow
例項都在自己的渲染程式裡執行頁面。當一個BrowseWindow
例項被銷燬後,相應的渲染程式也會被終止。主程式管理所有的 web 頁面和它們對應的渲染程式。每個渲染程式都是獨立的,它至關心它所執行的 web 頁面。
對開發者來說,比較關心的是主程式和渲染程式中的指令碼分別可以使用哪些 API。
首先 Electron API 提供了豐富的 API,其中一些 API 只能在主程式中使用,又有一些 API 只能在渲染程式中使用,而有一些主程式和渲染程式都可以使用。
然後對於 Node.js 的 API,以及第三方 npm 包,主程式和渲染程式都可以直接使用。
最後,由於渲染程式執行在 chromium 的頁面中,所有還可以是有瀏覽器提供的 API,如 DOM 操作 API 等。
API | 主程式 | 渲染程式 |
---|---|---|
Electron API | 部分 | 部分 |
Node.js API/module | 是 | 是 |
瀏覽器 API | 否 | 是 |
在瞭解了 Electron 之後,後面我們會看到 VSCode 中哪些程式碼是執行在主程式中,哪些程式碼是執行在渲染程式中。
依賴注入
依賴注入作為一個設計模式,前端開發者可能使用的不多,但在 VSCode 的原始碼中隨處可見,所以這裡簡單介紹下。首先看依賴注入的定義:
在軟體工程中,依賴注入是一種為一類物件提供依賴的物件的設計模式。被依賴的物件稱為
Service
,注入則是指將被依賴的物件Service
傳遞給使用服務的物件(稱為Client
),從而客戶Client
不需要主動去建立(new)依賴的服務Service
,也不需要通過工廠模式去獲取依賴的服務Service
。
在典型的依賴注入模式中,存在以下幾類角色:
- 被依賴和使用的物件,即
Service
- 使用服務的客戶物件,即
Client
- 客戶使用服務的介面定義,
Interface
- 注入器:負責建立服務物件並提供給 Client,通常也負責建立客戶物件
而依賴注入的實現有幾種形態,其中常見的一種的建構函式式的依賴注入:Client 在其建構函式的引數中申明所依賴的 Service,如下 TypeScript 程式碼所示:
class Client {
constructor(serviceA: ServiceA, serviceB: ServiceB) {
// 注入器在建立Client的時候,將依賴的 Service 通過建構函式引數傳遞給 Client
// Client此時即可將依賴的服務儲存在自身狀態內:
this.serviceA = serviceA;
this.serviceB = serviceB;
}
}
複製程式碼
通過這種模式,Client 在使用的時候不需要去自己構造需要的 Service 物件,這樣的好處之一就就是將物件的構造和行為分離,在引入介面後,Client 和 Service 的依賴關係只需要介面來定義,Client 在建構函式引數中主需要什麼依賴的服務介面,結合注入器,能給客戶物件更多的靈活性和解耦。
最後,在 VSCode 的原始碼中,大部分基礎功能是被實現為服務物件,一個服務的定義分為兩部分:
- 服務的介面
- 服務的標識:通過 TypeScript 中的裝飾器實現
Client 在申明依賴的 Service 時,同樣時在建構函式引數中申明,例項如下:
class Client {
constructor(
@IModelService modelService: IModelService,
@optional(IEditorService) editorService: IEditorService,
) {
// ...
this.modelService = modelService;
this.editorService = editorService;
}
}
複製程式碼
這裡,申明的客戶物件Client
,所依賴的Service
有IModelService
和IEditorService
,其中裝飾器@IModelService
是 ModelService 的標識,後面的IModelService
只是 TypeScript 中的介面定義;@optional(IEditorService)
是 EditorService 的標識,同時通過optional
的裝飾申明為可選的依賴。
最後,在程式碼是實際使用Client
物件時,需要通過注入器提供的instantiationService
來例項化的到 Client 的例項:
const myClient = instantiationService.createInstance(Client);
複製程式碼
原始碼組織
在瞭解了 Electron 和依賴注入之後,我們就可以來看看 VSCode 自身的原始碼組織了。
VSCode Core
首先 VSCode 整體由其核心core
和內建的擴充套件Extensions
組成,core
是實現了基本的程式碼編輯器、和 VSCode 桌面應用程式,即 VSCode workbench;同時提供擴充套件 API,允許內建的擴充套件和第三方開發的擴充套件程式來擴充套件 VSCode Core 的能力。
首先看Core
的原始碼組織,Core
的原始碼分為下列目錄:
src/vs/base
: 定義基礎的工具方法和基礎的 DOM UI 控制元件src/vs/code
: Monaco Editor 程式碼編輯器:其中包含單獨打包釋出的 Monaco Editor 和只能在 VSCode 的使用的部分src/vs/platform
: 依賴注入的實現和 VSCode 使用的基礎服務 Servicessrc/vs/workbench
: VSCode 桌面應用程式工作臺的實現src/vs/code
: VSCode Electron 應用的入口,包括 Electron 的主程式指令碼入口
其次,由於 VSCode 依賴 Electron,而在上述我們提到了 Electron 存在著主程式和渲染程式,而它們能使用的 API 有所不到,所以 VSCode Core
中每個目錄的組織也按照它們能使用的 API 來組織安排。在 Core 下的每個子目錄下,按照程式碼所執行的目標環境分為以下幾類:
common
: 只使用 JavaScript API 的原始碼,可能執行在任何環境browser
: 需要使用瀏覽器提供的 API 的原始碼,如 DOM 操作等node
: 需要使用Node.js
提供的 API 的原始碼electron-browser
: 需要使用 Electron 渲染程式 API 的原始碼electron-main
: 需要使用 Electron 主程式 API 的原始碼
按照上述規則,即src/vs/workbench/browser
中的原始碼只能使用基本的 JavaScript API 和瀏覽器提供的 API,而src/vs/workbench/electron-browser
中的原始碼則可以使用 JavaScript API,瀏覽器提供的 API、Node.js
提供的 API、和 Electron 渲染程式中的 API。
VSCode Extensions
在 VSCode 程式碼倉庫中,出了上述的src/vs
的Core
之外,還有一大塊即 VSCode 內建的擴充套件,它們原始碼位於extensions
內。
首先 VSCode 作為程式碼編輯器,但與各種程式碼編輯的功能如語法高亮、補全提示、驗證等都時有擴充套件實現的。所以在 VSCode 的內建擴充套件內,一大部分都是各種程式語言的支援擴充套件,如:extensions\html
、extensions\javascript
、extensions\cpp
等等,大部分語言擴充套件中都會出現如.tmTheme
、.tmLanguage
等 TextMate 的語法定義。
還有一類內建的擴充套件是 VSCode 主體擴充套件,如 VSCode 預設主體extensions/theme-defaults
等。
參考
本篇簡單了介紹了在 VSCode 原始碼閱讀之前的需要的一些準備工作,主要是 Electron 應用的結構、依賴注入設計模式、和 VSCode 原始碼和大體組織情況。
下篇預告:從命令列輸入code
命令到出現 VSCode 桌面應用程式,VSCode 的程式碼是的執行流程是怎樣的?
對我們團隊感興趣的可以關注專欄,關注github或者傳送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~