# 架構概念與環境建置,前後端分離概念解析與實務開發工作流說明
## 前端網站開發相關基礎知識
在網頁設計中,我們需要掌握三種語法與角色,才能打造完善的網站:
HTML (HyperText Markup Language,超文字標示語言) 負責內容結構與資訊標記;
CSS (Cascading Style Sheets,階層式樣式表) 負責頁面外觀與排版;
JavaScript (JS) 負責互動行為與動態功能。
三者缺一不可。

> 圖片上的 5 與 3 分別是 HTML5 與 CSS3,只是顯示目前使用的主流版本而已。
### 前端開發三大基礎語言
#### 1. **HTML (Hyper Text Markup Language)**
* 中文:超文字標示語言
* **用途**:負責「內容結構」與「資訊標記」
* **比喻**:就像「建築物的骨架」
* **特色**:
* 定義標題、段落、清單、表格、圖片等
* 沒有 HTML,網頁就會是一片空白
---
#### 2. **CSS (Cascading Style Sheets)**
* 中文:階層式樣式表
* **用途**:負責「外觀樣式」與「頁面排版」
* **比喻**:就像「房子的裝潢與油漆」
* **特色**:
* 控制字體大小、顏色、位置、背景、動畫
* 沒有 CSS,HTML 雖然能顯示,但閱讀體驗很差
---
#### 3. **JavaScript (JS)**
* **用途**:負責「互動行為」與「動態功能」
* **比喻**:就像「電線與開關系統,讓房子活起來」
* **特色**:
* 處理表單送出、按鈕點擊、聊天室訊息、登入註冊
* 讓網頁可以動態更新內容(例如購物車、即時通知)
---
#### 4. **三者缺一不可**
* **HTML** = 骨架 → 沒有就看不到內容
* **CSS** = 外觀 → 沒有就醜陋難讀
* **JavaScript** = 行為 → 沒有就無法互動
* 三者結合 → 完整的現代網頁
---
#### 5. **延伸到框架**
* React / Vue / Angular …
* 本質上都是 **HTML + CSS + JS 的延伸與封裝**
* 幫助開發更快、更有組織
---
📌 示意圖:

#### 補充概念圖
| 純 HTML 只具有線稿結構 | CSS 將賦予色彩與樣式等 | JavaScript 將會提供額外控制 |
| -------- | -------- | -------- |
|  |  |  |
<!-- --- -->
<br/>
### HTML 基礎知識學習
HTML 是一種 **標籤式語言**(Markup Language)。
它的核心概念就是:使用對應的「標籤 (tag)」來描述網頁內容。
👉 例子:段落標籤 `<p>`
```html
<p>我是一個段落標籤</p>
```
* `<p>` → 開始標籤 (paragraph)
* `</p>` → 結束標籤
* 中間文字 → 在網頁上顯示的內容
像是以下的範例就是用 p 標籤包覆了一段文字,而 p 標籤其實就是 paragraph(段落) 的縮寫,代表這一個區域是一個文字段落。
```htmlmixed=
<p>
我是一個段落標籤
</p>
```
#### 範例呈現畫面

#### 標籤的屬性 (Attributes) 與事件 (Events)
##### 屬性 (Attributes)
屬性是 **加在標籤上的附加資訊**,用來改變標籤的顯示方式或增加功能。
* 常見屬性:
* `id` → 元素唯一名稱
* `class` → 元素分類(常用於 CSS 選取)
* `src` → 超連結目標網址(用於 `<img>`、`<script>`)
* `href` → 超連結網址(用於 `<a>`)
* `alt` → 圖片替代文字(用於 `<img>`)
👉 範例:
```html
<!-- 在錨點的元素上,加上 href(連結)class(類別)的屬性(Attributes) -->
<a href="https://google.com" class="link">前往 Google</a>
<!-- 在錨點的元素上,加上 src(連結)與alt(說明)的屬性(Attributes) -->
<img src="dog.jpg" alt="一張小狗的圖片" />
```
> a 標籤的全稱是 Anchor(錨點)
:::warning
a 是 Anchor(錨點)的縮寫。
注意:不同標籤支援的屬性不同,例如 href 只適用於 `<a>`、`<link>`等。
HTML 就像是一個巨大的拼圖遊戲,每個標籤都是拼圖的一塊,而實際上網頁也確實是由大量的物件拼湊而成,並且HTML 也就是這些「物件」的其中一環。
:::
---
##### 事件 (Events / 事件屬性)
事件多數是 **事件觸發**,通常搭配 JavaScript 使用。
* 常見事件:
* `onclick` → 點擊時觸發
* `onmouseover` → 滑鼠移入時觸發
* `onchange` → 表單輸入變更時觸發
👉 範例:
```html
<!-- 在按鈕的元素上,加上 onclick(點擊事件) 的事件 -->
<button onclick="alert('你點擊了按鈕!')">點我</button>
```
#### 延伸說明
* HTML 標籤有不同用途,例如:
* `<h1>`:標題
* `<p>`:段落
* `<a>`:超連結
* `<img>`:圖片
* 大部分標籤成對出現:`<標籤>內容</標籤>`
* 部分標籤是「自閉合的」:`<br />` 換行、`<hr />` 水平線、`<img />` 圖片
#### 總結
* HTML = **標籤式語言**
* 透過標籤來描述內容
* `<p>` 是最基本的段落標籤
* 標籤有兩種形式:**成對元素** 與 **自閉合元素**
* 練習寫出幾個常見標籤,能快速熟悉 HTML 結構
> HTML5 中 `<br>`、`<hr>`、`<img>` 屬 void elements,不需要 `</br>` 或 />;寫 `<br>`、`<hr>`、<img ...> 即可。(保留 /> 也能被容忍,但建議示範標準寫法)
<br/>
### 換個方法了解 HTML
HTML 的本質就是一個 **存放內容的載體**。
它透過各種標籤(段落、標題、超連結…)來幫助我們編排文章與內容。
#### 簡單的文件轉換
以下示範如何把 Word 文件中的文字,轉換成對應的 HTML 標籤:

