REST API設計最佳實務與常見錯誤

更新於 發佈於 閱讀時間約 23 分鐘

在前面的文章中,我們已經詳細介紹了 HTTP 協議,包括它的結構、請求方法、狀態碼等內容。如果你對 HTTP 協議還不夠熟悉,可以回顧之前的篇章。今天,我們將進一步深入探討 REST API 設計,重點關注如何運用 HTTP 協議來構建高效且擴展性強的 API,並介紹一些進階設計原則與實踐。


什麼是 REST API?

REST API(Representational State Transfer Application Programming Interface)是一種基於 HTTP 協議的 API 設計風格。它通過資源導向的設計,使應用之間的數據交換和操作更加簡單和直觀。REST API 的核心是將「資源」作為中心,並充分利用 HTTP 協議的特性(如方法、狀態碼和 Headers)。

簡單來說,REST API 就像是一個針對資源的服務,每個資源都有自己的 URL,而對這些資源的操作則是通過不同的 HTTP 方法來完成的。


REST API 設計的核心原則

設計一個優秀的 REST API 需要遵循一些核心原則,這不僅能保證功能的實現,也有助於保持 API 的一致性、可擴展性和可維護性。


1. 資源導向設計

REST API 的設計應以「資源」為中心,而非行為或操作。每個資源都應該有一個唯一的 URI(統一資源標識符),資源通常是應用中的某個實體(如「用戶」或「訂單」)。設計時,應通過 URI 來訪問具體資源。

範例:

  • /users:表示所有用戶的資源集合。
  • /users/123:表示 ID 為 123 的具體用戶資源。

這樣的設計使得 API 使用者可以輕易理解 API 的結構,並直觀地知道如何訪問或操作資源。


2. 正確使用 HTTP 方法

HTTP 方法定義了對資源應該進行的操作。在 REST API 中,正確使用這些方法至關重要,因為它們賦予 API 自然且一致的行為。

  • GET:檢索資源,用於從伺服器獲取資料。這是一個安全且無副作用的操作。
  • POST:創建資源,用於向伺服器提交數據並創建新資源。這是有副作用的操作。
  • PUT:更新資源,替換現有資源的整體內容,是冪等操作(多次執行結果一致)。
  • PATCH:部分更新資源,靈活地修改現有資源的一部分。
  • DELETE:刪除資源,移除指定的資源,也是冪等操作。

正確的操作方法可以提升 API 的一致性與可讀性,讓開發者清楚知道每個請求應該如何工作。


3. 無狀態性

REST API 是無狀態的,每一次的請求都是獨立的。伺服器不會保留客戶端的上下文信息,因此每個請求必須包含完成操作所需的所有信息。這種設計讓 API 更容易擴展,因為請求可以分散到不同的伺服器進行處理。

範例:

GET /users/123
Host: api.example.com
Authorization: Bearer <token>

這樣的請求包含了所有必需的信息,伺服器無需依賴之前的請求上下文。


4. 正確使用 HTTP 狀態碼

REST API 通過 HTTP 狀態碼反饋請求結果。正確地使用狀態碼能夠幫助 API 使用者快速了解請求的結果或錯誤原因。

常見狀態碼:

  • 200 OK:請求成功,並返回資源。
  • 201 Created:創建成功(適用於 POST 請求)。
  • 204 No Content:請求成功,但無需返回資源(適用於 DELETE 請求)。
  • 400 Bad Request:無效請求。
  • 401 Unauthorized:未經授權,請求需要身份驗證。
  • 404 Not Found:資源不存在。
  • 500 Internal Server Error:伺服器內部錯誤。

5. 支持過濾、分頁和排序

當 API 返回大量數據時,應該支持過濾、分頁和排序,這不僅提高了效率,也使得客戶端能夠靈活地篩選和處理數據。

範例:

GET /users?age=25&limit=10&page=2&sort=created_at

這樣的請求會過濾出年齡為 25 的用戶,並按創建時間排序,返回第二頁的 10 個結果。


6. 避免過度設計

