製作你的第一個 Atom 文字編輯器外掛

發表於2016-10-22

序言

這篇教程將會教你怎麼製作你的第一個 Atom 文字編輯器的外掛。我們將會製作一個山寨版的 Sourcerer,這是一個從 StackOverflow 查詢並使用程式碼片段的外掛。到教程結束時,你將會製作好一個將程式設計問題(用英語描述的)轉換成獲取自 StackOverflow 的程式碼片段的外掛,像這樣:

製作你的第一個 Atom 文字編輯器外掛

教程須知

Atom 文字編輯器是用 web 技術創造出來的。我們將完全使用 JavaScript 的 EcmaScript 6 規範來製作外掛。你需要熟悉以下內容:

教程的倉庫

你可以跟著教程一步一步走,或者看看 放在 GitHub 上的倉庫,這裡有外掛的原始碼。這個倉庫的歷史提交記錄包含了這裡每一個標題。

開始

安裝 Atom

根據 Atom 官網 的說明來下載 Atom。我們同時還要安裝上 apm(Atom 包管理器的命令列工具)。你可以開啟 Atom 並在應用選單中導航到 Atom > Install Shell Commands 來安裝。開啟你的命令列終端,執行apm -v 來檢查 apm 是否已經正確安裝好,安裝成功的話列印出來的工具版本和相關環境資訊應該是像這樣的:

生成骨架程式碼

讓我們使用 Atom 提供的一個實用工具建立一個新的 package(軟體包)來開始這篇教程。

  • 啟動編輯器,按下 Cmd+Shift+P(MacOS)或者 Ctrl+Shift+P(Windows/Linux)來開啟命令皮膚Command Palette。
  • 搜尋“Package Generator: Generate Package”並點選列表中正確的條目,你會看到一個輸入提示,輸入軟體包的名稱:“sourcefetch”。
  • 按下Enter鍵來生成這個骨架程式碼包,它會自動在 Atom 中開啟。

如果你在側邊欄沒有看到軟體包的檔案,依次按下 Cmd+K Cmd+B(MacOS)或者 Ctrl+KCtrl+B(Windows/Linux)。

製作你的第一個 Atom 文字編輯器外掛

Command Palette可以讓你通過模糊搜尋來找到並執行軟體包。這是一個執行命令比較方便的途徑,你不用去找導航選單,也不用刻意去記快捷鍵。我們將會在整篇教程中使用這個方法。

執行骨架程式碼包

在開始程式設計前讓我們來試用一下這個骨架程式碼包。我們首先需要重啟 Atom,這樣它才可以識別我們新增的軟體包。再次開啟命令皮膚,執行 Window: Reload 命令。

重新載入當前視窗以確保 Atom 執行的是我們最新的原始碼。每當需要測試我們對軟體包的改動的時候,就需要執行這條命令。

通過導航到編輯器選單的 Packages > sourcefetch > Toggle 或者在命令皮膚執行 sourcefetch:toggle 來執行軟體包的 toggle 命令。你應該會看到螢幕的頂部出現了一個小黑窗。再次執行這條命令就可以隱藏它。

製作你的第一個 Atom 文字編輯器外掛

“toggle”命令

開啟 lib/sourcefetch.js,這個檔案包含有軟體包的邏輯和 toggle 命令的定義。

 

toggle 是這個模組匯出的一個函式。根據模態皮膚的可見性,它通過一個三目運算子 來呼叫 showhide 方法。modalPanelPanel(一個由 Atom API 提供的 UI 元素) 的一個例項。我們需要在 export default 內部宣告 modalPanel 才可以讓我們通過一個例項變數 this 來訪問它。

上面的語句讓 Atom 在使用者執行 sourcefetch:toggle 的時候執行 toggle 方法。我們指定了一個 匿名函式() => this.toggle(),每次執行這條命令的時候都會執行這個函式。這是事件驅動程式設計(一種常用的 JavaScript 模式)的一個範例。