---
#### 錦上添花的內容
在基礎結構上,我們還可以加入更多標籤,例如 **段落**、**粗體**、**斜體**,讓文章更有層次:

<br/>
### HTML 的基本結構解析
在前面的課程中,我們已經學習了 HTML 的基本概念與常見標籤。接下來,讓我們看看一個完整的 HTML 頁面最初的樣子。
---
#### 最基礎的 HTML 結構
以下的程式碼,是網頁正常運作所需的最基本結構:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
我們網頁的內容
</body>
</html>
```
📌 小提醒:
其實,如果我們只輸入 `<p>我是一個段落標籤</p>`,瀏覽器依然能正確顯示,這是因為瀏覽器會自動補齊缺少的標籤。
但在專業開發中,我們仍然要寫完整結構,否則不利於維護與擴充。
---
#### 開啟文字編輯器開始撰寫
了解了基本結構後,我們就可以動手建立第一個 HTML 頁面。
1. 開啟 **Visual Studio Code**
2. 建立一個檔案,副檔名為 `.html`(例如 `index.html`)
3. 在檔案中輸入驚嘆號 `!`,VS Code 會自動跳出語法片段
4. 按下 Enter,就能快速生成完整的 HTML5 基本架構
---
#### 自動生成的 HTML 範例
VS Code 幫我們補齊的內容會長這樣:
```html
<!DOCTYPE html>
<!-- lang 通常預設會是 en,但我們可以將其改為 建議改 zh-Hant(或 zh-TW 也可) -->
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
</html>
```
* `<!DOCTYPE html>` → 告訴瀏覽器這是 HTML5 文件
* `<html lang="en">` → 指定語言(繁體中文可改成 `zh`)
* `<meta charset="UTF-8" />` → 設定字元編碼,避免亂碼
* `<meta name="viewport" ... />` → 讓網頁能在手機等裝置自適應
:::warning
注意事項 ⚠️
請務必先建立一個資料夾,並用 VS Code 開啟這個資料夾,再把 `index.html` 放進去。
之後我們會安裝一個 VS Code 擴充套件,直接在編輯器中打開 HTML 頁面,方便測試。
:::
<br/>
### 常見標籤預覽
在基礎的建設完成之後,我們就可以開始來撰寫我們的 HTML 頁面了,但實際上在 HTML 頁面的撰寫上,我們其實保有很高的自由度,另因網頁標籤可能會因 CSS 與 Javascript 而有所異動,因此 HTML 的撰寫其實沒有固定的準則,不過當然,設定擋放置在 head 與內容放在 body 中,是最主要的鐵則。
#### 線上標籤查詢平台
因為 HTML 的標籤非常大量,因此這邊只會介紹比較常用的標籤,如果有想要暸解更多標籤的同學,可以點擊下方兩個連結進行閱覽。
[MDN HTML 元素表](https://developer.mozilla.org/zh-TW/docs/Web/HTML/Element)
[HTML 索引線上查詢](https://htmlreference.io/)
#### HTML5 結構標籤
| 標籤名稱 | 簡單說明 |
| --- | --- |
| `<header>` | 網頁頁首,經常包含 logo、標題、導航列或介紹文字 |
| `<nav>` | 網站操作導航列 |
| `<aside>` | 跟主要內文沒有直接聯繫的側欄,常用來做功能按鈕設計與輔助說明文字 |
| `<main>` | 主要內容 |
| `<article>` | 文章 |
| `<section>` | 章節段落 |
| `<footer>` | 網頁結尾,經常包含版權資訊、聯絡方式 |
#### 結構範例
以下的範例結構,僅為一個參考的佈局方式

> 雖然很多人都將 HTML5 標籤作為主要切版結構進行設計,但在實務將其融入頁面組件等開發也可以協助我們的語易化更加順暢。
<br/>
### JavaScript 快速入門
#### 1. JavaScript 怎麼執行?
JS 可以直接嵌在 HTML 裡,或以外部檔案引入。
```html
<!-- 方式一:內嵌 -->
<script>
console.log('Hello JS');
</script>
<!-- 方式二:外部檔案 -->
<script src="assets/javascript/script.js"></script>
```
💡 開發時請開啟瀏覽器 DevTools(F12)→ **Console** 觀察輸出與錯誤。
---
#### 2. 變數與常數:`let` / `const`
* `const`:宣告**不會被重新指定**的變數(推薦預設用它)
* `let`:需要重新指定時使用
* `var`:舊語法,不建議
```javascript
const siteName = 'My Todo'; // 不會被重新指定
let count = 0; // 需要更新時用 let
count += 1;
```
---
#### 3. 基本型別(為 JSON 對照做準備)
* **Number**:`42`, `3.14`
* **String**:`'text'`、`"text"`、`` `Hi ${name}` ``
* **Boolean**:`true` / `false`
* **null**(空值) / **undefined**(未定義)
* **Object**、**Array**
```javascript
const n = 42;
const s = `Hello`;
const ok = true;
const user = { name: 'Alex', age: 33 };
const list = [1, 2, 3];
```
---
#### 4. 物件與陣列(理解 JSON 的關鍵)
**物件(Object)**:鍵值對
**陣列(Array)**:有序集合
```javascript
const book = {
title: 'My Book',
pages: 220,
author: { name: 'Alex', email: 'test@foo.com' },
};
console.log(book.title); // 點記法
console.log(book['author']['name']); // 括號記法(動態鍵名時用)
```
[詳細教材](https://hackmd.io/@react-course/S13wL00DR)
---
#### 5. 函式與箭頭函式
```javascript
function add(a, b) { return a + b; }
const add2 = (a, b) => a + b;
```
> 箭頭函式若是後方沒有使用 {} 進行包覆,後面那段程式的執行結果將會自動 return
---
#### 6. 常用語法糖
* **樣板字串**(易於組 HTML / 訊息):
```javascript
const title = 'Learn JS';
const li = `<li class="item">${title}</li>`;
```
* **解構 / 展開**(處理物件、陣列很方便):
```javascript
const { name, email } = book.author;
const withId = { id: 1, ...book };
const more = [0, ...list, 4];
```
* **三元運算子**(簡寫 if/else):
```javascript
const label = list.length ? '有資料' : '沒有資料';
```
---
#### 7. 陣列方法(渲染列表的核心)
* `map`:把陣列**一一轉換**成新陣列(很適合轉成 HTML 字串)
* `filter`:**過濾**
* `find`:找出第一個符合的元素
```javascript
const todos = [{ title: 'A' }, { title: 'B' }];
const html = todos.map(t => `<li>${t.title}</li>`).join('');
// => "<li>A</li><li>B</li>"
```
---
#### 8. DOM 操作與事件(Todo & 表單必備)
```javascript
const form = document.querySelector('.todo-list__form');
const input = document.querySelector('.todo-list__input');
const list = document.querySelector('.todo-list__items');
form.addEventListener('submit', (e) => {
e.preventDefault(); // 阻止表單預設送出
const title = input.value.trim(); // 去頭尾空白
if (!title) return;
// 1) 更新資料 → 2) 重新渲染 → 3) 清空輸入
// (實作見 Todo 範例)
input.value = '';
});
```
---
#### 9. 迷你練習
**目標**:把輸入加入清單並顯示(不含樣式,對齊 Todo 範例的思路)
```html
<form id="f"><input id="i" /><button>新增</button></form>
<ul id="u"></ul>
<script>
const f = document.getElementById('f');
const i = document.getElementById('i');
const u = document.getElementById('u');
const data = [];
const render = () => u.innerHTML = data.map(x => `<li>${x}</li>`).join('');
f.addEventListener('submit', e => {
e.preventDefault();
const v = i.value.trim();
if (!v) return;
data.push(v);
i.value = '';
render();
});
render();
</script>
```
[線上範例](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/javascript-base)
---
#### 10. 易錯點清單
* `==` vs `===`:請用 **`===`**(嚴格相等)
* `const` vs `let`:預設用 **`const`**,需要重新指定才用 `let`
* `innerHTML` 會解析 HTML,**輸入若來自使用者需小心**(課程中先使用可信資料)
<br/>
## 建立現代 Web 開發的整體觀念
### 傳統網頁的運作方式
在早期的網站設計中,每一個頁面幾乎都是一份獨立的 HTML 文件。
* 當使用者切換頁面時,瀏覽器會向伺服器請求一個新的 HTML 檔,並整頁刷新。
* 可以把它想像成 **翻書**:每翻一頁,看到的就是一份全新的 HTML 頁面。
📌 優點:簡單直觀,容易實作。
📌 缺點:當專案變大時,重複的內容(例如導覽列、頁尾)需要在每個檔案中分別維護,修改起來非常麻煩。
---
### 維護困境
舉個例子:
* 在 10 頁的小網站裡,如果要更換導覽列的 LOGO,你需要修改 10 份 HTML。
* 在 1000 頁的大型網站中,這個動作就會變成一場災難。
這就像是一本有 1000 頁的書,每一頁都寫著書名,如果要改書名,就得把每一頁都改一次。
---
### 現代 Web 架構的解法
為了解決這個問題,現代 Web 開發採用了 **前後端分離** 與 **組件化設計** 的方式:
* **前端**:專注於 UI 與使用者體驗,負責渲染頁面。
* **後端**:專注於提供資料與邏輯,通常透過 API (REST/GraphQL) 對外服務。
* **共用組件**:像是導覽列、頁尾、側邊欄,可以在多個頁面重複使用,只需要維護一份程式碼。
這種方式大幅降低了維護成本,也讓不同團隊(前端、後端)可以同時開發,提升效率。
---
### 單頁應用程式(SPA, Single Page Application)
現代框架(例如 React、Vue、Angular)常採用 **單頁應用程式 (SPA)** 的模式:
* 使用者只載入一次 HTML,之後的操作都透過 **JavaScript 動態切換內容**。
* 當使用者切換頁面時,瀏覽器不再整頁刷新,而是只更新需要改變的部分。
* 後端只提供 **API 資料**,而不是整份 HTML。
📌 優點:
* 體驗更流暢,像在操作 App
* 前後端職責清晰
* 可重複使用的組件,讓維護更容易
#### 範例示意圖

在這個範例中:
* 右邊的 Aside(側邊欄)保持不變
* 只有主要內容區(Main Content)會隨使用者操作而更新
* 這就是 **前後端分離 + 組件化開發** 的實際應用
---
### 總結
* **傳統架構**:每個頁面獨立,維護困難
* **現代架構**:前後端分離,透過 API 提供資料,前端負責畫面
* **SPA 模式**:一次載入,動態更新,體驗更流暢
* **組件化**:共用元件只需要維護一次,大幅降低開發成本
<br/>
## 全端(Full Stack)是什麼?
全端開發 = **前端(Front End)+後端(Back End)+資料庫(Database)**。
可把網站比喻成餐廳:
* **前端**:店面外觀、動線與擺設(使用者看到與操作的部分)
* **後端**:廚房處理流程(商業邏輯)
* **資料庫**:食材與庫存(資料)
* **API/資料傳輸**:服務生把餐點送到客人手上(前後端溝通)
---
### 什麼時候不需要後端?
不是所有網站都要後端與資料庫。像**形象官網、個人頁**等內容固定、更新頻率低的網站,用**靜態網站**即可;
若要登入、下單、即時更新等 **動態功能**,才需要後端與資料庫。
---
### 雛形/設計的重要性
雛形與視覺設計不屬於 Full Stack 本身,卻像**建築藍圖**:
與工程可分離,但對做出**好用又好看**的網站至關重要。
#### 範例示意圖

<br/>
## 了解前後端分離的架構模式與傳統架構的差異
在軟體與網站開發中,如何組織程式碼是一件非常重要的事。良好的架構能讓專案 **更好維護、更易於擴充、也更便於測試**。
常見的兩種設計模式是 **MVC (Model-View-Controller)** 與 **MVVM (Model-View-ViewModel)**。
這兩者的目標相同:把「資料邏輯」和「顯示介面」分開,但它們的資料流與角色定位並不一樣。
---
### MVC 架構
MVC 是一種傳統且廣泛使用的架構模式,常見於伺服器端框架(例如:Ruby on Rails、Laravel、ASP.NET MVC)。
* **Model(模型)**
* 負責資料邏輯與狀態管理
* 例如:存取資料庫、處理計算
* **View(視圖)**
* 負責顯示內容給使用者
* 例如:HTML 頁面、報表畫面
* **Controller(控制器)**
* 負責接收使用者操作並進行協調
* 例如:當使用者按下「送出表單」 → Controller 呼叫 Model → 更新資料 → 將結果傳給 View 顯示
📌 MVC 的特點
* 資料流向清楚(Controller → Model → View)
* 適合中小型專案或後端模板直出的網站
* 缺點:前端互動較多時,Controller 會變得複雜
---
#### MVC 架構示意圖

> 在典型的 MVC 網站中:
>
> * 使用者(User)觸發事件(點擊按鈕、請求頁面)
> * Controller 負責協調並呼叫 Model
> * Model 從資料庫(DB)取得資料並處理
> * 最後 Controller 把資料交給 View 渲染給使用者
---
### MVVM 架構
MVVM 是隨著前端框架(如 Vue、Angular)興起而流行的架構模式。它強調 **資料綁定 (Data Binding)**,讓「資料改變」與「畫面更新」能自動同步。
* **Model(模型)**
* 同 MVC,負責資料邏輯與狀態
* **View(視圖)**
* 使用者介面,負責顯示資料與接收互動
* **ViewModel(視圖模型)**
* 負責管理 View 與 Model 的橋樑
* 提供「資料狀態」給 View,並處理事件邏輯
* 透過 **雙向資料綁定**,當 Model 更新 → View 自動更新;當使用者輸入 → Model 自動更新
📌 MVVM 的特點
* 透過資料綁定,開發效率大幅提升
* 前端互動複雜時,程式碼更容易維護
* 缺點:小型專案可能過度設計,增加複雜度
---
#### MVVM 架構示意圖

> 在 MVVM 的世界裡:
>
> * 使用者(User)操作 View
> * ViewModel 自動處理資料流與事件
> * Model 更新後,透過 ViewModel 立即反應到畫面上
> * 不需要像 MVC 一樣透過 Controller 手動調用
---
### MVC 與 MVVM 的比較
| 特點 | MVC | MVVM |
| ---------- | ------------ | --------------- |
| **控制邏輯位置** | 在 Controller | 在 ViewModel |
| **資料流** | 單向,需手動更新 | 雙向,自動同步 |
| **適合場景** | 傳統伺服器渲染頁面 | 前端框架 (SPA, PWA) |
| **優點** | 結構清晰,後端常用 | 開發效率高,互動性強 |
| **缺點** | 前端互動代碼繁瑣 | 小專案可能過度設計 |
📌 總結:
* **MVC** 適合傳統網站或後端為主的系統。
* **MVVM** 更適合現代前端框架與單頁應用程式(SPA)。
<br>
## JSON 基礎教學
### 為什麼需要 JSON?
在現代 Web 開發中,前端與後端分離後,雙方必須透過 **資料交換格式** 來溝通。
這些格式要同時滿足幾個條件:
* 簡單易讀
* 易於解析與生成
* 能被不同語言支援
* 安全且結構清晰
早期常用的格式是 **XML**,但由於語法繁瑣,逐漸被 **JSON (JavaScript Object Notation)** 取代。
如今,JSON 幾乎成為網頁與伺服器之間傳遞資料的事實標準。
👉 延伸閱讀:
* [XML 教學](https://www.w3schools.com/xml/)
* [JSON 官方網站](https://www.json.org/json-en.html)
---
### JSON 是什麼?
JSON(JavaScript Object Notation)是一種 **輕量級資料交換格式**。
它的語法和 JavaScript 物件相似,但它本身只是一種「格式」,並不是程式語言。
📌 特點:
* 語法簡單,容易理解
* 幾乎所有程式語言都支援
* 對前端開發者特別友善(因為與 JavaScript 語法相近)
JSON 可以表示的基本資料型別:
* **數值 (Number)**:整數、小數、科學記號
* **字串 (String)**:必須用雙引號 `"`
* **布林值 (Boolean)**:`true` / `false`
* **陣列 (Array)**:有序集合,用 `[]` 包裹
* **物件 (Object)**:無序的「鍵-值對」,用 `{}` 包裹
* **空值 (null)**:代表空資料