REST API 設計應保持簡潔和易於使用。過度設計會讓 API 變得難以理解和維護。應避免在 URI 中混入動詞或具體操作,如 /getAllUsers/deleteUserById。這些操作應通過 HTTP 方法來實現,而非通過 URI。


進階設計:版本控制與擴展性

隨著 API 的演變,應用需求的變化可能會影響 API 的設計。因此,版本控制和擴展性在設計過程中是不可忽視的要素。


1. 版本控制

API 需要隨著時間推進進行升級或變更,因此保持對現有使用者的兼容性至關重要。常見的版本控制方法包括:

  • URI 版本控制:如 /v1/users
  • Header 版本控制:通過 HTTP Header 指定版本號,如 Accept: application/vnd.example.v1+json
  • 參數版本控制:使用查詢參數來指定版本,如 /users?version=1

在實際應用中,URI 版本控制最為常見和直觀。


2. 向後兼容與擴展性

在設計 API 時,要盡量避免對現有字段的移除或修改。應通過新增字段或路由來進行擴展,以保證對現有使用者的兼容性。

範例:

{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"new_field": "new_value" // 新增字段,保持向後兼容
}

這樣設計的 API 可以保持現有功能不變,並允許新需求的擴展。


不好的 REST API 設計範例

以下是一個設計不佳的 REST API 範例,並解釋了其中的問題:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private static List<User> Users = new List<User>
{
new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 },
new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 25 }
};