Atom 命令

命令只是使用者觸發事件時使用的一些字串識別符號,它定義在軟體包的名稱空間內。我們已經用過的命令有:

  • package-generator:generate-package
  • Window:reload
  • sourcefetch:toggle

軟體包對應到命令,以執行程式碼來響應事件。

進行你的第一次程式碼更改

讓我們來進行第一次程式碼更改——我們將通過改變 toggle 函式來實現逆轉使用者選中文字的功能。

改變 “toggle” 函式

如下更改 toggle 函式。

測試你的改動

  • 通過在命令皮膚執行 Window: Reload 來重新載入 Atom。
  • 通過導航到 File > New 來建立一個新檔案,隨便寫點什麼並通過游標選中它。
  • 通過命令皮膚、Atom 選單或者右擊文字然後選中 Toggle sourcefetch 來執行sourcefetch:toggle 命令。

更新後的命令將會改變選中文字的順序:

製作你的第一個 Atom 文字編輯器外掛

sourcefetch 教程倉庫 檢視這一步的全部程式碼更改。

Atom 編輯器 API

我們新增的程式碼通過用 TextEditor API 來訪問編輯器內的文字並進行操作。讓我們來仔細看看。

頭兩行程式碼獲取了 TextEditor 例項的一個引用。變數的賦值和後面的程式碼被包在一個條件結構裡,這是為了處理沒有可用的編輯器例項的情況,例如,當使用者在設定選單中執行該命令時。

呼叫 getSelectedText 方法可以讓我們訪問到使用者選中的文字。如果當前沒有文字被選中,函式將返回一個空字串。

我們選中的文字通過一個 JavaScript 字串方法 來逆轉。最後,我們呼叫 insertText 方法來將選中的文字替換為逆轉後的文字副本。通過閱讀 Atom API 文件,你可以學到更多關於 TextEditor 的不同的方法。

瀏覽骨架程式碼

現在我們已經完成第一次程式碼更改了,讓我們瀏覽骨架程式碼包的程式碼來深入瞭解一下 Atom 的軟體包是怎樣構成的。

主檔案

主檔案是 Atom 軟體包的入口檔案。Atom 通過 package.json 裡的條目設定來找到主檔案的位置:

這個檔案匯出一個帶有生命週期函式(Atom 在特定的事件發生時呼叫的處理函式)的物件。

  • activate 會在 Atom 初次載入軟體包的時候呼叫。這個函式用來初始化一些諸如軟體包所需的使用者介面元素的物件,以及訂閱軟體包命令的處理函式。
  • deactivate 會在軟體包停用的時候呼叫,例如,當使用者關閉或者重新整理編輯器的時候。
  • serialize Atom 呼叫它在使用軟體包的過程中儲存軟體包的當前狀態。它的返回值會在 Atom 下一次載入軟體包的時候作為一個引數傳遞給 activate

我們將會重新命名我們的軟體包命令為 fetch,並移除一些我們不再需要的使用者介面元素。按照如下更改主檔案:

“啟用”命令

為了提升效能,Atom 軟體包可以用時載入。我們可以讓 Atom 在使用者執行特定的命令的時候才載入我們的軟體包。這些命令被稱為 啟用命令,它們在 package.json 中定義:

有一些軟體包需要在 Atom 啟動的時候被載入,例如那些改變 Atom 外觀的軟體包。在那樣的情況下,activationCommands 會被完全忽略。

“觸發”命令

選單項

menus 目錄下的 JSON 檔案指定了哪些選單項是為我們的軟體包而建的。讓我們看看menus/sourcefetch.json

這個 context-menu 物件可以讓我們定義右擊選單的一些新條目。每一個條目都是通過一個顯示在選單的label 屬性和一個點選後執行的命令的 command 屬性來定義的。

