1. 程式人生 > 實用技巧 >Vue3、Vuex、Typescript 專案實踐、工具類封裝

Vue3、Vuex、Typescript 專案實踐、工具類封裝

原文: 本人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上就報錯;一臉懵逼的那種, vitegithub 提 Issues 都沒用, 維護人員隨便回答了下就把我的 Issues 給關了,我也是醉了;
  • 3)不過自己還是很期待 vite 的, 等待他成熟吧, 在正式使用;
  • 4)涉及點:目前只貼出專案初期的幾個功能
    • webpack require 自動化註冊路由、自動化註冊非同步組價
    • axios 請求封裝(請求攔截、響應攔截、取消請求、統一處理)
    • vuex 業務模組化、 接管請求統一處理

三、專案搭建

可參考上篇文章 Vue3.0 進階、環境搭建、相關API的使用

  1. vue-cli、vue 下載最新版本
  2. 執行命令 vue create my_app_name
  3. 執行完上面命令接下來選擇手動配置(第三個),不要選擇預設配置,有很多我們用不上,我的選擇如下圖:

三、專案主要功能

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


五、結語

以上為個人實際專案開發總結, 有不對之處歡迎留言指正