Решения некоторых распространенных проблем в проектах Vue

внешний интерфейс Vue.js

Эта статья разрешает эксклюзивное использование общедоступной учетной записи сообщества разработчиков Nuggets, включая, помимо прочего, редактирование, пометку оригинальности и другие права.

Есть некоторые проблемы, которые не ограничиваются Vue, но также применимы к другим типам проектов SPA.

1. Контроль доступа к странице и проверка входа

Контроль разрешений страницы

Что означает контроль прав доступа к страницам?

То есть у веб-сайта разные роли, такие как администраторы и обычные пользователи, и страницы, к которым могут получить доступ разные роли, разные. Если страница имеет несанкционированный доступ к роли, то необходимо ввести ограничения.

Один из способов — сделать управление динамическим добавлением маршрутов и меню, а страницы, к которым нет доступа, не добавляются в таблицу маршрутизации — это один из способов. Дополнительные сведения см. в разделе «Динамическое меню» в следующем разделе.

Другой способ — все страницы находятся в таблице маршрутизации, просто чтобы судить о правах доступа при доступе. Если у вас есть разрешение, разрешите доступ, если у вас нет разрешения, запретите его и перейдите на страницу 404.

идеи

в каждом маршрутеmetaВ атрибуте добавьте роль, которая может получить доступ к маршруту доrolesвнутри. После того, как пользователь входит в систему каждый раз, роль пользователя возвращается. Потом при заходе на страницу проставить маршрутmetaАтрибут сравнивается с ролью пользователя, если роль пользователя находится в маршрутизации.rolesЕсли он есть, к нему можно получить доступ, а если его нет, доступ запрещен.

пример кода

информация о маршрутизации

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')
    },
]

управление страницей

// 假设角色有两种:admin 和 user
// 这里是从后台获取的用户角色
const role = 'user'
// 在进入一个页面前会触发 router.beforeEach 事件
router.beforeEach((to, from, next) => {
    if (to.meta.roles.includes(role)) {
        next()
    } else {
        next({path: '/404'})
    }
})

Подтверждение входа

Как правило, после однократного входа на веб-сайт другие страницы веб-сайта могут быть доступны напрямую без повторного входа в систему. мы можем пройтиtokenилиcookieДля этого используется следующий код, показывающий, как использоватьtokenКонтроль авторизации при входе.

router.beforeEach((to, from, next) => {
    // 如果有token 说明该用户已登陆
    if (localStorage.getItem('token')) {
        // 在已登陆的情况下访问登陆页会重定向到首页
        if (to.path === '/login') {
            next({path: '/'})
        } else {
            next({path: to.path || '/'})
        }
    } else {
        // 没有登陆则访问任何页面都重定向到登陆页
        if (to.path === '/login') {
            next()
        } else {
            next(`/login?redirect=${to.path}`)
        }
    }
})

2. Динамическое меню

По оценкам, при написании системы управления фоном многие сталкивались с таким требованием: динамически добавлять маршруты и меню в соответствии с фоновыми данными. Зачем это делать? Поскольку разные пользователи имеют разные разрешения, страницы, к которым они могут получить доступ, различаются.

Динамически добавлять маршруты

Использование vue-routeraddRoutesметод для динамического добавления маршрутов.

Давайте посмотрим на официальное введение:

router.addRoutes

router.addRoutes(routes: Array<RouteConfig>)

Динамически добавляйте дополнительные правила маршрутизации. Параметр должен бытьroutesМассив опциональных требований.

Например:

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

Приведенный выше код имеет тот же эффект, что и следующий код

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

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

В процессе динамического добавления маршрутов, если есть страница 404, ее необходимо добавить в конец, иначе страница будет перенаправлена ​​на страницу 404 после добавления страницы при входе в систему.

Таким образом, это правило должно быть добавлено последним.

{path: '*', redirect: '/404'}

Динамически генерировать меню

Предположим, что данные, возвращаемые из фона, выглядят так:

// 左侧菜单栏数据
menuItems: [
    {
        name: 'home', // 要跳转的路由名称 不是路径
        size: 18, // icon大小
        type: 'md-home', // icon类型
        text: '主页' // 文本内容
    },
    {
        text: '二级菜单',
        type: 'ios-paper',
        children: [
            {
                type: 'ios-grid',
                name: 't1',
                text: '表格'
            },
            {
                text: '三级菜单',
                type: 'ios-paper',
                children: [
                    {
                        type: 'ios-notifications-outline',
                        name: 'msg',
                        text: '查看消息'
                    },
                ]
            }
        ]
    }
]

Давайте посмотрим, как превратить его в строку меню, я использовал это здесьiviewкомпонентов, не изобретая велосипед.

