Blast:一個能執行 Raycast 擴充功能的 React.js 渲染器
2023 年新年快樂!新年有什麼一定要做的事呢?對我來說就是挖新坑寫新專案無誤!這次的專案叫做 Blast,就如標題寫的,它是一個「能執行 Raycast 擴充功能的 React.js 渲染器(Renderer)」。
Raycast 和 React.js
對於第一次聽到 Raycast 的朋友,我以前也寫過一篇推坑文,Raycast 是一套 macOS 用的啟動器軟體(Launcher),提供一個關鍵字搜尋的介面,輸入關鍵字,就能快速啟動程式或進行其他操作。過往類似的軟體也不少,最知名的就是 Alfred,但 Raycast 最不同的一點,就是可以用 React.js 來寫的擴充功能。這邊我之前的文章也有簡略帶過,大家可以看官方的 todo-list 範例感受一下,有點像在寫 React Native,雖然寫的是 JavaScript,但卻能有原生的介面而非網頁。
React.js 相信近年熟悉前端開發的朋友應該都多少接觸過(斷言),React.js 提供了聲明式(Declarative)的 API 來開發使用者介面(User Interface),而使用者介面可以用階層、樹狀的結構來表示,去看看設計師 Figma/illustrator/Sketch 裡的圖層和群組物件就有感覺了,有的設計工具甚至還提供「輸出 React.js 元件」的功能。
剛提到了「用 React 來實作使用者元件」,React 核心就是一套 Virtual DOM/Diff/Update 的邏輯,無關終端平臺為合,Android、iOS 還是 Desktop,只要你實作 React 提供的介面,什麼平臺都可以寫 React,所以才有了 React Native 等一眾自定渲染器(Custom Renderer)。
Raycast 能夠用 React 來寫擴充功能,也是內嵌(ㄑㄧㄢ,誰這裡念砍我就…)Node.js 並實作渲染器。寫到這,Raycast 和 React 和 Renderer 的關係,大家應該多瞭解一咪咪了吧 XD 下面就來講講我為何決定做這個題目。
動機
七個字:Raycast 我推我超!
從擴充功能 API 上線一年多以來,我已經寫了十個 Raycast 擴充功能,還因此收到了 Raycast 寄來的開發者大禮包。Raycast 有著 Tier 0 的開發者體驗,雖說頭幾個寫的擴充功能拿來練手成分比較高,但後期開發的擴充功能如 HackMD ,我自己都天天在用啊,開筆記搜筆記都超快超方便的(棒讀業配環節)
另一個比較黑的理由就是我內心的平衡人真島大哥在作祟!隨著 Raycast 開發者社群的壯大,截止行文當日(2023/1/2)已上架 742 個擴充功能,雖然擴充功能的開源授權為 MIT,但除了拿來做擴充功能開發做參考外,這些擴充功能也只能跑在 Raycast 的封閉平臺上,除了僅限一家也僅限 macOS 一個平臺。
我就想到 VSCodium 這個專案,雖然 VSCode 也是開放程式碼,但實際上的發佈版,還是包了遙測和一些追蹤碼。欸,全球的軟黑產業鏈似乎都動起來了!VSCodium 就是把 VSCode 有疑慮的授權程式碼部分和遙測通通拔掉,重新發佈的「乾淨版」。這樣 484 在臭各種 SaaS 產品啊 XD
至於擴充功能商店的部分當然也不放過,Open VSX 是一個提供相容 VSCode 編輯器的擴充功能商店。VSCode 至今已經被作為各個開源 IDE/編輯器的基礎元件來使用,比如用 Theia IDE 做的 IDE 們,就是相容 VSCode 擴充功能的 IDE。Open VSX 讓這些支援的編輯器也能免費擼 VSCode 平臺的擴充功能,打不贏就加入!
總之,「讓 Raycast 的擴充功能被跨平臺並開放地使用」,就是本專案的目標!Show me what you got!
開發 Blast
Blast 就是基於上述理由所發起的專案。這兩週陸陸續續的開發過程可以分成三個階段:
- 架構設計、技術選擇與基本踩雷試錯
- React 渲染器 & 後端開發
- Client App 開發
架構設計、技術選擇與基本踩雷試錯
開發過程和往常一樣,都丟在我的 GitHub 上,這次還額外開了 Project Board 來試用。
一開始我就決定目標:要把 Raycast 官方擴充功能範例中 Todo List 原汁原味一刀未減的跑起來。寫完這張 Ticket 後,就開始研究 Custom React renderer 該怎麼實作,最後找到 Jam Risser 的 Building a Custom React Renderer,演講內容和程式碼範例都相當清楚,值得參考。
再來就是撞牆階段,一開始問 ChatGPT 用 Rollup 該怎麽設定 package alias,因為原始的 Raycast 擴充功能會用 import { XXX } from '@raycast/api'
,而 @raycast/api
正式要替換為我自己實作的部分。沒想到開始用 Rollup 才是噩夢的開始,因為我要做的是 Application,鐵定會有一堆有的沒的相依套件,但我本來想把寫的程式打包成 ESM,於是在浪費一堆時間後就直接果斷換回熟悉的 Webpack,半小時內搞定 XD
在瘋狂搗鼓 React Renderer 並有了基本理解後,我設計了以下的架構:
明明是個本機跑的 Launcher App 卻還要弄前後端?這裡的設計我很大的參考了早期的 React native。要寫一個自己的 React Renderer,我們會用到 react-reconciler
這個套件,並且實現 Host Config,大概會像這個檔案一樣:
import Reconciler from "react-reconciler"; const MyCustomReactRenderer = Reconciler({ createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) { // .... }, appendChildToContainer(container, child) { // ... } })
Host Config 就是 Reconciler 函式的參數。你會看到一些類似操作 DOM 的方法,例如 appendChild
,removeChild
等,因為那就是實作給 React 內部更新機制:Reconciliation 呼叫的方法。React 經由一系列騷操作,決定要更新的內容時,就會呼叫 Host Config 提供的方法,更新內容到你的目標平臺上。比如 ReactDOM 就是實作了操作 DOM 的 Host Config,React.js 才能將元件渲染到網頁上。
上一段極其簡陋的說明了 React renderer 和 reconciler 的關係。說回來 React Native,在之前的 React Native 版本,Renderer 部分運用 Bridge Service,讓 JS engine 與 Native 部分溝通,你可以想象 React 內部在構建一顆樹狀結構,所以呼叫 appendChild
那些方法,Renderer 會把這些操作和參數佇列轉成 JSON 送到 Bridge,讓訂閱 Bridge 的 Native 端也一樣建造一棵樹(Shadow Tree),Native 端再由這顆樹狀的資料結構將原生的元件產生出來。
之前的 React Native 用 JSON Bridge 來溝通,那 Blast 也用這個方法就好?但 React Native 傳送的是建造樹的操作過程(operation),在 Native 端還要實作各個方法,讓 Native 端也能依照操作建立樹結構。不過我沒有甚麼效能要求,直接透過 JSON 序列化傳送整顆樹過去還比較快,使用者端甚至還可以用 React.js 直接把樹渲染,反正 React 會幫我處裡 Diffing/Re-render,我只要負責把 React Component Tree 建出來就好啦!Use React Everywhere!Learn Once Write Everywhere!諾貝爾獎是我的啦!好耶!!!努力!未來!A Beautiful Star!
以上就是在摸索中慢慢確立的 Blast 架構,剩下來的就是:實作!實作!實作!
React 渲染器 & 後端開發
老實說這部分在主架構實作完後就沒遇到太多困難,都是在填肉。比較值得一提的是我用了 rpc-websocket
這套雙向 RPC 溝通的 Library。
為何需要雙向?在上面提到的 Blast 架構中,前端需要拿到後端建立的 Component Tree(client request server),後端在更新時也需要通知前端(server request client),前端有事件需要觸發時,也需要通知後端(client request server),所以請求其實是雙向的。RPC library 會幫你處理好 function parameter serialization/deserialization/dynamic event registration 等麻煩事,所以直接找一套現有的還是最快。謝謝你,開源超人。
Client App 開發
最後就是麻煩但挑戰性相對本專案後端低的前端部分。本來想試一下 Tauri 還是 Wails 的,但查了一下 menubar API 的部分還是那個萬惡 JS World 的 Electron 支援比較完整及簡單,反正就是層 Web 皮嘛,就還是用了 Electron(逃)
下面的動圖是 Blast 前端的 DevTool,可以看到目前渲染的 JSON Component Tree 長怎麼樣,也可以送 rpc
的事件給後端。
元件部分,除了 Tailwind/TypeScript/React.js 基本三套組外,還用了之前被分享到 Raycast Slack 社群的 cmdk,他也是深深受到了 Raycast API 的影響,看看他 API 長那什麼樣子就知道。cmdk 還實作了 Raycast 完全に一致 的主題,所以我就直接搬過來用了,哈,哈。
做到可以 Demo 的程度啦:Todo List Demo
欸,你以為這張要放在最前面嗎,放這麼下面就是要騙你捲到最後啦!
以下的錄影,就展示了在 Blast 使用 Raycast Todo list 擴充功能的 Create/Read/Delete 等操作。除了擴充功能本身一行未改,就連圖示也是原汁原味,滿足感之高,值得在半夜吶喊大吼吵醒各位室友們!!
雖然 Demo Driven Development/Talk DD/Blog DD 的推力只能讓我實作到這,但還是想慢慢把剩下來的 Raycast API 弄完啊,如果看我幾週後都沒 commit,那大概就是這樣了 XD(哪樣)
除了我實作的 Blast 之外,最近還看到兩個啟動器專案,彷彿都受到了 Raycast 的感召,下面簡單介紹一下:
Sunbeam Launcher
作者本人也是 Raycast 擴充功能的貢獻者之一,我也是看到他在 Slack 社群裡分享的。Raycast heavily inspired,從專案名稱就看的出來 XD
Sunbeam 的擴充功能就是輸出 JSON 格式的 Shell Script,所以可以用各種語言來寫,或是編譯成執行檔,反正 output 出來就沒你的事了。Sunbeam cli 會提供類似 Raycast 的 TUI 當做使用者介面來用,夠 Geek!夠帥!
真 GUI 部分,目前實作是直接拿 xterm.js 跑 sunbeam cli 的 TUI 來當 UI,直到昨天都還在 commit 而已,讓我們期待後續發展。
Script kit
開源 & MIT Licensed,同樣基於萬惡 Electron,JavaScript 生態系。原本看名稱以為是只支援到 Script 粒度,看了 API 發現能做的事還不少,Launcher 界明日之星!
說著說著我們 Launcher 光譜圖都可以畫滑起來了,從前端選擇、跨平臺與否、生態圈 API 開放程度以及技術選擇,做產品總是個大坑啊,二ㄏ、二ㄏ
鳴謝
謝謝 ChatGPT,在我寫扣無聊的時候陪我聊天,在我懶的 Google 的時候幫助坑我。
謝謝 Copilot,在我懶的打字的時候幫助我成為更好的 Tab 鍵工程師。
謝謝孤獨搖滾,在我累的時候,心中的虹太陽燃起了我的動力。
本篇文章還沒有使用 AI 校稿,但是我的 GitHub README 有 ChatGPT 幫忙潤稿。