Vue3 + Vite2 + TypeScript + Pinia(Vuex)+JSX build an enterprise level development scaffold [out of the box]

Posted by adaykin on Fri, 25 Feb 2022 03:17:59 +0100

With the popularity of Vue3, more and more projects have begun to use Vue3. In order to quickly enter the development state, I recommend a set of enterprise level development scaffolding out of the box. The framework uses: Vue3 + Vite2 + TypeScript + JSX + Pinia(Vuex) + Antd. If you don't talk much nonsense, just start rolling.
The scaffold is divided into two versions, Vuex version and Pinia version, according to the use status library. The following is the relevant code address:
Vuex version,
Pinia version

Preparation for construction

  1. Vscode : code writing artifact necessary for front-end users
  2. Chrome : developer friendly browser (standard browser for programmers)
  3. Nodejs & npm : configure the local development environment. After installing Node, you will find that npm will be installed together (V12 +)

When using npm to install dependency packages, you will find that it is very slow. It is recommended to use cnpm and yarn instead.

Scaffold directory structure

├── src
│   ├── App.tsx
│   ├── api                     # Interface management module
│   ├── assets                  # Static resource module
│   ├── components              # Common component module
│   ├── mock                    # mock interface simulation module
│   ├── layouts                 # Public custom layout
│   ├── main.ts                 # Entry file
│   ├── public                  # Common resource module
│   ├── router                  # route
│   ├── store                   # vuex State Library
│   ├── types                   # Declaration file
│   ├── utils                   # Common method module
│   └── views                   # View module
├── tsconfig.json
└── vite.config.js

What is Vite

Next generation front-end development and construction tools
Vite (French means "fast", pronounced / vit /, pronounced the same as "veet") is a new front-end construction tool, which can significantly improve the front-end development experience. It mainly consists of two parts:

Vite is intended to provide out of the box configuration and its Plug in API And JavaScript API It brings a high degree of scalability and complete type support.

You can Why Vite Learn more about the original design intention of the project.

What is Pinia

Pinia.js is a new generation of state manager, which is developed by Vue It is developed by members of the JS team, so it is also considered to be the next generation Vuex, namely Vuex5 x. In vue3 0 is also highly respected for its use in projects

Pinia.js has the following characteristics:

  • More complete typescript support than Vuex;
  • It is light enough, and the volume after compression is only 1.6kb;
  • Remove mutations and only state, getters and actions (support synchronization and asynchrony);
  • Compared with Vuex, it is more convenient to use. Each module is independent and has better code segmentation. There is no module nesting, and store s can be used freely

install

npm install pinia --save

Create Store

  • Create a new src/store directory and create an index Ts and export the store

    import { createPinia } from 'pinia'
    
    const store = createPinia()
    
    export default store
  • In main Introduction into TS
import { createApp } from 'vue'
import store from './store'

const app = createApp(App)

app.use(store)

Define State

Create src/store/modules, and add common under modules according to module division ts

import { defineStore } from 'pinia'

export const CommonStore = defineStore('common', {
  // State Library
  state: () => ({
    userInfo: null, //User information
  }),
})

Get State

There are many ways to obtain state, the most commonly used are as follows:

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

Get using computed

const userInfo = computed(() => common.userInfo)

Use storeToRefs provided by Pinia

import { storeToRefs } from 'pinia'
import { CommonStore } from '@/store/modules/common'

...
const commonStore = CommonStore()
const { userInfo } = storeToRefs(commonStore)

Modify State

There are three ways to modify state:

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

const commonStore = CommonStore()
commonStore.setUserInfo('Cao Cao')

Getters

export const CommonStore = defineStore('common', {
  // State Library
  state: () => ({
    userInfo: null, //User information
  }),
  getters: {
    getUserInfo: (state) => state.userInfo
  }
})

Get using the same State

Actions

Pinia endows Actions with greater functions. Compared with Vuex, Pinia removes Mutations and only relies on Actions to change the Store state. Both synchronous and asynchronous can be placed in Actions.

Synchronous action

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

Asynchronous actions

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

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

actions between modules call 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
    },
}

Pinia plugin persist plug-in for data persistence

install

