:::info
本文件連結:https://hackmd.io/o-BW3WhjRWOJjCUfqJ4YJg
此為 2025 Vue3 前端新手營課程資料,相關資源僅授權給該課程學員。
:::
# 第一週:Vue 環境、基礎概念與 Vue 指令
## 課前說明
- 課前影音 / 錄影回放:https://courses.hexschool.com/courses/enrolled/2793063
- 每日任務:Discord 上發布
- 討論頻道:https://discord.com/channels/801807326054055996/1382641046550216705
- 每週挑戰:
- HackMD 上繳交
- RPG 程式勇者:
- 課程捷徑
- 課程講義
- 任務繳交(證書)
- 進階任務 - 元件拆分與資料傳遞
- 最終挑戰 - Todolist 新手證書任務
- 最終挑戰 - Todolist API 整合證書任務
- 提醒老師錄影
## Vite 基本概念
官網的安裝方式:https://vitejs.dev/guide/
可選方式:
```docker
✔ Project name: … vue-2025-week01
✔ Select a framework: › Vue
✔ Select a variant: › Customize with create-vue ↗
Vue.js - The Progressive JavaScript Framework
✔ 是否使用 TypeScript? … 否
✔ 是否啟用 JSX 支援? … 否
✔ 是否引入 Vue Router 進行單頁應用程式開發? … 否
✔ 是否引入 Pinia 用於狀態管理? … 否
✔ 是否引入 Vitest 用於單元測試 … 否
✔ 是否要引入一款端對端(End to End)測試工具? › 不需要
✔ 是否引入 ESLint 用於程式碼品質檢測? … 是
✔ 是否引入 Prettier 用於程式碼格式化? … 是
✔ 是否引入 Vue DevTools 7 擴充元件以協助偵錯?(試驗性功能) … 皆可
```
- Vite 資料夾結構說明
- Vite 編譯正式版本說明
- Vite 部署說明
### 建立新檔案
- 建立一個新的 Vue File 來進行說明
- Vue File 包含 template, script, style
- ESModules 的運用
### Ref
- 基礎定義值的方式
- 取值、賦予值的方式
- 優點:
- 適用全部情境
- 缺點:
- 要打上 .value
### Reactive
- 可以用來定義物件
- 優點:
- 不用加上 .value
- 缺點:
- 不能重新賦予值
- 僅能用在物件型別
> 簡而言之:官方推薦 ref 到底就好了
>
## Vue 的指令集
https://hackmd.io/@hexschool/S1DJeKTdL/%2FRhud3_1PR9qv1RJyMfwUmA

