`#React` `#六角前端課程` `#學習筆記` `#骨力走傱` ## JavaScript 同步與非同步 ### 🔸 簡介 #### 同步語言 * 依序執行。 * 開發時會避免使用 `alert()`、`confirm()`、`prompt()` 這類的同步對話框,這會阻塞 JavaScript 的執行,導致使用者會無法與進行任何互動,這是不好的使用體驗;或者要等待 `for` 迴圈執行完大筆資料後,才能進行下一行程式碼,也是不好的使用體驗。 #### 非同步語言 * 不會依序執行,當有多行非同步語言時,無法確定哪一行會先執行。 * 若只有一行非同步語言的話,一定是最後執行。 * AJAX、`setTimeout()` ... ### 🔸 程式碼如何等待資料回傳? #### Promise ```javascript= const promiseSetTimeout = (status) => { return new Promise((resolve, reject) => { setTimeout(() => { if (status) { resolve('promiseSetTimeout 成功') } else { reject('promiseSetTimeout 失敗') } }, 0); }) }; promiseSetTimeout(1) .then((res) => { console.log(res); console.log(1); return promiseSetTimeout(1); // 使用 return 傳出結果 }) .then((res) => { // 再次使用 .then 接收 console.log(res); console.log(2); }) .catch((err) => { console.log(err); }) ``` * 基於 Promise 建構的函式。 * `const promiseSetTimeout = (status) => { ... };` 傳入參數,決定結果是成功或失敗。 * `return new Promise((resolve, reject) => { ... };` 建構 Promise 的程式碼。 * `resolve`:成功。 * `reject`:失敗。 * 若直接執行 `promiseSetTimeout();` 會看不到結果,因為必須使用 `.then` 接收結果。 * 接收多個 Promise 的結果時,請避免使用巢狀結構接收,而是使用 `return` 傳出結果後,再使用 `.then` 接收。 * 若未使用 `.catch` 捕捉錯誤,會回傳 Uncaught (in promise) 的錯誤,表示未告知如何處理這個錯誤,接著會中斷同一區塊中(`{}`)程式碼的執行,因此需要設定捕捉錯誤的方法。 #### Async / Await ```javascript= // 改寫前 const promiseSetTimeout = (status) => { return new Promise((resolve, reject) => { setTimeout(() => { if (status) { resolve('promiseSetTimeout 成功') } else { reject('promiseSetTimeout 失敗') } }, 10); }) }; // 改寫後 async function fn() { const data = await promiseSetTimeout(1); // await 必須接 Promise console.log(data); }; // 箭頭函式 const fn = async () => { const data = await promiseSetTimeout(1); console.log(data); }; // 立即函式 (async function fn() { const data = await promiseSetTimeout(1); console.log(data); })(); // 捕捉錯誤 async function fn() { try{ const data = await promiseSetTimeout(1); console.log(data); } catch (error) { console.log(error); } }; // 應用 axios (async () => { try{ const res = await axios.get("..."); const { results } = res.data; console.log(results); } catch (error) { console.log(error); } })(); ``` * Async / Await 是 Promise 的語法糖,因此對 Promise 越熟悉,有助於使用 Async / Await。 * `async` 標記一個函式為「非同步函式」。 * `await` 運算子用於等待 Promise 的結果。它將非同步邏輯以同步語法呈現,取代了傳統的 `.then()` 鏈式調用,顯著提升代碼的可讀性。 * 注意 `async` 只有一個作用域 `{}`,故變數命名不能重複。 * 常見錯誤: * 忘記加 Async 或 Await。 * Await 後方不是接 Promise,例如 `SetTimeout();`。 * 若在「等待 Promise 結果」且「沒有捕捉錯誤」的情況,會中斷同一區塊中(`{}`)程式碼的執行,因此需要設定捕捉錯誤的方法(`catch`)。 ## API 與 RESTful API ### 🔸 簡介 > API(Application Programming Interface,應用程式介面)是一組定義不同軟體系統之間如何互動的規則和協定。 > > 簡單來說,**API 的主要目的是調用某些功能或服務,而不需要重新去寫這些功能。這使得軟體開發變得更加高效**,並且能夠利用其他系統或服務提供的功能。 > 舉例,你(使用者)去餐廳用餐,告訴服務員(API)你想吃什麼,接著服務生(API)會轉達給廚師(後台系統),廚師(後台系統)會幫你處理好菜色,再透過服務員(API)將餐點端給你(使用者)。—— [API串接功能,你不可不知道的網頁技術知識](https://www.lohaslife.cc/post/article-api-knowledge) ![VS---14’33”](https://hackmd.io/_uploads/SJkOtAB-Zg.jpg) ### 🔸 RESTful API 與請求方法 > RESTful API 是一種基於 HTTP 協議的 API 設計風格,通過簡單的 URL 和 HTTP 方法來操作資源。—— [新手指南:什麼是 RESTful API?](https://realnewbie.com/posts/what-is-restful-api) > 其核心思想是,每一個 URL 都代表一個資源,而不同的 HTTP 方法(如 GET、POST、PUT、DELETE)用來對這些資源進行不同的操作。 > * `GET` /users:取得用戶的資料。 > * `GET` /users/{id}:取得特定用戶的資料。 > * `POST` /users:新增新的用戶資料。 > * `PUT` /users/{id}:修改特定用戶的資料。 > * `DELETE` /users/{id}:刪除用戶資料。 ### 🔸 如何運作? ![VS---56’05”](https://hackmd.io/_uploads/Hk7vDIJrWx.jpg) * 瀏覽器中的網頁無法直接與資料庫連線,因此需要透過伺服器作為中介,負責資料的驗證、處理與傳遞。 * 發送請求(Request)一定是由前端(網頁)發請,後端(伺服器)不能發送請求。 ### 🔸 課程 API #### 文件 * [申請](https://ec-course-api.hexschool.io/) * [課程 API 文件](https://hexschool.github.io/ec-courses-api-swaggerDoc/) #### 路徑差異 * 後台管理者:/v2/api/{api_path}/admin/products/all * 訪客:/v2/api/{api_path}/products/all 兩者差異在於路徑有沒有 **admin**,藉此區分使用者身分。 ### 🔸 基本結構 ![螢幕擷取畫面 (1706)](https://hackmd.io/_uploads/ryiU8wkHbl.png) 不是所有 API 需要夾帶參數,但驗證類的通常需要。 ### 🔸 基本寫法 > 以客戶取得 `get` 產品為例。 ```javascript= const url = "https://ec-course-api.hexschool.io/"; const path = "***"; // 用樣板字面值組路徑 // console.log(`${url}/v2/api/${path}/products/all`) const fn = async () => { try { const res = await axios.get(`${url}/v2/api/${path}/products/all`) const { products } = res.data; console.log(products); } catch (err) { console.log(err); } }; fn(); ``` ## 登入驗證流程 ### 🔸 基本流程 ![VS---62’31”](https://hackmd.io/_uploads/SkAls8yrWx.jpg) ### 🔸 儲存 Token #### 登入流程 ```javascript= function login() { const username = emailInput.value; const password = pwInput.value; const user = { // 文件要求的格式 username, password } axios.post(`${url}/admin/signin`, user) .then((res) => { const { token, expired } = res.data; // 使用解構取出 // 將 token 存在 cookie 的方法 document.cookie = `hexToken=${ token }; expires=${ new Date(expired) }`; }) .catch((err) => { console.dir(err); }) } ``` * 將帳號密碼送到登入 API。 * 從回傳資料中取得 `token` 與 `expired`。 * 將 `token` 存成 Cookie,並設定到期時間(後端設定)。 #### Cookie 寫法說明 ```javascript= // 基本範例 document.cookie = "someCookieName=true; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/"; // 使用情境 document.cookie = `hexToken=${token}; expires=${new Date(expired)}`; ``` * [Document.cookie](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie#%E7%A4%BA%E4%BE%8B_2_%E5%BE%97%E5%88%B0%E5%90%8D%E4%B8%BA_test2_%E7%9A%84_cookie) * 把登入用的 `token` 存入瀏覽器,讓後續請求可以驗證身分。 #### Cookie 組成拆解 `hexToken=${token}` * `hexToken` 自訂的 Cookie 名稱。 * `token` 後端發給前端的身分驗證字串。 * 將 `token` 存成名為 `hexToken` 的 Cookie。 #### expires `expires=${new Date(expired)}` * `expires` Cookie 的有效期限。 * `expired` 後端提供的到期時間。 * 必須用 `new Date()` 轉換,瀏覽器才能正確解析。 * `Date()` 回傳時間的「字串結果」。 * `new Date()` 回傳 Date 物件,包含時間資料與操作方法。 * 設定 Cookie 到期時間時,必須使用 `new Date()`。 #### uid 補充說明 * `uid`(User ID)。 * 唯一識別使用者。 * 區分不同帳號,用於追蹤使用者相關資料或行為。 ### 🔸 取出與夾帶 Token #### 驗證登入狀態 ```javascript= checkBtn.addEventListener('click', checkLogin); // 取得 Token ( 僅需要設定一次 ) function checkLogin() { const token = document.cookie.replace( /(?:(?:^|.*;\s*)hexToken\s*\=\s*([^;]*).*$)|^.*$/, "$1" ); // 下次發送 axios 時 會自動將 token 夾帶至 headers 裡 axios.defaults.headers.common['Authorization'] = token; // 確認是否登入 axios.post(`${url}/api/user/check`) .then((res) => { console.log(res.data); }) .catch((err) => { console.dir(err); }); } ``` * 從 Cookie 取出 `token`。 * 設定 Axios 預設 headers。 * 呼叫 API 確認登入狀態。 #### 取得 Cookie 中的 Token ```javascript= const token = document.cookie.replace( /(?:(?:^|.*;\s*)hexToken\s*\=\s*([^;]*).*$)|^.*$/, "$1" ); ``` * `hexToken` Cookie 的名稱(key)。 * 正規表達式用途: * 從所有 Cookie 中。 * 找出名為 `hexToken` 的值。 * `$1`:代表被擷取到的 `token` 內容。 * 把存在 Cookie 裡的 `token` 取出來。 #### 將 Token 加入 Axios 預設標頭 `axios.defaults.headers.common['Authorization'] = token;` * [Global axios defaults](https://github.com/axios/axios?tab=readme-ov-file#global-axios-defaults) * 作用: * 設定 Axios 的全域預設請求標頭。 * 之後所有 Axios 請求都會自動帶上 `Authorization: token`。 * 為什麼只需要設定一次: * `defaults` 是全域設定。 * 不需要在每個 API 都手動加 headers。 * 實務建議: * 將這段放在「初始化階段」(例如程式一開始)。 * 重新整理頁面後,只要 Cookie 還在,就能維持登入狀態。 #### 驗證是否登入 ```javascript= axios.post(`${url}/api/user/check`) .then((res) => { console.log(res.data); }) .catch((err) => { console.dir(err); }); ``` * 後端會從 `Authorization` 讀取 `token`。 * 驗證 `token` 是否有效。 * 回傳登入狀態結果。 #### 查看夾帶的 Token ![螢幕擷取畫面 (1707)](https://hackmd.io/_uploads/HkIwQOyrZg.png) ### 🔸 總結 > **登入成功後儲存 token → 初始化時取出 token → 發送請求時自動夾帶 token** #### 1️⃣ 儲存 Token(登入成功後) * 來源:後端回傳 `token`、`expired`。 * 動作: ```javascript document.cookie = `hexToken=${token}; expires=${new Date(expired)}`; ``` * 目的:將登入狀態「持久化」,讓重新整理頁面後仍能識別使用者。 #### 2️⃣ 取出 Token(初始化或需要驗證時) * 來源:瀏覽器 Cookie。 * 動作:使用 `document.cookie + 正規表達式` 取得 `hexToken`。 * 目的:把瀏覽器中保存的 `token` 取回到 JavaScript 使用。 #### 3️⃣ 夾帶 Token(發送 API 請求) * 動作: ```javascript axios.defaults.headers.common['Authorization'] = token; ``` * 目的:讓每次 API 請求都自動帶上身分驗證資訊,後端才能判斷使用者身分。 #### 關鍵觀念 :::warning 「取出 token」和「夾帶 token」**邏輯上是兩步,但實務上常寫在同一段初始化程式中**。 ::: ## 其他方法 ### 🔸 新增產品 ```javascript= axios.post(`${url}/api/${path}/admin/product`, product) // product 是提交的資料 .then((res) => { console.log(res.data) }) .catch((err) => { console.dir(err); }) ``` ### 🔸 刪除產品 ```javascript= axios.delete(`${url}/api/${path}/admin/product/{id}`) // 產品 id .then((res) => { console.log(res.data) }) .catch((err) => { console.dir(err); }) ``` :::warning 若要新增單筆資料,記得將路徑 products 修改成 product,複數反之。 ::: ### 🔸 檔案上傳 ```javascript= const url = "https://ec-course-api.hexschool.io/v2"; // 請加入站點 Base URL const path = ""; // 請加入個人 API Path // 驗證 const token = document.cookie.replace( /(?:(?:^|.*;\s*)hexToken\s*\=\s*([^;]*).*$)|^.*$/, "$1"); axios.defaults.headers.common['Authorization'] = token; const fileInput = document.querySelector('#file'); fileInput.addEventListener('change', upload); function upload() { // 撰寫上傳的 API 事件 const file = fileInput.files[0]; const formData = new FormData(); formData.append("file-to-upload", file); // 使用 append() 對應 "file-to-upload" 欄位、夾帶檔案 → axios.post(`${url}/api/${path}/admin/upload`, formData) .then((res) => { console.log(res.data) }) .catch((err) => { console.dir(err); }) } ``` * `const file = fileInput.files[0];` * 類陣列,使用 `[]` 取值。 * `new FormData();` * JS 用來產生表單格式的方法。 * `formData.append("file-to-upload", file);` * 使用 `append()` 對應 `"file-to-upload"` 欄位、夾帶檔案 → 第 15 行。 ## Vite 安裝與環境建置環境 ### 🔸 為什麼要使用 Vite? > Vite 讓前端開發從「繁瑣設定」變成「直接開發」。 #### 1. 快速建立開發環境 * 不需從零設定環境。 * 透過預設選項即可立即開始開發。 #### 2. 適合開發單頁式應用(SPA) * 快速建立前端應用服務。 * 支援現代前端框架(如 React、Vue)。 #### 3. 整合性高的開發工具 * 將多種開發流程整合在同一工具中。 * 降低環境設定與維護成本。 #### 4. 套件管理一致化 * 由 CDN 改為 NPM 管理套件。 * 依賴關係與版本更可控。 ### 🔸 環境建立 ![螢幕擷取畫面 (1708)](https://hackmd.io/_uploads/HJ5GOFkHWl.png) ``` npm create vite@latest ``` * Project name: (自定義專案名稱) * Select a framework: › React * Select a variant: › JavaScript ### 🔸 安裝 NPM ``` npm install ``` ### 🔸 運行方式 ``` npm run dev ``` * 運行開發環境。 * `dev` 並不是 npm 的指令,是透過 package.json 檔案新增指令的。 ### 🔸 專案環境結構說明 #### 1. `node_modules` * 存放專案所使用的第三方套件。 * 由套件管理工具自動產生,不需手動修改。 #### 2. `public` * 不會被編譯。 * 存放靜態資源(如圖片、favicon)。 * 內容會直接以原始檔案形式提供給瀏覽器。 #### 3. `src` * 會被編譯。 * 主要開發目錄,所有應用程式邏輯都在此。 * `main.jsx` 為專案進入點(entry point)。 * 所有元件與程式碼必須從此檔案被引入,才會納入編譯流程。 #### 4. `eslint.config` * 程式碼品質與風格檢查工具設定檔。 * 用於檢查錯誤、潛在問題與一致的撰寫規範。 #### 5. `.gitignore` * 設定 Git 要忽略的檔案與資料夾。 * 通常包含不需版控的檔案(如 `node_modules`、環境設定檔)。 #### 6. `package.json`、`package-lock.json` * 專案與套件的描述檔。 * 記錄: * 安裝的套件。 * 套件版本。 * 專案指令(scripts)。 * `package-lock.json` 用於鎖定實際安裝的套件版本。 #### 7. `vite.config` * Vite 的設定檔。 * 用來設定: * 專案啟動方式。 * 編譯與打包行為。 * 編譯後輸出的調整方式。 ### 🔸 專案部署流程(GitHub Pages) #### 1. 將專案放上 GitHub * 建立 GitHub Repository。 * 專案需先完成 `commit` 並 `push`。 #### 2. 透過 Git 指令操作部署流程 * 本地端以 git 管理版本與上傳程式碼。 #### 3. 建立正式版檔案 ``` npm run build ``` * 產生可部署的靜態檔案。 * Vite 預設輸出至 `dist` 資料夾。 #### 4. 安裝 `gh-pages` 套件 ``` npm install gh-pages --save-dev ``` * 用途:將靜態檔案一鍵部署至 GitHub Pages。 * 屬於開發工具,因此通常放在 `devDependencies`。 #### 5. 設定部署指令 在 `package.json` 的 `"scripts"` 中加入: ``` "deploy": "gh-pages -d dist" ``` * `gh-pages`:執行部署套件。 * `-d dist`:指定要部署的資料夾。 * `-d` = **directory(目錄)**。 #### 6. 執行部署 ``` npm run deploy ``` * 若成功,終端機會顯示 `Published`。 * 代表已成功部署到 GitHub Pages。 #### 總結 * `build`:產生靜態檔案。 * `gh-pages`:負責把 `dist` 推到 GitHub Pages。 * `deploy`:實際執行部署的指令。 ### 🔸 調整靜態檔案路徑(Vite) > GitHub Pages 不是從根目錄部署時,必須設定 `base`,否則靜態資源會找不到。 #### 問題與說明 * Vite 預設使用**絕對路徑**載入靜態資源。 * 部署到 **GitHub Pages(非根目錄)** 時,資源路徑會錯誤。 * 必須調整為對應 Repository 的路徑。 #### `vite.config.js` 設定 ```javascript export default defineConfig({ base: process.env.NODE_ENV === "production" ? "/Repository名稱/" : "/", plugins: [react()], }) ``` #### 1. `base` * 設定專案的基礎路徑(所有靜態資源的前綴)。 #### 2. 環境判斷 ```javascript process.env.NODE_ENV === "production" ``` * 判斷目前是否為正式環境: | 指令 | 環境 | 路徑 | | --------------- | ---- | ---------------- | | `npm run dev` | 開發環境 | `/` | | `npm run build` | 正式環境 | `/Repository名稱/` | #### 為什麼這樣做? * 本地開發仍維持 `/`,避免路徑錯誤。 * 部署後資源能正確從 GitHub Pages 的子路徑載入。 ## 在 React 中操作 API ### 🔸 關注點分離下的事件處理觀念 > 不找 DOM,而是改狀態;DOM 只是結果。 ```jsx= <button type="button" id="login" onClick={() => login()}> // 把方法綁按鈕上 登入 </button> ``` 在強調關注點分離的框架(如 React)中,不直接操作或選取 DOM 元素,而是以「事件處理函式」取代,例如: * `onClick` * `onChange` * `onSubmit` 事件發生時: * 改變狀態(state)。 * 由狀態驅動畫面更新。 ### 🔸 JavaScript vs React 的寫法 #### JavaScript ```javascript= const emailInput = document.querySelector('#email'); const pwInput = document.querySelector('#password'); loginBtn.addEventListener('click', login); function login() { const username = emailInput.value; const password = pwInput.value; const user = { username, password } axios.post(`${url}/admin/signin`, user) .then((res) => { const { token, expired } = res.data; document.cookie = `hexToken=${token}; expires=${new Date(expired)}`; }) .catch((err) => { console.dir(err); }) } ``` #### React ```jsx= // 建立 React 元件 function App() { // useState : 同時定義狀態與更新機制 const [data, setData] = useState({ username: "", password: "", }); // 串接 API 與送出 data 給後端 async function login() { try { const res = await axios.post(`${url}/admin/signin`, data) } catch (error) { console.log(error); } } // 單一事件處理器對應多個 input function eventHandler(e) { const { value, name } = e.target; setData({ ...data, [name]: value, }); } // React 會根據目前 state 把 JSX 轉成畫面 return ( <> <div> <input type="email" id="email" name="username" onChange={(e) => { eventHandler(e); }} /> <input type="email" id="email" name="password" onChange={(e) => { eventHandler(e); }} /> <button type="button" id="login" onClick={() => login()}> 登入 </button> </div> </> ); } ``` ### 🔸 將畫面與資料抽離(關注點分離) #### 核心概念 * 初學時,常把「畫面渲染」與「資料處理」寫在同一段程式碼,可先這樣寫沒關係,理解後再逐步抽離。 * 抽離的目的: * 降低重複程式碼。 * 提高可讀性與可維護性。 * 讓事件處理專心做「資料更新」。 #### 關注點分離前(畫面+資料混在一起) ```jsx= <input type="password" id="password" name="password" onChange={(e) => { const { value, name } = e.target; setData({ ...data, [name]: value, }); }} /> ``` * `<input>` 本身就有 `name` 屬性,可作為資料欄位的 key。 * 透過解構,直接取得輸入值與欄位名稱。 * `const { value, name } = e.target;` * 使用動態屬性,根據不同 `input` 的 `name`,更新對應的 `state`(元件「目前的資料狀態」,React 知道後會重新渲染畫面)。 * `[name]: value` * 因此,同一段邏輯可處理多個 `input`。 * (點擊帳號)`[name]: value` → `{ name } = e.target` → `name="username"` * (點擊密碼)`[name]: value` → `{ name } = e.target` → `name="password"` #### 關注點分離後(抽離事件處理) 1️⃣ 抽出事件處理邏輯 ```jsx= { const { value, name } = e.target; setData({ ...data, [name]: value, }); ``` 2️⃣ 定義成 function ```jsx= function eventHandler(e) { const { value, name } = e.target; setData({ ...data, [name]: value, }); } ``` 3️⃣ 在畫面中只負責「呼叫」 ```jsx= <input type="email" id="email" name="username" onChange={(e) => eventHandler(e)} /> ``` ### 🔸 setData:取代原本的 data #### useState 更新是「覆蓋」 ```jsx= onChange={(e) => { const { value } = e.target; setData({ ...data, // 保留其他欄位 username: value // 只更新這個欄位 }); }} ``` * `setData` 會「整個取代 state」,不展開(複製一份)就會把其他欄位覆蓋。 #### 展開運算符在做什麼? ```jsx= setData({ ...data, username: value }); ``` 等同於: ```jsx= { password: ..., // 複製一份 保留原始值 remember: ..., // 複製一份 保留原始值 username: value // 新值 覆蓋原始值 } ``` * 先把原本的 `data` 全部複製過來。 * 再覆蓋 `username`。 ### 🔸 安裝 axios ``` npm install axios ``` ``` import axios from 'axios'; ``` * [axios](axios) * 先暫停程式碼 `ctrl + c` * 執行 `npm install axios`、接著引入 `import axios from 'axios';` * 最後再啟動程式碼 `npm run dev` * 可以在 `package-lock` 裡看到新增的套件 ### 🔸 總結:React 的核心思維 #### 狀態管理 ``` const [data, setData] = useState(...) ``` * `data`:狀態本身(目前的資料)。 * `setData`:更新這份狀態的唯一方法。 * `useState` 做的是: * 建立一份「狀態」。 * 提供一個「觸發重新渲染的更新函式」。 * 不是只「定義更新方法」,而是同時定義狀態與更新機制。 #### 行為與副作用 ``` function login() ``` * 負責「送出目前的 `data`」。 * 使用 `axios.post` 與後端 `/admin/signin` 溝通。 * 屬於行為(behavior)/副作用(side effect)。 * 行為:使用者觸發後要「做的事」。 * 副作用:會影響 React 以外世界的行為。 #### 事件與資料流 ``` function eventHandler(e) ``` * 從 `e.target` 拿到: * `name`:決定要改哪個欄位。 * `value`:新的值。 * 使用 `[name]: value` 動態更新對應欄位。 * 搭配 `...data` 保留其他欄位不被覆蓋。 * 這是單一事件處理器對應多個 `input` 的標準寫法。 #### 畫面渲染 ``` return 之後的程式碼 ``` * `return` 回傳的是 JSX。 * React 會根據目前 `state`,把 JSX 轉成畫面。 * 當 `setData` 被呼叫 → `state` 改變 → App 重新執行 → JSX 重新計算。