---
# System prepended metadata

title: 筆記｜Json-Server
tags: [學習筆記, React]

---

`#React` `#六角前端課程` `#學習筆記` `#持續更新` `#骨力走傱`

## 環境教學
### 🔸 [JSON-Server](https://github.com/typicode/json-server)
### 🔸 安裝步驟
:::warning
若要使用參數 `_expand`，請安裝 0.17.4 版。
因為 Json-Server v1.x 不支援 `_expand`。
```
npm install json-server@0.17.4
npx json-server --watch db.json --port 3000
```
:::
* 下載
```
npm install json-server
```
* 新增測試檔案 → `db.json`
```json=
{
  "posts": [
    { "id": "1", "title": "a title", "views": 100 },
    { "id": "2", "title": "another title", "views": 200 }
  ],
  "comments": [
    { "id": "1", "text": "a comment about post 1", "postId": "1" },
    { "id": "2", "text": "another comment about post 1", "postId": "1" }
  ],
  "profile": {
    "name": "typicode"
  }
}
```
* 執行
```
npx json-server db.json
```
* 若有回應以下資訊，表示成功。
```
Watching db.json...

( ˶ˆ ᗜ ˆ˵ )

Index:
http://localhost:----/

Static files:
Serving ./public directory if it exists

Endpoints:
http://localhost:----/posts
http://localhost:----/comments
http://localhost:----/profile
```

### 🔸 與一般伺服器架構差異
#### 常見、實務流程
［瀏覽器］→［伺服器］→［資料庫］
［瀏覽器］←［伺服器］←［資料庫］
* 需要了解後端與資料庫知識。

#### JSON-Server
［瀏覽器］→［JSON-Server］→［db.json］
［瀏覽器］←［JSON-Server］←［db.json］
* 在沒有後端服務的情況下，可使用 JSON-Server 快速建立模擬 API。
* `db.json` 作為資料來源，扮演簡化版資料庫的角色，用來提供與修改 API 回傳的資料。

### 🔸 如何讀取 JSON-Server 建立的 API？
:::warning
建議先建立兩個獨立的資料夾，分別用於前端專案（網頁開發）與後端模擬環境（JSON Server），以利開發與管理。
:::

