--- published: true date: "2023-01-02 23:40" --- # Blast:一個能執行 Raycast 擴充功能的 React.js 渲染器 2023 年新年快樂!新年有什麼一定要做的事呢?對我來說就是挖新坑寫新專案無誤!這次的專案叫做 Blast,就如標題寫的,它是一個「能執行 Raycast 擴充功能的 React.js 渲染器(Renderer)」。 ## Raycast 和 React.js 對於第一次聽到 [Raycast](https://www.raycast.com/) 的朋友,我以前也寫過一篇[推坑文][blog-post],Raycast 是一套 macOS 用的啟動器軟體(Launcher),提供一個關鍵字搜尋的介面,輸入關鍵字,就能快速啟動程式或進行其他操作。過往類似的軟體也不少,最知名的就是 [Alfred](https://www.alfredapp.com/),但 Raycast 最不同的一點,就是可以用 React.js 來寫的擴充功能。這邊我之前的文章也有[簡略帶過][blog-post],大家可以看官方的 [todo-list 範例](https://github.com/raycast/extensions/tree/main/examples/todo-list)感受一下,有點像在寫 React Native,雖然寫的是 JavaScript,但卻能有原生的介面而非網頁。 [blog-post]: https://yukai.dev/blog/2022/05/20/raycast-for-developers 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 等[一眾自定渲染器](https://github.com/chentsulin/awesome-react-renderer)(Custom Renderer)。 Raycast 能夠用 React 來寫擴充功能,也是內嵌(ㄑㄧㄢ,誰這裡念砍我就...)Node.js 並實作渲染器。寫到這,Raycast 和 React 和 Renderer 的關係,大家應該多瞭解一咪咪了吧 XD 下面就來講講我為何決定做這個題目。 ## 動機 七個字:Raycast 我推我超! 從擴充功能 API 上線一年多以來,我已經寫了[十個 Raycast 擴充功能](https://www.raycast.com/Yukai),還因此收到了 Raycast 寄來的[開發者大禮包](https://www.facebook.com/photo.php?fbid=6185229718163261&set=p.6185229718163261&type=3)。Raycast 有著 **Tier 0** 的開發者體驗,雖說頭幾個寫的擴充功能拿來練手成分比較高,但後期開發的擴充功能如 [HackMD](https://www.raycast.com/Yukai/hackmd) ,我自己都天天在用啊,開筆記搜筆記都超快超方便的(棒讀業配環節) 另一個比較黑的理由就是我內心的[平衡人真島大哥](https://lycoris-recoil.com/)在作祟!隨著 Raycast 開發者社群的壯大,截止行文當日(2023/1/2)已上架 742 個擴充功能,雖然擴充功能的開源授權為 MIT,但除了拿來做擴充功能開發做參考外,這些擴充功能也只能跑在 Raycast 的封閉平臺上,除了僅限一家也僅限 macOS 一個平臺。 我就想到 [VSCodium](https://vscodium.com/) 這個專案,雖然 VSCode 也是開放程式碼,但實際上的發佈版,還是包了遙測和一些追蹤碼。欸,全球的**軟黑產業鏈**似乎都動起來了!VSCodium 就是把 VSCode 有疑慮的授權程式碼部分和遙測通通拔掉,重新發佈的「乾淨版」。這樣 484 在臭各種 SaaS 產品啊 XD 至於擴充功能商店的部分當然也不放過,[Open VSX](https://open-vsx.org/) 是一個提供相容 VSCode 編輯器的擴充功能商店。VSCode 至今已經被作為各個開源 IDE/編輯器的基礎元件來使用,比如用 Theia IDE 做的 IDE 們,就是相容 VSCode 擴充功能的 IDE。Open VSX 讓這些支援的編輯器也能免費擼 VSCode 平臺的擴充功能,打不贏就加入! 總之,「**讓 Raycast 的擴充功能被跨平臺並開放地使用**」,就是本專案的目標!Show me what you got! ## 開發 Blast [Blast](https://github.com/Yukaii/blast) 就是基於上述理由所發起的專案。這兩週陸陸續續的開發過程可以分成三個階段: - 架構設計、技術選擇與基本踩雷試錯 - React 渲染器 & 後端開發 - Client App 開發 ### 架構設計、技術選擇與基本踩雷試錯 開發過程和往常一樣,都丟在我的 [GitHub](https://github.com/Yukaii/blast) 上,這次還額外開了 [Project Board](https://github.com/users/Yukaii/projects/4) 來試用。 一開始我就[決定目標](https://github.com/Yukaii/blast/issues/3):要把 Raycast 官方擴充功能範例中 [Todo List](https://github.com/raycast/extensions/tree/main/examples/todo-list) 原汁原味一刀未減的跑起來。寫完這張 Ticket 後,就開始研究 Custom React renderer 該怎麼實作,最後找到 Jam Risser 的 [Building a Custom React Renderer](https://www.youtube.com/watch?v=SXx-CymMjDM),演講內容和程式碼範例都相當清楚,值得參考。 再來就是撞牆階段,一開始問 ChatGPT 用 Rollup 該怎麽設定 package alias,因為原始的 Raycast 擴充功能會用 `import { XXX } from '@raycast/api'`,而 `@raycast/api` 正式要替換為我自己實作的部分。沒想到開始用 Rollup 才是噩夢的開始,因為我要做的是 Application,鐵定會有一堆有的沒的相依套件,但我本來想把寫的程式打包成 ESM,於是在浪費一堆時間後就直接果斷換回熟悉的 Webpack,半小時內搞定 XD 在瘋狂搗鼓 React Renderer 並有了基本理解後,我設計了以下的架構: ![](https://hackmd.io/_uploads/rJmutOg9j.png) 明明是個本機跑的 Launcher App 卻還要弄前後端?這裡的設計我很大的參考了早期的 React native。要寫一個自己的 React Renderer,我們會用到 `react-reconciler` 這個套件,並且實現 Host Config,大概會像[這個檔案一樣](https://github.com/Yukaii/blast/blob/b201eded481b8c54b3e3f38e2284a44a86dd0aee/src/reconciler.ts#L39-L77): ```javascript 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](https://github.com/facebook/react/blob/de7d1c90718ea8f4844a2219991f7115ef2bd2c5/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js),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`](https://github.com/elpheria/rpc-websockets) 這套雙向 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](https://tauri.app/) 還是 [Wails](https://wails.io/) 的,但查了一下 menubar API 的部分還是那個萬惡 JS World 的 Electron 支援比較完整及簡單,反正就是層 Web 皮嘛,就還是用了 Electron(逃) 下面的動圖是 Blast 前端的 DevTool,可以看到目前渲染的 JSON Component Tree 長怎麼樣,也可以送 `rpc` 的事件給後端。 ![](https://hackmd.io/_uploads/BJqFK_x9s.png) 元件部分,除了 Tailwind/TypeScript/React.js 基本三套組外,還用了之前被分享到 Raycast Slack 社群的 [cmdk](https://github.com/pacocoursey/cmdk),他也是深深受到了 Raycast API 的影響,看看他 API 長那什麼樣子就知道。cmdk 還實作了 Raycast **完全に一致** 的主題,所以我就直接搬過來用了,哈,哈。 ## 做到可以 Demo 的程度啦:Todo List Demo 欸,你以為這張要放在最前面嗎,放這麼下面就是要騙你捲到最後啦! 以下的錄影,就展示了在 Blast 使用 Raycast Todo list 擴充功能的 Create/Read/Delete 等操作。除了擴充功能本身一行未改,就連[圖示也是原汁原味](https://icon.ray.so/),滿足感之高,值得在半夜吶喊大吼吵醒各位室友們!! ![](https://hackmd.io/_uploads/ry79Ydl9o.png) 雖然 Demo Driven Development/Talk DD/Blog DD 的推力只能讓我實作到這,但還是想慢慢把剩下來的 Raycast API 弄完啊,如果看我幾週後都沒 commit,那大概就是這樣了 XD(哪樣) 除了我實作的 [Blast](https://github.com/Yukaii/blast) 之外,最近還看到兩個啟動器專案,彷彿都受到了 Raycast 的感召,下面簡單介紹一下: ### [Sunbeam Launcher](https://github.com/sunbeamlauncher/sunbeam) 作者本人也是 Raycast 擴充功能的貢獻者之一,我也是看到他在 Slack 社群裡分享的。Raycast heavily inspired,從專案名稱就看的出來 XD Sunbeam 的擴充功能就是輸出 JSON 格式的 [Shell Script](https://github.com/sunbeamlauncher/sunbeam/tree/main/examples),所以可以用各種語言來寫,或是編譯成執行檔,反正 output 出來就沒你的事了。Sunbeam cli 會提供類似 Raycast 的 TUI 當做使用者介面來用,夠 Geek!夠帥! [真 GUI 部分](https://github.com/sunbeamlauncher/sunbeam-gui),目前實作是直接拿 xterm.js 跑 sunbeam cli 的 TUI 來當 UI,直到昨天都還在 commit 而已,讓我們期待後續發展。 ### [Script kit](https://www.scriptkit.com/) 開源 & MIT Licensed,同樣基於萬惡 Electron,JavaScript 生態系。原本看名稱以為是只支援到 Script 粒度,看了 [API](https://github.com/johnlindquist/kit/blob/main/API.md) 發現能做的事還不少,Launcher 界明日之星! --- 說著說著我們 Launcher 光譜圖都可以畫~~滑~~起來了,從前端選擇、跨平臺與否、生態圈 API 開放程度以及技術選擇,做產品總是個大坑啊,二ㄏ、二ㄏ ## 鳴謝 謝謝 [ChatGPT](https://chat.openai.com/chat),在我寫扣~~無聊的時候陪我聊天~~,在我懶的 Google 的時候幫助~~坑~~我。 謝謝 [Copilot](https://github.com/features/copilot),在我懶的打字的時候幫助我成為更好的 Tab 鍵工程師。 謝謝[孤獨搖滾](https://bocchi.rocks/),在我累的時候,心中的虹太陽燃起了我的動力。 本篇文章還沒有使用 AI 校稿,但是我的 [GitHub README](https://github.com/Yukaii/blast) 有 ChatGPT 幫忙潤稿。