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

### 🔸 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}:刪除用戶資料。
### 🔸 如何運作?

* 瀏覽器中的網頁無法直接與資料庫連線,因此需要透過伺服器作為中介,負責資料的驗證、處理與傳遞。
* 發送請求(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**,藉此區分使用者身分。
### 🔸 基本結構

不是所有 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();
```
## 登入驗證流程
### 🔸 基本流程

### 🔸 儲存 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

### 🔸 總結
> **登入成功後儲存 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 管理套件。
* 依賴關係與版本更可控。
### 🔸 環境建立

```
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 重新計算。