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