This article has authorized the exclusive use of the public account of the Nuggets Developer community, including but not limited to editing, marking the original rights.

Some of these issues are not limited to Vue but also apply to other types of SPA projects.

1. Page permission control and login verification

Page permission control

What does page permission control mean?

A website has different roles, such as administrators and ordinary users, requiring different roles to access different pages. If a page is accessed by a character who is not authorized to do so, then you have to limit it.

One way to do this is by dynamically adding routes and menus, and not adding unreachable pages to the routing table. This is one way. See dynamic Menus in the next section for details.

The alternative is to have all the pages in the routing table, just check the role permissions when accessing them. If there is permission to allow access, no permission to reject, jump to 404 page.

Train of thought

In the meta attribute of each route, add roles with access to that route to roles. Each time a user logs in, the role of the user is returned. When the user visits the page, the meta attribute of the route is compared with the user’s role. If the user’s role is in the roles of the route, the user is allowed to access the page. If the user’s role is not in the roles of the route, the user is denied access.

Code sample

Routing information

routes: [
    {
        path: '/login'.name: 'login'.meta: {
            roles: ['admin'.'user']},component: () = > import('.. /components/Login.vue')}, {path: 'home'.name: 'home'.meta: {
            roles: ['admin']},component: () = > import('.. /views/Home.vue')},]Copy the code

Page control

// Suppose there are two types of roles: admin and user
// Here is the user role obtained from the background
const role = 'user'
// The router.beforeEach event is emitted before entering a page
router.beforeEach((to, from, next) = > {
    if (to.meta.roles.includes(role)) {
        next()
    } else {
        next({path: '/ 404'})}})Copy the code

Login authentication

The site is generally logged in once, and then the other pages of the site can be directly accessed without logging in again. We can do this by token or cookie. The following code shows how to use token to control login authentication.

router.beforeEach((to, from, next) = > {
    // If there is a token, the user has logged in
    if (localStorage.getItem('token')) {
        // Accessing the login page while logged in redirects to the home page
        if (to.path === '/login') {
            next({path: '/'})}else {
            next({path: to.path || '/'}}})else {
        // Any page visited without login is redirected to the login page
        if (to.path === '/login') {
            next()
        } else {
            next(`/login? redirect=${to.path}`)}}})Copy the code

2. Dynamic menu

Write background management system, it is estimated that many people have encountered such a requirement: according to the background data dynamically add routing and menu. Why do you do that? Because different users have different permissions, they can access different pages.

Dynamically Adding routes

The addRoutes method of vue-router can be used to dynamically addRoutes.

Take a look at the official introduction:

router.addRoutes

router.addRoutes(routes: Array<RouteConfig>)
Copy the code

Dynamically add more routing rules. The argument must be an array that matches the Routes option.

Here’s an example:

const router = new Router({
    routes: [{path: '/login'.name: 'login'.component: () = > import('.. /components/Login.vue')}, {path: '/'.redirect: '/home']})},Copy the code

The code above has the same effect as the code below

const router = new Router({
    routes: [{path: '/'.redirect: '/home'},
    ]   
})