// GET: api/users
[HttpGet("action")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult<IEnumerable<User>> GetAllUsers()
{
return Ok(Users);
}

// POST: api/users/action/add
[HttpPost("action/add")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult<User> CreateNewUser([FromBody] User newUser)
{
newUser.Id = Users.Max(u => u.Id) + 1;
Users.Add(newUser);
return Ok(newUser); // 設計錯誤:應該返回 201 Created
}

// PUT: api/users/action/update/{id}
[HttpPut("action/update/{id}")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult UpdateUserDetails(int id, [FromBody] User updatedUser)
{
var user = Users.FirstOrDefault(u => u.Id == id);
if (user == null)
{
return NotFound();
}
user.Name = updatedUser.Name;
user.Email = updatedUser.Email;
user.Age = updatedUser.Age;
return Ok(user); // 設計錯誤:應該返回 204 No Content
}

// DELETE: api/users/action/delete/{id}
[HttpDelete("action/delete/{id}")]//設計錯誤: 違背了資源導向的設計原則
public ActionResult RemoveUser(int id)
{
var user = Users.FirstOrDefault(u => u.Id == id);
if (user == null)
{
return NotFound();
}
Users.Remove(user);
return Ok(); // 設計錯誤:應該返回 204 No Content
}

// 自定義操作方法,存在設計缺陷
[HttpPost("custom-action")] // 設計錯誤:不明確的操作,無法清楚理解該操作的目的
public ActionResult CustomOperation([FromBody] CustomRequest request)
{
// 根據 OperationType 字段來決定具體的操作
if (request.OperationType == "updateEmail")
{
var user = Users.FirstOrDefault(u => u.Id == request.UserId);
if (user == null)
{
return NotFound("User not found");
}
user.Email = request.Data; // 更新用戶的電子郵件
}
else if (request.OperationType == "deleteUser")
{
var user = Users.FirstOrDefault(u => u.Id == request.UserId);
if (user == null)
{
return NotFound("User not found");
}
Users.Remove(user); // 刪除用戶
}
else if (request.OperationType == "addNote")
{
var user = Users.FirstOrDefault(u => u.Id == request.UserId);
if (user == null)
{
return NotFound("User not found");
}
// 為用戶添加備註(在現有的資料上增加新數據)
// 假設用戶模型中存在 Notes 屬性
user.Notes.Add(request.Data);
}
else
{
return BadRequest("Unknown operation");
}

// 無論進行什麼操作,最終都返回相同的回應
return Ok("Custom operation performed");
}
}

public class CustomRequest
{
public string OperationType { get; set; }
public int UserId { get; set; }
public string Data { get; set; }
}

為什麼這個設計不佳?

  1. 混亂的 URI 設計
    • URI 包含 action 和其他具體操作動詞,如 action/add,這違背了資源導向的設計原則。應將 URI 專注於資源,而非操作行為。
    • 改進建議:應使用 /users 來表示所有用戶,使用 /users/{id} 來表示具體用戶,而不是 action/add 等不清晰的操作。
  2. 不正確的 HTTP 方法使用
    • 在 CreateNewUser 和 UpdateUserDetails 中,錯誤地使用了 200 OK 作為回應。正確的設計應該是使用 201 Created 和 204 No Content。
    • 改進建議:對於創建操作,應使用 201 Created,並返回新資源的 URI;對於更新操作,應使用 204 No Content。
  3. 錯誤的狀態碼返回
    • 在刪除和更新操作中返回了 200 OK,而不是更合適的 204 No Content,這讓使用者無法準確理解操作結果。
  4. 不明確的操作
    • CustomOperation 使用 OperationType 來決定執行的邏輯,這違反了單一職責原則,增加了 API 的複雜度,讓使用者難以理解具體的操作內容。
    • 改進建議:將不同的操作分開,給每個操作設計專門的 API 端點,而非依賴於操作參數。

針對第4點「不明確的操作」特別說明~為什麼這樣的設計是錯誤的?

  1. 操作多樣且含混不清
    • 這個 API 端點 custom-action 允許一個請求執行多種不同的操作,例如更新用戶的電子郵件、刪除用戶、或添加備註。
    • 當 API 使用者看到這個端點時,他們無法一眼確定它會執行什麼操作,因為具體的行為是由 OperationType 字段決定的。這與 REST API 的設計原則背道而馳,因為 REST API 強調「資源導向」,每個操作應該有一個明確且一致的 URI 和 HTTP 方法。
  2. 多個行為綁定在一個端點上
    • 在同一個端點中處理多種不相關的行為,違反了單一職責原則。API 使用者需要理解並處理多種操作類型,這增加了使用 API 的複雜度。
    • 例如,updateEmail 應該有一個獨立的端點,如 PUT /users/{id}/email,而 deleteUser 應該對應 DELETE /users/{id},這樣使用者可以直觀地知道每個端點具體執行什麼操作。
  3. 返回一致但無意義的結果
    • 無論請求進行了什麼操作,最後總是返回相同的回應 "Custom operation performed"。這並沒有提供足夠的上下文來告訴客戶端實際完成了什麼操作。如果發生錯誤或某些操作部分完成,用戶將無法得知具體情況。
  4. 不合適的狀態碼使用
    • 這個 API 端點對於每一個操作,無論是更新、刪除還是添加,最終都會返回 200 OK。然而,正確的做法是針對不同操作使用相應的狀態碼。例如: 對於更新操作,應返回 204 No Content。 對於創建新資源或數據,應返回 201 Created。 對於刪除操作,應返回 204 No Content。

改進後的 REST API 設計範例

[ApiController] 
[Route("api/[controller]")] // 定義這個控制器的路由,"api/[controller]" 會自動將控制器名稱替換為 "users"
public class UsersController : ControllerBase
{
// 模擬用戶資料的靜態列表
private static List<User> Users = new List<User>
{
new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 }, // 預設用戶 1
new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 25 } // 預設用戶 2
};

// GET: api/users
[HttpGet] // 定義 GET 請求,用於獲取所有用戶資料
public ActionResult<IEnumerable<User>> GetUsers()
{
// 返回 200 OK 狀態碼,並傳回用戶列表
return Ok(Users);
}

// POST: api/users
[HttpPost] // 定義 POST 請求,用於創建新的用戶
public ActionResult<User> CreateUser([FromBody] User newUser)
{
// 分配新用戶的 ID,使用現有用戶的最大 ID 加 1
newUser.Id = Users.Max(u => u.Id) + 1;
// 將新用戶加入用戶列表
Users.Add(newUser);
// 返回 201 Created 狀態碼,並附上新創建用戶
return CreatedAtAction(nameof(GetUsers), new { id = newUser.Id }, newUser);
}

// PUT: api/users/{id}
[HttpPut("{id}")] // 定義 PUT 請求,用於更新指定 ID 的用戶資料
public ActionResult UpdateUser(int id, [FromBody] User updatedUser)
{
// 根據 ID 查找現有用戶
var user = Users.FirstOrDefault(u => u.Id == id);
// 如果用戶不存在,返回 404 Not Found
if (user == null)
{
return NotFound();
}
// 更新用戶的資料
user.Name = updatedUser.Name;
user.Email = updatedUser.Email;
user.Age = updatedUser.Age;
// 返回 204 No Content,表示更新成功但不需要回傳內容
return NoContent();
}

// DELETE: api/users/{id}
[HttpDelete("{id}")] // 定義 DELETE 請求,用於刪除指定 ID 的用戶
public ActionResult DeleteUser(int id)
{
// 根據 ID 查找現有用戶
var user = Users.FirstOrDefault(u => u.Id == id);
// 如果用戶不存在,返回 404 Not Found
if (user == null)
{
return NotFound();
}
// 從列表中移除用戶
Users.Remove(user);
// 返回 204 No Content,表示刪除成功但不需要回傳內容
return NoContent();
}
}