同一個檔案中的這個 menu 物件用來定義外掛的自定義應用選單。我們如下重新命名它的條目:

鍵盤快捷鍵

命令還可以通過鍵盤快捷鍵來觸發。快捷鍵通過 keymaps 目錄的 JSON 檔案來定義:

以上程式碼可以讓使用者通過 Ctrl+Alt+O(Windows/Linux) 或 Cmd+Alt+O(MacOS) 來觸發 toggle 命令。

重新命名引用的命令為 fetch

通過執行 Window: Reload 命令來重啟 Atom。你應該會看到 Atom 的右擊選單更新了,並且逆轉文字的功能應該還可以像之前一樣使用。

sourcefetch 教程倉庫 檢視這一步所有的程式碼更改。

使用 NodeJS 模組

現在我們已經完成了第一次程式碼更改並且瞭解了 Atom 軟體包的結構,讓我們介紹一下 Node 包管理器(npm) 中的第一個依賴項模組。我們將使用 request 模組發起 HTTP 請求來下載網站的 HTML 檔案。稍後將會用到這個功能來扒 StackOverflow 的頁面。

安裝依賴

開啟你的命令列工具,切換到你的軟體包的根目錄並執行:

這兩條命令將 request 模組新增到我們軟體包的依賴列表並將模組安裝到 node_modules 目錄。你應該會在package.json 看到一個新條目。@ 符號的作用是讓 npm 安裝我們這篇教程需要用到的特定版本的模組。執行 apm install 是為了讓 Atom 知道使用我們新安裝的模組。

下載 HTML 並將記錄列印在開發者控制檯

通過在 lib/sourcefetch.js 的頂部新增一條引用語句引入 request 模組到我們的主檔案:

現在,在 fetch 函式下面新增一個新函式 download 作為模組的匯出項:

這個函式用 request 模組來下載一個頁面的內容並將記錄輸出到控制檯。當 HTTP 請求完成之後,我們的回撥函式會將響應體作為引數來被呼叫。

最後一步是更新 fetch 函式以呼叫 download 函式:

fetch 函式現在的功能是將 selection 當作一個 URL 傳遞給 download 函式,而不再是逆轉選中的文字了。讓我們來看看這次的更改:

  • 通過執行 Window: Reload 命令來重新載入 Atom。
  • 開啟開發者工具。為此,導航到選單中的 View > Developer > Toggle Developer Tools
  • 新建一個檔案,導航到 File > New
  • 輸入一個 URL 並選中它,例如:http://www.atom.io
  • 用上述的任意一種方法執行我們軟體包的命令:

製作你的第一個 Atom 文字編輯器外掛

開發者工具讓 Atom 軟體包的除錯更輕鬆。每個 console.log 語句都可以將資訊列印到互動控制檯,你還可以使用 Elements 選項卡來瀏覽整個應用的視覺化結構——即 HTML 的文字物件模型(DOM)

sourcefetch 教程倉庫 檢視這一步所有的程式碼更改。

用 Promises 來將下載好的 HTML 插入到編輯器中

理想情況下,我們希望 download 函式可以將 HTML 作為一個字串來返回,而不僅僅是將頁面的內容列印到控制檯。然而,返回文字內容是無法實現的,因為我們要在回撥函式裡面訪問內容而不是在 download 函式那裡。

我們會通過返回一個 Promise 來解決這個問題,而不再是返回一個值。讓我們改動 download 函式來返回一個 Promise:

Promises 允許我們通過將非同步邏輯封裝在一個提供兩個回撥方法的函式裡來返回獲得的值(resolve 用來處理請求成功的返回值,reject 用來向使用者報錯)。如果請求返回了錯誤我們就呼叫 reject,否則就用resolve 來處理 HTML。

讓我們更改 fetch 函式來使用 download 返回的 Promise:

