With the popularity of Vue3, more and more projects have started using Vue3. To get into development mode quickly, I recommend a set of enterprise-level scaffolding for out-of-the-box development using Vue3 + Vite2 + TypeScript + JSX + Pinia(Vuex) + Antd. Cut the crap. Just get started.

The scaffold can be divided into two versions Vuex version and Pinia version according to the different state library. The code addresses are as follows: Vuex version and Pinia version

Preparations for construction

  1. Vscode: front-end people must write code magic tool

  2. Chrome: Developer-friendly browser (standard browser for programmers)

  3. Nodejs & NPM: Configure the local development environment. After installing Node, you will find NPM installed as well (V12+).

NPM takes a long time to install dependency packages. You are advised to use CNPM and YARN instead.

Scaffold directory structure

├─ SRC │ ├─ app.tsX │ ├─ API # ├─ Assets # ├─ Public Component # ├─ Mock # ├─ │ ├─ Public # ├─ Router # State Library │ ├─ Types # │ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├.txtCopy the code

What is a Vite

Next generation front-end development and build tools

Vite (French for “fast”, pronounced /vit/, also known as “veet”) is a new front-end build tool that dramatically improves the front-end development experience. It mainly consists of two parts:

  • A development server that provides rich built-in features based on native ES modules, such as surprisingly fast module hot update (HMR).
  • A set of build instructions that use Rollup to package your code and that are pre-configured to output highly optimized static resources for production.

Vite is intended to provide out-of-the-box configuration, while its plug-in API and JavaScript API provide a high degree of extensibility and complete type support.

You can learn more about the purpose of the project in why Vite.

What is a Pinia

Pinia. Js is the next generation of state manager, developed by members of vuue. Js team

Pinia.js has the following features:

  • More complete typescript support than Vuex;
  • Lightweight enough, compressed volume is only 1.6KB;
  • Remove mutations, only state, getters, actions (support synchronous and asynchronous);
  • Compared with Vuex, it is more convenient to use, each module is independent, better code segmentation, no module nesting, and can be used freely between stores

The installation

npm install pinia --save
Copy the code

Create the Store

  • Create a new SRC /store directory, create index.ts under it, and export store
import { createPinia } from 'pinia'

const store = createPinia()

export default store
Copy the code
  • Introduced in main.ts
import { createApp } from 'vue'
import store from './store'

const app = createApp(App)

app.use(store)
Copy the code

Define the State

In new SRC /store/modules, add common.ts under modules

import { defineStore } from 'pinia'