每日小結

設計一個優秀的 REST API 不僅需要實現功能,還必須著眼於 API 的可用性、可維護性和擴展性。堅持資源導向設計、正確使用 HTTP 方法和狀態碼、保持無狀態性、並考慮版本控制和擴展性,這些設計原則能讓 API 更加一致、易於使用且具備彈性。通過避免常見的設計錯誤,我們可以提升 API 的品質,讓使用者在開發和使用過程中獲得更好的體驗。

留言
avatar-img
留言分享你的想法!
avatar-img
ChiYu Code Journey
0會員
12內容數
歡迎來到 ChiYu Code Journey!這裡是我分享技術心得與開發經驗的空間,主要內容涵蓋 C#、.Net、API 開發及雲端等程式主題。偶爾也會分享一些日常生活點滴,像是我與我家可愛的法鬥相處的趣事等。希望在這裡能和大家一起學習、交流,一同踏上這段程式旅程!
ChiYu Code Journey的其他內容
2025/01/23
在前一篇文章中,我們探討了非同步程式設計的基本概念,並介紹了如何使用 Task、Task<T>、async 和 await 來設計非同步操作。然而,非同步程式設計並非總是那麼直截了當。在實際開發中,開發者經常會遇到一些挑戰,這些挑戰主要來自於高併發、多執行緒以及非同步操作的特性。
2025/01/23
在前一篇文章中,我們探討了非同步程式設計的基本概念,並介紹了如何使用 Task、Task<T>、async 和 await 來設計非同步操作。然而,非同步程式設計並非總是那麼直截了當。在實際開發中,開發者經常會遇到一些挑戰,這些挑戰主要來自於高併發、多執行緒以及非同步操作的特性。
2025/01/22
在 C# 中,非同步程式設計是一種有效提升應用性能、併發處理能力的重要技術。今天,我們將詳細介紹 C# 中的非同步核心概念:async、await、Task 和 Task<T>,說明它們的使用方式,並探討一些進階應用。最後,我們會簡單介紹一些非同步程式設計中常見的問題。 什麼是非同步程式設計?
2025/01/22
在 C# 中,非同步程式設計是一種有效提升應用性能、併發處理能力的重要技術。今天,我們將詳細介紹 C# 中的非同步核心概念:async、await、Task 和 Task<T>,說明它們的使用方式,並探討一些進階應用。最後,我們會簡單介紹一些非同步程式設計中常見的問題。 什麼是非同步程式設計?
2025/01/21
這篇文章探討非同步編程的優缺點,並提供在設計非同步系統時需要注意的事項。非同步編程允許程式在等待 I/O 操作完成的同時,繼續執行其他工作,提高響應速度和資源利用率。然而,非同步程式設計也增加了系統複雜性,需要謹慎處理錯誤和確保代碼可讀性。
Thumbnail
2025/01/21
這篇文章探討非同步編程的優缺點,並提供在設計非同步系統時需要注意的事項。非同步編程允許程式在等待 I/O 操作完成的同時,繼續執行其他工作,提高響應速度和資源利用率。然而,非同步程式設計也增加了系統複雜性,需要謹慎處理錯誤和確保代碼可讀性。
Thumbnail
看更多
你可能也想看
Thumbnail
TOMICA第一波推出吉伊卡哇聯名小車車的時候馬上就被搶購一空,一直很扼腕當時沒有趕緊入手。前陣子閒來無事逛蝦皮,突然發現幾家商場都又開始重新上架,價格也都回到正常水準,估計是官方又再補了一批貨,想都沒想就立刻下單! 同文也跟大家分享近期蝦皮購物紀錄、好用推薦、蝦皮分潤計畫的聯盟行銷!
Thumbnail
TOMICA第一波推出吉伊卡哇聯名小車車的時候馬上就被搶購一空,一直很扼腕當時沒有趕緊入手。前陣子閒來無事逛蝦皮,突然發現幾家商場都又開始重新上架,價格也都回到正常水準,估計是官方又再補了一批貨,想都沒想就立刻下單! 同文也跟大家分享近期蝦皮購物紀錄、好用推薦、蝦皮分潤計畫的聯盟行銷!
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
在現代網路與雲端架構中,負載平衡(Load Balancer)、橫向擴展(Scale Out)、以及 API 溝通機制是不可或缺的基礎。本文帶你快速理解負載平衡如何分散流量、系統如何透過擴展應對成長需求,以及 API 在不同服務間扮演的溝通角色。
Thumbnail
在現代網路與雲端架構中,負載平衡(Load Balancer)、橫向擴展(Scale Out)、以及 API 溝通機制是不可或缺的基礎。本文帶你快速理解負載平衡如何分散流量、系統如何透過擴展應對成長需求,以及 API 在不同服務間扮演的溝通角色。
Thumbnail
在產品開發過程中,PM 對技術概念的理解深度可能會影響需求落地精準度 與開發時程可控性,若能掌握一些基本技術用語,不僅能幫助 PM 更好地理解技術限制與實作可能性,更能提升與工程師的溝通效率。
Thumbnail
在產品開發過程中,PM 對技術概念的理解深度可能會影響需求落地精準度 與開發時程可控性,若能掌握一些基本技術用語,不僅能幫助 PM 更好地理解技術限制與實作可能性,更能提升與工程師的溝通效率。
Thumbnail
撰寫的API規格書是軟體PM必學技能,能有效提升開發效率並減少溝通誤差。本文分享API規格書從需求收集、設計架構到版本管理。PM不需寫程式,但需理解API邏輯,成為開發與需求方的溝通橋樑,讓專案更順暢、開發團隊更高效!
Thumbnail
撰寫的API規格書是軟體PM必學技能,能有效提升開發效率並減少溝通誤差。本文分享API規格書從需求收集、設計架構到版本管理。PM不需寫程式,但需理解API邏輯,成為開發與需求方的溝通橋樑,讓專案更順暢、開發團隊更高效!
Thumbnail
工作上即便有SOP仍會發現未執行或是執行效果不彰,有時並未只是作業人員的問題,我認為撰寫SOP同時須確認五項原則,包括減少認知落差、親自操作以確保實用性、保持簡單易懂、人員理解背後邏輯,以及建立防呆機制。透過這些原則,可以有效提高作業效率和產品品質,降低錯誤發生的機率,達到SOP最大效用。
Thumbnail
工作上即便有SOP仍會發現未執行或是執行效果不彰,有時並未只是作業人員的問題,我認為撰寫SOP同時須確認五項原則,包括減少認知落差、親自操作以確保實用性、保持簡單易懂、人員理解背後邏輯,以及建立防呆機制。透過這些原則,可以有效提高作業效率和產品品質,降低錯誤發生的機率,達到SOP最大效用。
Thumbnail
這本書大概花了一個禮拜的零碎時間看完,其實收穫很多,很多原則不僅僅適用於微服務,也適用在單體式應用被部署在很多節點上,加上跟過去的經驗比較,更能體會到書中的觀點
Thumbnail
這本書大概花了一個禮拜的零碎時間看完,其實收穫很多,很多原則不僅僅適用於微服務,也適用在單體式應用被部署在很多節點上,加上跟過去的經驗比較,更能體會到書中的觀點
Thumbnail
這本書是在 Agile Tour 2015 聽完作者本人演講後買的,不過,還真的沒挪出什麼時間把它看完,直到最近因為一些因素終於有時間把它看完了,所以上述很多摘錄都是心有戚戚焉。看是看完了,也別因為我上面都把好處寫出來就覺得看板方法好棒棒,一定也可以適用在你的團隊,真的嗎?
Thumbnail
這本書是在 Agile Tour 2015 聽完作者本人演講後買的,不過,還真的沒挪出什麼時間把它看完,直到最近因為一些因素終於有時間把它看完了,所以上述很多摘錄都是心有戚戚焉。看是看完了,也別因為我上面都把好處寫出來就覺得看板方法好棒棒,一定也可以適用在你的團隊,真的嗎?
Thumbnail
SOP是工作中很常聽見的詞,全名叫「標準化製作流程」,主要是讓執行有依據,通常是為了複雜的事而設計的內部程序,對新手來說,SOP 能加快新人的學習速度和降低出錯的機率。可是啊,站在專業服務工作的角度,尤其工作內容是人與人之間需要密切的互動這種類型時,完全照 SOP來,真的是最好的方式嗎?
Thumbnail
SOP是工作中很常聽見的詞,全名叫「標準化製作流程」,主要是讓執行有依據,通常是為了複雜的事而設計的內部程序,對新手來說,SOP 能加快新人的學習速度和降低出錯的機率。可是啊,站在專業服務工作的角度,尤其工作內容是人與人之間需要密切的互動這種類型時,完全照 SOP來,真的是最好的方式嗎?
Thumbnail
無論是參與面試或釐清需求,系統設計常常令人感到困惑,不知如何進行。 我想,原因大概有兩種。 首先,不同領域的系統通常都有不同的設計要素,如何找出這些要素進行提問? 其次,即便提出疑問找出重點,接下來要如何針對這些要素進行高層設計或規劃? 《系統設計面試指南》對於系統設計的初步規劃或高層結構提供合適的
Thumbnail
無論是參與面試或釐清需求,系統設計常常令人感到困惑,不知如何進行。 我想,原因大概有兩種。 首先,不同領域的系統通常都有不同的設計要素,如何找出這些要素進行提問? 其次,即便提出疑問找出重點,接下來要如何針對這些要素進行高層設計或規劃? 《系統設計面試指南》對於系統設計的初步規劃或高層結構提供合適的
Thumbnail
這個世界不斷在變動,網路科技亦是如此,但「以人為本」的商業常態並沒有改變,這篇要從後台管理系統的重要性分享一些看法,讓新手工程師們可以從不同角度去學習應對,大家可以參考看看。
Thumbnail
這個世界不斷在變動,網路科技亦是如此,但「以人為本」的商業常態並沒有改變,這篇要從後台管理系統的重要性分享一些看法,讓新手工程師們可以從不同角度去學習應對,大家可以參考看看。
Thumbnail
許多的工作流程是從過去堆疊至今的習慣。也可以這麼說...有些工作職掌的作業流程,是可以減去不必要做的。更甚至有些在作業執行時,一併產生的單據,不但沒有管控效果,其目的對於當事者來說,並未有著明確的意思...
Thumbnail
許多的工作流程是從過去堆疊至今的習慣。也可以這麼說...有些工作職掌的作業流程,是可以減去不必要做的。更甚至有些在作業執行時,一併產生的單據,不但沒有管控效果,其目的對於當事者來說,並未有著明確的意思...
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News