:::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 ![空白](https://hackmd.io/_uploads/HyiMY2DK0.png) 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' }">過輕: &lt; 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. 如何有效降低腦力負擔