export const CommonStore = defineStore('common', {
  / / state library
  state: () = > ({
    userInfo: null.// User information})})Copy the code

Access to the State

There are several ways to get state. The most common ones are:

import { CommonStore } from '@/store/modules/common'
// Omit defineComponent here
setup(){
    const commonStore = CommonStore()
    return () = >(
        <div>{commonStore.userInfo}</div>)}Copy the code

Obtain using computed

const userInfo = computed(() = > common.userInfo)
Copy the code

Use storeToRefs provided by Pinia

import { storeToRefs } from 'pinia'
import { CommonStore } from '@/store/modules/common'.const commonStore = CommonStore()
const { userInfo } = storeToRefs(commonStore)
Copy the code

Change the State

There are three ways to modify state:

  1. Direct modification (not recommended)
commonStore.userInfo = "Cao cao
Copy the code
  1. Through $patch
commonStore.$patch({
    userInfo:"Cao cao
})
Copy the code
  1. Modify the store through actions
export const CommonStore = defineStore('common', {
  / / state library
  state: () = > ({
    userInfo: null.// User information
  }),
  actions: {
    setUserInfo(data) {
      this.userInfo = data
    },
  },
})
Copy the code
import { CommonStore } from '@/store/modules/common'

const commonStore = CommonStore()
commonStore.setUserInfo("Cao cao)
Copy the code

Getters

export const CommonStore = defineStore('common', {
  / / state library
  state: () = > ({
    userInfo: null.// User information
  }),
  getters: {
    getUserInfo: (state) = > state.userInfo
  }
})
Copy the code

Get with the same State

Actions

Pinia gives Actions a greater function. Compared with Vuex, Pinia has removed Mutations and only relies on Actions to change the Store state. Both synchronous and asynchronous can be placed in Actions.

The synchronous action

export const CommonStore = defineStore('common', {
  / / state library
  state: () = > ({
    userInfo: null.// User information
  }),
  actions: {
    setUserInfo(data) {
      this.userInfo = data
    },
  },
})
Copy the code

Asynchronous actions

.actions: {
   async getUserInfo(params) {
      const data = await api.getUser(params)
      return data
    },
}
Copy the code

Internal actions call each other

.actions: {
   async getUserInfo(params) {
      const data = await api.getUser(params)
      this.setUserInfo(data)
      return data
    },
    setUserInfo(data){
       this.userInfo = data
    }
}
Copy the code

Modules call actions to each other

import { UserStore } from './modules/user'.actions: {
   async getUserInfo(params) {
      const data = await api.getUser(params)
      const userStore = UserStore()
      userStore.setUserInfo(data)
      return data
    },
}
Copy the code

The pinia-plugin-persist plug-in implements data persistence

The installation

npm i pinia-plugin-persist --save
Copy the code

use

// src/store/index.ts

import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'

const store = createPinia().use(piniaPluginPersist)

export default store
Copy the code

Corresponding to use in store

export const CommonStore = defineStore('common', {
  / / state library
  state: () = > ({
    userInfo: null.// User information
  }),
  // Enable data caching
  persist: {
    enabled: true.strategies: [{storage: localStorage.// sessionStorage by default
        paths: ['userInfo'].// Specify the storage state, do not write to store all},],}})Copy the code

Fetch

To better support TypeScript and count Api requests, axiOS is rewrapped here

Structure directory:

// 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 Environment variable
const BaseUrl = import.meta.env.VITE_API_BASE_URL as string

// create an axios instance
const service: AxiosInstance = axios.create({
  baseURL: BaseUrl, // Formal environment
  timeout: 60 * 1000.headers: {},})/** * Request to intercept */
service.interceptors.request.use(
  (config: AxiosRequestConfig) = > {
    config.headers.common.Authorization = getToken() // Request header with token
    config.headers.common.token = getToken()
    return config
  },
  (error) = > Promise.reject(error),
)

/** * Response interception */
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 error'.content: 'Token invalid, please log in again! '.onOk: () = > {
            sessionStorage.clear()
          },
        })
      } else if (code == 200) {
        if (status) {
          // The interface request succeeded
          msg && Message.success(msg) // if MSG is returned in the background, the message will be displayed
          return Promise.resolve(response) // Return success data
        }
        // The interface is abnormal
        msg && Message.warning(msg) // if MSG is returned in the background, the message will be displayed
        return Promise.reject(response) // Return abnormal data
      } else {
        // The interface is abnormal
        msg && Message.error(msg)
        return Promise.reject(response)
      }
    }
    return response
  },
  (error) = > {
    if (error.response.status) {
      switch (error.response.status) {
        case 500:
          Notification.error({
            message: 'Warm reminder'.description: 'Service is abnormal. Please restart the server! ',})break
        case 401:
          Notification.error({
            message: 'Warm reminder'.description: 'Service is abnormal. Please restart the server! ',})break
        case 403:
          Notification.error({
            message: 'Warm reminder'.description: 'Service is abnormal. Please restart the server! ',})break
        // 404 request does not exist
        case 404:
          Notification.error({
            message: 'Warm reminder'.description: 'Service is abnormal. Please restart the server! ',})break
        default:
          Notification.error({
            message: 'Warm reminder'.description: 'Service is abnormal. Please restart the server! ',}}}return Promise.reject(error.response)
  },
)

interface Http {
  fetch<T>(params: AxiosRequestConfig): Promise<StoreState.ResType<T>>
}

const http: Http = {
  // Same usage as AXIos (including all requests built into AXIos)
  fetch(params) {
    return new Promise((resolve, reject) = > {
      service(params)
        .then((res) = > {
          resolve(res.data)
        })
        .catch((err) = > {
          reject(err.data)
        })
    })
  },
}

export default http['fetch']

Copy the code

use

// src/api/user.ts

import qs from 'qs'
import fetch from '@/utils/fetch'
import { IUserApi } from './types/user'

const UserApi: IUserApi = {
  / / login
  login: (params) = > {
    return fetch({
      method: 'post'.url: '/login'.data: params,
    })
  }
}

export default UserApi

Copy the code

The type definition

/ * * * interface returns the Types * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
// Login returns the result
export interface ILoginData {
  token: string
  userInfo: {
    address: string
    username: string
  }
}

/ * * * interface parameter Types * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
// Login parameters
export interface ILoginApiParams {
  username: string / / user name
  password: string / / password
  captcha: string / / verification code
  uuid: string // Verification code UUID
}

/ * * * interface definition Types * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
export interface IUserApi {
  login: (params: ILoginApiParams) = > Promise<StoreState.ResType<ILoginData>>
}

Copy the code

Router4

  1. Based on the routing
// 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}},]Copy the code

Title: navigation display text; Hidden: Indicates whether to hide the route in the navigation. (True: do not display false: display)

  1. Dynamic Routing (Permission Routing)
// src/router/router.ts

router.beforeEach(
  async (
    to: RouteLocationNormalized,
    from: RouteLocationNormalized,
    next: NavigationGuardNext,
  ) => {
    const token: string = getToken() as string
    if (token) {
      // The routing list is loaded for the first time and the project requires dynamic routing
      if(! isAddDynamicMenuRoutes) {try {
          // Get the dynamic routing table
          const res: any = await UserApi.getPermissionsList({})
          if (res.code == 200) {
            isAddDynamicMenuRoutes = true
            const menu = res.data
            // Generate a standard format route from the routing table
            constmenuRoutes: any = fnAddDynamicMenuRoutes( menu.menuList || [], [], ) mainRoutes.children = [] mainRoutes.children? .unshift(... menuRoutes, ... Routes)// Dynamically add routes
            router.addRoute(mainRoutes)
            // Note: this step is critical, otherwise the navigation will not get the route
            router.options.routes.unshift(mainRoutes)
            // Local store button permissions set
            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}Failed to request menu list and permissions, jump to login page!! `.'color:orange')}},else {
        if (to.path == '/' || to.path == '/login') {
          next(from)}else {
          next()
        }
      }
    } else {
      isAddDynamicMenuRoutes = false
      if(to.name ! ='login') {
        next({ name: 'login' })
      }
      next()
    }
  },
)
Copy the code

Layouts component

Scaffolding offers a variety of typesetting layouts, with the following directory structure:

  • Blanklayout. TSX: BlankLayout for route distribution only
  • Routelayout.tsx: Body layout, content display section, containing breadcrumbs
  • Levelbasiclayout. TSX displays multi-level layout, suitable for routing at level 2 or higher

  • SimplifyBasicLayout. TSX simplified multi-level layout, suitable for routing at level 2 or higher

Related references

  • Pinia website
  • Vue3 website
  • Vite
  • Antd Design Vue

The last

That’s all for now. JSX syntax will be added later. If this article is of any help to you, don’t forget to give it a thumbs up at ❤️. If there are mistakes and deficiencies in this article, you are welcome to point out in the comment area, and put forward your valuable opinions!

Finally share this scaffold address: Github address, Gitee address