Vue3 + Vite2 + TypeScript + Pinia(Vuex)+JSX 搭建企業級開發腳手架【開箱即用】

陽晨@發表於2022-02-25
隨著Vue3的普及,已經有越來越多的專案開始使用Vue3。為了快速進入開發狀態,在這裡向大家推薦一套開箱即用的企業級開發腳手架,框架使用:Vue3 + Vite2 + TypeScript + JSX + Pinia(Vuex) + Antd。廢話不多話,直接上手開擼。
該腳手架根據使用狀態庫的不同分為兩個版本Vuex版、Pinia版,下面是相關程式碼地址:
Vuex版
Pinia版

搭建需準備

  1. Vscode : 前端人必備寫碼神器
  2. Chrome :對開發者非常友好的瀏覽器(程式設計師標配瀏覽器)
  3. 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")是一種新型前端構建工具,能夠顯著提升前端開發體驗。它主要由兩部分組成:

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的三種方式:

  1. 直接修改(不推薦)
commonStore.userInfo = '曹操'
  1. 通過$patch
commonStore.$patch({
    userInfo:'曹操'
})
  1. 通過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,不寫則儲存所有
      },
    ],
  },
})

WX20220224-151530.png

Fetch

為了更好的支援TypeScript,統計Api請求,這裡將axios進行二次封裝

結構目錄:

WX20220224-155540@2x.png

// 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

  1. 基礎路由

    // 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:顯示)
  2. 動態路由(許可權路由)
// 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佈局元件

腳手架提供多種排版佈局,目錄結構如下:

layout.png

  • BlankLayout.tsx: 空白布局,只做路由分發
  • RouteLayout.tsx: 主體佈局,內容顯示部分,包含麵包屑
  • LevelBasicLayout.tsx 多級展示佈局,適用於2級以上路由
    LevelBasicLayout.png
  • SimplifyBasicLayout.tsx 簡化版多級展示佈局,適用於2級以上路由

SimplifyBasicLayout.png

相關參考連結

最後

文章暫時就寫到這,後續會增加JSX語法部分,如果本文對您有什麼幫助,別忘了動動手指點個贊❤️。
本文如果有錯誤和不足之處,歡迎大家在評論區指出,多多提出您寶貴的意見!

最後分享本腳手架地址:github地址
gitee地址

相關文章