<!-- 菜单栏 -->
<Menu ref="asideMenu" theme="dark" width="100%" @on-select="gotoPage" 
accordion :open-names="openMenus" :active-name="currentPage" @on-open-change="menuChange">
    <!-- 动态菜单 -->
    <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>

Код не нужно слишком внимательно читать, достаточно понять принцип, на самом деле он через три разаv-forПодмассив непрерывно зацикливается для создания трехуровневого меню.

Однако это динамическое меню имеет недостатки, то есть поддерживает только трехуровневые меню. Лучший подход — инкапсулировать процесс создания меню в компоненты и вызывать их рекурсивно, чтобы можно было поддерживать бесконечные меню. В салатном меню нужно определить, есть ли подменю, и если есть, то вызвать компонент рекурсивно.

динамическая маршрутизацияПотому что это было сказано вышеaddRoutesДавайте посмотрим, как это сделать сейчас.

Сначала перечислите все маршруты страниц проекта, а затем используйте данные, возвращенные из фона, для динамического сопоставления, добавьте маршруты, которые могут совпадать, и не добавляйте те, которые не могут совпадать. Наконец, используйте эти недавно сгенерированные данные маршрутизации с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')
    }
}

// 传入后台数据 生成路由表
menusToRoutes(menusData)

// 将菜单信息转成对应的路由信息 动态添加
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')
    })

    // 最后添加404页面 否则会在登陆成功后跳到404页面
    result.push(
        {path: '*', 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)
        })
    }
}

Код реализации динамического меню размещен вgithub, соответственно размещенные в этом проектеsrc/components/Index.vue,src/permission.jsиsrc/utils/index.jsв файле.

3. Обновить вперед, но не назад

Требование первое:

На странице со списком при первом входе запрашивать данные.

Щелкните элемент списка, чтобы перейти на страницу сведений, а затем вернуться на страницу списка со страницы сведений без обновления.

То есть, когда вы входите на страницу списка с других страниц, вам нужно обновиться, чтобы получить данные, и не обновляться при возврате на страницу списка со страницы сведений.

решение

существуетApp.vueнастраивать:

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

Предположим, что страница спискаlist.vue, страница сведенийdetail.vue, оба из которых являются подкомпонентами.

мы вkeep-aliveДобавьте имя страницы списка, кэшируйте страницу списка.

затем на странице спискаcreatedДобавьте запрос ajax в функцию, чтобы данные запрашивались только при первом входе на страницу списка.Когда вы переходите со страницы списка на страницу сведений, а затем возвращаетесь со страницы сведений, страница списка не обновляться. Это должно решить проблему.

Требование второе:

На основании требования 1 добавляется дополнительное требование: соответствующий элемент списка может быть удален на странице сведений, а данные необходимо обновлять и повторно получать при возврате на страницу списка.

Мы можем сделать это в файле конфигурации маршрутизацииdetail.vueдобавить однуmetaАтрибуты.

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

этоmetaатрибут, который можно передать на странице сведенийthis.$route.meta.isRefreshчитать и ставить.

После установки этого свойства вам необходимоApp.vueУстановить часы в файле$routeАтрибуты.

    watch: {
       $route(to, from) {
           const fname = from.name
           const tname = to.name
           if (from.meta.isRefresh || (fname != 'detail' && tname == 'list')) {
               from.meta.isRefresh = false
   				// 在这里重新请求数据
           }
       }
   },

Таким образом, вам не нужна страница спискаcreatedВ функции для запроса данных используется Ajax, и он размещен унифицированным образом.App.vueиметь дело с.

Есть два условия для запуска данных запроса:

  1. При входе в список с других страниц (кроме страницы сведений) необходимо запрашивать данные.
  2. При возврате со страницы сведений на страницу списка, если страница сведенийmetaв атрибутеisRefreshзаtrue, также необходимо повторно запросить данные.

Когда мы удаляем соответствующий элемент списка на странице сведений, мы можем удалить страницу сведенийmetaв атрибутеisRefreshустановить какtrue. Затем вернитесь на страницу списка, и страница снова обновится.

Решение второе

Для второго требования на самом деле есть более лаконичное решение, заключающееся в использовании router-view.keyАтрибуты.

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

Прежде всего, keep-alive делает все страницы кэшированными.Если вы не хотите кэшировать страницу маршрутизации и хотите перезагрузить ее, вы можете передать случайную строку при переходе, чтобы ее можно было перезагрузить. Например, если вы входите на страницу сведений со страницы списка, а затем удаляете параметр на странице списка со страницы сведений, вам необходимо обновить страницу списка, когда вы вернетесь со страницы сведений. Мы можем перейти следующим образом:

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