router.addRoutes([
    {
        path: '/login'.name: 'login'.component: () = > import('.. /components/Login.vue')})Copy the code

If a 404 page exists during the dynamic route adding process, you must add it last. Otherwise, you will be redirected to the 404 page after the page is added during login.

Like this, this rule must be added last.

{path: The '*'.redirect: '/ 404'}
Copy the code

Dynamically generated menus

Suppose the data returned from the background looks like this:

// Left menu bar data
menuItems: [{name: 'home'.// The route name to jump to is not a path
        size: 18./ / icon size
        type: 'md-home'./ / type icon
        text: 'home' // Text content
    },
    {
        text: 'Secondary menu'.type: 'ios-paper'.children: [{type: 'ios-grid'.name: 't1'.text: 'form'
            },
            {
                text: 'Three level menu'.type: 'ios-paper'.children: [{type: 'ios-notifications-outline'.name: 'msg'.text: 'View messages'},]}]Copy the code

Let’s see how to convert it to a menu bar, so I’m using iView components here, so I don’t have to reinvent the wheel.

<! -- Menu bar -->
<Menu ref="asideMenu" theme="dark" width="100%" @on-select="gotoPage" 
accordion :open-names="openMenus" :active-name="currentPage" @on-open-change="menuChange">
    <! -- Dynamic menu -->
    <div v-for="(item, index) in menuItems" :key="index">
        <Submenu v-if="item.children" :name="index">
            <template slot="title">
                <Icon :size="item.size" :type="item.type"/>
                <span v-show="isShowAsideTitle">{{item.text}}</span>
            </template>
            <div v-for="(subItem, i) in item.children" :key="index + i">
                <Submenu v-if="subItem.children" :name="index + '-' + i">
                    <template slot="title">
                        <Icon :size="subItem.size" :type="subItem.type"/>
                        <span v-show="isShowAsideTitle">{{subItem.text}}</span>
                    </template>
                    <MenuItem class="menu-level-3" v-for="(threeItem, k) in subItem.children" :name="threeItem.name" :key="index + i + k">
                        <Icon :size="threeItem.size" :type="threeItem.type"/>
                        <span v-show="isShowAsideTitle">{{threeItem.text}}</span>
                    </MenuItem>
                </Submenu>
                <MenuItem v-else v-show="isShowAsideTitle" :name="subItem.name">
                    <Icon :size="subItem.size" :type="subItem.type"/>
                    <span v-show="isShowAsideTitle">{{subItem.text}}</span>
                </MenuItem>
            </div>
        </Submenu>
        <MenuItem v-else :name="item.name">
            <Icon :size="item.size" :type="item.type" />
            <span v-show="isShowAsideTitle">{{item.text}}</span>
        </MenuItem>
    </div>
</Menu>
Copy the code

The code does not need to look too carefully to understand the principle, in fact, by three times v-for repeatedly through the subarray loop, to generate a three-level menu.

However, this dynamic menu has a defect, that is, it only supports three levels of menus. A better approach is to encapsulate the process of generating a menu as a component and then call it recursively, thus supporting an infinite level of menus. In the lettuce menu, you need to determine if there are submenus and recursively call components if there are.

Dynamic routing because we’ve talked about using addRoutes, now let’s see how we do it.

First of all, all the page routes of the project are listed, and then the data returned by the background is dynamically matched. The routes that can be matched are added, and the routes that cannot be matched are not added. Finally, add the newly generated routing data to the routing table using addRoutes.

const asyncRoutes = {
    'home': {
        path: 'home'.name: 'home'.component: () = > import('.. /views/Home.vue')},'t1': {
        path: 't1'.name: 't1'.component: () = > import('.. /views/T1.vue')},'password': {
        path: 'password'.name: 'password'.component: () = > import('.. /views/Password.vue')},'msg': {
        path: 'msg'.name: 'msg'.component: () = > import('.. /views/Msg.vue')},'userinfo': {
        path: 'userinfo'.name: 'userinfo'.component: () = > import('.. /views/UserInfo.vue')}}// Pass in background data to generate routing table
menusToRoutes(menusData)

// Change the menu information into the corresponding routing information to dynamically add
function menusToRoutes(data) {
    const result = []
    const children = []

    result.push({
        path: '/'.component: () = > import('.. /components/Index.vue'),
        children,
    })

    data.forEach(item= > {
        generateRoutes(children, item)
    })

    children.push({
        path: 'error'.name: 'error'.component: () = > import('.. /components/Error.vue')})// Add a 404 page at the end of the page otherwise you will jump to the 404 page after successful login
    result.push(
        {path: The '*'.redirect: '/error'},)return result
}

function generateRoutes(children, item) {
    if (item.name) {
        children.push(asyncRoutes[item.name])
    } else if (item.children) {
        item.children.forEach(e= > {
            generateRoutes(children, e)
        })
    }
}
Copy the code

The code implementation of the dynamic menu is stored on Github in the project’s SRC/Components/index.vue, SRC/Permission. js, and SRC /utils/index.js files, respectively.

3. Forward refresh and backward refresh

Requirements:

In a list page, the first time you enter, request data.

Click a list item, jump to the details page, and then back from the details page to the list page without refreshing.

That is, when you go from another page to the list page, you refresh to get the data, and when you go from the details page to the list page, you don’t refresh.

The solution

Settings in app.vue:

        <keep-alive include="list">
            <router-view/>
        </keep-alive>
Copy the code

Suppose the list page is list.vue and the detail page is detail.vue, both of which are child components.

We add the name of the list page in keep-alive and cache the list page.

Then add an Ajax request to the created function of the list page so that the data is requested only when the list page is first entered, and the list page is not refreshed when it jumps from the list page to the detail page and comes back. That should solve the problem.

Requirement 2:

On the basis of requirement 1, another requirement is added: the corresponding list item can be deleted in the detail page, and then the data needs to be refreshed when returning to the list page.

We can add a meta attribute to detail.vue on the routing configuration file.

{ path: '/detail', name: 'detail', component: () => import('.. /view/detail.vue'), meta: {isRefresh: true} },Copy the code

This meta property can be read and set in the detail page with this.$route.meta. IsRefresh.

After setting this property, also set the watch $route property in the app. vue file.

    watch: {
       $route(to, from) {
           const fname = from.name
           const tname = to.name
           if (from.meta.isRefresh || (fname ! ='detail' && tname == 'list')) {
               from.meta.isRefresh = false
   				// Rerequest data here}}},Copy the code

This eliminates the need for Ajax requests in the created function of the list page and puts them in app.vue.

There are two conditions for triggering the request data:

  1. When a list comes in from another page (except the details page), you need to request data.
  2. Return from the details page to the list page if the details pagemetaProperties of theisRefreshtrueThe data also needs to be rerequested.

When we remove the corresponding list item from the detail page, we can set isRefresh to true in the meta property of the detail page. Return to the list page and the page will refresh.

Solution 2

A more concise solution to requirement 2 is to use the key property of the router-view.

<keep-alive>
    <router-view :key="$route.fullPath"/>
</keep-alive>
Copy the code

First keep-alive allows all pages to be cached. When you want to reload a routed page without caching it, you can pass a random string on the jump so that it can be reloaded. For example, if you enter the details page from the list page and delete an option in the list page, you will need to refresh when you return to the list page from the details page. You can skip as follows:

this.$router.push({
    path: '/list'.query: { 'randomID': 'id' + Math.random() },
})
Copy the code

Such schemes are relatively simple.

4. Display and close loading under multiple requests

In general, vue, combined with Axios’ interceptor control loading display and off, looks like this:

Configure a global loading in app. vue.

    <div class="app">
        <keep-alive :include="keepAliveData">
            <router-view/>
        </keep-alive>
        <div class="loading" v-show="isShowLoading">
            <Spin size="large"></Spin>
        </div>
    </div>
Copy the code

Also set up the AXIos interceptor.

 // Add request interceptor
 this.$axios.interceptors.request.use(config= > {
     this.isShowLoading = true
     return config
 }, error= > {
     this.isShowLoading = false
     return Promise.reject(error)
 })

 // Add a response interceptor
 this.$axios.interceptors.response.use(response= > {
     this.isShowLoading = false
     return response
 }, error= > {
     this.isShowLoading = false
     return Promise.reject(error)
 })
Copy the code

The function of this interceptor is to open loading before a request and turn it off when a request ends or an error occurs.

This is fine if there is only one request at a time. But if you have more than one concurrent request, you have a problem.

For example:

If two requests are made at the same time now, the interceptor this.isShowLoading = true will be loaded before the request is made.

Now one request has ended. This.isshowloading = false The interceptor turns off loading, but another request does not end for some reason.

As a result, loading is disabled before a page request is completed. As a result, the page cannot run properly, resulting in poor user experience.

The solution

Add a loadingCount variable to count the number of requests.

loadingCount: 0
Copy the code

Add two more methods to add or subtract loadingCount.

    methods: {
        addLoading() {
            this.isShowLoading = true
            this.loadingCount++
        },

        isCloseLoading() {
            this.loadingCount--
            if (this.loadingCount == 0) {
                this.isShowLoading = false}}}Copy the code

Now the interceptor looks like this:

        // Add request interceptor
        this.$axios.interceptors.request.use(config= > {
            this.addLoading()
            return config
        }, error= > {
            this.isShowLoading = false
            this.loadingCount = 0
            this.$Message.error('Network exception, please try again later')
            return Promise.reject(error)
        })

        // Add a response interceptor
        this.$axios.interceptors.response.use(response= > {
            this.isCloseLoading()
            return response
        }, error= > {
            this.isShowLoading = false
            this.loadingCount = 0
            this.$Message.error('Network exception, please try again later')
            return Promise.reject(error)
        })
Copy the code

The interceptor does the following:

Each time a request is initiated, loading is opened and loadingCount is incremented by 1.

Each time a request ends, loadingCount is reduced by 1 and loadingCount is determined to be 0. If it is 0, loading is disabled.

In this way, loading is disabled because a request ends prematurely in multiple requests.

During route switchover, the previous request was cancelled

Using AXIos, you can cancel previously incomplete requests while switching routes.

See this article for a detailed analysis of the axios switching route to cancel the specified request and cancel the duplicate request coexist. Here is the implementation:

I created a new Vue project and set up two routes a and B. Each time I entered the route, I issued a GET request:

// a.vue
<template>
    <div class="about">
        <h1>This is an a page</h1>
    </div>
</template>

<script>
import { fetchAData } from '@/api'

export default {
    created() {
        fetchAData().then(res= > {
            console.log('A Routing request completed')}}}</script>
Copy the code
// b.vue
<template>
    <div class="about">
        <h1>This is an b page</h1>
    </div>
</template>

<script>
import { fetchBData } from '@/api'

export default {
    created() {
        fetchBData().then(res= > {
            console.log('B Route request completed')}}}</script>
Copy the code

As you can see from the figure above, a request is made each time the route is entered, and a sentence is printed when the request is complete. Now I turn the speed down, and every time I click route A, I switch to route B. The purpose is to cancel the request of ROUTE A (the black bar on the page is the loading diagram).

Here’s a GIF of the experiment:

As you can see, the request for the routing page of A was successfully cancelled.

I put this Vue DEMO project on Github, if you are interested, you can try it yourself.

5. Form printing

The component required for printing is print-js

Plain form printing

Normal table printing will follow the example provided by the component.

printJS({
    printable: id, // DOM id
    type: 'html'.scanStyles: false,})Copy the code

Element-ui table printing (same for tables in other component libraries)

The Element-UI table, which appears to be one table, is actually made up of two tables.

The head of the table is a table, and the body is a table, which leads to a problem: the body and the head are misaligned when printing.

In addition, when the scroll bar appears in the table, it will also cause dislocation.

The solution

My idea is to combine the two tables into one table. When the print-js component prints, it actually extracts the content in the DOM corresponding to the ID and prints it. So, before passing in the ID, you can first extract the contents of the table header and insert it into the second table, thus merging the two tables, at this time there will be no mismatch problem.

function printHTML(id) {
    const html = document.querySelector(The '#' + id).innerHTML
    // Create a DOM
    const div = document.createElement('div')
    const printDOMID = 'printDOMElement'
    div.id = printDOMID
    div.innerHTML = html

    // Extract the contents of the first table, the table header
    const ths = div.querySelectorAll('.el-table__header-wrapper th')
    const ThsTextArry = []
    for (let i = 0, len = ths.length; i < len; i++) {
        if(ths[i].innerText ! = =' ') ThsTextArry.push(ths[i].innerText)
    }

    // Delete unnecessary headers
    div.querySelector('.hidden-columns').remove()
    // The contents of the first table are extracted and no longer useful
    div.querySelector('.el-table__header-wrapper').remove()

    // Insert the contents of the first table into the second table
    let newHTML = '<tr>'
    for (let i = 0, len = ThsTextArry.length; i < len; i++) {
        newHTML += '<td style="text-align: center; font-weight: bold">' + ThsTextArry[i] + '</td>'
    }

    newHTML += '</tr>'
    div.querySelector('.el-table__body-wrapper table').insertAdjacentHTML('afterbegin', newHTML)
    // Add the new DIV to the page for printing and then delete it
    document.querySelector('body').appendChild(div)
    
    printJS({
        printable: printDOMID,
        type: 'html'.scanStyles: false.style: 'table { border-collapse: collapse }' // Table style
    })

    div.remove()
}
Copy the code

6. Download the binary file

There are two ways to download files in the front end. One is that the background provides a URL and then downloads it with window.open(URL). The other is that the background directly returns the binary content of the file and then the front end transforms it to download it again.

Since the first method is relatively simple, we will not discuss it here. This article mainly explains how to implement the second method.

The second method involves using Blob objects, as described in the MDN documentation:

A Blob object represents an immutable, raw data-like file object. Blobs don’t necessarily represent data in JavaScript’s native format

Specific method of use

axios({
  method: 'post'.url: '/export',
})
.then(res= > {
  // Assume that data is the binary data returned
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download'.'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})
Copy the code

Open the downloaded file and see if the result is correct.

A bunch of gibberish…

There must be something wrong.

It turns out that there’s a problem with the responseType parameter, which is the data type that the server responds to. Since we’re returning binary data in the background, we’ll set it to an ArrayBuffer, and we’ll see if it works.

axios({
  method: 'post'.url: '/export'.responseType: 'arraybuffer',
})
.then(res= > {
  // Assume that data is the binary data returned
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download'.'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})
Copy the code

This time there is no problem, the file can be opened normally, the content is normal, no longer gibberish.

Determine whether to download the file based on the background interface content

The author’s project has a large number of pages that require files to be downloaded, and this requirement is a bit perverted.

Specific requirements are as follows

  1. If the amount of data in the downloaded file meets the requirements, the file can be downloaded normally. (The amount of data to be downloaded varies on each page, so the file cannot be written to the front end.)
  2. If the file is too large, the background returns{code: 199999, MSG: 'File is too large, please reset query item ', data: null}And then the front end will report an error.

First, we know that the interface response data type for downloading files is ArrayBuffer. The data returned, whether binary files or JSON strings, is actually received by the front-end as an ArrayBuffer. Therefore, we need to determine the contents of the arrayBuffer and convert it to a string when we receive the data to determine whether there is code: 199999. If yes, an error message is displayed. If no, the file is normal and can be downloaded. The concrete implementation is as follows:

axios.interceptors.response.use(response= > {
    const res = response.data
    // Check whether the response data type is ArrayBuffer. True indicates the interface to download files. False indicates the normal interface
    if (res instanceof ArrayBuffer) {
        const utf8decoder = new TextDecoder()
        const u8arr = new Uint8Array(res)
        // Convert binary data to a string
        const temp = utf8decoder.decode(u8arr)
        if (temp.includes('{code:199999')) {
            Message({
            	// The string is converted to a JSON object
                message: JSON.parse(temp).msg,
                type: 'error'.duration: 5000,})return Promise.reject()
        }
    }
    // Normal type interface, omitted code...
    return res
}, (error) = > {
    // omit code...
    return Promise.reject(error)
})
Copy the code

7. The console.log statement is automatically ignored

export function rewriteLog() {
    console.log = (function (log) {
        return process.env.NODE_ENV == 'development'? log : function() {}},console.log))
}
Copy the code

By introducing this function in main.js and executing it once, you can achieve the effect of ignoring console.log statements.