就說我很認真在選封面圖片嘛

4. 網頁控制面板

上一篇文章結尾我自己挖的坑:「網頁控制面板留到下一篇」,現在該來乖乖填坑了。

我想要做的很簡單:打開網頁,用 Discord 帳號登入,選一個機器人所在的伺服器,然後就可以在網頁上搜尋歌曲、暫停、跳過、查看播放佇列——所有你在 Discord 裡用斜槓指令能做的事,都能在網頁上完成,而且是即時同步的。

既然是自己做著好玩的專案,我索性選了個沒用過的新玩具:SvelteKit 來當前端框架,順便學點新東西。

4.1 SvelteKit 的路由

SvelteKit 的路由系統挺好懂的,基本上就是資料夾長怎樣,網址就長怎樣。舉個例子:

src/routes/
├── +page.svelte                          → /
├── dashboard/
│   ├── +page.svelte                      → /dashboard
│   └── [guildId]/
│       └── +page.svelte                  → /dashboard/abc123
└── auth/
    ├── login/+server.ts                  → /auth/login
    ├── callback/+server.ts               → /auth/callback
    └── logout/+server.ts                 → /auth/logout

每個路由可以有兩個搭檔文件:

  • +page.server.ts:在伺服器端執行,負責載入資料(例如查詢資料庫、呼叫 API)。它回傳的資料會自動傳給對應的 .svelte 頁面。
  • +page.svelte:在瀏覽器端渲染的 UI 元件,透過 $props() 就能拿到伺服器端載入的資料。

另外還有一種叫 +server.ts,它不負責畫畫面,純粹用來處理後端的 API 請求。我們接下來要講的Discord 登入,就是靠它來搞定的。

4.2 Discord OAuth2 登入

要讓大家在網頁上控制機器人,第一步總得先知道「你是誰」對吧?這時候就遇到了第一個大難題:標準的 OAuth2 登入流程。聽起來好像很複雜,其實就是三步:

  1. 點擊「用 Discord 登入」,網頁把你重新導向到 Discord 的授權頁面,問你:「這個應用程式想看你的大頭貼和伺服器清單,可以嗎?」
  2. 使用者按下同意,Discord 就會把你送回我的網站,並且在網址上塞給你一個「一次性授權碼」。
  3. 我的網頁後端拿到這個碼,跑去跟 Discord 換真正的「Access Token」,然後查出你的名字和頭像,最後發一個簽名過的 Cookie 給你,這樣就算登入成功啦!

這裡所有的 Token 交換都發生在伺服器端,瀏覽器永遠不會碰到 Access Token。文檔看了好久才搞定這個破登入

4.3 伺服器選擇器

登入之後,使用者會看到一個伺服器選擇頁面。但這裡有個問題:我總不能把你加入的所有私人伺服器都列出來吧?我只能顯示你跟機器人同時都在的伺服器。

所以載入這個頁面時,後端會做兩件事:

  1. 用你的的 Access Token 去問 Discord:「他加入了哪些伺服器?」
  2. 透過 WebSocket 去問機器人:「你現在在哪些伺服器裡?」

然後取兩個清單的交集——只顯示使用者和機器人同時都在的伺服器。點擊任意一個伺服器卡片,就會進入該伺服器的音樂控制頁面。

4.4 即時音樂控制

這才是整個網頁面板最好玩的部分。

進入控制頁面之後,頁面會直接從瀏覽器跟機器人的 WebSocket 伺服器建立連線,並且「訂閱」這個伺服器的狀態更新。一旦訂閱成功,機器人就會把當前的播放狀態推送過來:正在播什麼歌、佇列裡有什麼、有沒有暫停、播到哪了。

之後,不管狀態怎麼變化——不管是有人在 Discord 裡打了 /skip,還是有人在網頁上按了暫停,機器人都會把最新的狀態推送給所有訂閱了這個伺服器的瀏覽器。

4.5 事件匯流排(Event Bus)

那怎麼讓 Discord 指令和網頁控制互相同步?

舉個例子:我在 Discord 打了 /pause,網頁上的暫停按鈕也應該要跟著變。反過來,我在網頁上按了跳過,Discord 裡的播放佇列也要跟著更新。

我一開始忘了處理這個同步問題,導致在 Discord 裡暫停音樂後,網頁完全沒反應——因為斜槓指令只是直接操作了播放器,沒有通知任何人「我改了狀態」。

解決方法是加一個簡單的事件匯流排(Event Bus)。不管狀態是從哪裡被改變的(Discord 指令、網頁按鈕、歌曲自然播完),都會經過同一個通知機制。WebSocket 伺服器監聽這個事件,然後把最新狀態廣播給所有訂閱中的瀏覽器。

Discord 指令 ──→ 改變佇列狀態 ──→ 發送事件 ──→ 廣播給網頁

網頁控制按鈕 ──→ 改變佇列狀態 ──→ 發送事件 ──→ 廣播給網頁

歌曲自然播完 ──→ 改變佇列狀態 ──→ 發送事件 ──→ 廣播給網頁

所有的路徑最終都匯聚到同一個出口,這就保證了不管變化從哪裡來,所有的客戶端都會同步更新。

結語

到這裡,一個帶有網頁控制面板的 Discord 音樂機器人就基本成形了。

1.00
給你看一眼這個超醜的介面~

回頭看,其實整個專案最有趣的部分不是寫 UI~~(其實也沒寫多少)~~,而是搞清楚這些不同的系統之間要怎麼溝通:機器人怎麼跟 Lavalink 溝通、網頁怎麼跟機器人溝通、Discord 指令和網頁操作怎麼互相同步。

目前的介面還很陽春,功能上也還有不少可以改進的地方(例如搜尋結果列表、拖曳排序佇列、音量調整等等)。也許之後有興趣了,可以再回來改進一下UI介面設計。

能有耐心看到這裡的話...我真的好愛你!感謝您看到這裡!如果你也在折騰類似的,希望這篇文章能給你一些靈感。有任何問題都歡迎留言!

留言區