1. View 常用的指令
1. 資料渲染在 HTML 的內容上
- {{ }}
2. 請套用在 HTML 的屬性上
- v-for (迴圈,類似 forEach)
- v-bind (HTML 屬性套用)
- v-on(addEventListener)
- v-if, v-else(if…else 判斷)
3. 雙向綁定
4. 觸發監聽
- Vue 初始化必須要知道的事情
- 請務必先定義資料結構
```=javascript
<script setup>
import { ref } from 'vue'
const inputValue = ref(3)
function showValue() {
alert(`你輸入的是:${inputValue.value}`)
}
</script>
<template>
<input v-bind:value="inputValue" type="text" />
<p>{{ inputValue }}</p>
<p>{{ inputValue }}</p>
<p>{{ inputValue }}</p>
<p>{{ inputValue }}</p>
<p>{{ inputValue }}</p>
<button v-on:click="showValue" >
顯示輸入值
</button>
<p >目前輸入:{{ inputValue }}</p>
</template>
<style>
body {
font-family: sans-serif;
}
</style>
```
## v-for
```=javascript
<script setup>
import { ref } from 'vue'
const students = ref([
{ id: 's001', name: '洧杰', score: 90 },
{ id: 's002', name: '小花', score: 85 },
{ id: 's003', name: '阿明', score: 78 },
{ id: 's004', name: '佩佩', score: 92 }
])
</script>
<template>
<div class="p-6">
<h2 class="text-xl font-bold mb-4">👨🎓 學生名單</h2>
<ul>
<li
v-for="student in students"
:key="student.id"
class="mb-2 p-2 border rounded"
>
<p>🆔 學號:{{ student.id }}</p>
<p>📛 姓名:{{ student.name }}</p>
<p>📈 分數:{{ student.score }}</p>
</li>
</ul>
</div>
</template>
```
## 情境 Code
### 範例一:BMI 計算機(V-model、v-bind、v-if 練習)
:::spoiler
```=javascript
<script setup>
import { ref } from 'vue'
// 響應式數據
const height = ref('')
const weight = ref('')
const bmi = ref(0)
const bmiStatus = ref('')
const statusColor = ref('#95a5a6')
// 計算 BMI 的函數
function calculateBMI() {
if (!height.value || !weight.value) {
bmi.value = 0
bmiStatus.value = ''
statusColor.value = '#95a5a6'
return
}
const heightInMeters = height.value / 100
bmi.value = (weight.value / (heightInMeters * heightInMeters)).toFixed(1)
// 計算狀態
const bmiValue = parseFloat(bmi.value)
if (bmiValue < 18.5) {
bmiStatus.value = '體重過輕'
statusColor.value = '#3498db'
} else if (bmiValue < 24) {
bmiStatus.value = '正常範圍'
statusColor.value = '#27ae60'
} else if (bmiValue < 27) {
bmiStatus.value = '體重過重'
statusColor.value = '#f39c12'
} else {
bmiStatus.value = '肥胖'
statusColor.value = '#e74c3c'
}
}
</script>
<template>
<div class="container">
<h1>BMI 計算器</h1>
<div class="form-group">
<label>身高 (公分):</label>
<!-- v-model 雙向綁定 -->
<input
v-model="height"
type="number"
placeholder="請輸入身高"
>
</div>
<div class="form-group">
<label>體重 (公斤):</label>
<!-- v-model 雙向綁定 -->
<input
v-model="weight"
type="number"
placeholder="請輸入體重"
>
</div>
<!-- 顯示輸入的數值 -->
<div class="input-display">
<p>您輸入的身高: {{ height || '未輸入' }} 公分</p>
<p>您輸入的體重: {{ weight || '未輸入' }} 公斤</p>
</div>
<!-- 計算按鈕 -->
<div class="button-group">
<button
@click="calculateBMI"
:disabled="!height || !weight"
>
計算 BMI
</button>
</div>
<!-- BMI 結果顯示 -->
<div class="result" v-if="bmi > 0">
<h2>您的 BMI 值: {{ bmi }}</h2>
<!-- v-bind 綁定樣式 -->
<h3 :style="{ color: statusColor }">{{ bmiStatus }}</h3>
</div>
<!-- BMI 參考標準 -->
<div class="reference">
<h4>BMI 參考標準:</h4>
<ul>
<li :style="{ color: '#3498db' }">過輕: < 18.5</li>
<li :style="{ color: '#27ae60' }">正常: 18.5 - 23.9</li>
<li :style="{ color: '#f39c12' }">過重: 24 - 26.9</li>
<li :style="{ color: '#e74c3c' }">肥胖: ≥ 27</li>
</ul>
</div>
</div>
</template>
<style scoped>
.container {
max-width: 400px;
margin: 50px auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h1 {
text-align: center;
color: #2c3e50;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.input-display {
background-color: #f8f9fa;
padding: 10px;
border-radius: 4px;
margin: 15px 0;
}
.input-display p {
margin: 5px 0;
font-size: 14px;
color: #666;
}
.button-group {
text-align: center;
margin: 20px 0;
}
button {
padding: 12px 24px;
font-size: 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover:not(:disabled) {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.result {
text-align: center;
margin: 20px 0;
padding: 15px;
background-color: #f0f0f0;
border-radius: 4px;
}
.reference {
margin-top: 30px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
}
.reference h4 {
margin-top: 0;
color: #2c3e50;
}
.reference ul {
list-style: none;
padding: 0;
}
.reference li {
padding: 2px 0;
font-weight: bold;
}
</style>
```
:::
### 範例二:字數倒數計算器(computed)
:::spoiler
```=JavaScript
<script setup>
import { ref, computed } from 'vue'
// 響應式數據
const userInput = ref('')
const maxLength = ref(50)
// 計算剩餘字數 (使用 computed)
const remainingChars = computed(() => {
return maxLength.value - userInput.value.length
})
// 清空輸入
const clearInput = () => {
userInput.value = ''
}
</script>
<template>
<div class="container">
<h1>字數倒數計算器</h1>
<!-- 設定字數限制 -->
<div class="setting-group">
<label>字數限制:</label>
<!-- v-model 雙向綁定 -->
<input
v-model="maxLength"
type="number"
min="10"
max="200"
>
</div>
<!-- 文字輸入區 -->
<div class="input-group">
<label>請輸入文字:</label>
<!-- v-model 雙向綁定文字內容 -->
<textarea
v-model="userInput"
placeholder="開始輸入您的文字..."
rows="4"
></textarea>
</div>
<!-- 顯示統計資訊 -->
<div class="info-box">
<p>已輸入: {{ userInput.length }} 字</p>
<p>字數限制: {{ maxLength }} 字</p>
<!-- v-bind 根據剩餘字數改變顏色 -->
<p :style="{ color: remainingChars >= 0 ? 'green' : 'red' }">
剩餘: {{ remainingChars }} 字
</p>
</div>
<!-- 即時預覽 -->
<div class="preview-box">
<h3>預覽:</h3>
<div class="preview-text">
{{ userInput || '您輸入的文字會顯示在這裡...' }}
</div>
</div>
<!-- 操作按鈕 -->
<div class="button-area">
<!-- v-bind 控制按鈕是否可點擊 -->
<button
@click="clearInput"
:disabled="userInput.length === 0"
>
清空
</button>
<button
:disabled="remainingChars < 0"
:style="{ backgroundColor: remainingChars < 0 ? 'gray' : '#007bff' }"
>
{{ remainingChars < 0 ? '字數超過!' : '確定' }}
</button>
</div>
</div>
</template>
<style scoped>
.container {
max-width: 500px;
margin: 30px auto;
padding: 20px;
font-family: Arial, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
.setting-group, .input-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="number"] {
width: 100px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
resize: vertical;
}
.info-box {
background-color: #f5f5f5;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.info-box p {
margin: 5px 0;
font-size: 16px;
}
.preview-box {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.preview-box h3 {
margin-top: 0;
color: #555;
}
.preview-text {
min-height: 50px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
white-space: pre-wrap;
}
.button-area {
display: flex;
gap: 10px;
}
button {
flex: 1;
padding: 10px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
color: white;
}
button:first-child {
background-color: #6c757d;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
button:hover:not(:disabled) {
opacity: 0.9;
}
</style>
```
:::
| 差異重點 | **Computed(推薦)** | **普通函式 (methods)** |
|-----------|--------------------|------------------------|
| **快取機制** | ✅ 內建快取,依賴資料沒變就重用結果 | ❌ 無快取,每次呼叫都重新執行 |
| **重新計算時機** | 只有相依 reactive 資料變動才重新計算 | 每次模板重新渲染或手動呼叫都會執行 |
| **用途定位** | 適合做「資料衍生」:如 **總價、格式化日期** | 適合做「觸發動作」:如 **按鈕事件、API 呼叫** |
| **在模板中的寫法** | 像變數:`{{ totalPrice }}` | 像函式:`@click="addToCart()"` |
| **效能/可讀性** | 複雜或昂貴運算 ➜ 效能佳、語意清楚 | 簡單即時計算 ➜ 寫法直觀、上手快 |
## 關注點分離
Vue.js 的關注點分離強調將視圖層 (UI) 與資料層 (Data) 分開。視圖層使用模板語法來描述界面,而資料層使用 `data` 物件管理狀態,並通過雙向數據綁定 (`v-model`) 使視圖和資料同步更新。這種分離提升了代碼的可維護性和重用性,使開發更高效。
將畫面轉變為資料:
```
<table>
<thead>
<tr>
<th>代辦事項名稱</th>
<th>到期日</th>
<th>是否已完成</th>
</tr>
</thead>
<tbody>
<tr>
<td>購買雜貨</td>
<td>2024-07-30</td>
<td>是</td>
</tr>
<tr>
<td>完成報告</td>
<td>2024-08-01</td>
<td>否</td>
</tr>
<tr>
<td>清理房間</td>
<td>2024-07-28</td>
<td>是</td>
</tr>
<tr>
<td>計劃假期</td>
<td>2024-08-05</td>
<td>否</td>
</tr>
<tr>
<td>處理稅務</td>
<td>2024-08-10</td>
<td>否</td>
</tr>
</tbody>
</table>
```
## 作業:
餐點管理工具
- Level 1:將菜單轉為資料格式
- Level 2:可以重新設定菜單的庫存數量
- Level 3(挑戰):可以重新設定品項名稱
作業需求:
1. 將以下的表格轉為使用資料,並使用 Vue 進行渲染。
2. 數量的部分可以點擊,並且調整庫存數量。
3. 庫存數量不會低於 0。
4. 新增欄位 “編輯”,按下後可以修改該欄位的品項名稱(按下確認後執行更換)
```html
<table>
<thead>
<tr>
<th scope="col">品項</th>
<th scope="col">描述</th>
<th scope="col">價格</th>
<th scope="col">庫存</th>
</tr>
</thead>
<tbody>
<tr>
<td>珍珠奶茶</td>
<td><small>香濃奶茶搭配QQ珍珠</small></td>
<td>50</td>
<td><button>-</button>20<button>+</button></td>
</tr>
<tr>
<td>冬瓜檸檬</td>
<td><small>清新冬瓜配上新鮮檸檬</small></td>
<td>45</td>
<td><button>-</button>18<button>+</button></td>
</tr>
<tr>
<td>翡翠檸檬</td>
<td><small>綠茶與檸檬的完美結合</small></td>
<td>55</td>
<td><button>-</button>34<button>+</button></td>
</tr>
<tr>
<td>四季春茶</td>
<td><small>香醇四季春茶,回甘無比</small></td>
<td>45</td>
<td><button>-</button>10<button>+</button></td>
</tr>
<tr>
<td>阿薩姆奶茶</td>
<td><small>阿薩姆紅茶搭配香醇鮮奶</small></td>
<td>50</td>
<td><button>-</button>25<button>+</button></td>
</tr>
<tr>
<td>檸檬冰茶</td>
<td><small>檸檬與冰茶的清新組合</small></td>
<td>45</td>
<td><button>-</button>20<button>+</button></td>
</tr>
<tr>
<td>芒果綠茶</td>
<td><small>芒果與綠茶的獨特風味</small></td>
<td>55</td>
<td><button>-</button>18<button>+</button></td>
</tr>
<tr>
<td>抹茶拿鐵</td>
<td><small>抹茶與鮮奶的絕配</small></td>
<td>60</td>
<td><button>-</button>20<button>+</button></td>
</tr>
</tbody>
</table>
```
解答連結:
- 執行範例:https://www.casper.tw/2024-vue-homework/#/week1
- 原始碼:https://github.com/Wcc723/2024-vue-homework/blob/main/src/views/Week1View.vue
- 作業繳交連結:https://hackmd.io/9Q-arVa6RdGjeBoF2XPHMQ
## AI 分享
## 公司目前導入 AI 細節
1. 有提供 ChatGPT
2. 無法針對全面性去解決,但可以部分單點內容去調整
3. 如何有效降低腦力負擔