隨著Vue3
的普及,已經有越來越多的專案開始使用Vue3。為了快速進入開發狀態,在這裡向大家推薦一套開箱即用
的企業級開發腳手架,框架使用:Vue3
+Vite2
+TypeScript
+JSX
+Pinia(Vuex)
+Antd
。廢話不多話,直接上手開擼。
該腳手架根據使用狀態庫的不同分為兩個版本Vuex版、Pinia版,下面是相關程式碼地址:
Vuex版、
Pinia版
搭建需準備
- Vscode : 前端人必備寫碼神器
- Chrome :對開發者非常友好的瀏覽器(程式設計師標配瀏覽器)
- Nodejs & npm :配置本地開發環境,安裝 Node 後你會發現 npm 也會一起安裝下來 (V12+)
使用npm安裝依賴包時會發現非常慢,在這裡推薦使用cnpm、yarn代替。
腳手架目錄結構
├── src
│ ├── App.tsx
│ ├── api # 介面管理模組
│ ├── assets # 靜態資源模組
│ ├── components # 公共元件模組
│ ├── mock # mock介面模擬模組
│ ├── layouts # 公共自定義佈局
│ ├── main.ts # 入口檔案
│ ├── public # 公共資源模組
│ ├── router # 路由
│ ├── store # vuex狀態庫
│ ├── types # 宣告檔案
│ ├── utils # 公共方法模組
│ └── views # 檢視模組
├── tsconfig.json
└── vite.config.js
什麼是Vite
下一代前端開發與構建工具
Vite(法語意為 "快速的",發音/vit/
,發音同 "veet")是一種新型前端構建工具,能夠顯著提升前端開發體驗。它主要由兩部分組成:
- 一個開發伺服器,它基於 原生 ES 模組 提供了 豐富的內建功能,如速度快到驚人的 模組熱更新(HMR)。
- 一套構建指令,它使用 Rollup 打包你的程式碼,並且它是預配置的,可輸出用於生產環境的高度優化過的靜態資源。
Vite 意在提供開箱即用的配置,同時它的 外掛 API 和 JavaScript API 帶來了高度的可擴充套件性,並有完整的型別支援。
你可以在 為什麼選 Vite 中瞭解更多關於專案的設計初衷。
什麼是Pinia
Pinia.js 是新一代的狀態管理器,由 Vue.js團隊中成員所開發的,因此也被認為是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的專案中使用也是備受推崇
Pinia.js 有如下特點:
- 相比Vuex更加完整的 typescript 的支援;
- 足夠輕量,壓縮後的體積只有1.6kb;
- 去除 mutations,只有 state,getters,actions(支援同步和非同步);
- 使用相比Vuex更加方便,每個模組獨立,更好的程式碼分割,沒有模組巢狀,store之間可以自由使用
安裝
npm install pinia --save
建立Store
新建 src/store 目錄並在其下面建立 index.ts,並匯出store
import { createPinia } from 'pinia' const store = createPinia() export default store
- 在main.ts中引入
import { createApp } from 'vue'
import store from './store'
const app = createApp(App)
app.use(store)
定義State
在新建src/store/modules,根據模組劃分在modules下新增common.ts
import { defineStore } from 'pinia'
export const CommonStore = defineStore('common', {
// 狀態庫
state: () => ({
userInfo: null, //使用者資訊
}),
})
獲取State
獲取state有多種方式,最常用一下幾種:
import { CommonStore } from '@/store/modules/common'
// 在此省略defineComponent
setup(){
const commonStore = CommonStore()
return ()=>(
<div>{commonStore.userInfo}</div>
)
}
使用computed獲取
const userInfo = computed(() => common.userInfo)
使用Pinia提供的storeToRefs
import { storeToRefs } from 'pinia'
import { CommonStore } from '@/store/modules/common'
...
const commonStore = CommonStore()
const { userInfo } = storeToRefs(commonStore)
修改State
修改state的三種方式:
- 直接修改(不推薦)
commonStore.userInfo = '曹操'
- 通過$patch
commonStore.$patch({
userInfo:'曹操'
})
- 通過actions修改store
export const CommonStore = defineStore('common', {
// 狀態庫
state: () => ({
userInfo: null, //使用者資訊
}),
actions: {
setUserInfo(data) {
this.userInfo = data
},
},
})
import { CommonStore } from '@/store/modules/common'
const commonStore = CommonStore()
commonStore.setUserInfo('曹操')
Getters
export const CommonStore = defineStore('common', {
// 狀態庫
state: () => ({
userInfo: null, //使用者資訊
}),
getters: {
getUserInfo: (state) => state.userInfo
}
})
使用同State獲取
Actions
Pinia賦予了Actions更大的職能,相較於Vuex,Pinia去除了Mutations,僅依靠Actions來更改Store狀態,同步非同步都可以放在Actions中。
同步action
export const CommonStore = defineStore('common', {
// 狀態庫
state: () => ({
userInfo: null, //使用者資訊
}),
actions: {
setUserInfo(data) {
this.userInfo = data
},
},
})
非同步actions
...
actions: {
async getUserInfo(params) {
const data = await api.getUser(params)
return data
},
}
內部actions間相互呼叫
...
actions: {
async getUserInfo(params) {
const data = await api.getUser(params)
this.setUserInfo(data)
return data
},
setUserInfo(data){
this.userInfo = data
}
}
modules間actions相互呼叫
import { UserStore } from './modules/user'
...
actions: {
async getUserInfo(params) {
const data = await api.getUser(params)
const userStore = UserStore()
userStore.setUserInfo(data)
return data
},
}
pinia-plugin-persist 外掛實現資料持久化
安裝
npm i pinia-plugin-persist --save
使用
// src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const store = createPinia().use(piniaPluginPersist)
export default store
對應store中的使用
export const CommonStore = defineStore('common', {
// 狀態庫
state: () => ({
userInfo: null, //使用者資訊
}),
// 開啟資料快取
persist: {
enabled: true,
strategies: [
{
storage: localStorage, // 預設儲存在sessionStorage裡
paths: ['userInfo'], // 指定儲存state,不寫則儲存所有
},
],
},
})
Fetch
為了更好的支援TypeScript,統計Api請求,這裡將axios進行二次封裝
結構目錄:
// src/utils/fetch.ts
import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'
import { getToken } from './util'
import { Modal } from 'ant-design-vue'
import { Message, Notification } from '@/utils/resetMessage'
// .env環境變數
const BaseUrl = import.meta.env.VITE_API_BASE_URL as string
// create an axios instance
const service: AxiosInstance = axios.create({
baseURL: BaseUrl, // 正式環境
timeout: 60 * 1000,
headers: {},
})
/**
* 請求攔截
*/
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers.common.Authorization = getToken() // 請求頭帶上token
config.headers.common.token = getToken()
return config
},
(error) => Promise.reject(error),
)
/**
* 響應攔截
*/
service.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status == 201 || response.status == 200) {
const { code, status, msg } = response.data
if (code == 401) {
Modal.warning({
title: 'token出錯',
content: 'token失效,請重新登入!',
onOk: () => {
sessionStorage.clear()
},
})
} else if (code == 200) {
if (status) {
// 介面請求成功
msg && Message.success(msg) // 後臺如果返回了msg,則將msg提示出來
return Promise.resolve(response) // 返回成功資料
}
// 介面異常
msg && Message.warning(msg) // 後臺如果返回了msg,則將msg提示出來
return Promise.reject(response) // 返回異常資料
} else {
// 介面異常
msg && Message.error(msg)
return Promise.reject(response)
}
}
return response
},
(error) => {
if (error.response.status) {
switch (error.response.status) {
case 500:
Notification.error({
message: '溫馨提示',
description: '服務異常,請重啟伺服器!',
})
break
case 401:
Notification.error({
message: '溫馨提示',
description: '服務異常,請重啟伺服器!',
})
break
case 403:
Notification.error({
message: '溫馨提示',
description: '服務異常,請重啟伺服器!',
})
break
// 404請求不存在
case 404:
Notification.error({
message: '溫馨提示',
description: '服務異常,請重啟伺服器!',
})
break
default:
Notification.error({
message: '溫馨提示',
description: '服務異常,請重啟伺服器!',
})
}
}
return Promise.reject(error.response)
},
)
interface Http {
fetch<T>(params: AxiosRequestConfig): Promise<StoreState.ResType<T>>
}
const http: Http = {
// 用法與axios一致(包含axios內建所有請求方式)
fetch(params) {
return new Promise((resolve, reject) => {
service(params)
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err.data)
})
})
},
}
export default http['fetch']
使用
// src/api/user.ts
import qs from 'qs'
import fetch from '@/utils/fetch'
import { IUserApi } from './types/user'
const UserApi: IUserApi = {
// 登入
login: (params) => {
return fetch({
method: 'post',
url: '/login',
data: params,
})
}
}
export default UserApi
型別定義
/**
* 介面返回結果Types
* --------------------------------------------------------------------------
*/
// 登入返回結果
export interface ILoginData {
token: string
userInfo: {
address: string
username: string
}
}
/**
* 介面引數Types
* --------------------------------------------------------------------------
*/
// 登入引數
export interface ILoginApiParams {
username: string // 使用者名稱
password: string // 密碼
captcha: string // 驗證碼
uuid: string // 驗證碼uuid
}
/**
* 介面定義Types
* --------------------------------------------------------------------------
*/
export interface IUserApi {
login: (params: ILoginApiParams) => Promise<StoreState.ResType<ILoginData>>
}
Router4
基礎路由
// src/router/router.config.ts const Routes: Array<RouteRecordRaw> = [ { path: '/403', name: '403', component: () => import(/* webpackChunkName: "403" */ '@/views/exception/403'), meta: { title: '403', permission: ['exception'], hidden: true }, }, { path: '/404', name: '404', component: () => import(/* webpackChunkName: "404" */ '@/views/exception/404'), meta: { title: '404', permission: ['exception'], hidden: true }, }, { path: '/500', name: '500', component: () => import(/* webpackChunkName: "500" */ '@/views/exception/500'), meta: { title: '500', permission: ['exception'], hidden: true }, }, { path: '/:pathMatch(.*)', name: 'error', component: () => import(/* webpackChunkName: "404" */ '@/views/exception/404'), meta: { title: '404', hidden: true }, }, ]
title: 導航顯示文字;hidden: 導航上是否隱藏該路由 (true: 不顯示 false:顯示)
- 動態路由(許可權路由)
// src/router/router.ts
router.beforeEach(
async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
const token: string = getToken() as string
if (token) {
// 第一次載入路由列表並且該專案需要動態路由
if (!isAddDynamicMenuRoutes) {
try {
//獲取動態路由表
const res: any = await UserApi.getPermissionsList({})
if (res.code == 200) {
isAddDynamicMenuRoutes = true
const menu = res.data
// 通過路由表生成標準格式路由
const menuRoutes: any = fnAddDynamicMenuRoutes(
menu.menuList || [],
[],
)
mainRoutes.children = []
mainRoutes.children?.unshift(...menuRoutes, ...Routes)
// 動態新增路由
router.addRoute(mainRoutes)
// 注:這步很關鍵,不然導航獲取不到路由
router.options.routes.unshift(mainRoutes)
// 本地儲存按鈕許可權集合
sessionStorage.setItem(
'permissions',
JSON.stringify(menu.permissions || '[]'),
)
if (to.path == '/' || to.path == '/login') {
const firstName = menuRoutes.length && menuRoutes[0].name
next({ name: firstName, replace: true })
} else {
next({ path: to.fullPath })
}
} else {
sessionStorage.setItem('menuList', '[]')
sessionStorage.setItem('permissions', '[]')
next()
}
} catch (error) {
console.log(
`%c${error} 請求選單列表和許可權失敗,跳轉至登入頁!!`,
'color:orange',
)
}
} else {
if (to.path == '/' || to.path == '/login') {
next(from)
} else {
next()
}
}
} else {
isAddDynamicMenuRoutes = false
if (to.name != 'login') {
next({ name: 'login' })
}
next()
}
},
)
Layouts佈局元件
腳手架提供多種排版佈局,目錄結構如下:
- BlankLayout.tsx: 空白布局,只做路由分發
- RouteLayout.tsx: 主體佈局,內容顯示部分,包含麵包屑
- LevelBasicLayout.tsx 多級展示佈局,適用於2級以上路由
- SimplifyBasicLayout.tsx 簡化版多級展示佈局,適用於2級以上路由
相關參考連結
最後
文章暫時就寫到這,後續會增加JSX語法部分,如果本文對您有什麼幫助,別忘了動動手指點個贊❤️。
本文如果有錯誤和不足之處,歡迎大家在評論區指出,多多提出您寶貴的意見!