# Express.js 簡易 API 實作及 CORS 跨域請求 Express.js 基礎篇,實作 GET, PUT, PATCH, POST, DELETE API。 ## 創建 API 路由模組 基本寫法如下: ```javascript= // apiRouter.js const express = require('express'); const apiRouter = express.Router(); // bind your router here.. module.exports = apiRouter; ``` ```javascript= // app.js const express = require("express"); const app = express(); const router = require("./apiRouter"); app.use("/api", router); app.listen(3000, () => { console.log("http://127.0.0.1:80/"); }); ``` ## 編寫 GET API 用 `apiRouter.get()` 來寫一個 `get /user API`,將從 client 端通過查詢字串傳過來的數據再原封不動的傳回去。 ```javascript= // apiRouter.js const express = require("express"); const apiRouter = express.Router(); apiRouter.get("/user", (req, res) => { // 獲取從 client 端通過查詢字串傳過來的數據 const query = req.query; res.send({ status: 0, // 0: 成功, 1: 失敗 msg: "GET請求成功!", data: query, // 響應給 client 端的具體數據 }); }); module.exports = apiRouter; ``` ![](https://i.imgur.com/GdEmTJ8.png) ## 編寫 POST API 基本和 get 是一樣的,只不過 POST 是從 body 抓取 client 端傳過來的數據。 ```javascript= // apiRouter.js apiRouter.post("/user", (req, res) => { // 獲取 client 端通過請求體(body) 發送到 server 端的 url-encoded 數據 const body = req.body; res.send({ status: 0, // 0: 成功, 1: 失敗 msg: "POST請求成功!", data: body, // 響應給 client 端的具體數據 }); }); ``` **注意:如果要獲取 url-encoded 格式的請求體(body)數據,必須配置中間件** `app.use(express.urlencoded({ extended: false }));` ```javascript= // app.js const express = require("express"); const app = express(); const router = require("./apiRouter"); app.use(express.urlencoded({ extended: false })); // 配置 urlencoded 中間件 app.use("/api", router); app.listen(3000, () => { console.log("http://127.0.0.1:80/"); }); ``` ![](https://i.imgur.com/JZ7x1UD.png) ## CORS 跨域資源共享 ### 什麼是跨域 先說明一下什麼叫做跨域請求? > CORS(Cross-Origin Resource Sharing, 跨域資源共享) 由一系列 HTTP 響應頭組成,這些 HTTP 響應頭決定瀏覽器是否阻止前端 JS 程式碼跨域獲取資源。瀏覽器的同源安全策略默認會阻止網頁跨域獲取資源,但如果 API server 配置了 cors 相關的 HTTP 響應頭就可以解除瀏覽器端的跨域訪問限制。 ![](https://i.imgur.com/Cgwr4r6.png) 先寫一個網頁,網頁中有兩個按鈕,用 jQuery 分別給兩個按鈕進行 get 和 post 請求: ```htmlembedded= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <script src="https://cdn.staticfile.org/jquery/1.10.0/jquery.min.js"></script> </head> <body> <button id="btn1">GET</button> <button id="btn2">POST</button> <script> $(function () { // 1. 測試 get API $("#btn1").on("click", () => { $.ajax({ type: "get", url: "http://127.0.0.1:3000/api/user", data: { name: "王小明", age: 22 }, success: (res) => { console.log(res); }, }); }); // 2. 測試 post API $("#btn2").on("click", () => { $.ajax({ type: "post", url: "http://127.0.0.1:3000/api/user", data: { name: "小王", age: 33 }, success: (res) => { console.log(res); }, }); }); }); </script> </body> </html> ``` 我們直接在本機開啟這個網頁,開啟**開發者工具 - console**,然後按下 GET 和 POST 按鈕,我們就能在控制台中看到如下字樣: ![](https://i.imgur.com/cxisaby.png) 當要請求的 API 和請求的協議、域名、端口號任何一個不同,就存在跨域問題。 ``` Access to XMLHttpRequest at 'http://127.0.0.1:3000/api/user?name=%E7%8E%8B%E5%B0%8F%E6%98%8E&age=22' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. GET http://127.0.0.1:3000/api/user?name=%E7%8E%8B%E5%B0%8F%E6%98%8E&age=22 Access to XMLHttpRequest at 'http://127.0.0.1:3000/api/user' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. POST http://127.0.0.1:3000/api/user ``` ### 解決 API 的跨域問題 剛才我們用 jQuery 所寫的 get, post 請求不支持跨域請求,解決 API 跨域問題的方案主要有兩種: 1. CORS (主流的解決方案) 2. JSONP (有缺陷的解決方案,只支持 GET 請求) ### 使用 Cors 中間件解決跨域問題 Cors 是 Express 的一個第三方中間件,通過安裝及配置 Cors 中間件就能很方便的解決跨域問題。 1. 首先安裝 Cors `npm install cors` 2. 導入 Cors 中間件 `const cors = require("cors");` 3. 用 `app.use(cors());` 全局配置 Cors 中間件 ```javascript= // app.js const express = require("express"); const app = express(); const router = require("./apiRouter"); const cors = require("cors"); // 導入 cors 中間件 app.use(cors()); // 配置 cors 中間件 app.use(express.urlencoded({ extended: false })); app.use("/api", router); app.listen(3000, () => { console.log("http://127.0.0.1:80/"); }); ``` 如此一來,打開剛剛的 html 網頁按下 GET 和 POST button 就能順利獲得響應: ![](https://i.imgur.com/BLcNqjd.png) **CORS的注意事項:** 1. CORS 主要在後端進行配置,前端無需做任何額外的配置。 2. CORS 在瀏覽器中有兼容性,只有支持 XMLHttpRequest Level 2 的瀏覽器才能正常訪問開啟了 CORS 的 server API (例如:Chrome 4+, Firefox 3.5+, IE 10+) ### CORS 響應頭部 #### Access-Control-Allow-Origin 響應頭部中可以攜帶一個 Access-Control-Allow-Origin 字段: ``` Access-Control-Allow-Origin: <origin> | * ``` 其中 origin 參數的值指定了允許訪問該資源的外域 URL,如下方程式碼只允許 http://test.com/ 的請求: ```javascript= res.setHeader("Access-Control-Allow-Origin", "http://test.com/"); ``` 如果值設為通配符 `*` 表示允許來自任何域名的請求: ```javascript= res.setHeader("Access-Control-Allow-Origin", "*"); ``` #### Access-Control-Allow-Headers 在默認情況下,CORS 僅支持 client 端向 server 端發送如下的九個請求頭(不用背): * Accept * Accept-Language * Content-Language * DPR * Downlink * Save-Data * Viewport-Width * Width * Content-Type:值僅限於 text/plain, multipart/form-data, application/x-www-form-urlencoded 三者之一 如果 client 端要向 server 端發送額外的請求頭訊息,則需要在 server 端通過 `Access-Control-Allow-Headers` 對額外的請求頭進行聲明,否則請求會失敗。 ```javascript= res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Custom-Header"); ``` ### Access-Control-Allow-Methods 默認情況下 CORS 僅支持 client 端發起 **GET, POST, HEAD** 請求,如果 client 端希望通過 PUT, DELETE 等方式請求 server 端的資源,則需要在 server 端過 `Access-Control-Allow-Methods` 來指名實際請求所允許使用的 HTTP 方法。 ```javascript= // 只允許 POST,GET,DELETE,HEAD 請求方法 res.setHeader("Access-Control-Allow-Methods", "POST,GET,DELETE,HEAD"); // 允許所有的 HTTP 請求方法 res.setHeader("Access-Control-Allow-Methods", "*"); ``` ### CORS請求的分類 client 端在請求 CORS API 時,根據請求方式和請求頭的不同,可以將 CORS 請求分為兩大類,分別為: 1. **簡單請求** - **同時滿足**以下兩種條件即為簡單請求: - 請求方式:**GET, POST, HEAD 三者之一**。 - HTTP頭部信息**不超過**以下幾種:無自定義頭部字段, Accept, Accept-Language, Content-Language, DPR, Downlink, Save-Data, Viewport-Width, Width, Content-Type(值僅限於 text/plain, multipart/form-data, application/x-www-form-urlencoded 三者之一) 2. **預檢請求** - 只要符合以下**任何一個**請求即為預檢請求: - 請求方式:GET, POST, HEAD **之外**的請求方法類型。 - 請求頭中**包含**自定義頭部字段。 - 向 server 發送了 **application/json** 格式的數據。 在瀏覽器與 server 正式通信前,瀏覽器會先發送 option 請求進行預檢,以獲知 server 是否允許該實際請求,所以這一次的 option 請求稱為預檢請求。 Server 成功響應預檢請求後才會發送真正的請求,並且攜帶真實的數據。 #### 簡單請求和預檢請求的區別 - **簡單請求**:Client 和 Server 之間**只會發生一次請求**。 - **預檢請求**:Client 和 Server 之間會發生兩次請求,option 預檢請求成功之後才會發起真正的請求。 假設我把剛剛的 jQuery 請求改為 PUT: ```javascript= $("#btn2").on("click", () => { $.ajax({ type: "put", url: "http://127.0.0.1:3000/api/user", data: { name: "小王", age: 33 }, success: (res) => { console.log(res); }, }); }); ``` apiRouter 加上一個 put API: ```javascript= // apiRouter.js apiRouter.put("/user", (req, res) => { const body = req.body; res.send({ status: 0, msg: "PUT請求成功!", data: body, }); }); ``` 我們開啟 test.html 然後將開發者工具打開到 Network 標籤頁,點下 PUT 的按鈕就會發現 network 多出了兩次請求,第一次的 request method 是 OPTIONS,第二次才是 PUT: ![](https://i.imgur.com/HmPt8AK.png) 這個 options 就是預檢請求,當預檢請求成功後才會去做實際的 PUT 請求。 那簡單請求就是最基本的 GET, POST, HEAD,沒什麼好說的了。