Такая схема относительно проще.

4. Отображение и закрытие загрузки по нескольким запросам

В общем, объединение перехватчика axios в vue управляет загрузкой отображения и закрытием, например:

существует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>

Также установите перехватчик axios.

 // 添加请求拦截器
 this.$axios.interceptors.request.use(config => {
     this.isShowLoading = true
     return config
 }, error => {
     this.isShowLoading = false
     return Promise.reject(error)
 })

 // 添加响应拦截器
 this.$axios.interceptors.response.use(response => {
     this.isShowLoading = false
     return response
 }, error => {
     this.isShowLoading = false
     return Promise.reject(error)
 })

Функция этого перехватчика — включить загрузку перед запросом и выключить, когда запрос заканчивается или возникает ошибка.

Это отлично работает, если есть только один запрос за раз. Но если есть несколько одновременных запросов одновременно, будут проблемы.

Пример:

Если два запроса сделаны одновременно, перед запросом перехватчикthis.isShowLoading = trueВключите загрузку.

Сейчас запрос закончился.this.isShowLoading = falseПерехватчик отключает загрузку, но очередной запрос почему-то не заканчивается.

Следствием этого является то, что запрос страницы не был выполнен, но загрузка закрыта.Пользователь будет думать, что загрузка страницы завершена, и в результате страница не может нормально работать, что приводит к плохому взаимодействию с пользователем.

решение

добавить однуloadingCountПеременная для подсчета количества запросов.

loadingCount: 0

Добавьте еще два метода для исправленияloadingCountВыполнение операций увеличения и уменьшения.

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

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

Перехватчик теперь выглядит так:

        // 添加请求拦截器
        this.$axios.interceptors.request.use(config => {
            this.addLoading()
            return config
        }, error => {
            this.isShowLoading = false
            this.loadingCount = 0
            this.$Message.error('网络异常,请稍后再试')
            return Promise.reject(error)
        })

        // 添加响应拦截器
        this.$axios.interceptors.response.use(response => {
            this.isCloseLoading()
            return response
        }, error => {
            this.isShowLoading = false
            this.loadingCount = 0
            this.$Message.error('网络异常,请稍后再试')
            return Promise.reject(error)
        })

Функция этого перехватчика:

Всякий раз, когда делается запрос, включайте загрузку, и в то же времяloadingCountплюс 1.

Всякий раз, когда запрос заканчивается,loadingCountвычтите 1 и оценитеloadingCountБудь он 0, если он 0, отключите загрузку.

Это может решить проблему, связанную с тем, что запрос из нескольких запросов заканчивается раньше, что приводит к закрытию загрузки.

При переключении маршрутов отменить предыдущий запрос

С помощью axios можно отменить незавершенные запросы при переключении маршрутов.

Для конкретного анализа, пожалуйста, смотрите эту статьюМаршрут переключения Axios для отмены указанного запроса и отмены схемы сосуществования дублирующих запросов. Ниже показан эффект реализации:

Я создал новый проект Vue и установилa bДва маршрута, каждый раз при вводе маршрута инициируется одинgetпросить:

// 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 路由请求完成')
        })
    }
}
</script>
// 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 路由请求完成')
        })
    }
}
</script>

Как видно из рисунка выше, запрос инициируется всякий раз, когда вводится маршрут, а после завершения запроса печатается предложение. Теперь я уменьшаю скорость интернета, и каждый раз, когда я выбираю маршрут a, я сразу же переключаюсь на маршрут b. Цель — отменить запрос страницы маршрутизации (черная полоса на странице — диаграмма загрузки).

Следующая анимация является экспериментальным эффектом:

Как видите, запрос страницы маршрута был успешно отменен.

Я поставил этот проект Vue DEMO наgithubДа, вы можете попробовать сами, если вам интересно.

5. Печать форм

Компоненты, необходимые для печати,print-js

нормальная форма печати

Печать общей формы может быть непосредственно имитирована примером, предоставленным компонентом.

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

печать формы element-ui (то же самое для форм в других библиотеках компонентов)

Таблица element-ui, которая на первый взгляд выглядит как таблица, на самом деле состоит из двух таблиц.

Заголовок — это таблица, а тело таблицы — это таблица, что приводит к проблеме: при печати тело таблицы и заголовок таблицы смещаются.

Кроме того, когда в таблице появляется полоса прокрутки, это также вызывает дислокацию.

решение

