### GDSC NYUST x 資訊創客社 <br> ### 網頁前端開發讀書會 #### Vue3 進階課程 <br> #### 2023/12/11 ( Mon ) 19:00 - 21:00 #### 講師:楊鈞元 Charles #### 本次課程影片:(⚒️製作中) <img src="" height="200px"> --- ## 前言 ---- 建置環境請參考上次的 [🔗Vue3 基礎課程](https://hackmd.io/@GDSC-NYUST/SkiZJXPZ6/%2Fu_Ujxb_eRii_XEKV0DetHg) --- ### 元件進階概念 - 如何客製化元件 ---- #### 認識元件 ( Components ) ![image](https://hackmd.io/_uploads/SJDvpRWIT.png) ---- ![image](https://hackmd.io/_uploads/HJ3MA0bU6.png) ---- Table 範例 (未拆出子元件) ```html= <script setup> const persons = [ { id: 1, name: '張三', age: 25, city: '台北' }, { id: 2, name: '李四', age: 30, city: '新竹' }, { id: 3, name: '王五', age: 28, city: '台中' }, { id: 4, name: '陳六', age: 35, city: '高雄' }, { id: 5, name: '林七', age: 22, city: '台南' }, { id: 6, name: '吳八', age: 32, city: '桃園' }, { id: 7, name: '張九', age: 27, city: '新北' }, { id: 8, name: '劉十', age: 29, city: '基隆' }, { id: 9, name: '林十一', age: 31, city: '苗栗' }, { id: 10, name: '黃十二', age: 26, city: '嘉義' }, ]; </script> <template> <div style="border: 3px solid black"> <h1>這是一個表格</h1> <div class="custom-table"> <h2>這是表格標題</h2> <table> <thead> <tr> <th>編號</th> <th>姓名</th> <th>年齡</th> <th>城市</th> </tr> </thead> <tbody> <tr v-for="person in persons" :key="person.id"> <td>{{ person.id }}</td> <td>{{ person.name }}</td> <td>{{ person.age }}</td> <td>{{ person.city }}</td> </tr> </tbody> </table> </div> </div> </template> <style scoped> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; } th { padding-top: 12px; padding-bottom: 12px; text-align: left; background-color: #4caf50; color: white; } .custom-table { background-color: beige; } </style> ``` ---- Table 範例 (拆成子元件) - App.vue (父層) ```html= <script setup> import customTable from './components/DataTable.vue'; </script> <template> <div style="border: 3px solid black"> <h1>這是一個表格</h1> <customTable /> </div> </template> ``` ---- Table 範例 (拆成子元件) - src/components/DataTable.vue (子層) ```html= <script setup> const persons = [ { id: 1, name: '張三', age: 25, city: '台北' }, { id: 2, name: '李四', age: 30, city: '新竹' }, { id: 3, name: '王五', age: 28, city: '台中' }, { id: 4, name: '陳六', age: 35, city: '高雄' }, { id: 5, name: '林七', age: 22, city: '台南' }, { id: 6, name: '吳八', age: 32, city: '桃園' }, { id: 7, name: '張九', age: 27, city: '新北' }, { id: 8, name: '劉十', age: 29, city: '基隆' }, { id: 9, name: '林十一', age: 31, city: '苗栗' }, { id: 10, name: '黃十二', age: 26, city: '嘉義' }, ]; </script> <template> <div class="custom-table"> <h2>這是表格標題</h2> <table> <thead> <tr> <th>編號</th> <th>姓名</th> <th>年齡</th> <th>城市</th> </tr> </thead> <tbody> <tr v-for="person in persons" :key="person.id"> <td>{{ person.id }}</td> <td>{{ person.name }}</td> <td>{{ person.age }}</td> <td>{{ person.city }}</td> </tr> </tbody> </table> </div> </template> <style scoped> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; } th { padding-top: 12px; padding-bottom: 12px; text-align: left; background-color: #4caf50; color: white; } .custom-table { background-color: beige; } </style> ``` ---- <img src="https://hackmd.io/_uploads/SJ_rsUMIa.png" style="height:300px" /> - Child 對 Parnet 發出事件 - Parent 對 Child 傳遞屬性 ---- 複習 - v-bind -> :prop - v-on -> @evnet <br> | v-bind | v-on | | -------- | -------- | | ![image](https://hackmd.io/_uploads/rJGIh8fI6.png) | ![image](https://hackmd.io/_uploads/BkyL28M86.png) | ---- #### 如何溝通父子元件? [🔗 官方文檔](https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits) [🔗 完整 API 清單](https://vuejs.org/api/index.html) ---- <ul style="font-size:39px"> <li>defineProps:子元件提供父元件傳入 properties</li> <li>defineEmits:子元件提供父元件存取 event</li> <li>defineExpose:子元件提供父元件存取 properties</li> <li>defineModel:讓父子元件 properties 成為雙向綁定</li> </ul> <br>(更多API請參考官方文檔) ---- #### defineProps & defineEmit [🔗 展示Playgound](https://stackblitz.com/edit/vue3-vite-starter-pwpeb8?file=src%2Fcomponents%2FCustomBtn.vue) ---- #### defineProps & defineEmit - App.vue ```html= <script setup> import DemoButton from '@/components/CustomBtn.vue' import { ref } from 'vue' // - DemoButton const count = ref(0) const stock = ref(100) const title = ref('按鈕被按了:0次') const clickBtn = () => { count.value++ title.value = `按鈕被按了:${count.value}次` } </script> <template> <DemoButton :title="title" btnLabel="點我" @change="clickBtn()" /> <DemoButton title="點擊領取庫存商品" btnLabel="點我領取商品" @change="stock--" /> <span>庫存:{{ stock }}</span> </template> ``` ---- #### defineProps & defineEmit - src/components/CustomBtn.vue ```html= <script setup> const props = defineProps({ title: { type: String, }, btnLabel: { type: String, default: '預設按鈕文字', }, }) const emit = defineEmits(['change']) </script> <template> <h1>{{ title }}</h1> <div class="card"> <button @click="emit('change')">{{ btnLabel }}</button> </div> </template> <style> h1 { font-size: 20px; margin: 0; } </style> ``` ---- #### defineExpose [🔗 展示 Playground](https://stackblitz.com/edit/vue3-vite-starter-7g9wv2?file=src%2Fcomponents%2FCustomChild.vue,src%2FApp.vue) ---- #### defineExpose - App.vue ```html= <script setup> import { ref, onMounted } from 'vue' import Child from '@/components/CustomChild.vue' const childRef = ref(null) const result = ref() const getResult = () => { result.value = childRef.value.result } </script> <template> <Child ref="childRef"></Child> <div> <button @click="getResult">取得結果</button> <div> <span>得到的結果:{{ result }}</span> </div> </div> </template> ``` ---- #### defineExpose - src/components/CustomChild.vue ```html= <script setup> import { ref, computed } from 'vue' const inputValue = ref('') // - 計算費氏數列 const result = computed(() => { const num = Number(inputValue.value) if (Number.isNaN(num)) return '請輸入數字' if (num < 0) return '請輸入大於 0 的數字' if (num === 0) return 0 if (num === 1) return 1 let a = 0 let b = 1 let c = 0 for (let i = 2; i <= num; i++) { c = a + b a = b b = c } return c }) defineExpose({ result, }) </script> <template> <label>計算費氏數列:</label> <input v-model="inputValue" type="text" placeholder="請輸入數字" /> </template> ``` ---- #### defineModel [🔗 使用 defineModel 前 - 展示Playgound](https://stackblitz.com/edit/vue3-vite-starter-s7pvxg?file=src%2FApp.vue,src%2Fcomponents%2FCustomInput.vue) [🔗 自定義 ModelValue - 官方文檔](https://vuejs.org/guide/components/v-model.html#component-v-model) ---- #### 使用 defineModel 後 - App.vue ```html= <script setup> import DemoInput from '@/components/CustomInput.vue' import { ref } from 'vue' // - DemoInput const message = ref('可以在這裡預填內容') </script> <template> <div style="margin-top: 24px"> <DemoInput v-model="message" label="第1個問題" /> <DemoInput v-model="message" label="第2個問題" /> <DemoInput v-model="message" label="第3個問題" /> <DemoInput v-model="message" label="第4個問題" /> <DemoInput v-for="index in 5" :key="index" :label="`第${index + 4}個問題`" /> </div> </template> ``` ---- #### 使用 defineModel 後 - src/components/CustomInput.vue ```html= <script setup> const inputValue = defineModel() const props = defineProps({ label: String, }) </script> <template> <div> <label>{{ label }}</label> <input v-model="inputValue" placeholder="輸入內容..." /> </div> </template> <style> input { padding: 10px; margin: 5px; border: 2px solid #ccc; border-radius: 4px; font-size: 16px; } </style> ``` ---- 如果出現 defineModel() is not defined 請到 vite.config.js 設定 ```javascript= import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue({ script: { defineModel: true, }, }), ], }); ``` ---- #### 延伸閱讀 - Fallthrough Attributes (傳遞屬性) [🔗 官方文檔](https://vuejs.org/guide/components/attrs.html#accessing-fallthrough-attributes-in-javascript) ---- ### Slot (槽位) [🔗 官方文檔](https://vuejs.org/guide/components/slots.html) ---- ![image](https://hackmd.io/_uploads/BycozPzIT.png) ---- ![image](https://hackmd.io/_uploads/BJ_2fDfLa.png) [🔗 Playground](https://play.vuejs.org/#eNp9U8Fu2zAM/RVCPeTS2isKBEPmBWiHAtswbMO2oy+qTdvqZEmQ6CxdkX8fLceJna65We898pHP0rO4dS7ZdChWIguFV44gIHVunRvVOusJ7mTAL/LJdgSVty0skvQI9aWL3GTpUMtVfCBsnZaEfALIjuJ4ZmTk4aJBWaLf48w01+uP6BFaVTcEDwgSnKwRSJHGLGV63yKdeLzoWmIlOz3aMeXWt9zHy9pL10BlPVDDJlIZKKwhNJRkab/yUW9KkMayzIM1OKHPW1fW0nQhF/dZBAi2xWgmCwJlKvtKx/44S2zCiksxT77/afPaUm2g0DKE97mIbsqgz8XodJp30JbAyBZZPnCs5Z/J8GG4WU3WhzavP9VPFNlpHFO/gXvpN6nJUt5nnkG8YIGe9LDwIIbnofjBeh71iqxbwbXbcuhalXBRFMW7QVBYbf0KLpbL5R6pOKSroP7iCt4kb7GN8C7e6GjCmVPgJCtVJ4/BGo48mvXxtk5p9N8cKWtCLlbjGLmQWts/nyNGvsPLES8aLH7/B38M2x7LxXePAf0Gc3HgSPoaaaDvf37FLX8fyNaWnWb1GfIHcgxdP+Mgu+tMyWNPdHHaT/G1K1P/CvdbfhJhXKofNIYS9bnge/fhzOrHcW+Sm32YO7H7B0y5bRw=) ---- [🔗 應用 slot 寫 table 範例](https://play.vuejs.org/#eNqNVtlO20AU/ZWp+5CXLEC6pmmkgnhoVbVVl6e6D248EINjW/aEglCktoFWlKWoG1spRKpEKIIidVEIlK+JE/zUX+gdOx47xklBQrLvuWfuuXfOjDPJ3dC0+FgBcykubWR1SSPIwKSgZXgFISmvqTpB/YKBbwsTaoGgIV3No0g84YUoOXKNV2h+VlUMgkSBCOg6ekwjCE0iSUyh3ihShDxOoYh59KtenYlEkTAMr30XoygrkQkKvN0355YiqBj1E/sYsbG+YK6tucRkDyM2Pu03dw6CxCQjNhdm67UPrOIVf8V6dTdIvMCI1soPc3qXVfSkWjvL1tpUkAi4K/XLUr1aYhWhB1+P86d6vOQNZxEq7rCKHrFRLpmf3wWJl9umerDOKgLgDSdkqjADl/hmxpx/wYhXPakbNWv1VZAIuNcjEOvV50wt7HGLezK71Ng8VbQXtsyd7GGJkmtzrDDMwC28PNM83nbJT8Ba6YTjTPAkvBCc12SBYNuh6XOxGDKnK1apYm0unlR2/x7NNae3zN1Fc7pUP/wNEXNvtbk6ZZW/N7/WUCzm0HyWTtjLIoC6/DkpPpq9DERcNeh8DgtiK0rjOnumbzk4G7KhCcp1nuvt4bnMyetvVNjx9km50tg8alRWrPJcOkFy3hIJtgY8+ptuK/tUFSc6lhUzkghkT5cTNLfem4vzYcDBT+tPOQzYWDCrL9sAnz67MBqLDak69CeBOCQp9j3Acyg1iida0bgkQuvtK0+CORwIFYshlV2cOqd7BhipewI1WDDjjFMeUlV3y0Om7NvcPv/egu9MMOPewRlLpn33KgTaUC7KtV+69Mam7h/ISbI4oMJVrWDFRhyXBw6KKI25RQ1ZJfZJBL3Utb49OeXbjHOsoJNTRnUO3+C4AGXgCD8yJGUYCUjTVY1uP8lh+i+I7qFrrUlJN0QRqQDqrQz4cBAQjyDSUh8yKirbfXEXIdT+HehtfdK8Ln2Kmd6enhDr9AWiQdMH0pOhi1zovEiHrqjdztIVzevWVcCYgb00ax/h3vyfQ5nAdMLxkM+WYEpigM4haTg+YqgKeHKSpvJcFgwpyVi/qxEJfhXwXArZCMUEWVaf3bJjRC/g1kcCODmcHQ2JjxjjNMZz93RsYH0M8xzDiKAPY5gBhQcf3MHj8MzAvCoWZMjuAt7HhioXqEYnrb+giCDbl2ervWn/EgKDPzQGx2FXDLcpKpRmOl8rnoPjR89ip9Y9ucl40ubxSpEr/gMNDTFL) --- ### Vue Router (路由) ---- #### 介紹 Vue Router Vue Router 是 Vue.js 官方提供的路由管理工具。 讓前端模擬路由實現單頁式應用 (SPA, Single Page Application), 切換畫面時不需要向後端發出請求 [🔗 深入了解 Vue Router 與前後端路由](https://book.vue.tw/CH4/4-1-vue-router-intro.html) ---- #### 安裝 Vue Router ```script= yarn add vue-router@4 ``` 更新 main.js ```javascript= import { createApp } from 'vue' import './style.css' import App from './App.vue' import route from '/src/route'; const app = createApp(App) app.use(route); app.mount('#app') ``` ---- #### 配置設定檔 (src/route/index.js) ```javascript= import { createRouter, createWebHistory } from 'vue-router'; const routes = [ { path: '/', name: 'MainLayout', component: () => import('/src/layouts/MainLayout.vue'), children: [ { path: 'home', name: 'Home', component: () => import('/src/pages/HomePage.vue'), }, ], }, ]; const router = createRouter({ history: createWebHistory(), // history: createWebHashHistory(), routes, }); export default router; ``` ---- - 路由連結 (router-link) [🔗 Playground](https://stackblitz.com/edit/vitejs-vite-smaqdz?file=src%2Froute%2Findex.js) ---- - 程式化導航 (router.push, router.replace) ``` // 將 router.push 換成 router.replace,用法一樣 // 差在不會在瀏覽器留下歷史紀錄 // 透過字串指定 URL router.push('/users/eduardo') // 透過物件與 path 指定 router.push({ path: '/users/eduardo' }) // 透過物件與 name 指定 router.push({ name: 'Home' }) // 與具名路由、params 搭配 router.push({ name: 'user', params: { username: 'eduardo' } }) // 以 query 指定目標的 query string router.push({ path: '/register', query: { plan: 'private' } }) ``` ---- #### 導航防護 (Navigation Guards) [🔗 延伸閱讀](https://book.vue.tw/CH4/4-4-navigation-guards.html) --- ### Vite [🔗 官方文檔](https://vitejs.dev/) ---- #### 簡介 [🔗 什麼是 Vite](https://www.explainthis.io/zh-hant/swe/what-is-vite) <div style="text-align:left; font-size:38px"> Vite 同樣是 Vue 的作者,尤雨溪 (Evan You)建立的專案, 是一個 build tool,做為開發伺服器,核心是用來建立開箱即用的 Web 應用程式,且不僅 Vue 能使用,幾乎所有前端JS框架都採用了 Vite 作為標準的 build tool </div> ---- 對新手來說,我們只需要配置好 vite.config.js 執行 `yarn dev` (或其他 package manager) 就能瞬間得到一個具有 HMR(熱重載) 的本地伺服器 ![image](https://hackmd.io/_uploads/rkKFJZEU6.png) ---- #### 配置 vite.config.js [🔗 官方文檔](https://vitejs.dev/config/) ```javascript= // - 標準內容 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], }) ``` ---- 透過設定 alias,還能讓資料夾指定更方便 ```javascript= import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, }) ``` 由於所有的檔案、頁面都會放在 /src 中存取 這樣設定後,原先要透過相對路徑或是 /src/... 就能改以 @ 取代 src,在檢視上會更加明確 ---- 當你的後端 API 與當前環境不同 domain 設定 Proxy 是個方便的做法 ```javascript= export default defineConfig({ server: { proxy: { // 字串寫法 '/foo': 'http://localhost:4567', // 選項寫法 '/api': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') }, // 正則寫法 '^/fallback/.*': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/fallback/, '') }, } } }) ``` ---- #### 更多設定參數 [🔗 參考懶人包 - 1](https://ithelp.ithome.com.tw/articles/10270465?sc=hot) [🔗 參考懶人包 - 2](https://hackmd.io/@Jui-Cheng/Hk9fJ5bKi) --- ### Axios [🔗 官方文檔](https://axios-http.com/docs/example) ---- #### 簡介 <div style="text-align:left; font-size:38px"> axios 是一個以 promise 為基礎的 HTTP 請求工具,可以用來傳送 HTTP 要求和接收 HTTP 回應,可以用在瀏覽器和 node.js 環境中,在瀏覽器環境中使用 XMLHttpRequest。 </div> ---- JS 前後端交互 API 演進 <div style="text-align:center; font-size:42px"> XMLHttpRequest => JQuery ajax => fetch => Axios </div> [🔗 關於AJAX與那些前端的request方法](https://medium.com/unitsexhibition/%E9%97%9C%E6%96%BCajax%E8%88%87%E9%82%A3%E4%BA%9B%E5%89%8D%E7%AB%AF%E7%9A%84request%E6%96%B9%E6%B3%95-720a7c9cd220) ---- #### 現代前端選擇 Axios 的理由 - 從瀏覽器創建XMLHttpRequests - 從node.js 創建 http請求 - 支持 Promise API - 符合最新的ES規範 - 攔截請求和響應 - 轉換請求數據和響應數據 - 支援取消請求 - 自動轉換JSON數據 - 客戶端支持防禦XSRF攻擊 ---- #### 安裝 Axios ```script= yarn add axios ``` ---- #### 配置設定檔 (src/axios/index.js) ```javascript= import axios from 'axios'; const api = axios.create({ // baseURL: , timeout: 5 * 1000, headers: { 'Content-Type': 'application/json' }, // withCredentials: true, // 跨域請求發送Cookie }); export default api; ``` ---- 到 main.js 引入 ```javascript= // ... import api from 'src/axios'; // ... app.provide('api', api); // ... ``` ---- #### 到任意vue檔進行 HTTP request - 同步操作範例 ```javascript= import { inject } from 'vue'; const api = inject('api'); const getData = () => { try { api.get('https://randomuser.me/api/'); console.log(res); } catch (err) { console.log(err); } }; ``` ---- #### 到任意vue檔進行 HTTP request - 異步操作範例 ```javascript= import { inject } from 'vue'; const api = inject('api'); const getData = async () => { try { const res = await api.get('https://randomuser.me/api/'); console.log(res); } catch (err) { console.log(err); } }; ``` --- ### Pinia [🔗 官方文檔](https://pinia.vuejs.org/) ---- #### 簡介 <div style="text-align:left; font-size:38px"> Pinia 是 Vue 的狀態管理(statement management)工具,基於 Vuex 進化改版,同時 Vuex 也已經不再更新,Pinia 是 Vue 官方目前唯一推薦使用的狀態管理工具 </div> ---- #### Pinia 的特點 - Devtool 支援 - 自動熱更新 (HMR) - Plugin 支援 - 完整的 TypeScript 支援 - Server Side Rendering 支援 ---- #### 安裝 Pinia ```script= yarn add pinia ``` ---- 到 main.js 引入 ```javascript= // ... import { createPinia } from 'pinia' // ... const pinia = createPinia() // ... app.use(pinia) // ... ``` ---- #### Demo 在 (src/stores) 建立 `名稱.js` ---- #### 建立 counter.js ```javascript= import { defineStore } from "pinia"; export const useCounterStore = defineStore('counter', { //定義狀態初始值 state: () => ({ count: 0, name: 'Eduardo' }), //對狀態加工的 getters,如同 computed getters: { doubleCount: (state) => state.count * 2, }, //定義使用到的函式,可以為同步和非同步,如同 method actions: { increment() { this.count++ }, }, }) ``` ---- 在需要使用的 vue 檔 ```javascript= <script setup> import { useCounterStore } from "src/stores/counter.js"; const counterStore = useCounterStore(); </script> <template> <div> <h2>counter</h2> <!-- 取得的 state 會響應式更新 --> <p>{{ counterStore.count }}</p> <!-- 可以呼叫 store 內定義的方法來修改 state --> <button @click="counterStore.increment">+</button> <!-- 也可以直接修改 state --> <button @click="counterStore.count++">+</button> </div> </template> ``` --- ### Vue i18n [🔗 官方文檔](https://vue-i18n.intlify.dev/) [🔗 基礎範例](https://ithelp.ithome.com.tw/articles/10324500?sc=rss.iron) ---- #### 簡介 <div style="text-align:left; font-size:38px"> i18n 全名為 Internationalization,也就是國際化的意思,因為單字太長,所以中間的 18 個字母被縮寫為 18,再加上開頭和結尾的字母,就組成了 i18n。 JavaScript i18n API 可以幫助我們對網站進行多語言翻譯,讓它們可以輕鬆適應使用不同語言使用者的需求。 Vue i18n 顧名思義,就是為 Vue 專案實現 i18n,量身訂做的工具 </div> ---- - 安裝 Vue i18n Plugin 到 Vue ```script= yarn add vue-i18n@9 ``` <br> *注意 Vue3 專案必須要用 v9.x 之後的版本,<br> v8.x以前相容 Vue 2 專案 ---- 建立語言包 (src/i18n/language.json) <div style="text-align:left; font-size:31px"> *可以自己決定分層,例如都放在同個json、依不同語言/功能分json,或者直接寫在 js,不建立json都可以 </div> ```json= { "zh-TW": { "welcome": "這是中文範例" }, "en-US": { "welcome": "This is English example" } } ``` ---- 設定進入點 (src/i18n/index.js) ```json= import { createI18n } from "vue-i18n"; import message from "./language.json"; const i18n = new createI18n({ locale: "zh-TW", // 設定默認語言為繁體中文 messages: message, // 使用引入的 language.json 中的消息作為語言資源。 fallbackLocale: "zh-TW", // 如果找不到匹配的語言翻譯,默認顯示繁體中文。 }); export { i18n }; ``` ---- 到 main.js 引入 ```javascript= // ... import { i18n } from "/src/i18n"; // 引入 I18n 套件 // ... app.use(i18n) // ... ``` ---- #### Demo ```html= <div> <button type="button" @click="$i18n.locale = 'zh-TW'">中文</button> <button type="button" @click="$i18n.locale = 'en-US'">英文</button> <h2>{{ $t('welcome') }}</h2> </div> ``` ---- #### 方便管理 i18n 的 Extension ![image](https://hackmd.io/_uploads/SkFKPQE8a.png) --- ## 結語和問答 ---- ### 補充學習資源 ---- ### Q & A ---- #### 如何得到整套解決方案 下週課程<br> Quasar Framework Vue3 最強大的企業級跨平台框架 幫你集成管理所有你需要的擴充套件 幫你準備好大量精美又好用的 Components 不必再自己花一堆時間處理麻煩的 CSS 樣式
{"title":"Vue3 進階課程","description":"一個平易近人、高效能且多功能的漸進式JavaScript 框架用於建立面向於 Desktop, Mobile, WebGL, Terminal 應用程式的前端Vue.js 基於標準的 HTML、CSS 和 JavaScript因此最廣泛應用就是用在建立網頁","contributors":"[{\"id\":\"f8142aa2-66aa-4867-821d-2f1ffff7a7ba\",\"add\":23538,\"del\":4285}]"}
    566 views
   Owned this note