前面的幾篇文章已經大致介紹了 Vue 3 的常用 API,現在綜合起來做一個實戰演練
配合完整代碼食用更香哦,項目地址:https://github.com/wisewrong/test-vue3-demo
一、初始化
首先通過 Vue-CLI 創建一個 Vue 3 項目,詳細流程可以參考《Vue3.x 從零開始(一)》
vue create test-vue3-demo
勾選 TypeScript、Router、Vuex,版本選用 Vue 3.x,其他的選項可以自行選擇,拿不准就直接回車選擇默認
初始化完成后的項目是這樣的:
store 目錄用來維護基於 Vuex 開發的狀態倉庫
router 目錄維護基於 vue-router 開發的路由配置
main.ts 是項目的入口文件,在這里將 Router 和 Vuex 載入項目中:
二、頭部導航( Router )
首先需要創建頭部導航 <header> 組件,header 屬於公共組件,可以放到 components 目錄下
header 上有 Home 和 About 兩個頁面的導航入口,點擊導航可以跳轉到對應的頁面
這個功能可以通過 vue-router 提供的 <router-link> 來實現
<router-link> 是經過封裝的 <a> 標簽,它需要接收一個路由地址 to,類似於 <a> 標簽的 href
<router-link to="/home">Home</router-link>
如果目標路由地址配置了組件,就能在父組件的 <router-view> 中渲染對應組件
路由的配置文件是 src/router/index.ts ,可以配置路由信息,包括路由地址和對應的組件
不過 <router-link> 只適合導航菜單這種【只需要跳轉頁面,不需要做其他操作】的場景
更多的時候我們需要在函數中進行路由跳轉,這時候可以使用 this.$router.push()
三、登錄框彈窗( teleport + slot )
首先來完成彈窗組件 <modal>
從業務上來講,這個彈窗組件是在 <header> 上打開的,也就是說 <header> 會是 <modal> 的父組件
如果按照傳統開發組件的方式,<modal> 會渲染到父組件 <header> 的 DOM 節點下
但從交互的層面來說,彈窗是一個全局性的強交互,組件應該渲染到外部,比如 body 標簽
在 Vue 3 中提供了一個新的解決方案 teleport
在組件中用 <teleport> 組件包裹需要渲染到外部的模板,然后通過 to 指定渲染的 DOM 節點
<teleport to="body"> <div> <!-- 組件內容 --> </div> </teleport>
to 接收一個可以被 querySelector 識別的字符串參數,用於查找目標 DOM 節點,該 DOM 節點必須在組件外部
這里將 modal 組件渲染到了body 標簽
上面的代碼還用到了插槽 <slot>
這個標簽允許父組件向子組件插入自定義的模板內容,在 <modal> 組件中可以讓父組件編輯彈窗的內容
如果組件中需要配置多個 <slot> 標簽,還可以用 name 來給 <slot> 命名
<div class="modal-header"> <slot name="header"> <!-- 這里是 slot-header 的默認模板 --> <span class="modal-title">{{title}}</span> <button class="modal-close"></button> </slot> </div>
然后在父組件中通過 <template v-slot:name> 向指定的 slot 插入內容
<template v-slot:header> <div> 這里是 slot-header 的內容 </div> </template>
四、完成彈窗表單( $refs + Vuex )
接下來開發登錄窗的表單組件 <sign-in-form>
組件的內容十分簡單,就是兩個輸入框 <input />,不多介紹,重點在於獲取表單數據
由於這個 <form> 組件是 <header> 的子組件,所以我們需要在 <header> 獲取 <form> 的數據並提交
Vue 中可以通過 ref 屬性獲取自定義組件的實例
比如上面的代碼就在 <sign-in-form> 組件上指定了 ref="signInForm"
然后就能在 header 組件中通過 this.$refs.signinForm 獲取到表單組件的實例,並直接使用它的 methods 或者 data
// 沒有找到適合 $ref 的類型斷言,只好用 any const data = (this.$refs.signInForm as any).getValue(); // getValue 是 signInForm 組件中的 methods
現在獲取到了登錄信息,正常來說需要用登錄信息請求登錄接口,如果用戶名和密碼正確,接口會返回用戶信息
這里我們就跳過請求接口的過程,直接把登錄信息當做用戶信息
用戶信息對於整個項目來說是一個共用信息,我們可以選擇暫存在 localStorage 或 sessionStorage 中,也可以使用 Vuex 來管理
如果在使用 Vue-CLI 創建項目時勾選了 Vuex,就能在 src/store/index.ts 中維護公共變量和方法
然后在組件中通過 this.$store 來使用 Vuex 提供的 API
Vuex 中有 State、Getter、Mutation、Action、Module 五個核心屬性
其中 State 就像是 Vue 組件中的相應數據 data,Getter 類似於計算屬性 computed
然后 Mutation 和 Action 都可以看做 methods,區別在於:
Mutation 是同步函數,用來更新 state(在嚴格模式下只能通過 mutation 來更新 state)
// Vuex const state = { user: 'wise wrong' }; const mutations = { // 所有 mutation 的第一個參數都是 state,后面的參數在調用時傳入 updateUser(state, payload) { state.user = payload; } }; // 組件中通過 commit 來調用 mutations export default { // ... methods: { handler() { this.$store.commit('updateUser', 'new user'); } }, };
而 Action 可以看做 Mutation 的父級,用來提交 Mutation,而且可以包含異步函數
// Vuex const actions = { // action 的第一個參數是 context,其中包含 commit,用來調用 mutation fetchUser(context, payload) { fetch('/api', payload) .then((res) => { // 調用 mutation context.commit('updateUser', res.data); }) .catch() } } // 組件中通過 dispatch 來調用 action export default { // ... methods: { handler() { this.$store.dispatch('fetchUser', {id: 123}); } }, };
回到我們的項目上來,由於我們用的是 TypeScript,所以需要提前定義 state 的類型
// 用戶信息 export interface UserState { user: string; password: string; } // state 的根類型 export interface RootState { userInfo: UserState; }
然后創建 state.ts 和 mutations.ts
然后在組件中通過 commit 調用 mutation 以更新用戶信息:
由於在 Vuex 4 中刪除了對全局屬性 $store 的類型支持,所以上面的截圖中 $store 被標紅,代碼也無法運行
為解決該問題,可以在 src 目錄下創建一個 shims-vuex.d.ts 文件,手動聲明 $store
import { Store } from 'vuex'; declare module "@vue/runtime-core" { interface ComponentCustomProperties { $store: Store; } }
但我更推薦使用 Vue 提供的輔助函數 map
對於 state,我們可以在 computed 中使用 mapState 將需要的 state 映射到當前組件
import { mapState } from 'vuex'; export default { // ... computed: { // 組件本身的計算屬性 localComputed () { /* ... */ }, // 使用對象展開運算符將 state 混入當前組件 ...mapState([ 'userInfo', // 以數組的形式傳入 state 的鍵名 ]) }, methods: { test() { // 以計算屬性的形式使用 state console.log(this.user); } } }
同樣的,對於 mutations 可以通過 mapMutations 混入 methods 中
import { mapMutations } from 'vuex'; export default { // ... methods: { // 組件本身的方法 test() { // 以 methods 的形式調用 mutation this.updateUserInfo(); }, ...mapMutations([ // 混入名為 updateUserInfo 的 mutation 'updateUserInfo', ]) } }
上面 this.$store.commit 的調用方式就可以替換成:
到這里為止已經具備了一個簡單的 Vue 項目的雛形
接下來會打造一個綜合性的 Todo List,並真正用上 Composition API