---
### JSON 的範例
```json
{
"type": "book",
"pages": 220,
"title": "My Book",
"author": {
"name": "Alex",
"age": 33,
"email": "test@foo.com",
"directions": "Lorem ipsum dolor sit amet consectetur adipisicing elit."
}
}
```
---
### 在 JavaScript 中使用 JSON
#### 物件轉 JSON
使用 `JSON.stringify()`
```javascript
var book = {
type: 'book',
pages: 220,
title: 'My Book',
author: {
name: 'Alex',
age: 33,
email: 'test@foo.com',
},
};
console.log(JSON.stringify(book));
// {"type":"book","pages":220,"title":"My Book","author":{"name":"Alex","age":33,"email":"test@foo.com"}}
```
---
#### JSON 轉物件
使用 `JSON.parse()`
```javascript
var jsonData = '{"type":"book","pages":220,"title":"My Book","author":{"name":"Alex","age":33,"email":"test@foo.com"}}';
console.log(JSON.parse(jsonData));
// {type: "book", pages: 220, title: "My Book", author: {…}}
```
<br/>
### JSON 與 localStorage
瀏覽器提供 **localStorage**,可以在同一個網域下保存資料,即使重新整理或關閉瀏覽器也不會立即消失。
📌 注意:localStorage 只能存放 **字串**,所以常搭配 JSON 使用。