#### 前端網頁
* 使用 [axios](https://github.com/axios/axios) 發出網路請求 `get` 讀取資料。
```javascript=
axios.get("JSON-Server 的網址")
    .then((res) => {
        console.log(res);
    });

// {data: Array(3), status: 200, …} 這裡的內容會跟 db.json 一樣
```
* 使用 `post` 新增內容至 `db.json`。
```javascript=
axios
  .post("JSON-Server 的網址", { 根據 db.json 的格式填寫 })
  .then((res) => {
    console.log(res);
  });

// 再使用 get 查看 會發現多一筆剛剛新增的資料
```

#### JSON-Server
* 預設資料
```json=
{
  "posts": [
    {
      "id": "1",
      "title": "a title",
      "views": 100
    },
    {
      "id": "2",
      "title": "another title",
      "views": 200
    },
    {
      "id": "3",
      "title": "another fish",
      "views": 300
    },
    {
      "id": "6792",
      "title": "fish",
      "views": 400
    },
    {
      "id": "d071",
      "title": "cat",
      "views": 50
    }
  ]
}
```

### 🔸 講解 db.json 架構設計
```json=
{
  "posts": [
    { "id": "1", "title": "a title", "views": 100 },
    { "id": "2", "title": "another title", "views": 200 }
  ],
  "comments": [
    { "id": "1", "text": "a comment about post 1", "postId": "1" },
    { "id": "2", "text": "another comment about post 1", "postId": "1" }
  ],
  "profile": { // 這裡是單筆資料
    "name": "typicode"
  }
}
```
```json=
// 格式是固定的
// 但資源名稱與內容可自定義
{
  "自定義資源": [
    { 自定義物件內容 },
  ]
}

// 自定義
{
  "addItems": [
    { "id": "1", "itemName": "apple_red" },
  ]
}
```
* 最外層為物件 `{}`。
* 第一層為資源，例如 `"posts"`、`"comments"`、`"profile"`，可自定義。
    * 裡面要接陣列 `[]`。
* 若內容為複數，自定義名稱記得加上 **s** 表示。
* 若有新增資源，就要重新開啟伺服器。
* `id` 是 `db.json` 自行產生的。
* 除了多筆資料外，`db.json` 也允許存放單筆資料，通常是模擬使用者設定、個人資料或系統狀態等需求。

### 🔸 路由機制
> 路由是「網址（URL）對應到畫面內容」的規則與機制。
#### 規則
```json=
GET    /posts
GET    /posts/:id
POST   /posts
PUT    /posts/:id
PATCH  /posts/:id
DELETE /posts/:id

# Same for comments
```
* 可使用 `id` 指定特定資料。

#### 取得資料
```javascript=
// 取得所有資料
axios
  .get("http://localhost:----/posts")
  .then((res) => {
    console.log(res.data);  // [{…}, {…}, {…}, {…}]
  });

// 取得特定資料
axios
  .get("http://localhost:----/posts/1") // posts 後方接了 /1
  .then((res) => {
    console.log(res.data); // {id: '1', title: 'a title', views: 100}
  });
```

#### 修改資料
```javascript=
// 修改前
axios
  .get("http://localhost:----/posts/1")
  .then((res) => {
    console.log(res.data); // {id: '1', title: 'a title', views: 100}
  });

// 修改後
axios
  .patch("http://localhost:----/posts/1",{
    "title": "Happy New Year"
  })
  .then((res) => {
    console.log(res.data); // {id: '1', title: 'Happy New Year', views: 100}
  });
```
* 使用 `patch` 修改，並新增參數提交要修改的內容（格式也是依照 `db.json` 的規則撰寫）。
* 無法單獨修改物件中的資料，要整筆覆蓋。
    ```json=
    "...":[
        "...":{...},
        "language":{　// 要選取這裡整筆修改
            "zh-tw":"B產品", // 無法單獨修改這筆
            "en-us":"B product"
        }
    ]
    ```

#### 刪除資料
```javascript=
// 刪除前
axios
  .get("http://localhost:----/posts")
  .then((res) => {
    console.log(res.data);  // [{…}, {…}, {…}, {…}] 4 筆資料
  });

// 刪除
axios
  .delete("http://localhost:----/posts/1") // 刪除第 1 筆資料
  .then((res) => {
    console.log(res.data);
  });

// 刪除後
axios
  .get("http://localhost:----/posts")
  .then((res) => {
    console.log(res.data);  // [{…}, {…}, {…}] 3 筆資料
  });
```

#### 單筆資源的路由
```json=
GET   /profile
PUT   /profile
PATCH /profile
```
* 在 `db.json` 中可以定義 `"profile"` 這類單筆資源，但在實務開發中，API 通常會設計為多筆資料結構（陣列），才能針對特定項目進行新增、修改或刪除等操作。
* 若僅有單筆資料，使用 `db.json` 作為資料管理的效益較低，不過在模擬使用者設定、個人資料或系統狀態等情境下，仍符合開發需求。

#### 比較
| 比較項目           | 單筆資料（Object）               | 多筆資料（Array）            |
| ------------------ | -------------------------------- | ---------------------------- |
| 資料結構           | 物件 `{}`                        | 陣列 `[]`                    |
| 常見用途           | 使用者個人資料、設定值、系統狀態 | 清單、列表、可重複的業務資料 |
| JSON-Server 範例   | `"profile": {}`                  | `"profiles": []`             |
| 是否需要 id        | 通常不需要                       | 必須有 `id`                  |
| API 路由形式       | `/profile`                       | `/profiles`、`/profiles/:id` |
| 對前端路由的影響   | 畫面多為單一頁或設定頁           | 常搭配列表頁 / 詳細頁切換    |
| 實務常見程度       | 中                               | 高                           |

### 🔸 查詢運算子
> 過去文件中以 Filter 稱呼，但現在改用 Conditions、Range 等等表示篩選功能。

#### 格式
`/products?price_gt=500`

* `products` 資源名稱
* `?` 開始查詢參數
* `price_gt` `price` 欄位 + `_gt` 運算子（大於）
* `=` query string 的語法分隔 key 與 value
* `500` 用來篩選的值
* 整體意思：從 `products` 中篩選出 `price > 500` 的資料

#### 基本比對
| 運算子   | 意義   | 範例                       |
| ----- | ---- | ------------------------ |
| `=`   | 等於   | `/posts?id=1`            |
| `ne`  | 不等於  | `/posts?status_ne=draft` |
| `gt`  | 大於   | `/posts?views_gt=1000`   |
| `gte` | 大於等於 | `/posts?views_gte=1000`  |
| `lt`  | 小於   | `/posts?views_lt=5000`   |
| `lte` | 小於等於 | `/posts?views_lte=5000`  |

#### 全文與模糊搜尋
| 運算子  | 意義             | 範例                   |
| ------- | ---------------- | ---------------------- |
| `q`     | 全文搜尋         | `/posts?q=react`       |
| `_like` | 模糊比對（正則） | `/posts?title_like=js` |

#### 多條件篩選
`GET /posts?status=published&views_gte=1000`
* JSON-Server 的多條件為 `AND`，不支援 `OR`。

#### 排序
| 參數     | 意義     | 範例                             |
| -------- | -------- | -------------------------------- |
| `_sort`  | 排序欄位 | `/posts?_sort=views`             |
| `_order` | 排序方向 | `/posts?_sort=views&_order=desc` |

#### 分頁
| 參數     | 意義         | 範例                       |
| -------- | ------------ | -------------------------- |
| `_page`  | 頁碼（從 1） | `/posts?_page=1`           |
| `_limit` | 每頁筆數     | `/posts?_page=1&_limit=10` |

### 🔸 todolist restful API 練習
:::spoiler 首頁
```javascript=
const txt = document.querySelector('.txt');
const save = document.querySelector('.save');
const list = document.querySelector('.list');
const _url = "http://localhost:----"; // Json-Server
let data = [];

// 初始化
function init(){
    axios.get(`${_url}/todoLists`)
    .then(function(response){
        data=response.data;
        renderData()
    })
};

// 預設載入初始化環境
init();

function renderData(){
  let str = '';
  data.forEach(function (item,index) {
    str+=`<li><a href="page.html?id=${item.id}">${item.content}</a> <input class="delete" type="button" data-num="${item.id}" value="刪除待辦"></li>`
  })
  list.innerHTML = str;
};

// 新增待辦功能
save.addEventListener('click',function(e){
  if (txt.value=="") {
    alert("請輸入內容");
    return;
  }
  let obj = {};
  obj.content = txt.value
  axios.post(`${_url}/todoLists`,obj)
  .then(function(res){
    init();
  })
});

// 刪除待辦功能
list.addEventListener("click",function(e){
  if(e.target.getAttribute("class")!=="delete"){
    return;
  }
  let num = e.target.getAttribute("data-num");
  axios.delete(`${_url}/todoLists/${num}`)
  .then(function(res){
    alert("刪除成功！");
    init();
  })
});
```
:::

:::spoiler 分頁
```javascript=
// http://127.0.0.1:----/page.html?id=3
const id = location.href.split("=")[1];
const _url = "http://localhost:----";

axios.get(`${_url}/todoLists/${id}`)
    .then(function(response){
        document.querySelector("h1").textContent = response.data.id
        document.querySelector(".content").textContent = response.data.content
    });
```
:::
* Window 物件中的 Location 物件（`window.location`）代表當前瀏覽器視窗顯示的文件 URL，提供獲取和操作當前頁面 URL 的資訊與功能。

## 資料關聯教學
### 🔸 資料的拆分
```json=
{
  "users": [
    {
      "id": "a-1",
      "userName": "小花"
    },
    {
      "id": "a-2",
      "userName": "小白"
    }
  ],
  "posts": [
    { "id": "1", "postBody": "Lorem ipsum.", "userId": "a-1" },
    { "id": "2", "postBody": "dolor sit.", "userId": "a-2" }
  ]
}
```
* 為了方便維護與擴充，建議將不同性質的資料拆分成獨立資源，例如使用者、貼文等。
* 在貼文、留言或評論等資料結構中，常以 `userId` 作為關聯欄位，用來表示該資料是由哪一位使用者建立（可使用參數 `_expand` 展開）。

### 🔸 _expand 展開關聯資源
```javascript=
axios
  .get("http://localhost:----/posts?_expand=user")
  .then((res) => {
    console.log(res.data);
  });

// {id: '1', 
// postBody: '這家店很好吃！', 
// userId: 'a-1', 
// user: {
//     id: 'a-1', 
//     userName: '小花'
// }}
```
* **必須遵循外鍵規則**，`_expand=<資源名>` 是根據 `<資源名>Id` 的值，去對應另一個資源的 `id`，把資料「附加」回來的。
* `userId` 是關聯依據，`_expand=user` 是路由查詢方式。
* `_expand` 只適合「一筆對一筆」。

### 🔸 如何取得（GET）、新增（POST）留言資料
#### 取得貼文資料
```json=
{
  "users": [
    {
      "id": "a-1",
      "userName": "小花"
    },
    {
      "id": "a-2",
      "userName": "小白"
    }
  ],
  "posts": [
    { "id": "1", "postBody": "Lorem ipsum dolor sit.", "userId": "a-1" },
    { "id": "2", "postBody": "cing elit.", "userId": "a-2" }
  ],
  "comments": [
    { "id": "1", "text": "a comment about post 1", "postId": "1","userId": "a-1" },
    { "id": "2", "text": "another comment about post 1", "postId": "1","userId": "a-2" }
  ]
}
```
```javascript=
// 取得所有貼文資料
axios
  .get("http://localhost:----/posts")
  .then((res) => {
    console.log(res.data); // [{…}, {…}]
  });

// 取得第一筆貼文資料
axios
  .get("http://localhost:----/posts/1")
  .then((res) => {
    console.log(res.data); // {id: '1', postBody: 'Lorem ipsum dolor sit.', userId: 'a-1'}
  });

// 取得第二筆貼文資料
axios
  .get("http://localhost:----/posts/2")
  .then((res) => {
    console.log(res.data); // { "id": "2", "postBody": "cing elit.", "userId": "a-2" }
  });
```
* `GET /posts` 取得所有貼文資料。
* `GET /posts/:id` 取得特定貼文資料。

#### 取得留言資料
```javascript=
// 取得 posts/1 貼文底下的所有留言
axios
  .get("http://localhost:----/posts/1/comments")
  .then((res) => {
    console.log(res.data); // [{…}, {…}]
  });
```
* 在 `comments` 的資源中有 `postId`，表示說這一則留言是來自 `posts` 資源中的哪一筆 `id`。
* 故路由改成 `/posts/2/comments`，回傳的會是空陣列，因為沒有在 `posts/2` 底下留言。

:::warning
```
"comments": [
    { "id": "1", "text": "a comment about post 1", "postId": "1","userId": "a-1" }
```
這一則留言（`"comments" 的 "id": "1"`），是來自於 `"postId": "1"`（`"posts"` 的 `"id": "1"` ）。
:::

#### 搭配 `_expand` 附加使用者資訊
* `GET　/posts/1/comments?_expand=user`

#### 新增留言資料
```json=
axios
  .post("http://localhost:----/posts/1/comments",{
    "text": "Lorem ipsum dolor sit amet.",
    "userId": "a-2"
  })
  .then((res) => {
    console.log(res.data); // [{…}, {…}, {…}]
  });
```
```json=
{
    "text": "Lorem ipsum dolor sit amet.",
    "userId": "a-2",
    "postId": "1", // 自動補上
    "id": "hF2XqCd"　// 自動補上
}
```
* 只要送出 `text` 與 `userId` 即可，`db.json` 會根據路由得知這筆資料，是新增在 `posts` `id:1` 底下的 `comments`，接著會自動補上 `id` 與 `postId` 的資訊。

### 🔸 如何同時取得貼文與對應留言的資料？
其中一種做法是，同時對兩隻 API 發送網路請求，並且組合在畫面上。
* 取得貼文 `GET /posts/1?_expand=user`
    * `?_expand=user` 顯示貼文者。
* 取得對應留言 `GET /posts/1/comments?_expand=user`
    * `?_expand=user` 顯示留言者。

### 🔸 貼文與留言的 API 介接設計
```javascript=
// 貼文詳細資料
const id = location.href.split("=")[1];

function init(){
    if(id==undefined){
        alert("您的操作錯誤，將移轉到首頁");
        location.href = "./posts.html";
    }
    getPost(id);
    getComments(id);
};

init();// 預設載入初始化環境

// 取得貼文資料
function getPost(id){
    axios.get(`${_url}/posts/${id}?_expand=user`)
    .then(function(response){
        document.querySelector("h1").textContent =  JSON.stringify(response.data);
    })
}

// 取得留言資料
function getComments(id){
    axios.get(`${_url}/posts/${id}/comments?_expand=user`)
    .then(function(response){
        console.log(response);
        document.querySelector(".content").textContent = JSON.stringify(response.data);
    })
}
```

```javascript=
// 貼文
const list = document.querySelector('.list');

let data = [];
function init(){
    axios.get(`${_url}/posts`)
    .then(function(response){
        data=response.data;
        renderData()
    })
}

init();　// 預設載入初始化環境

function renderData(){
  let str = '';
  data.forEach(function (item,index) {
    str+=`<li>${item.body}<a href="postDetail.html?id=${item.id}">觀看全部留言</a></li>`
  })
  list.innerHTML = str;
}
```
* `const id = location.href.split("=")[1];`
    * `location.href` 取得目前頁面的完整 URL（字串）。
    * `.split("=")`，以 = 為分隔符，把整個 URL 切成陣列。
    * 回傳結果是一個「字串陣列」。
    * `[1]` 取陣列中索引為 1 的元素，也就是 `=` 右邊的內容。
* 當有多個頁面會用到某一筆重複的資料時，可以賦予在變數上，建立名為 config 的 .js 檔，統一存放、管理。
    * `const _url = "http://localhost:----";`
    * `{_url}/posts`