乐趣区

关于vue.js:Vue3-Vite2-TypeScript-PiniaVuexJSX-搭建企业级开发脚手架开箱即用

随着 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”)是一种新型前端构建工具,可能显著晋升前端开发体验。它次要由两局部组成:

  • 一个开发服务器,它基于 原生 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 的三种形式:

  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,不写则存储所有
      },
    ],
  },
})

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

  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 布局组件

脚手架提供多种排版布局,目录构造如下:

  • BlankLayout.tsx: 空白布局,只做路由散发
  • RouteLayout.tsx: 主体布局,内容显示局部,蕴含面包屑
  • LevelBasicLayout.tsx 多级展现布局,实用于 2 级以上路由
  • SimplifyBasicLayout.tsx 简化版多级展现布局,实用于 2 级以上路由

相干参考链接

  • Pinia 官网
  • Vue3 官网
  • Vite
  • Antd Design Vue

最初

文章临时就写到这,后续会减少 JSX 语法局部,如果本文对您有什么帮忙,别忘了动动手指导个赞❤️。
本文如果有谬误和不足之处,欢送大家在评论区指出,多多提出您贵重的意见!

最初分享本脚手架地址:github 地址、
gitee 地址

退出移动版