github.com/Yukaii
Daily Oops

Daily Oops!

https://yukai.dev/blog/2023/01/02/blast-the-raycast-extension-react-rendererBlast:一個能執行 Raycast 擴充功能的 React.js 渲染器https://fed.brid.gy/
January 2, 2023

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 的方法,例如 appendChildremoveChild 等,因為那就是實作給 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 EverywhereLearn 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 幫忙潤稿。

https://fed.brid.gy/
本篇文章驕傲的使用 HackMD 發佈
Yukai Huanghttps://yukai.dev

Hi

This is Yukai Huang's personal website.

Here you can read my recent posts, play with my side projects before, or get to know me more.

安久吧!

https://fed.brid.gy/