原文: 本人github文章
關注公眾號: 微信搜索 前端工具人
; 收貨更多的干貨
一、開篇
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'
import { IRouter } from './interface'
// 獲取所有vue文件
function getComponent() {
return require.context('../views', true, /\.vue$/);
}
// 首字母轉換大寫
function letterToUpperCase(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// 首字母轉換小寫
function letterToLowerCase(str: string): string {
return str.charAt(0).toLowerCase() + str.slice(1);
}
export const asyncComponent = (): void => {
// 獲取文件全局對象
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 = (): IRouter[] => {
const routerList: IRouter[] = [];
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 data
}
}, 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, IAxiosResponseData } from '../../index'
import * as Types from './types'
import { IHomeState, ICity, IAccessControl, ICommonlyUsedDoor, AGetCtiy } from './interface'
import qs from 'qs';
import * as API from './api'
const state: IHomeState = {
cityList: [],
communityId: 13,
commonlyUsedDoor: {
doorControlId: '',
doorControlName: ''
},
accessControlList: []
}
const home: Module<IHomeState, IGlobalState> = {
namespaced: true,
state,
actions: {
// 獲取小區列表
async [Types.GET_CITY_LIST]({ commit }) {
const result = await API.getCityList<IAxiosResponseData>()
if (result.code !== 0) return
commit(Types.GET_CITY_LIST, result.data)
},
// 獲取小區門禁列表
async [Types.GET_ACCESS_CONTROL_LIST]({ commit }) {
const result = await API.getCityAccessControlList<IAxiosResponseData>({
communityId: state.communityId
})
if (result.code !== 0) return
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";
import qs from 'qs';
/**
* 該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}`, {
communityId: 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>
5. login
文件代碼
<template>
<div class="login-container">
<p>手機號登錄</p>
<van-cell-group>
<van-field
v-model="phone"
required
clearable
maxlength="11"
label="手機號"
placeholder="請輸入手機號" />
<van-field
v-model="sms"
center
required
clearable
maxlength="6"
label="短信驗證碼"
placeholder="請輸入短信驗證碼">
<template #button>
<van-button
size="small"
plain
@click="getSmsCode">{{isSend ? `${second} s` : '發送驗證碼'}}</van-button>
</template>
</van-field>
</van-cell-group>
<div class="login-button">
<van-button
:loading="isLoading"
size="large"
@click="onLogin"
loading-text="正在登錄..."
type="primary">登錄</van-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { useStore } from "vuex";
import { IGlobalState } from "@/store";
import * as Types from "@/store/modules/Login/types";
import { Toast } from 'vant'
import router from '@/router'
export default defineComponent({
name: 'login',
isRouter: true,
setup(props, ctx) {
let store = useStore <IGlobalState> ()
const state = reactive({
sms: '',
phone: '',
second: 60,
isSend: false,
isLoading: false
})
const phoneRegEx = /^[1][3,4,5,6,7,8,9][0-9]{9}$/
// 獲取驗證碼
const getSmsCode = async () => {
localStorage.removeItem('h5_sessionId')
store.commit(`login/${Types.SAVE_PHONE}`, state.phone)
if (!phoneRegEx.test(state.phone)) return Toast({
message: '手機號輸入有誤!',
duration: 2000
})
store.dispatch(`login/${Types.GET_SMS_CODE}`, state.phone).then(res => {
if (res.code !== 0) return
Toast({
message: '驗證碼已發送至您手機, 請查收',
duration: 2000
})
state.isSend = true
const timer = setInterval(() => {
state.second--;
if (state.second <= 0) {
state.isSend = false
clearInterval(timer);
}
}, 1000);
})
}
// 登錄
const onLogin = () => {
state.isLoading = true
store.commit(`login/${Types.SAVE_SMS_CODE}`, state.sms)
store.dispatch(`login/${Types.ON_LOGIN}`).then(res => {
state.isLoading = false
if (res.code !== 0) return
localStorage.setItem('h5_sessionId', res.data.sessionId)
store.commit(`login/${Types.SAVE_USER_INFO}`, res.data)
router.push('/index')
})
}
return {
...toRefs(state),
onLogin,
getSmsCode
}
}
})
</script>
<style lang="less" scoped>
.login-container {
padding: 0 20px;
>p {
padding: 50px 20px 40px;
font-size: 40px;
}
.login-button {
margin-top: 50px;
}
}
</style>
四、項目ui
五、結語
以上為個人實際項目開發總結, 有不對之處歡迎留言指正