instructions

Because the company need, I as a pure back-end engineer, has spent more than half a year self-study vue, leng was forced into a small whole stack, of course, small full stack declares to sound good, in fact is a depth of the front and back side are not chicken dishes, in the know, or you will forget their own shortcomings and understand well the truth, You can always make more wheels and take more notes 🙂

So RECENTLY I had a chance to reconstruct the bad project I wrote when I just learned VUejs. During the reconstruction, I made some notes on specific modules as follows

  • routing
  • State management
  • Rights management
  • Control encapsulation and use
  • Mixed with
  • The data simulation
  • Package optimization and user experience

If you don’t want to go that far you can check out the world’s largest gay dating site

Enter rotten pen tip mode

routing

1. Load routes

// Load the page directly
import page from '@/views/page';
// lazy page loading() = >import('@/views/page');
// Specify lazy loading with a package name. Multiple pages can be packed into a single JS for loading() = >import(/* webpackChunkName: "group-page" */'@/views/page1');
(a)= > import(/* webpackChunkName: "group-page" */'@/views/page2');
(a)= > import(/* webpackChunkName: "group-page" */'@/views/page3');
Copy the code

2. 404 routing

// Load a 404 page
import page404 from '@/views/page404';
// Place the following route configuration at the end of the routing table and redirect to the Page404 component page if the path does not match all previous routes
{ path: The '*'.component: page404}
Copy the code

3. Route interception

// Interceptor before route jump
router.beforeEach((to, from, next) = >{});// Interceptor after route jump
router.afterEach(to= >{});// Interceptor for route jump errors
router.onError((err) = >{});Copy the code

4. Dynamic routes

Dynamic routing is usually used with page-level permission control

// Dynamically add accessible routes using router.addRoutes
router.addRoutes(addRouters)
// The hack method ensures that addRoutes is completenext({ ... to,replace: true }) // set the replace: true so the navigation will not leave a history record 
Copy the code

5. Animate route loading

The loading animation during route loading is used together with lazy route loading

// Define a route loading flag in state management
const app = {
  state: {
    routerLoading: false.// Route loading transitioned
  },
  mutations: {
    // Change the loading state of a route
    UPDATE_ROUTER_LOADING(state, status) {
      state.routerLoading = status
    }
  }
}

// Change the loading state in the route interceptor
router.beforeEach((to, from, next) = > {
  store.commit('UPDATE_ROUTER_LOADING'.true); // Display route loading animation
});
router.afterEach(to= > {
  store.commit('UPDATE_ROUTER_LOADING'.false);
});
router.onError(err= > {
  console.error(err); // for bug
  store.commit('UPDATE_ROUTER_LOADING'.false);
});

// Define the loading animation in router-view
// Element-UI provides the v-loading instruction to use directly
<router-view v-loading="$store.getters.routerLoading"></router-view>
Copy the code

State management

1. The little knowledge

  • Data changes in state need to be triggered by mutation or action
  • Methods in mutation must be synchronization functions
  • An action can contain any asynchronous operation and can return a Promise
  • Mutation and actions can be repeated, and will be called sequentially when called. Getters must be unique

2. Multiple modules

When services are complex, multiple modules in status management can be used. Note the following

  • With the exception of state, which adds the hierarchy based on the alias of the module at the time of composition, everything else is merged at the root level, so getters, commit, and Dispatch obtained in the callback function are global
  • The callback parameter of mutation is only state, which is the state tree of the current module. The following is the same
  • The action callback parameters are state, rootState, getters, Commit, and Dispatch. If you need to invoke other actions in the Action, you can invoke dispatch directly
  • The callback parameters of the getter are state, rootState, and getters
  • Modules can interact with each other through the rootState of the callback
  • Mutation and action will be triggered in sequence if the same name occurs
// Multi-module implementation app and user for each sub-module
export default new Vuex.Store({
    modules: {
        app,
        user
    },
    getters
})
Copy the code

3. Auxiliary functions

Vuex also provides some helper functions in addition to Store objects

  • MapState, mapGetters Maps the state and getters properties in store to the local calculation properties of the VUE component
import { mapState } from 'vuex'
computed: mapState([ 
    // Map this.name to this.$store.state.name
    'name'
])