Моя идея состоит в том, чтобы объединить две таблицы в одну таблицу,print-jsКогда компонент печатается, он фактически извлекает и печатает содержимое в DOM, соответствующее идентификатору. Таким образом, перед передачей идентификатора вы можете извлечь содержимое таблицы, в которой находится заголовок, и вставить его во вторую таблицу, тем самым объединив две таблицы.В это время не будет проблемы с несовпадением при печати.

function printHTML(id) {
    const html = document.querySelector('#' + id).innerHTML
    // 新建一个 DOM
    const div = document.createElement('div')
    const printDOMID = 'printDOMElement'
    div.id = printDOMID
    div.innerHTML = html

    // 提取第一个表格的内容 即表头
    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)
    }

    // 删除多余的表头
    div.querySelector('.hidden-columns').remove()
    // 第一个表格的内容提取出来后已经没用了 删掉
    div.querySelector('.el-table__header-wrapper').remove()

    // 将第一个表格的内容插入到第二个表格
    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)
    // 将新的 DIV 添加到页面 打印后再删掉
    document.querySelector('body').appendChild(div)
    
    printJS({
        printable: printDOMID,
        type: 'html',
        scanStyles: false,
        style: 'table { border-collapse: collapse }' // 表格样式
    })

    div.remove()
}

6. Скачайте бинарники

Обычно есть два способа загрузки файлов во внешнем интерфейсе: один — указать URL-адрес в фоновом режиме, а затем использоватьwindow.open(URL)Скачать, другой - напрямую вернуть двоичное содержимое файла в фоновом режиме, а затем преобразовать его во внешнем интерфейсе перед загрузкой.

Поскольку первый метод относительно прост, он не будет здесь обсуждаться. В этой статье в основном объясняется, как реализовать второй метод.

Второй метод необходимо использоватьBlobОбъекты, как описано в документации mdn:

Объект Blob представляет собой неизменяемый объект, подобный файлу необработанных данных. Большие двоичные объекты не обязательно представляют данные в собственном формате JavaScript.

Как это использовать

axios({
  method: 'post',
  url: '/export',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  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)
})

Откройте загруженный файл и проверьте правильность результатов.

在这里插入图片描述

куча бреда...

Должно быть что-то не так.

Наконец обнаружил, что параметрresponseTypeПроблема,responseTypeОн представляет собой тип данных ответа сервера. Поскольку фон возвращает двоичные данные, мы устанавливаем его вarraybuffer, Далее проверьте правильность результата.

axios({
  method: 'post',
  url: '/export',
  responseType: 'arraybuffer',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  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)
})

На этот раз проблем нет, файл можно нормально открыть, и содержимое нормальное, больше не искажено.

Решите, загружать ли файл в соответствии с содержимым фонового интерфейса

В авторском проекте большое количество страниц, на которых есть необходимость скачивания файлов, и эта потребность немного ненормальна.

Конкретные требования заключаются в следующем.

  1. Если объем данных загружаемого файла соответствует требованиям, загрузите его в обычном режиме (предел объема загружаемых данных различен для каждой страницы, поэтому он не может быть записан насмерть на внешнем интерфейсе).
  2. Если файл слишком большой, фон возвращается{ code: 199999, msg: '文件过大,请重新设置查询项', data: null }, а затем внешний интерфейс сообщит об ошибке.

Давайте сначала проанализируем его.Прежде всего, согласно вышеизложенному, мы все знаем, что тип данных ответа интерфейса загруженного файлаarraybuffer. Независимо от того, являются ли возвращенные данные двоичным файлом или строкой JSON, интерфейс получает на самом делеarraybuffer. Так что мы должныarraybufferДелайте выводы о содержании данных, преобразовывайте их в строку при получении данных и оценивайте, есть лиcode: 199999. Если есть, то будет выдано сообщение об ошибке, если нет, то это обычный файл и его можно скачать. Конкретная реализация выглядит следующим образом:

axios.interceptors.response.use(response => {
    const res = response.data
    // 判断响应数据类型是否 ArrayBuffer,true 则是下载文件接口,false 则是正常接口
    if (res instanceof ArrayBuffer) {
        const utf8decoder = new TextDecoder()
        const u8arr = new Uint8Array(res)
        // 将二进制数据转为字符串
        const temp = utf8decoder.decode(u8arr)
        if (temp.includes('{code:199999')) {
            Message({
            	// 字符串转为 JSON 对象
                message: JSON.parse(temp).msg,
                type: 'error',
                duration: 5000,
            })

            return Promise.reject()
        }
    }
    // 正常类型接口,省略代码...
    return res
}, (error) => {
    // 省略代码...
    return Promise.reject(error)
})

7. Автоматически игнорировать операторы console.log

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

существуетmain.jsВнедрение этой функции и однократное ее выполнение может привести к игнорированию инструкции console.log.