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