Vue3、Vuex、TS 專案實踐
阿新 • • 發佈:2020-10-26
原文: 本人github文章
關注公眾號: 微信搜尋 web全棧進階
; 收貨更多的乾貨
一、開篇
vue3.0beta
版正式上線,作為新技術熱愛者,新專案將正式使用vue3.0
開發; 接下來總結(對自己技術掌握的穩固)介紹(分享有需要的猿友)- 上篇部落格介紹了
vue3.0
常用語法及開發技巧;有需要的請點選 Vue3.0 進階、環境搭建、相關API的使用 - 覺得對您有用的
github
點個star
唄 - 專案
github
地址:https://github.com/laijinxian/vue3-typescript-template
二、專案介紹(移動端)
- 1)技術棧:
vue3 + vuex + typescript + webpack + vant-ui + axios + less + postcss-pxtorem(rem適配)
- 2)沒用官方構建工具
vite
原因:vite
坑還真的不少,有時候正常寫法webpack
沒問題, 在vite
上就報錯;一臉懵逼的那種,vite
的github
提 Issues 都沒用, 維護人員隨便回答了下就把我的Issues
給關了,我也是醉了; - 3)不過自己還是很期待
vite
的, 等待他成熟吧, 在正式使用; - 4)涉及點:目前只貼出專案初期的幾個功能
webpack require
自動化註冊路由、自動化註冊非同步組價axios
請求封裝(請求攔截、響應攔截、取消請求、統一處理)vuex
業務模組化、 接管請求統一處理
三、專案搭建
可參考上篇文章 Vue3.0 進階、環境搭建、相關API的使用
vue-cli、vue
下載最新版本- 執行命令
vue create my_app_name
- 執行完上面命令接下來選擇手動配置(第三個),不要選擇預設配置,有很多我們用不上,我的選擇如下圖:
三、專案主要功能
1. webpack require
自動化註冊路由、自動化註冊非同步組價
// 該檔案在 utils 下的 global.ts // 區分檔案是否自動註冊為元件,vue檔案定義 isComponents 欄位; 區分是否自動註冊為路由定義 isRouter 欄位 // 使用方式分別在 main.ts 裡方法asyncComponent() 以及路由檔案router下的index.ts 方法 vueRouters() import { defineAsyncComponent } from 'vue' import { app } from '../main' // 獲取所有vue檔案 function getComponent() { return require.context('../views', true, /\.vue$/); } // 首字母轉換大寫 function letterToUpperCase(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } // 首字母轉換小寫 function letterToLowerCase(str: string) { return str.charAt(0).toLowerCase() + str.slice(1); } export const asyncComponent = function () { // 獲取檔案全域性物件 const requireComponents = getComponent(); requireComponents.keys().forEach((fileSrc: string) => { const viewSrc = requireComponents(fileSrc); const fileNameSrc = fileSrc.replace(/^\.\//, '') const file = viewSrc.default; if (viewSrc.default.isComponents) { // 非同步註冊元件 let componentRoot = defineAsyncComponent( () => import(`@/views/${fileNameSrc}`) ) app.component(letterToUpperCase(file.name), componentRoot) } }); }; // 獲取路由檔案 export const vueRouters = function () { const routerList: any = []; const requireRouters = getComponent(); requireRouters.keys().forEach((fileSrc: string) => { // 獲取 components 檔案下的檔名 const viewSrc = requireRouters(fileSrc); const file = viewSrc.default; // 首字母轉大寫 const routerName = letterToUpperCase(file.name); // 首字母轉小寫 const routerPath = letterToLowerCase(file.name); // 設定路由路徑 const fileNameSrc = fileSrc.replace(/^\.\//, ''); if (file.isRouter) { routerList.push({ path: `/${routerPath}`, name: `${routerName}`, component: () => import(`@/views/${fileNameSrc}`) }); } }); return routerList; };
2. axios
請求封裝(請求攔截、響應攔截、取消請求、統一處理)
import axios, { AxiosRequestConfig, AxiosResponse, Canceler } from 'axios'
import router from '@/router'
import { Toast } from 'vant'
if (process.env.NODE_ENV === 'development') {
// 開發環境
axios.defaults.baseURL = `https://test-mobileapi.qinlinkeji.com/api/`
} else {
// 正式環境
axios.defaults.baseURL = `正式環境地址`
}
let sourceAjaxList: Canceler[] = []
export const axionInit = () => {
axios.interceptors.request.use((config: AxiosRequestConfig) => {
// 設定 cancel token 用於取消請求 (當一個接口出現401後,取消後續多有發起的請求,避免出現好幾個錯誤提示)
config.cancelToken = new axios.CancelToken(function executor(cancel: Canceler): void {
sourceAjaxList.push(cancel)
})
// 存在 sessionId 為所有請求加上 sessionId
if (localStorage.getItem(`h5_sessionId`) && config.url!.indexOf('/user/login') < 0) config.url += ('sessionId=' + localStorage.getItem(`h5_sessionId`))
if (!config.data) config.data = {}
return config
}, function (error) {
// 丟擲錯誤
return Promise.reject(error)
})
axios.interceptors.response.use((response: AxiosResponse) => {
const { status, data } = response
if (status === 200) {
// 如果不出現錯誤,直接向回撥函式內輸出 data
if (data.code === 0) {
return data
} else if (data.code === 401) {
// 出現未登入或登入失效取消後面的請求
sourceAjaxList.length && sourceAjaxList.length > 0 && sourceAjaxList.forEach((ajaxCancel, index) => {
ajaxCancel() // 取消請求
delete sourceAjaxList[index]
})
Toast({
message: data.message,
duration: 2000
})
return router.push('/login')
} else {
return data
}
} else {
return response
}
}, error => {
const { response } = error
// 這裡處理錯誤的 http code or 伺服器或後臺報錯
if (!response || response.status === 404 || response.status === 500) {
if (!response) {
console.error(`404 error %o ${error}`)
} else {
if (response.data && response.data.message) {
Toast.fail({
message: '請求異常,請稍後再試!',
duration: 2000
})
}
}
}
return Promise.reject(error.message)
})
}
3. vuex
業務模組化、 接管請求統一處理
// 具體請看專案store目錄
import { Module } from 'vuex'
import { IGlobalState } from '../../index'
import * as Types from './types'
import { IHomeState, ICity, IAccessControl, ICommonlyUsedDoor } from './interface'
import axios from 'axios'
const state: IHomeState = {
cityList: [],
commonlyUsedDoor: {
doorControlId: '',
doorControlName: ''
},
accessControlList: []
}
const home: Module<IHomeState, IGlobalState> = {
namespaced: true,
state,
actions: {
// 獲取小區列表
async [Types.GET_CITY_LIST]({ commit }) {
const result = await axios.post(`auth/v2/getApplyListGroupByCommunityH5?`)
commit(Types.GET_CITY_LIST, result.data)
},
// 獲取小區門禁列表
async [Types.GET_ACCESS_CONTROL_LIST]({ commit }, data) {
const result = await axios.post(`doorcontrol/v2/queryUserDoor?`, { ...data })
commit(Types.GET_ACCESS_CONTROL_LIST, result.data.userDoorDTOS)
commit(Types.SET_COMMONLY_USERDOOR, result.data.commonlyUsedDoor)
},
},
mutations: {
// 設定小區列表
[Types.GET_CITY_LIST](state, cityList: ICity[]) {
if (cityList.length !== 0) state.cityList = cityList
},
// 設定小區門禁列表
[Types.GET_ACCESS_CONTROL_LIST](state, accessControlList: IAccessControl[]) {
if (accessControlList.length !== 0) return state.accessControlList = accessControlList
},
// 設定當前小區
[Types.SET_COMMONLY_USERDOOR](state, commonlyUsedDoor: ICommonlyUsedDoor) {
state.commonlyUsedDoor = commonlyUsedDoor
}
}
}
export default home
4. home
檔案程式碼
<template>
<div class="home-container">
<header>
<Suspense>
<template #default>
<HomeSwiper></HomeSwiper>
</template>
<template #fallback>
<div>...loading</div>
</template>
</Suspense>
</header>
<section>
<Suspense>
<template #default>
<HomeContent
:cityList="cityList"
:accessControlList="accessControlList"
></HomeContent>
</template>
<template #fallback>
<div>...loading</div>
</template>
</Suspense>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed, onMounted } from 'vue'
import { Store, useStore } from 'vuex'
import { IGlobalState } from "@/store";
import * as Types from "@/store/modules/Home/types";
/**
* 該hook目的:個人理解:
* 1、類似於全域性的公共方法;可以考慮提到工具類函式中
* 2、cityList, accessControlList 均是隻做為展示的資料,沒有後續的修改; 所以可考慮提取出來由父元件管理
* 3、假如該方法內部邏輯比較多,其他頁面又需要用到, 所以提取比較合適
* 4、當然自由取捨, 放到 steup 方法內部實現也沒問題, 但不利於其他頁面引用獲取
* 5、vuex actions、mutations 函式邏輯應儘可能的少,便於維護; 邏輯處理應在頁面內部
*/
function useContentData(store: Store<IGlobalState>) {
let cityList = computed(() => store.state.home.cityList)
let accessControlList = computed(() => store.state.home.accessControlList)
onMounted(() => {
if (cityList.value.length === 0) store.dispatch(`home/${Types.GET_CITY_LIST}`)
if (accessControlList.value.length === 0) store.dispatch(`home/${Types.GET_ACCESS_CONTROL_LIST}`, 13)
})
return {
cityList,
accessControlList
}
}
export default defineComponent({
name: 'home',
isComponents: true,
setup() {
let store = useStore<IGlobalState>()
let { cityList, accessControlList } = useContentData(store)
const state = reactive({
active: 0,
})
return {
...toRefs(state),
cityList,
accessControlList
}
}
})
</script>
<style scoped lang="less">
.home-container {
height: 100%;
background: #f6f6f6;
header {
overflow: hidden;
height: 500px;
background-size: cover;
background-position: center 0;
background-image: url("~@/assets/images/home_page_bg.png");
}
section {
position: relative;
top: -120px;
padding: 0 20px;
}
}
</style>
四、專案ui
五、結語
以上為個人實際專案開發總結, 有不對之處歡迎留言指正