#### 儲存資料
```javascript
localStorage.setItem('myCat', 'Tom');
```
#### 取得資料
```javascript
let cat = localStorage.getItem('myCat');
```
#### 刪除資料
```javascript
localStorage.removeItem('myCat');
```
#### 清空全部
```javascript
localStorage.clear();
```
---
### 總結
* JSON 是目前最主流的資料交換格式,取代了 XML
* 支援跨語言,前端與後端溝通的最佳選擇
* JavaScript 內建 `JSON.stringify()` 與 `JSON.parse()`
* 在瀏覽器端,可與 **localStorage** 搭配,實現資料儲存
<br/>
## TODO LIST 第一版練習
[範例連結](https://github.com/IffyArt/javascript-base-todo-list)
```bash=
project
│ index.html
│
└───assets
│ │
│ └───javascript
│ │ │ script.js
│ │
│ └───style
│ │ style.css
```
### HTML 樣板
```htmlembedded=
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 引入 Material Icon 庫 -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
/>
<link rel="stylesheet" href="assets/style/style.css" />
</head>
<body>
<article class="todo-list">
<form class="todo-list__form">
<input type="text" placeholder="輸入代辦內容" />
<button><span class="material-symbols-outlined"> add </span></button>
</form>
<ol class="todo-list__tabs">
<li class="active">全部</li>
<li>代辦</li>
<li>已完成</li>
</ol>
<ul class="todo-list__items"></ul>
</article>
<script src="assets/javascript/script.js"></script>
</body>
</html>
```
### CSS樣式
```css=
* {
margin: 0;
padding: 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 40px;
min-height: 100vh;
background-color: aliceblue;
}
.todo-list {
width: 400px;
}
.todo-list__form {
display: flex;
}
.todo-list__form input {
flex: 1;
padding: 15px;
outline: none;
border: none;
font-size: 1rem;
}
.todo-list__form button {
padding: 15px;
border: none;
background-color: lightslategray;
color: white;
}
.todo-list__tabs {
display: flex;
margin: 10px 0;
background-color: white;
list-style-type: none;
}
.todo-list__tabs > li {
padding: 15px 20px;
letter-spacing: 2px;
cursor: pointer;
transition: background-color 300ms, color 300ms;
}
.todo-list__tabs > li.active,
.todo-list__tabs > li:hover {
background-color: lightslategray;
color: white;
}
.todo-list__items {
display: flex;
flex-direction: column;
list-style-type: none;
}
.todo-list__items li {
display: inline-flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding: 10px 20px;
background-color: white;
}
.todo-list__not-found {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
background-color: lightslategray;
color: white;
}
```
### JavaScript 操作
```javascript=
// 去取得 localStorage 上方的資料,若沒有結果的話會回傳結果 null
// 原本使用兩個陣列是指空值合併運算符,但這個操作其實比較新,因此我們可以改用三元運算式進行操作
const todoListData = JSON.parse(localStorage.getItem('todoList'))
? JSON.parse(localStorage.getItem('todoList'))
: [];
// 去取得 DOM 上面 class 名稱是 todo-list 的節點
const todoList = document.querySelector('.todo-list');
// 去取得 todoList 中 class 名稱是 todo-list__form 的節點
const todoListFrom = todoList.querySelector('.todo-list__form');
// 去取得 todoListFrom 中的第一個 button 節點
const todoListFromButton = todoListFrom.querySelector('button');
// 去取得 todoList 中 class 名稱是 todo-list__tabs 的節點
const todoListTabs = todoList.querySelector('.todo-list__tabs');
// 去取得 todoList 中 class 名稱是 todo-list__items 的節點
const todoListItems = todoList.querySelector('.todo-list__items');
// 建立一個協助我們宣染頁面 todo list 的函式
const renderTodoList = () => {
// 在此處先宣告一個將要被宣染到 html 的變數,沒有跟賦值一起操作是為了讓過程比較好修改與理解
let todoListUI;
// 讓要被宣染到 html 的物件,指定成一個 map 出來的新陣列,此處將會回傳一個 html 模板
todoListUI = todoListData.map(
(element) =>
`<li>
<h2>${element.title}</h2>
<span>${element.status}</span>
</li>`,
);
// 這裡可以個別將 todoListUI 與 todoListData 列印出來,並觀察其中的差異
console.log(todoListData); // 陣列資料
console.log(todoListUI); // 要被宣染到 html 的內容
// 這裡會將 todoListItems 這個 node 節點所在的 html,置換為下方的其中一種方式
// 這裡藉由三元運算式的方式,去判斷目前陣列是否有內容(用長度判斷),若沒有內容的話新增一個提示區塊
todoListItems.innerHTML = todoListUI.length
? todoListUI.join('')
: '<div class="todo-list__not-found">目前沒有內容</div>';
};
// 幫 todoListFromButton 這個節點新增按鈕點擊事件
todoListFromButton.addEventListener('click', function (event) {
// 因為我們的 button 是放在 form 中,因此點擊時會直接觸發表單送出操作,這裡是將這一預設操作給停止
event.preventDefault();
// 新增一筆資料到我們的 todoListData 這個 array 上面
todoListData.push({ title: 'ssss', status: '代辦' });
// 將已經新增資料的陣列,轉成 JSON 格式之後上傳到 localStorage 上面,這裡的機碼(儲存位子)使用 todoList
localStorage.setItem('todoList', JSON.stringify(todoListData));
// 觸發畫面更新的函式,因為此時他所倚賴的陣列已經更新了內容,因此會在畫面上增加一筆項目
renderTodoList();
});
// 此處算是初始化的操作,沒有執行的話畫面預設會是空的內容
renderTodoList();
```
## Todo List 第二版
[範例連結](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/todo-list-v2)
```bash=
project
│ index.html
│
└───assets
│ │
│ └───javascript
│ │ │ script.js
│ │
│ └───style
│ │ style.css
```
### HTML 樣板
```htmlembedded=
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 引入 Material Icon 庫 -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
/>
<link rel="stylesheet" href="assets/style/style.css" />
</head>
<body>
<article class="todo-list">
<form id="search-todo-list" class="todo-list__form">
<input type="text" id="search" placeholder="請輸入搜尋關鍵字" />
<button><span class="material-symbols-outlined"> search </span></button>
</form>
<form id="create-todo-list" class="todo-list__form">
<input type="text" placeholder="輸入代辦內容" />
<button><span class="material-symbols-outlined"> add </span></button>
</form>
<ol class="todo-list__tabs">
<li class="active">全部</li>
<li>代辦</li>
<li>已完成</li>
</ol>
<ul class="todo-list__items"></ul>
<p class="todo-list__info"></p>
</article>
<script src="assets/javascript/script.js"></script>
</body>
</html>
```
### CSS樣式
```css=
* {
margin: 0;
padding: 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 40px;
min-height: 100vh;
background-color: aliceblue;
}
.todo-list {
width: 400px;
}
.todo-list__form {
display: flex;
}
.todo-list__form input {
flex: 1;
padding: 15px;
outline: none;
border: none;
font-size: 1rem;
}
.todo-list__form button {
padding: 15px;
border: none;
background-color: lightslategray;
color: white;
}
.todo-list__tabs {
display: flex;
margin: 10px 0;
background-color: white;
list-style-type: none;
}
.todo-list__tabs > li {
padding: 15px 20px;
letter-spacing: 2px;
cursor: pointer;
transition: background-color 300ms, color 300ms;
}
.todo-list__tabs > li.active,
.todo-list__tabs > li:hover {
background-color: lightslategray;
color: white;
}
.todo-list__items {
display: flex;
flex-direction: column;
list-style-type: none;
}
.todo-list__items li {
display: inline-flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding: 10px 20px;
background-color: white;
}
.todo-list__items > li > label {
display: inline-flex;
align-items: center;
flex: 1;
}
.todo-list__items > li > label > input {
margin-right: 10px;
}
.todo-list__not-found {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
background-color: lightslategray;
color: white;
}
```
### JavaScript 操作
```javascript=
// 去取得 DOM 上面 class 名稱是 todo-list 的節點
const todoList = document.querySelector('.todo-list');
// 去取得 todoList 中 class 名稱是 todo-list__form 的節點
const todoListFrom = todoList.querySelector('#create-todo-list');
// 去取得 todoListFrom 中的第一個 button 節點
const todoListFromButton = todoListFrom.querySelector('button');
// 去取得 todoListFrom 中的第一個 input 節點
const todoListFromInput = todoListFrom.querySelector('input');
// 去取得 todoList 中 id 名稱是 search-todo-list 的節點
const todoListSearch = todoList.querySelector('#search-todo-list');
// 去取得 todoListSearch 中的第一個 button 節點
const todoListSearchButton = todoListSearch.querySelector('button');
// 去取得 todoListSearch 中的第一個 input 節點
const todoListSearchInput = todoListSearch.querySelector('input');
// 去取得 todoList 中 class 名稱是 todo-list__tabs 底下的 li 節點
const todoListTabs = todoList.querySelectorAll('.todo-list__tabs li');
// 去取得 todoList 中 class 名稱是 todo-list__items 的節點
const todoListItems = todoList.querySelector('.todo-list__items');
// 去取得 todoList 中 class 名稱是 todo-list__info 的節點
const todoListInfo = todoList.querySelector('.todo-list__info');
// 去取得 localStorage 上方的資料,若沒有結果的話會回傳結果 null
// 使用短路求值的技巧做操作
let todoListData = JSON.parse(localStorage.getItem('todoList')) || [];
// 定義整個 todo list 的初始狀態
let currentStatus = '全部';
// 定義一個關鍵字
let keyword = '';
// 建立一個協助我們宣染頁面 todo list 的函式
const renderTodoList = () => {
// 在此處先宣告一個將要被宣染到 html 的變數,沒有跟賦值一起操作是為了讓過程比較好修改與理解
let todoListUI;
todoListUI =
currentStatus === '全部'
? todoListData
: todoListData.filter((element) => element.status === currentStatus);
todoListUI = todoListUI.filter((element) => element.title.includes(keyword));
// 讓要被宣染到 html 的物件,指定成一個 map 出來的新陣列,此處將會回傳一個 html 模板
todoListUI = todoListUI.map(
(element, index) =>
`<li>
<label for="check_${index}">
<input type="checkbox"
onclick="editTodoItemStatus(${element.id})"
${element.status !== '代辦' ? 'checked' : ''}
id="check_${index}">
<h2>${element.title}</h2>
</label>
<span>${element.status}</span>
<span class="material-symbols-outlined"
onclick="deleteTodoItem(${element.id})">
delete
</span>
</li>`,
);
// 這裡可以個別將 todoListUI 與 todoListData 列印出來,並觀察其中的差異
console.log(todoListData); // 陣列資料
console.log(todoListUI.join('')); // 要被宣染到 html 的內容
// 這裡會將 todoListItems 這個 node 節點所在的 html,置換為下方的其中一種方式
// 這裡藉由三元運算`式的方式,去判斷目前陣列是否有內容(用長度判斷),若沒有內容的話新增一個提示區塊
todoListItems.innerHTML = todoListUI.length
? todoListUI.join('')
: '<div class="todo-list__not-found">目前沒有內容</div>';
todoListInfo.innerHTML = `${
keyword ? '關鍵字:' + keyword : ''
}目前的${currentStatus}共: ${todoListUI.length} 筆`;
};
const updateLocalStorage = () => {
// 將已經新增資料的陣列,轉成 JSON 格式之後上傳到 localStorage 上面,這裡的機碼(儲存位子)使用 todoList
localStorage.setItem('todoList', JSON.stringify(todoListData));
};
const editTodoItemStatus = (id) => {
// 對原始資料的陣列進行搜尋,並取得符合輸入 id 的資料結果
const itemData = todoListData.find((element) => element.id === id);
// 對資料的狀態進行狀態上的判斷,隨後再將判斷出的結果進行賦值操作
itemData.status = itemData.status !== '代辦' ? '代辦' : '已完成';
// 觸發畫面繪製的函式
renderTodoList();
// 觸發 updateLocalStorage 的函式操作
updateLocalStorage();
};
const deleteTodoItem = (id) => {
// 將 todoListData 重新給予一個沒有包括傳入 id 物件的陣列
todoListData = todoListData.filter((element) => element.id !== id);
// 觸發畫面繪製的函式
renderTodoList();
// 觸發 updateLocalStorage 的函式操作
updateLocalStorage();
};
// 幫 todoListFromButton 這個節點新增按鈕點擊事件
todoListFromButton.addEventListener('click', function (event) {
// 因為我們的 button 是放在 form 中,因此點擊時會直接觸發表單送出操作,這裡是將這一預設操作給停止
event.preventDefault();
// 從 todoListFormInput 上方取得他的數值
const inputValue = todoListFromInput.value;
// 判斷 inputValue 在被剪裁去空格之後,是否還有內容值,若變為空字串則會被轉換為 false
if (!inputValue.trim()) {
// 將 todoListFromInput 的數值改為空字串
todoListFromInput.value = '';
// 將函示進行返回操作,當一個函式 return 時後面的程式將不會運作
return;
}
// 將 todoListFromInput 的數值改為空字串
todoListFromInput.value = '';
// 新增一筆資料到我們的 todoListData 這個 array 上面
todoListData.push({
// 幫資料使用目前時間建立一個 id
id: new Date().getTime(),
title: inputValue,
status: '代辦',
});
// 觸發 updateLocalStorage 的函式操作
updateLocalStorage();
// 觸發畫面更新的函式,因為此時他所倚賴的陣列已經更新了內容,因此會在畫面上增加一筆項目
renderTodoList();
});
// 幫 todoListFromButton 這個節點新增按鈕點擊事件
todoListSearchButton.addEventListener('click', function (event) {
// 因為我們的 button 是放在 form 中,因此點擊時會直接觸發表單送出操作,這裡是將這一預設操作給停止
event.preventDefault();
// 設定關鍵字為 input 的 value
keyword = todoListSearchInput.value;
// 觸發畫面更新的函式,因為此時他所倚賴的陣列已經更新了內容,因此會在畫面上增加一筆項目
renderTodoList();
});
// 對 todoListTabs 進行迴圈操作,幫每一個項目都執行一次函式
todoListTabs.forEach((element) => {
// 對目前被執行的項目新增一個點擊事件
element.addEventListener('click', () => {
// 對 todoListTabs 進行迴圈操作,將每一個項目的 class 上的 active 做刪除
todoListTabs.forEach((element) => element.classList.remove('active'));
// 對目前被點擊的項目新增一個 active 的 class
element.classList.add('active');
// 將當前的定義設定為點擊的文字內容
currentStatus = element.innerHTML;
// 觸發畫面更新的函式,
renderTodoList();
});
});
// 此處算是初始化的操作,沒有執行的話畫面預設會是空的內容
renderTodoList();
```