npm i pinia-plugin-persist --save

use

// src/store/index.ts

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

const store = createPinia().use(piniaPluginPersist)

export default store

Use in corresponding store

export const CommonStore = defineStore('common', {
  // State Library
  state: () => ({
    userInfo: null, //User information
  }),
  // Enable data cache
  persist: {
    enabled: true,
    strategies: [
      {
        storage: localStorage, // The default is stored in sessionStorage
        paths: ['userInfo'],  // Specify the storage state. If it is not written, all data will be stored
      },
    ],
  },
})

Fetch

In order to better support TypeScript and count Api requests, axios is encapsulated twice 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 interception
 */
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    config.headers.common.Authorization = getToken() // Request token on header
    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 login again!',
          onOk: () => {
            sessionStorage.clear()
          },
        })
      } else if (code == 200) {
        if (status) {
          // Interface request succeeded
          msg && Message.success(msg) // If msg is returned in the background, msg will be prompted
          return Promise.resolve(response) // Return success data
        }
        // Interface exception
        msg && Message.warning(msg) // If msg is returned in the background, msg will be prompted
        return Promise.reject(response) // Return exception data
      } else {
        // Interface exception
        msg && Message.error(msg)
        return Promise.reject(response)
      }
    }
    return response
  },
  (error) => {
    if (error.response.status) {
      switch (error.response.status) {
        case 500:
          Notification.error({
            message: 'reminder',
            description: 'Service exception, please restart the server!',
          })
          break
        case 401:
          Notification.error({
            message: 'reminder',
            description: 'Service exception, please restart the server!',
          })
          break
        case 403:
          Notification.error({
            message: 'reminder',
            description: 'Service exception, please restart the server!',
          })
          break
        // 404 request does not exist
        case 404:
          Notification.error({
            message: 'reminder',
            description: 'Service exception, please restart the server!',
          })
          break
        default:
          Notification.error({
            message: 'reminder',
            description: 'Service exception, please restart the server!',
          })
      }
    }
    return Promise.reject(error.response)
  },
)

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

const http: Http = {
  // The usage is consistent with axios (including all the built-in request modes of axios)
  fetch(params) {
    return new Promise((resolve, reject) => {
      service(params)
        .then((res) => {
          resolve(res.data)
        })
        .catch((err) => {
          reject(err.data)
        })
    })
  },
}

export default http['fetch']

use

// src/api/user.ts

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

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

export default UserApi

type definition

/**
 * Interface return result Types
 * --------------------------------------------------------------------------
 */
// Login return results
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>>
}

Router4

  1. Basic 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 },
      },
    ]

    title: navigation display text; hidden: whether to hide the route on the navigation (true: not shown, false: shown)

  2. 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 needs dynamic routing
      if (!isAddDynamicMenuRoutes) {
        try {
          //Get dynamic routing table
          const res: any = await UserApi.getPermissionsList({})
          if (res.code == 200) {
            isAddDynamicMenuRoutes = true
            const menu = res.data
            // Generate standard format route through route table
            const menuRoutes: any = fnAddDynamicMenuRoutes(
              menu.menuList || [],
              [],
            )
            mainRoutes.children = []
            mainRoutes.children?.unshift(...menuRoutes, ...Routes)
            // Dynamically add routes
            router.addRoute(mainRoutes)
            // Note: this step is very important, otherwise the route cannot be obtained by navigation
            router.options.routes.unshift(mainRoutes)
            // Local storage button permission collection
            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 permission, 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()
    }
  },
)

Layouts layout components

The scaffold provides a variety of typesetting layout, and the directory structure is as follows:

  • BlankLayout.tsx: blank layout, only routing distribution
  • RouteLayout.tsx: main layout, content display part, including bread crumbs
  • LevelBasicLayout.tsx multi-level display layout, applicable to routes above level 2
  • SimplifyBasicLayout.tsx simplified multi-level display layout is applicable to routes above level 2

Related reference links

last

The article will be written here for the time being, and the JSX syntax will be added later. If this article is of any help to you, don't forget to move your finger and click like ❤️.
If there are mistakes and shortcomings 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

Topics: TypeScript Vue.js