import { mapGetters } from 'vuex'
computed: {
    // Map this.name to this.$store.getters.name. mapGetters(['name'])}Copy the code
  • MapActions, mapMutations map the dispatch and commit methods in the store to the local methods of the VUE component
import { mapActions } from 'vuex'

methods: { 
    // Map this.loginbyusername () to this.$store.dispatch('LoginByUsername').. mapActions(['LoginByUsername' ]), 
    // Map this.login() to this.$store.dispatch('LoginByUsername'). mapActions({login: 'LoginByUsername'})}import { mapMutations } from 'vuex'

methods: { 
    // Map this.set_name () to this.new store.mit ('SET_NAME')]). mapMutations(['SET_NAME']),// Map this.setName() to this.code. store.mit ('SET_NAME')}). mapMutations({setName: 'SET_NAME'])}Copy the code

4. Data persistence plug-in

Use this plug-in when you want to refresh the page without losing state

/ / copied from https://github.com/robinvdvleuten/vuex-persistedstate
import createPersistedState from 'vuex-persistedstate'
import * as Cookies from 'js-cookie'

const store = new Store({
  // ...
  plugins: [
    createPersistedState({
      storage: {
        getItem: key =* Cookies.get(key),
        // Please see https://github.com/js-cookie/js-cookie#json, on how to handle JSON.
        setItem: (key, value) =* Cookies.set(key, value, { expires: 3.secure: true }),
        removeItem: key =* Cookies.remove(key)
      }
    })
  ]
})
Copy the code

5. Log plug-ins

This plug-in can be used in development environments where you want to be able to track state changes and output them

// createLogger is a built-in plug-in in vuex
import createLogger from 'vuex/dist/logger'

let vuexPlugins = [];
if(process.env.NODE_ENV ! = ='production') {// The development environment loads the plug-in
    vuexPlugins.push(createLogger); 
}

const store = new Store({
  // ...
  plugins: vuexPlugins
})

Copy the code

Rights management

1. Functions to be implemented

  • Routes are generated based on the user login permission table
  • Page level permission control
  • Dom element-level permission control
  • Handling login status failure

2. Route design

First we need to design the necessary parameters for the routing object

For permission management, the ROLES parameter represents the permissions that the route must have to access

In order to better display routing, the title and icon parameters are designed here for the menu display in the sidebar

While some routes do not need to be shown in the sidebar, the hidden parameter is used to tell the program which routes do not need to be shown

// First design the route object parameters
/** * hidden: true If hidden is true then display it on the left menu bar. The default is false * name:'router-name' Route name * meta: {roles: ['admin','editor'] permission list, used for page-level permission control, default not set means that any permission can access title: 'title' corresponds to the route in the left menu bar title name icon: 'icon-class' corresponds to the route in the left menu bar icon style} **/
Copy the code

Next we need to implement dynamic loading of routes

The required routes are loaded during system initialization, and the required routes are loaded based on the permission of the login user

// Define the necessary routing information to load during system initialization
export const constantRouterMap = [
  { path: '/login'.name: 'login'.meta: { title: "System Login".hidden: true }, component: login },
  { path: "/ 404".name: "page404".meta: { title: "The page is lost.".hidden: true }, component: page404 },
  { path: "/ 401".name: "page401".meta: { title: "Insufficient authority".hidden: true }, component: page401 }
]
// Define the layout page
const layout = (a)= > import(/* webpackChunkName: "group-index" */ '@/views/layout');
// Define asynchronously loaded routing information
export const asyncRouterMap = [
  {
    path: '/'.name: 'main'.redirect: '/dashboard'.hidden: true.component: layout,
    children: [{path: 'dashboard'.name: 'dashboard'.meta: { title: "Dashboard" }, component: (a)= > import(/* webpackChunkName: "group-index" */'@/views/dashboard'}]}, {path: '/permission'.name: 'permission'.meta: { title: "Permission Page".icon: "dbm d-icon-quanxian" },
    redirect: '/permission/adminpermission'.component: layout,
    children: [{path: "adminpermission".name: "adminPermission".meta: { title: "Administrator Rights Page".roles: ["admin"]},component: (a)= > import('@/views/permission/admin')}, {path: "watcherpermission".name: "watcherPermission".meta: { title: "Visitor Permission Page".roles: ["admin"."watcher"]},component: (a)= > import('@/views/permission/watcher')}, {path: "elementpermission".name: "elementPermission".meta: { title: "Element level permissions" }, component: (a)= > import('@/views/permission/element'}]}, {path: The '*'.redirect: '/ 404'.hidden: true}]Copy the code

3. Page-level permission control

Use route interception to implement page-level permission control

Intercept route redirect to determine whether a user logs in

The permission table is extracted from the user information and the asynchronous routing table is dynamically loaded by the addRoutes method

Each time a route is redirected, the user checks whether he/she has the access permission of the route to achieve dynamic permission matching

// Define whitelisting
const whiteList = ['/login'.'/ 404'.'/ 401'];
// Intercept route jumps
router.beforeEach((to, from, next) = > {
  store.commit('UPDATE_ROUTER_LOADING'.true); // Display route loading animation
  if (getToken()) {  / / token
    if (to.path === '/login') {
      next({ path: '/'})}else {
      if (store.getters.roles.length === 0) { // Check whether the current user has finished pulling the user information
        store.dispatch('GetUserInfo').then(data= > { // Pull user information
          const roles = data.roles // Permission table must be array, for example: ['admin','editer']
          store.dispatch('GenerateRoutes', { roles }).then((a)= > { // Generate an accessible routing table based on roles permissions
            router.addRoutes(store.getters.addRouters) // Dynamically add the accessible routing tablenext({ ... to,replace: true }) // the hack method ensures that addRoutes is complete,set the replace: true so the navigation will not leave a history record
          })
        }).catch(err= > { // Failed to pull the user information, indicating that the login status is invalid
          store.dispatch('FedLogOut').then((a)= > {
            Message.error('Login status invalid, please log in again');
            next({ path: '/login'}); })})}else {
        if (hasPermission(store.getters.roles, to.meta.roles)) { // Dynamic permission matching
          next();
        } else {
          next({ path: '/ 401'.replace: true.query: { noGoBack: true}}); }}}}else { / / not token
    if(whiteList.indexOf(to.path) ! = =- 1) { // Enter the whitelist directly
      next();
    } else {
      next('/login'); // Otherwise all redirects to the login page}}});Copy the code

4. Element-level permission control

Use custom directives to implement element-level permission control

Verify that the user has the required permissions for the bound element when it is inserted into the parent node

Determine whether to remove the element based on the authentication result

import store from '@/store' export default { inserted(el, binding, vnode) { const { value } = binding; Const roles = store.getters && store.getters. Roles; If (value && value instanceof Array && value.length > 0) {const permissionRoles = value; Const hasPermission = roles.some(role => {// Determine whether the user has the required permissions for the element return permissionRoles.includes(role); }) if (! HasPermission) {/ / insufficient permissions el parentNode && el. ParentNode. RemoveChild (el); }} else {throw new Error(' you must have permission to write, For example [' admin '] `)}}} / / use it on the vue components / / introduced and registered the import permission instructions permission from "@ / directive/permission/index. The js"; export default { directives: {permission}} // Use the permission directive <el-button v-permission="['admin']">admin visible </el-button> <el-button V - permission = "[' admin ', 'watcher']" > watcher visible < / el - button >Copy the code

Render function

1. How to package a component that supports Render

  • Start by creating a functional component
// Tables extend the implementation of functional components
// see https://github.com/calebman/vue-DBM/blob/master/src/components/table/expand.js
export default {
  name: 'TableExpand'.functional: true.// Mark the component as functional, which means it is stateless (no responsive data) and instance-free (no this context).
  props: {
    row: Object.// The current row object
    field: String./ / column name
    index: Number./ / line number
    render: Function // Render function
  },
  render: (h, ctx) = > { // Provide CTX as context
    const params = {
      row: ctx.props.row,
      field: ctx.props.field,
      index: ctx.props.index
    };
    returnctx.props.render(h, params); }};Copy the code
  • In the parent component
// see https://github.com/calebman/vue-DBM/blob/master/src/components/table/table.vue
import expand from "./expand.js";

<span v-if="typeof col.render ==='function'">
   <expand :field="col.field" :row="item" :render="col.render" :index="rowIndex"></expand>
</span>
Copy the code
  • Use the render function to render
// see https://github.com/calebman/vue-DBM/blob/master/src/views/demo/datatable/data-table.vue
// Introduce custom components
import IndexColumn from "@/components/business/index-column.vue";
/ / register
components: {
  // ...
  IndexColumn
}
/ / use
// Get the context of the current component
let self = this;
// Define the render function
render: (h, params) = >
  h("div", [
    h(IndexColumn, {
      props: {
        field: params.field,
        index: params.index,
        pagingIndex:
          (self.pagination.pageCurrent - 1) * self.pagination.pageSize
      },
      on: { "on-value-delete": self.deleteRow }
    })
  ])
Copy the code

Mixed with

1. The little knowledge

  • Mixin objects enjoy the life cycle of the mixin component
  • Component data takes precedence when data objects are mixed into conflicts
  • Object options (such as Methods, Components, directives) are mixed into the clash and fetch the key value pair of the component object
  • The hooks of the same name are mixed into arrays, and the hooks mixed into the object are called before the component’s own hooks

2. Application scenarios

  • You want some of the routing pages to be destroyed on departure, but you don’t want every routing page to define a local route
// Define mixin objects
export default {
  beforeRouteLeave(to, from, next) {
    if (to.meta && to.meta.destroy) {
      this.$destroy(); } next(); }}// Mix in component pages that require this functionality
import routeLeaveDestoryMixin from "routeleave-destory-mixin";
export default {
  // ...
  mixins: [routeLeaveDestoryMixin]
}
Copy the code
  • Data table custom text, number, time and file cell components, each component has the same data modification, focus selection and other methods, can be extracted into mixed objects, improve component reuse
// see https://github.com/calebman/vue-DBM/blob/master/src/components/business/render-column-mixin.js

// Define mixin objects
export default {
  // ...
  computed: {
    // Whether to select this cell
    inSelect() {
      if (this.cellClickData.index == this.index &&
        this.cellClickData.field == this.field) {
        this.focus();
        return true; }}},methods: {
    // Get focus
    focus() {
      let self = this;
      setTimeout(function () {
        if (self.$refs["rendercolumn"]) {
          self.$refs["rendercolumn"].focus(); }},100);
    },
    // Lose focus
    blur() {
      if (this.v ! =this.value) {
        this.$emit("on-value-change".this.field, this.index, this.v);
      }
      this.$emit("on-value-cancel".this.field, this.index);
    },
    // Data modification
    changeValue(val) {
      this.$emit("on-value-change".this.field, this.index, val);
      this.$emit("on-value-cancel".this.field, this.index); }},watch: {
    // Listen for parent component data changes
    value(val) {
      this.v = val; }}}/ / text columns
// see https://github.com/calebman/vue-DBM/blob/master/src/components/business/text-column.vue
<template>
  <div>
    <input v-show="inSelect" ref="rendercolumn" @blur="blur" @keyup="enter($event)" v-model="v" />
    <span v-show=! "" inSelect" class="cell-text">{{v}}</span>
  </div>
</template>
/ / time column
// see https://github.com/calebman/vue-DBM/blob/master/src/components/business/datetime-column.vue
<template>
  <div>
    <el-date-picker v-show="inSelect" ref="rendercolumn" v-model="v" type="datetime" @change="changeValue" @blur="blur"></el-date-picker>
    <span v-show=! "" inSelect">{{coverValue}}</span>
  </div>
</template>
Copy the code
  • When you want to reduce component complexity, you can use multiple mixins to separate the functionality of the core component
# see https://github.com/calebman/vue-DBM/tree/master/src/components/table├ ─ table │ cell - edit - mixins. Js# Cell editing│ classes - mixins. Js# table style│ scroll bar - control - a mixin. Js# table scroll│ table - empty - mixins. Js# No data processing│ table - resize - mixins. Js# Table adaptation│ table row - mouse - events - a mixin. Js# Mouse movement style changes
Copy the code

The data simulation

1. Functions to be implemented

  • Intercept Ajax requests and delay responses
  • Uniform data format for return
  • Response to different simulated data

2. Configure Mockjs to intercept Ajax requests

// see https://github.com/calebman/vue-DBM/blob/master/src/mock/index.js
/ / introduce Mockjs
import Mock from 'mockjs';
// Configure delay
Mock.setup({
  timeout: '300-1000'
});
// Configure interception
Mock.mock(/\/user\/login/.'post', loginAPI.loginByUsername);
Mock.mock(/\/user\/logout/.'post', loginAPI.logout);
Mock.mock(/\/user\/info\.*/.'get', loginAPI.getUserInfo);
Copy the code

3. Unified data format for responses

// see https://github.com/calebman/vue-DBM/blob/master/src/mock/response.js
Response: {* errCode: 00 Response result code * errMsg: 0000000 (success) Detailed response result code * data: NULL Specific data *} */
 
export default {
  / / success
  success: data= > {
    return {
      errCode: '00'.errMsg: '0000000 (success) '.data: data ? data : null}},/ / fail
  fail: (errCode, errMsg) = > {
    return {
      errCode: errCode ? errCode : '04'.errMsg: errMsg ? errMsg : '0401001 (unknown error) '.data: null}},// Permissions are insufficient
  unauthorized: (a)= > {
    return {
      errCode: '43'.errMsg: '4300001 (No access) '.data: null}}}Copy the code

4. Configure the response logic

// see https://github.com/calebman/vue-DBM/blob/master/src/mock/login.js

import { param2Obj } from '@/utils';
import Response from './response';

const userMap = {
  admin: {
    password: 'admin'.roles: ['admin'].token: 'admin'.introduction: 'I'm super administrator'.avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'.name: 'Super Admin'
  },
  watcher: {
    password: 'watcher'.roles: ['watcher'].token: 'watcher'.introduction: 'I'm a tourist'.avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'.name: 'Normal Watcher'}}export default {
  // Log in with a user name
  loginByUsername: config= > {
    const { username, password } = JSON.parse(config.body);
    if (userMap[username] && userMap[username].password === password) {
      return Response.success(userMap[username]);
    } else {
      return Response.fail("01"."0101001 (wrong username or password)")}},// Pull user information
  getUserInfo: config= > {
    const { token } = param2Obj(config.url);
    if (userMap[token]) {
      return Response.success(userMap[token]);
    } else {
      returnResponse.fail(); }},/ / logout
  logout: (a)= > Response.success()
}
Copy the code

5. Simulate random data

// see https://github.com/nuysoft/Mock/wiki

import Mock from 'mockjs';

// Random string
function mockStr() {
    let result = Mock.mock({ 'str': '@name' });
    return result.str;
}

// Random number
function mockNumber(min, max) {
    let key = 'num|' + min + The '-' + max;
    let param = {}
    param[key] = 100;
    return Mock.mock(param).num;
}

// Random decimal, up to three decimal places
function mockDecimal() {
    return Mock.Random.float(1.100.1.3)}// A random number group item
const arr = ["image2.jpeg"."image3.jpeg"."image4.jpeg"."image5.jpeg"."image6.jpeg"];
function mockOneFileAddress() {
    return Mock.mock({ 'oneFile|1': arr }).oneFile;
}

// Random date
function mockDate() {
    let mockDateStr = Mock.Random.datetime('yyyy-MM-dd HH:mm:ss');
    // Use momentjs here to parse it to Date
    let mockDate = moment(mockDateStr, 'YYYY-MM-DD HH:mm:ss').toDate();
    return mockDate;
}
Copy the code

Packaging optimization

1. Which parts to optimize

  • CDN optimization
  • Route lazy loading
  • Other optimization
  • The user experience

2. The CDN optimization

A frame or utility class like Vue, VUe-Router, moment, element-ui, etc. that provides a CDN can be imported directly into index. HTML, and then externals of webpack can be configured so that it does not add to the packaging configuration. Thus reduce the volume of app.js and Vendor. js

  • Use CDN to introduce dependent libraries in index.html
<! -- Network Request Tools -->
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<! -- vue -->
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.min.js"></script>
<! -- vue-router -->
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<! -- vuex -->
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<! -- Momentjs Chinese package -->
<script src="https://cdn.bootcss.com/moment.js/2.22.1/moment-with-locales.min.js"></script>
<! -- momentjs -->
<script src="https://cdn.bootcss.com/moment.js/2.22.1/locale/zh-cn.js"></script>
<! -- Element-UI style -->
<script src="https://cdn.bootcss.com/element-ui/2.3.6/theme-default/index.css"></script>
<! -- element-ui -->
<script src="https://cdn.bootcss.com/element-ui/2.3.6/index.js"></script>
Copy the code
  • Configure the webpack.base.conf.js file in the build folder
module.exports = {
  // ...
  externals: {
    'axios': 'axios'.'vue': 'Vue'.'vue-router': 'VueRouter'.'vuex': 'Vuex'.'moment': 'moment'.'element-ui': 'ELEMENT'}}Copy the code

3. Lazy route loading

Route lazy loading is essential in large single-page applications because it can split code according to route configuration and speed up first-screen rendering

See implementation of route Management

5. Other optimizations

  • Register as few global components as possible, and use the UI framework to refer to the documentation for on-demand loading
  • Gzip compression can be used together with the server to reduce transmission time
  • Consider increasing cache times in applications that update infrequently
  • Consider alternatives to a large library of tools like Moment or LoDash if you don’t use a lot of features

6. User experience

A single page applied to a certain size is still a slow process to optimize the first screen rendering anyway, so consider using a loading animation during the first screen rendering to tell the user that the system is being initialized

  • Start by defining a render animation in index.html
<body>
  <div id="app"></div>
  <! -- Loading animation for first screen rendering -->
  <div id="system-loading" class="showbox">
    <div class="loader">
      <svg class="circular" viewBox="25 25 to 50 50">
        <circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10" />
      </svg>
    </div>
    <div class="text">
      <span>Initializing the system...</span>
    </div>
  </div>
  <! -- built files will be auto injected -->
</body>
Copy the code
  • Then remove this loading from the Mounted hook of app. vue
export default {
  // ...
  mounted() {
    document.body.removeChild(document.getElementById("system-loading")); }};Copy the code