在我們新版的 fetch 函式裡,我們通過在 download 返回的 Promise 呼叫 then 方法來對 HTML 進行操作。這會將 HTML 插入到編輯器中。我們同樣會通過呼叫 catch 方法來接收並處理所有的錯誤。我們通過用Atom Notification API 來顯示警告的形式來處理錯誤。

看看發生了什麼變化。重新載入 Atom 並在一個選中的 URL 上執行軟體包命令:

製作你的第一個 Atom 文字編輯器外掛

如果這個 URL 是無效的,一個警告通知將會彈出來:

製作你的第一個 Atom 文字編輯器外掛

sourcefetch 教程倉庫 檢視這一步所有的程式碼更改。

編寫一個爬蟲來提取 StackOverflow 頁面的程式碼片段

下一步涉及用我們前面扒到的 StackOverflow 的頁面的 HTML 來提取程式碼片段。我們尤其關注那些來自採納答案(提問者選擇的一個正確答案)的程式碼。我們可以在假設這類答案都是相關且正確的前提下大大簡化我們這個軟體包的實現。

使用 jQuery 和 Chrome 開發者工具來構建查詢

這一部分假設你使用的是 Chrome 瀏覽器。你接下來可以使用其它瀏覽器,但是提示可能會不一樣。

讓我們先看看一張典型的包含採納答案和程式碼片段的 StackOverflow 頁面。我們將會使用 Chrome 開發者工具來瀏覽 HTML:

  • 開啟 Chrome 並跳到任意一個帶有采納答案和程式碼的 StackOverflow 頁面,比如像這個用 Python 寫的 hello world 的例子或者這個關於 C 來讀取文字內容的問題
  • 滾動視窗到採納答案的位置並選中一部分程式碼。
  • 右擊選中文字並選擇 檢查
  • 使用元素偵察器來檢查程式碼片段在 HTML 中的位置。

注意文字結構應該是這樣的:

  • 採納的答案通過一個 class 為 accepted-answerdiv 來表示
  • 程式碼塊位於 pre 元素的內部
  • 呈現程式碼片段的元素就是裡面那一對 code 標籤

製作你的第一個 Atom 文字編輯器外掛

現在讓我們寫一些 jQuery 程式碼來提取程式碼片段:

  • 在開發者工具那裡點選 Console 選項卡來訪問 Javascript 控制檯。
  • 在控制檯中輸入 $('div.accepted-answer pre code').text() 並按下Enter鍵。

你應該會看到控制檯中列印出採納答案的程式碼片段。我們剛剛執行的程式碼使用了一個 jQuery 提供的特別的 $函式。$ 接收要選擇的查詢字串並返回網站中的某些 HTML 元素。讓我們通過思考幾個查詢案例看看這段程式碼的工作原理:

上面的查詢會匹配所有 class 為 accepted-answer<div> 元素,在我們的案例中只有一個 div。

在前面的基礎上改造了一下,這個查詢會匹配所有在之前匹配的 <div> 內部的 <pre> 元素內部的 <code>元素。

text 函式提取並連線原本將由上一個查詢返回的元素列表中的所有文字。這也從程式碼中去除了用來使語法高亮的元素。

介紹 Cheerio

我們的下一步涉及使用我們建立好的查詢結合 Cheerio(一個伺服器端實現的 jQuery)來實現扒頁面的功能。

安裝 Cheerio

開啟你的命令列工具,切換到你的軟體包的根目錄並執行:

實現扒頁面的功能

lib/sourcefetch.jscheerio 新增一條引用語句:

現在建立一個新函式 scrape,它用來提取 StackOverflow HTML 裡面的程式碼片段:

最後,讓我們更改 fetch 函式以傳遞下載好的 HTML 給 scrape 而不是將其插入到編輯器:

我們扒取頁面的功能僅僅用兩行程式碼就實現了,因為 cheerio 已經替我們做好了所有的工作!我們通過呼叫load 方法載入 HTML 字串來建立一個 $ 函式,然後用這個函式來執行 jQuery 語句並返回結果。你可以在官方 開發者文件 檢視完整的 Cheerio API

