作者:王子亭
Atom 是 GitHub 在 2014 年發(fā)布的一款基于 Web 技術(shù)構(gòu)建的文本編輯器,我從 2014 年末開始使用 Atom 完成我的全部工作,對 Atom 很是喜愛,也創(chuàng)建了 Atom 的中文社區(qū)、翻譯了一部分 Atom 的文檔和博客。今天我將著重介紹 Atom 背后的故事,包括底層的 Electron、如何對 Atom 進(jìn)行定制、Atom 的插件化機(jī)制、Atom 在啟動(dòng)速度和渲染性能方面的優(yōu)化等。
GitHub 的聯(lián)合創(chuàng)始人之一 Chris Wanstrath 自 2008 年便有一個(gè)想法,希望使用 Web 技術(shù)構(gòu)建一個(gè)像 Emacs 一樣賦予開發(fā)者充分定制的能力的編輯器。但當(dāng)時(shí)他忙于他的主要工作 —— GitHub,所以 Atom 一度被擱置,直到 2011 年,GitHub 添加了一個(gè)使用 Ace 實(shí)現(xiàn)的在線編輯代碼的功能,這重新點(diǎn)燃了 Chris Wanstrath 對 Atom 的熱情,于是他開始在業(yè)余時(shí)間開發(fā) Atom。在 2011 年末,Atom 成為了 GitHub 的正式項(xiàng)目,也有了一些全職的同事加入,最后在 2014 年初 Atom 正式發(fā)布了,并于 2015 年發(fā)布了 1.0 版本。
所以 Atom 有什么亮點(diǎn)呢,我總結(jié)了這樣幾點(diǎn):
- 像 Sublime Text 一樣開箱即用
- 像 Emacs 一樣允許開發(fā)者充分地定制
- 基于 JavaScript 和 Web 技術(shù)構(gòu)建
- 開源且擁有一個(gè)活躍的社區(qū)
雖然 Sublime Text 之類的編輯器已經(jīng)足夠好用了,第一天學(xué)習(xí)編程的新手也可以快速上手,但它們僅提供了非常有限的拓展性;而在另外一個(gè)極端,像 Vim 和Emacs 這樣的編輯器雖然賦予了開發(fā)者充分定制的能力,但卻有著陡峭的學(xué)習(xí)曲線。雖然 Atom 的初衷可能并非如此,但 Atom 的確做到了兼顧易用性和可拓展性,在這兩種極端中間找到了一個(gè)平衡。
就像 Java 開發(fā)者會(huì)使用基于 Java 構(gòu)建的 Eclipse 或 IntelliJ IDEA、Clojure 開發(fā)者會(huì)使用基于 Lisp 的 Emacs 一樣,作為 JavaScript 開發(fā)者我們也需要一款基于 JavaScript 和 Web 基礎(chǔ)構(gòu)建的編輯器。我覺得用自己熟悉的語言和技術(shù)去改造工具,并從工具的實(shí)現(xiàn)中得到啟發(fā)這是很重要的一點(diǎn),就像我后面介紹的那樣,作為 Web 或 Node.js 開發(fā)者,我們都可以從了解 Atom 的設(shè)計(jì)和實(shí)現(xiàn)中受益。
Vim 和 Emacs 之所以能在過去幾十年始終保持活力,很大程度上是因?yàn)橹挥小搁_源」才能構(gòu)建一個(gè)持久的、具有生命力的社區(qū)。GitHub 當(dāng)然也意識(shí)到了這一點(diǎn),所以 Atom 同樣是開源的,并且它現(xiàn)在已經(jīng)有了一個(gè)活躍的社區(qū)。
Electron
Atom 是基于 Electron,這是一個(gè)幫助開發(fā)者使用 Web 技術(shù)構(gòu)建跨平臺(tái)的桌面應(yīng)用的工具,實(shí)際上 Electron 原本叫 Atom Shell,是專門為 Atom 設(shè)計(jì)的,后來才成為了一個(gè)獨(dú)立的項(xiàng)目。Electron 將 Chromium 和 Node.js 結(jié)合到了一起:Chromium 提供了渲染頁面和響應(yīng)用戶交互的能力,而 Node.js 提供了訪問本地文件系統(tǒng)和網(wǎng)絡(luò)的能力,也可以使用 NPM 上的幾十萬個(gè)第三方包。在此基礎(chǔ)之上,Electron 還提供了 Mac、Windows、Linux 三個(gè)平臺(tái)上的一些原生 API,例如全局快捷鍵、文件選擇框、托盤圖標(biāo)和通知、剪貼板、菜單欄等等。
基于 Electron 的應(yīng)用往往會(huì)有很大的體積,即使在打包壓縮之后通常也有 40MiB,這是因?yàn)?Electron 捆綁了整個(gè) Chromium 和 Node.js。但這也意味著你的應(yīng)用運(yùn)行在一個(gè)十分確定的環(huán)境下 —— 你總是可以使用最新版本 Chromium 和 Node.js 中的特性而不必顧及兼容性,這些新的特性往往會(huì)有更好的性能同時(shí)提高你的開發(fā)效率。
我們來試著用 Electron 編寫一個(gè)簡單的 Hello World:
const {app, BrowserWindow} = require('electron') let mainWindow app.on('ready', function() { mainWindow = new BrowserWindow({width: 800, height: 600}) mainWindow.loadURL(
file://${__dirname}/index.html) })
其中的 index.html:
<body> <h1>Hello World!</h1> We are using node <script>document.write(process.versions.node)</script>, Chrome <script>document.write(process.versions.chrome)</script>, and Electron <script>document.write(process.versions.electron)</script>. </body>
可以看到,我們就像在使用 NPM 上一個(gè)普通的包一樣在使用 Electron 來控制 Chromium 來創(chuàng)建窗口、加載頁面,你也可以控制 Chromium 來進(jìn)行截圖、管理 Cookie 和 Session 等操作;同時(shí)在頁面中我們也可以使用 process.versions 這樣的 Node.js API,最后我們的 Hello World 看起來是這樣的:
我們都知道 Chromium 使用了一種多進(jìn)程的架構(gòu),當(dāng)你在使用 Chromium 瀏覽網(wǎng)頁時(shí),你所打開的每一個(gè)標(biāo)簽頁和插件都對應(yīng)著一個(gè)操作系統(tǒng)中的進(jìn)程。在 Electron 中也沿用了這樣的架構(gòu),Electron 程序的入口點(diǎn)是一個(gè) JavaScript 文件,這個(gè)文件將會(huì)被運(yùn)行在一個(gè)只有 Node.js 環(huán)境的主線程中:
由主進(jìn)程創(chuàng)建出的每個(gè)窗口(頁面)都在一個(gè)獨(dú)立的進(jìn)程(被稱作渲染進(jìn)程)中運(yùn)行,有著自己的事件循環(huán),和其他窗口互相隔離,渲染進(jìn)程中同時(shí)有 Chromium 和 Node.js 環(huán)境,其中 Node.js 的事件循環(huán)被整合到了 Chromium 提供的 V8 中,兩個(gè)環(huán)境間可以無縫地、無額外開銷地相互調(diào)用。
而主進(jìn)程的主要工作就是管理渲染進(jìn)程,同時(shí)還負(fù)責(zé)調(diào)用 GUI 相關(guān)的原生 API(例如托盤圖標(biāo)),這是通常是來自操作系統(tǒng)的限制。渲染進(jìn)程如果需要調(diào)用這些 API,或者渲染進(jìn)程之間需要通訊也都需要通過和主進(jìn)程之間的 IPC(進(jìn)程間通訊)來實(shí)現(xiàn),Electron 也提供了幾個(gè)用于簡化 IPC 的模塊(ipcMain、ipcRenderer、remote),但今天我們就不詳細(xì)介紹了。
目前已經(jīng)有非常多基于 Electron 的應(yīng)用了,下面是一些我目前正在使用的應(yīng)用,借助于 Electron,這些應(yīng)用大部分都是跨平臺(tái)的:
- VS Code 是微軟的一款文本編輯器,也可以說是 Atom 的主要競爭產(chǎn)品。
- Slack 是一款即時(shí)通訊軟件。
- Postman 是一個(gè) HTTP API 調(diào)試工具。
- Hyper 是一個(gè)終端仿真器。
- Nylas N1 是一個(gè)郵件客戶端。
- GitKraken 是一個(gè) Git 的 GUI 客戶端。
- Medis 是一個(gè) Redis 的 GUI 客戶端。
- Mongotron 是一個(gè) MongoDB 的 GUI 客戶端。
定制 Atom
對 Electron 的介紹就到此為止了,畢竟今天的主角是 Atom。作為 JavaScript 開發(fā)者,當(dāng)我們聽說 Atom 是基于 Web 技術(shù)構(gòu)建起來的,相信大家的第一個(gè)反應(yīng)就是打開 Chromium 的 Developer Tools:
可以看到,整個(gè) Atom 都是一個(gè)網(wǎng)頁 —— 文本編輯區(qū)域也是通過大量的 DOM 模擬出來的。我們點(diǎn)開 Atom 的主菜單,可以看到幾個(gè)簡單的自定義入口:
- Config 對應(yīng) ~/.atom/config.cson 是 Atom 的主配置文件。
- Init Script 對應(yīng) ~/.atom/init.coffee 其中的代碼會(huì)在 Atom 啟動(dòng)時(shí)被執(zhí)行。
- Keymap 對應(yīng) ~/.atom/keymap.cson 用來定義按鍵映射。
- Snippets 對應(yīng) ~/.atom/snippets.cson 可以定義一些代碼補(bǔ)全片段。
- Stylesheet 對應(yīng) ~/.atom/styles.less 可以通過 CSS 修改 Atom 的樣式。
Atom 最初是用 CoffeeScript 編寫的,這是一個(gè)編譯到 JavaScript 的語言,在當(dāng)時(shí)彌補(bǔ)了 JavaScript 語言設(shè)計(jì)上的一些不足。但隨著后來 ES2015 標(biāo)準(zhǔn)和 Babel 這樣的預(yù)編譯器的出現(xiàn),CoffeeScript 的優(yōu)勢少了許多,因此 Atom 最近也開始逐步從 CoffeeScript 切換到了 Babel,但后文還是可能會(huì)出現(xiàn)一些 CoffeeScript 的代碼。
我們可以在 Stylesheet 中先嘗試用 Less —— 一種編譯到 CSS 的語言來修改一下 Atom 的外觀:
// To style other content in the text editor's shadow DOM, // use the ::shadow expression atom-text-editor::shadow .cursor { border-color: red; }
我們用 atom-text-editor::shadow .cursor 這個(gè)選擇器指定了 Atom 的文本編輯區(qū)域中的光標(biāo),然后將邊框顏色設(shè)置為了紅色,保存后你馬上就可以看到光標(biāo)變成了紅色:
我們也可以在 Init Script 中編寫代碼來給 Atom 添加功能。考慮這樣一個(gè)需求,在寫 Markdown 的時(shí)候我們經(jīng)常需要添加一些鏈接,而鏈接通常是我們從瀏覽器上復(fù)制到剪貼板里的,如果有個(gè)命令可以把剪貼板中的鏈接自動(dòng)添加到光標(biāo)所選的文字上就好了:
atom.commands.add('atom-text-editor', 'markdown:paste-as-link', () => { let selection = atom.workspace.getActiveTextEditor().getLastSelection() let clipboardText = atom.clipboard.read()
selection.insertText(
${selection.getText()}) })
在這段代碼中,我們用 atom.commands.add 向 Atom 的文本編輯區(qū)域添加了一個(gè)名為 markdown:paste-as-link 的命令。我們先從當(dāng)前激活的文本編輯區(qū)域(getActiveTextEditor)中獲取當(dāng)前選中的文字(getLastSelection),然后使用 Markdown 的語法將剪貼板中的鏈接插入到當(dāng)前的位置:
那我們?nèi)绾螆?zhí)行這個(gè)命令呢,雖然 Atom 也提供了一個(gè)類似 Sublime Text 的命令面板:
但在實(shí)際使用中,我們通常會(huì)通過快捷鍵來觸發(fā)命令,我們可以在 Keymap 中為這個(gè)命令映射一個(gè)快捷鍵:
'atom-workspace': 'ctrl-l': 'markdown:paste-as-link' 'ctrl-m ctrl-l': 'markdown:paste-as-link'
我們可以使用 ctrl-l 這樣的快捷鍵,也可以使用 ctrl-m ctrl-l 這種 Emacs 風(fēng)格的快捷鍵。
插件化架構(gòu)
"packageDependencies": { "atom-dark-syntax": "0.28.0", "atom-dark-ui": "0.53.0", // themes ... "about": "1.7.2", "archive-view": "0.62.0", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.14.1", "autocomplete-html": "0.7.2", "autocomplete-plus": "2.33.1", "autocomplete-snippets": "1.11.0", "autoflow": "0.27.0", "autosave": "0.23.2", "background-tips": "0.26.1", "bookmarks": "0.43.2", "bracket-matcher": "0.82.2", "command-palette": "0.39.1", "deprecation-cop": "0.55.1", "dev-live-reload": "0.47.0", "encoding-selector": "0.22.0", // ... }
當(dāng)我們打開 Atom 核心的 package.json 時(shí),你可以看到 Atom 默認(rèn)捆綁了多達(dá) 77 個(gè)插件來實(shí)現(xiàn)各種基礎(chǔ)功能。沒錯(cuò),Atom 的核心是一個(gè)僅有不足兩萬行代碼的骨架,任何「有意義」的功能都被以插件的形式實(shí)現(xiàn)。Atom 作為一個(gè)通用的編輯器,不太可能面面俱到地考慮各種需求,索性不如通過徹底的插件化來適應(yīng)各種不同類型的開發(fā)任務(wù)。
實(shí)際上在 Atom 中插件被稱為「Package(包)」,而不是「Plugin(插件)」或「Extension(拓展)」,但下文我們還會(huì)繼續(xù)使用「插件」這個(gè)詞。
在這張圖中我標(biāo)出了一些內(nèi)建的插件:
- tree-view 實(shí)現(xiàn)了左側(cè)的目錄和文件樹。
- tabs 實(shí)現(xiàn)了上方的文件切換選項(xiàng)卡。
- git-diff 實(shí)現(xiàn)了行號(hào)左側(cè)用來表示文件修改狀態(tài)的彩條。
- find-and-replace 實(shí)現(xiàn)了查找和替換的功能。
- status-bar 實(shí)現(xiàn)了下方的狀態(tài)欄。
- grammar-selector 實(shí)現(xiàn)了狀態(tài)欄上的語言切換器。
- one-dark-ui 實(shí)現(xiàn)了一個(gè)暗色調(diào)的編輯器主題。
- one-dark-syntax 實(shí)現(xiàn)了一個(gè)暗色調(diào)的語法高亮主題。
- language-coffee-script 實(shí)現(xiàn)了對 CoffeeScipt 的語法高亮方案。
- command-palette 實(shí)現(xiàn)了一個(gè)命令的模糊搜索器。
- fuzzy-finder 實(shí)現(xiàn)了一個(gè)文件的模糊搜索器。
- settings-view 實(shí)現(xiàn)了一個(gè) Atom 的設(shè)置界面。
- autocomplete-plus 實(shí)現(xiàn)了一個(gè)代碼補(bǔ)全的列表。
- autocomplete-css 實(shí)現(xiàn)了針對 CSS 的代碼補(bǔ)全建議。
這么多基礎(chǔ)的功能都是以插件的方式實(shí)現(xiàn)的,這意味著第三方開發(fā)者在編寫插件時(shí)所使用的 API 和這些內(nèi)建的插件是完全相同的。而不像其他一些并非完全插件化的編輯器,第三方的插件很難得到與內(nèi)建功能同樣的 API,會(huì)受到并不完整的 API 的限制。
這也意味著如果一個(gè)內(nèi)建的功能不夠好,社區(qū)可以開發(fā)出新的插件去替換掉內(nèi)建的插件。你可能會(huì)覺得這樣的情況不太可能發(fā)生,但其實(shí) Atom 的代碼補(bǔ)全插件就是一個(gè)例子,Atom 一開始內(nèi)建的代碼補(bǔ)全插件叫 autocomplete,功能較為簡陋,于是社區(qū)中出現(xiàn)了一個(gè)具有更強(qiáng)拓展性的 autocomplete-plus,受到了大家的好評,最后替換掉了之前的 autocomplete,成為了內(nèi)建插件。
Atom 的插件之間是可以互相交互的,例如 grammar-selector 等很多插件都會(huì)調(diào)用狀態(tài)欄的 API,來在狀態(tài)欄上添加按鈕或展示信息:
作為插件當(dāng)然是可以獨(dú)立地進(jìn)行更新的,而一旦更新就會(huì)不可避免地引入不兼容的 API 修改,如果 grammar-selector 依賴了一個(gè)較舊版本的 status-bar 的 API,而在之后 status-bar 更新了,并且引入了不兼容的 API 調(diào)整,那么 grammar-selector 對 status-bar 的調(diào)用就會(huì)失敗。
在 Node.js 中對于依賴版本的解決方案大家都很清楚 —— 每個(gè)包明確地聲明自己的依賴的版本,然后為每個(gè)包的每個(gè)版本單獨(dú)安裝一次,保證每個(gè)包都可以引用到自己想要的版本的依賴。但在 Atom 里這樣是行不通的,因?yàn)槟愕拇翱谏现挥幸粋€(gè)狀態(tài)欄,而不可能同時(shí)存在一個(gè) 0.58.0 版本的 status-bar 和一個(gè) 1.1.0 版本的 status-bar。
因此 Atom 提供了一個(gè)服務(wù)(Service)API,將被調(diào)用方抽象為服務(wù)的提供者,而將調(diào)用方抽象為服務(wù)的消費(fèi)者,插件可以聲明自己同時(shí)提供一個(gè)服務(wù)的幾個(gè)版本,通過 Semantic Versioning(語義化版本號(hào))表示,例如 status-bar 的 package.json 中有:
"providedServices": { "status-bar": { "description": "A container for indicators at the bottom of the workspace", "versions": { "1.1.0": "provideStatusBar", "0.58.0": "legacyProvideStatusBar" } } }
status-bar 同時(shí)提供了 status-bar 這項(xiàng)服務(wù)的兩個(gè)版本 —— 0.58.0 和 1.1.0,分別對應(yīng) provideStatusBar 和 legacyProvideStatusBar 這兩個(gè)函數(shù)。
而 grammar-selector 的 package.json 中有:
"consumedServices": { "status-bar": { "versions": { "^1.0.0": "consumeStatusBar" } } }
grammar-selector 聲明自己依賴 1.0.0 版本以上的 status-bar 服務(wù)。Atom 會(huì)在這中間按照 Semantic Versioning 做一個(gè)匹配,最后選擇 status-bar 提供的 1.1.1 版本,調(diào)用 status-bar 的 provideStatusBar 函數(shù),然后將結(jié)果傳入 grammar-selector 的 consumeStatusBar 函數(shù)。
通過服務(wù) API,Atom 插件之間的交互被簡化了 —— 一個(gè)插件不需要關(guān)心誰來消費(fèi)自己的服務(wù)、消費(fèi)哪個(gè)版本,也不需要關(guān)心誰來提供自己需要消費(fèi)的服務(wù),保證了插件能夠獨(dú)立地、平滑地進(jìn)行版本更新和 API 的迭代,也允許實(shí)現(xiàn)了相同服務(wù)的插件相互替代;如果用戶沒有安裝能夠提供對應(yīng)版本的服務(wù)的插件,那么就什么都不會(huì)發(fā)生。
正因如此,Atom 的很多插件甚至有了自己的小社區(qū),例如 linter 插件提供了展示語法風(fēng)格建議的功能,但針對具體語言和工具的只是則是由單獨(dú)的插件來完成的:
對于這樣一個(gè)嚴(yán)重依賴插件的社區(qū),插件質(zhì)量的參差不齊也是一個(gè)嚴(yán)重的問題,在 Atom 中,如果一個(gè)插件拋出了異常,就會(huì)出現(xiàn)下面這樣的提示:
如果你點(diǎn)擊創(chuàng)建「Create issue」的話,會(huì)自動(dòng)在插件的倉庫上創(chuàng)建一個(gè)包含調(diào)用棧、Atom 和操作系統(tǒng)版本、插件列表及版本、配置項(xiàng)、發(fā)生異常前的動(dòng)作的 Issue,幫助作者重現(xiàn)和修復(fù)異常;如果已經(jīng)有其他人提交過了這個(gè)異常,按鈕便會(huì)變成「View issue」,你可以到其他人提交的 Issue 中附和一下。
插件化 API
這一節(jié)我們將會(huì)介紹 Atom 是如何提供給插件定制的能力的,Atom 首先提供了很多全局的實(shí)例來管理特定對象的注冊和查詢,我們通常也稱這種設(shè)計(jì)為「注冊局模式(Registry Pattern)」,包括:
- atom.commands 管理編輯器中的命令。
- atom.grammars 管理對語言的支持。
- atom.views 管理狀態(tài)數(shù)據(jù)(Model)和用戶界面之間的映射。
- atom.keymaps 管理快捷鍵映射。
- atom.packages 管理插件。
- atom.deserializers 管理狀態(tài)數(shù)據(jù)的序列化和反序列化。
例如我們前面的 Markdown 粘貼鏈接的例子中:
atom.commands.add('atom-text-editor', 'markdown:paste-as-link', someAction)
我們通過 atom.commands 注冊了一個(gè)叫 markdown:paste-as-link 的命令并關(guān)聯(lián)到一個(gè)函數(shù)上;隨后其他插件(例如 command-palette)會(huì)從 atom.commands 中檢索并執(zhí)行這個(gè)命令:
let target = atom.views.getView(atom.workspace.getActiveTextEditor()) atom.commands.dispatch(target, 'markdown:paste-as-link')
從上面的代碼中我們可以看到,atom.commands.dispatch 在執(zhí)行一個(gè)命令時(shí)還需要指定一個(gè) DOM 元素,結(jié)合前面注冊命令和映射快捷鍵的例子,我們可以發(fā)現(xiàn) Atom 中的快捷鍵和命令實(shí)際上都是被注冊到一個(gè) CSS 選擇器上的。這是因?yàn)樵?Atom 這樣一個(gè)復(fù)雜的環(huán)境中,一個(gè)快捷鍵可能會(huì)被多次映射到不同的命令,例如下圖,我在存在代碼補(bǔ)全的選單的情況下按了一下 Tab 鍵:
Atom 內(nèi)建的按鍵映射調(diào)試插件(keybinding-resolver)告訴我們 Tab 鍵被同時(shí)映射到了 8 個(gè)命令上,每個(gè)映射都有一個(gè)相關(guān)聯(lián)的 CSS 選擇器(上圖中間一列)作為約束。Atom 會(huì)從當(dāng)前焦點(diǎn)所在的元素,逐級(jí)冒泡,直到找到一個(gè)離焦點(diǎn)最近的按鍵映射,在上面的例子中,因?yàn)楫?dāng)前焦點(diǎn)在代碼補(bǔ)全的選單上,所以 Tab 鍵最后被匹配到了 autocomplete-plus:confirm 這個(gè)命令;而如果當(dāng)前沒有代碼補(bǔ)全的選單,Tab 鍵則會(huì)被映射到 editor:indent。
我為 Atom 主界面中的各個(gè)可視組件畫了一個(gè)示意圖,Atom 中最核心的區(qū)域叫「窗格(Pane)」,窗格可以橫向或縱向被切分為多個(gè)窗格,窗格中可以是自定義的 DOM 元素(例如右側(cè)的設(shè)置界面),也可以是 TextEditor(當(dāng)然其實(shí)這也是一個(gè) DOM 元素)。在窗格構(gòu)成的核心區(qū)域之外,插件可以從四個(gè)方向添加「面板(Panel)」來提供一些次要的功能,面板中包含的也是自定義的 DOM 元素。可以想象,上圖中的那樣一個(gè)界面,是在兩個(gè)窗格的基礎(chǔ)上,先從底部添加一個(gè) find-and-replace 的面板,然后從左側(cè)添加一個(gè) tree-view 的面板,最后再從底部添加一個(gè) status-bar 的面板。
Workspace 對應(yīng)著 Atom 的一個(gè)窗口,TextEdtior 對應(yīng)著窗格中的一個(gè)文本編輯區(qū)域,可以算是 Atom 較為核心的組件了,我們來看看它們的 API 文檔:
Workspace(工作區(qū))和 TextEditor(文本編輯器)算是 Atom 的核心部分。從上圖中可以看到,Workspace 和 TextEditor 上首先提供了大量的事件訂閱函數(shù)(圖中僅列出了很少一部分),讓插件可以感知到用戶在 Workspace 在 TextEditor 中進(jìn)行的操作,例如 TextEditor 的 onDidChange 會(huì)在每次用戶修改文本時(shí)進(jìn)行回調(diào);然后也提供了大量的函數(shù)讓插件可以操作 TextEditor 中的文本,例如 getSelectedText 可以獲取到用戶當(dāng)前選擇的文本。
事件,或者說「訂閱者模式(Publish–subscribe pattern)」,在 Node.js 開發(fā)中我們也經(jīng)常用到,但和 Node.js 的 EventEmitter 略有不同,Atom 提供的 Emitter 提供了更方便地退訂事件的功能,所有事件訂閱函數(shù)都會(huì)返回一個(gè) Disposable,用于退訂這個(gè)事件訂閱。例如在 Atom 中,大部分插件的結(jié)構(gòu)是這樣的:
class SomePackage { activate() { this.disposable = workspace.observeTextEditors( () => { console.log('found a TextEditor') }) }
deactivate() { this.disposable.dispose() } }
activate 會(huì)在插件被加載時(shí)調(diào)用,這個(gè)插件為當(dāng)前和未來的每個(gè) TextEditor 注冊一個(gè)回調(diào),并將返回的 Disposable 保存在一個(gè)實(shí)例變量上;deactivate 會(huì)在插件被禁用時(shí)調(diào)用,在這里我們調(diào)用了之前的 Disposable 的 dispose 方法來退訂之前的事件。如果插件的每個(gè)事件訂閱都這樣實(shí)現(xiàn),那么 Atom 便可以在不重啟的情況下安裝、卸載、更新插件,實(shí)際上絕大部分插件也是這樣做的。
除此之外,Atom 還提供了很多其他的 API,但在此就不詳細(xì)介紹了:
- atom.config、atom.clipboard、atom.project 提供了對配置項(xiàng)、剪貼板、通知的管理。
- Color、Selection、File、GitRepository 提供了對顏色、文本選擇、文件、Git 倉庫的抽象。
這也是我選擇了 Atom 而不是它的主要竟品 —— VS Code 的原因:Atom 始終都將可定制性放在第一位,從一開始就是核心僅僅提供 API,而將大部分功能交由插件實(shí)現(xiàn),插件和內(nèi)建功能使用的是同樣的 API,從 1.0 之后幾乎沒添加過新功能,我覺得這是一個(gè)非常優(yōu)雅的設(shè)計(jì);VS Code 還是 Visual Studio 的路線,提供一個(gè)對用戶而言好用的、高性能的 IDE,后來才出現(xiàn)插件機(jī)制,而且很多功能都在核心中,有時(shí)第三方插件不能夠得到和內(nèi)建功能一樣的對待。
優(yōu)化啟動(dòng)速度
因?yàn)?Atom 插件化的架構(gòu),默認(rèn)就捆綁了 77 個(gè)插件,大多數(shù)用戶在實(shí)際使用時(shí)都會(huì)有超過一百個(gè)插件,加載這些插件就花費(fèi)了啟動(dòng)階段的大部分時(shí)間,讓人覺得 Atom 啟動(dòng)緩慢。
Atom 也做了很多嘗試來優(yōu)化啟動(dòng)速度,首先比如延遲加載插件,對于像我們前面提到的為 Markdown 粘貼鏈接這樣功能單一的插件,可以在 package.json 中聲明自己提供的功能:
{ "name": "markdown-link", "activationCommands": { "atom-text-editor": "markdown:paste-as-link" } }
這樣 Atom 便可以延遲對這個(gè)插件的完整加載,只記錄這個(gè)插件所提供的命令,markdown:paste-as-link 也會(huì)出現(xiàn)在命令面板中,但只有當(dāng)這個(gè)命令第一次被用到的時(shí)候,Atom 才會(huì)完整地加載這個(gè)插件。
顯然這個(gè)特性非常依賴于插件的作者,如果插件沒有在 package.json 中做這樣的聲明,Atom 就不知道它提供了怎樣的功能,也就不得不在啟動(dòng)時(shí)完整地加載這個(gè)插件。為此,Atom 默認(rèn)捆綁了一個(gè) timecop 插件,可以記錄并展示啟動(dòng)階段的耗時(shí):
Atom 非常善于通過「社會(huì)化」的方式維護(hù)社區(qū),因?yàn)橛辛?timecop,終端用戶也可以感知到導(dǎo)致啟動(dòng)緩慢的插件,并在 GitHub 上向作者反饋(Atom 要求所有插件的源代碼必須托管在 GitHub)。在 Atom 1.0 發(fā)布時(shí),有一些 API 的行為有調(diào)整,Atom 也是通過類似的方式向終端用戶展示未遷移到最新的 API 的插件,督促作者來進(jìn)行修改。
作為 Node.js 開發(fā)者我們都知道 node_modules 中有著大量的小文件,讀取這些小文件要比讀取單個(gè)大文件慢得多,尤其對于非固態(tài)硬盤而言。我做了一個(gè)簡單的統(tǒng)計(jì),Atom 的代碼目錄(包括 node_modules)中有著 12068 個(gè)文件,這些文件的讀取顯然需要花費(fèi)啟動(dòng)階段的很多時(shí)間:
于是 Atom 借助 Electron 提供的 ASAR 歸檔格式,將整個(gè) node_modules 和其他的代碼文件打包成了一個(gè)單個(gè)的文件,這樣 Atom 在啟動(dòng)時(shí)只需要讀取這一個(gè)文件,省下了很多的時(shí)間。
優(yōu)化渲染性能
在 Atom 的早期版本中,當(dāng)你打開一個(gè)代碼量較大的文件時(shí),文本編輯區(qū)域就會(huì)出現(xiàn)卡頓。前面我們提到,Atom 的整個(gè)窗口其實(shí)就是一個(gè)網(wǎng)頁,如果網(wǎng)頁渲染速度達(dá)不到 60fps —— 也就是無法總是在 16 毫秒內(nèi)完成一次渲染,就會(huì)出現(xiàn)人可以感受到的卡頓。所以我們下面介紹的渲染性能優(yōu)化思路其實(shí)是適用于所有的 Web 應(yīng)用的,只是很少有應(yīng)用能夠有著 Atom 這樣復(fù)雜的頁面。
在網(wǎng)頁渲染的過程主要分為「重排(Reflow)」和「重繪(Repaint)」,重排就是重新計(jì)算頁面中各元素的位置,重繪則是將元素在指定的位置繪制出來。這其中重排是絕大部分卡頓的原因,因?yàn)樵谝粋€(gè)復(fù)雜的頁面中可能有幾萬甚至幾十萬個(gè)元素,它們的位置有著復(fù)雜的依賴關(guān)系,難以并行地進(jìn)行計(jì)算。
眾所周知 JavaScript 是基于事件循環(huán)單線程地運(yùn)行的,每當(dāng)事件循環(huán)中的一個(gè)函數(shù)執(zhí)行完成,如果它修改了 DOM,瀏覽器就會(huì)嘗試進(jìn)行重排和重繪來更新頁面的顯示,如果我們將對 DOM 的修改分散在事件循環(huán)中的多個(gè)函數(shù)中,就會(huì)多次觸發(fā)不必要的重排和重繪,所以優(yōu)化渲染性能有兩個(gè)關(guān)鍵的思路:
- 避免直接地、頻繁地、反復(fù)地操作 DOM
- 保持 DOM 樹盡可能地小
為了將對 DOM 的操作集中到一起,我們有必要引入一個(gè)抽象層,也就是所謂的 Virtual DOM,我們總是在 Virtual DOM 上進(jìn)行修改,而后再由 Virtual DOM 將我們的多次修改合并,一起更新到真正的 DOM 上。Atom 一開始使用了 React 所提供的 Virtual DOM,不過后來為了更細(xì)粒度的控制,切換到了一個(gè)自行實(shí)現(xiàn)的 Virtual DOM 上:
在采用了 Virtual DOM 之后也意味著插件不能夠直接操作 Atom 的文本編輯區(qū)域的 DOM 了,為此 Atom 提供了 Marker 和 Decoration 這兩個(gè)機(jī)制來允許插件間接地與文本編輯區(qū)域交互,Marker 和 Decoration 相當(dāng)于是對 Virtual DOM 的進(jìn)一步封裝:
Marker 是對一段文本的動(dòng)態(tài)封裝,所謂動(dòng)態(tài)是說它并不是單純地記錄「行號(hào)」和「列數(shù)」,而是即使周圍的文本被編輯,Marker 也可以維持在正確的位置,Atom 中文本編輯區(qū)域的很多功能都是基于 Marker 實(shí)現(xiàn)的,例如光標(biāo)、選區(qū)、高亮、行號(hào)左側(cè) git-diff 的提示、行號(hào)右側(cè) linter 的提示等。
Marker 只是對一段文本的表示,而 Decoration 用來向 Marker 上添加自定義的樣式即 CSS 的 class,以便插件通過樣式表在編輯區(qū)域展示信息:
let range = editor.getSelectedBufferRange() // invalidate: never, surround, overlap, inside, touch let marker = editor.markBufferRange(range, {invalidate: 'overlap'}) // type: line, line-number, highlight, overlay, gutter, block editor.decorateMarker(marker, {type: 'highlight'}, {class: 'highlight-selected'})
在這段代碼中,我們先從當(dāng)前選擇的文本創(chuàng)建了一個(gè) Marker,invalidate 屬性代表了它如何追蹤對這段文本的修改;然后我們向這個(gè) Marker 上創(chuàng)建了一個(gè) Decoration,向這個(gè) Marker 所表示的文本區(qū)域添加一個(gè)叫 highlight-selected 的 CSS class,type屬性代表了這個(gè) CSS class 被添加到什么位置。
隨后我們便可以添加一個(gè)樣式表,為我們的 CSS class 添加樣式:
.highlights { .highlight-selected .region { border-radius: 3px; box-sizing: border-box; background-color: transparent; border-width: 1px; border-style: solid; }
// ... }
Atom 通過 Marker 和 Decoration 這樣高層次的抽象,避免了插件直接去操作最關(guān)鍵的性能瓶頸 —— 文本編輯區(qū)域的 DOM,避免了插件反復(fù)修改 DOM 引起的重排。
在之前版本的 Atom 中,當(dāng)你打開一個(gè)大文件時(shí),整個(gè)文件都會(huì)被渲染成 DOM 作為一個(gè)大的頁面,供你在 Atom 的窗口中滾動(dòng)地瀏覽文件。顯然這樣會(huì)額外渲染非常多的 DOM 元素,也不符合我們前面提到的「保持 DOM 盡可能小」的思路,導(dǎo)致 Atom 無法打開大文件。
因此 Atom 現(xiàn)在會(huì)將文本編輯區(qū)域的每若干行劃分為一個(gè)塊(Tile),僅去渲染可見的塊,而不是渲染整個(gè)文件。當(dāng)用戶滾動(dòng)編輯區(qū)域時(shí),新的塊會(huì)被繪制,不可見的塊會(huì)被銷毀:
除了渲染導(dǎo)致的卡頓之外,因?yàn)?JavaScript 是單線程的,如果進(jìn)行 CPU 密集的操作(例如在大量文件中進(jìn)行正則搜索),也會(huì)阻塞事件循環(huán),導(dǎo)致卡頓。就像普通的 Node.js 程序一樣,如果希望進(jìn)行 CPU 密集的計(jì)算,最好放到單獨(dú)的進(jìn)程而不是主進(jìn)程,Atom 內(nèi)建的搜索功能就是這樣實(shí)現(xiàn)的:
scan = (regex, options={}, iterator) -> deferred = Q.defer()
task = Task.once require.resolve('./scan-handler'), regex, options, -> task.on 'scan:result-found', (result) -> iterator(result)
deferred.promise
那么今天的主要內(nèi)容就這么多了,接下來我想推薦幾個(gè)我覺得非常好用的插件:
- git-plus 可以讓你在命令面板中直接執(zhí)行 git diff、git push 這樣的命令。
- file-icons 可以給 tree-view 中的文件添加一個(gè)美觀的圖標(biāo)。
- local-history 可以在你每次保存或編輯器失去焦點(diǎn)時(shí)在特定目錄保存一份快照,以防萬一。
- highlight-selected 可以像 Sublime Text 一樣高亮當(dāng)前文件中和你選擇的單詞一樣的單詞。
- linter 是一個(gè)語法風(fēng)格檢查的框架,如果你寫 JavaScript 的話可以使用 linter-eslint 進(jìn)行檢查。
- 對于特定語言有一些專門的代碼補(bǔ)全插件,例如 JavaScript 可以使用 atom-ternjs,TypeScript 可以使用 atom-typescript,React 可以使用 react。
Atom 本身是開源項(xiàng)目,也有著活躍的社區(qū):
- GitHub - atom/atom: The hackable text editor(主倉庫)
- Documentation(文檔)
- Documentation(Electron 文檔)
- Atom Discussion(官方論壇)
- Atom Blog - Atom Blog(官方博客)
- atom-china.org/(中文論壇)
- Atom China Community · GitHub(文檔和博客的中文翻譯)
其他參考鏈接:
- gnu.org/software/emacs/(維基百科)
- Electron IPC: remote
- Essential Electron
- Building a Desktop Application with Electron
- Atom 1.0 - Atom Blog(中文版)
- Autocomplete just got a whole lot better (中文版)
- Semantic Versioning 2.0.0
- GitHub - electron/asar: Simple extensive tar-like archive format with indexing
Moving Atom To React - GitHub - atom/etch: Builds components using a simple and explicit API around virtual-dom
- Decorations - Atom Blog
- Rendering Improvements