# 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;
```

## 編寫 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/");
});
```

## CORS 跨域資源共享
### 什麼是跨域
先說明一下什麼叫做跨域請求?
> CORS(Cross-Origin Resource Sharing, 跨域資源共享) 由一系列 HTTP 響應頭組成,這些 HTTP 響應頭決定瀏覽器是否阻止前端 JS 程式碼跨域獲取資源。瀏覽器的同源安全策略默認會阻止網頁跨域獲取資源,但如果 API server 配置了 cors 相關的 HTTP 響應頭就可以解除瀏覽器端的跨域訪問限制。

先寫一個網頁,網頁中有兩個按鈕,用 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 按鈕,我們就能在控制台中看到如下字樣:

當要請求的 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 就能順利獲得響應:

**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:

這個 options 就是預檢請求,當預檢請求成功後才會去做實際的 PUT 請求。
那簡單請求就是最基本的 GET, POST, HEAD,沒什麼好說的了。