測試更新後的軟體包

重新載入 Atom 並在一個選中的 StackOverflow URL 上執行 soucefetch:fetch 以檢視到目前為止的進度。

如果我們在一個有采納答案的頁面上執行這條命令,程式碼片段將會被插入到編輯器中:

製作你的第一個 Atom 文字編輯器外掛

如果我們在一個沒有采納答案的頁面上執行這條命令,將會彈出一個警告通知:

製作你的第一個 Atom 文字編輯器外掛

我們最新的 fetch 函式給我們提供了一個 StackOverflow 頁面的程式碼片段而不再是整個 HTML 內容。要注意我們更新的 fetch 函式會檢查有沒有答案並顯示通知以提醒使用者。

sourcefetch 教程倉庫 檢視這一步所有的程式碼更改。

實現用來查詢相關的 StackOverflow URL 的谷歌搜尋功能

現在我們已經將 StackOverflow 的 URL 轉化為程式碼片段了,讓我們來實現最後一個函式——search,它應該要返回一個相關的 URL 並附加一些像“hello world”或者“快速排序”這樣的描述。我們會通過一個非官方的google npm 模組來使用谷歌搜尋功能,這樣可以讓我們以程式設計的方式來搜尋。

安裝這個 Google npm 模組

通過在軟體包的根目錄開啟命令列工具並執行命令來安裝 google 模組:

引入並配置模組

lib/sourcefetch.js 的頂部為 google 模組新增一條引用語句:

我們將配置一下 google 以限制搜尋期間返回的結果數。將下面這行程式碼新增到引用語句下面以限制搜尋返回最熱門的那個結果。

實現 search 函式

接下來讓我們來實現我們的 search 函式:

以上程式碼通過谷歌來搜尋一個和指定的關鍵詞以及程式語言相關的 StackOverflow 頁面,並返回一個最熱門的 URL。讓我們看看這是怎樣來實現的:

我們使用使用者輸入的查詢和當前所選的語言來構造搜尋字串。比方說,當使用者在寫 Python 的時候輸入“hello world”,查詢語句就會變成 hello world in python site:stackoverflow.com。字串的最後一部分是谷歌搜尋提供的一個過濾器,它讓我們可以將搜尋結果的來源限制為 StackOverflow。

我們將 google 方法放在一個 Promise 裡面,這樣我們可以非同步地返回我們的 URL。我們會傳遞由 google返回的所有錯誤並且會在沒有可用的搜尋結果的時候返回一個錯誤。否則我們將通過 resolve 來解析最熱門結果的 URL。

更新 fetch 來使用 search

我們的最後一步是更新 fetch 函式來使用 search 函式:

讓我們看看發生了什麼變化:

  • 我們選中的文字現在變成了使用者輸入的 query
  • 我們使用 TextEditor API 來獲取當前編輯器選項卡使用的 language
  • 我們呼叫 search 方法來獲取一個 URL,然後通過在得到的 Promise 上呼叫 then 方法來訪問這個 URL

我們不在 download 返回的 Promise 上呼叫 then 方法,而是在前面 search 方法本身鏈式呼叫的另一個then 方法返回的 Promise 上面接著呼叫 then 方法。這樣可以幫助我們避免回撥地獄

sourcefetch 教程倉庫 檢視這一步所有的程式碼更改。

測試最終的外掛

大功告成了!重新載入 Atom,對一個“問題描述”執行軟體包的命令來看看我們最終的外掛是否工作,不要忘了在編輯器右下角選擇一種語言。

製作你的第一個 Atom 文字編輯器外掛

下一步

現在你知道怎麼去 “hack” Atom 的基本原理了,通過 分叉 sourcefetch 這個倉庫並新增你的特性 來隨心所欲地實踐你所學到的知識。

相關文章