Эта статья разрешает эксклюзивное использование общедоступной учетной записи сообщества разработчиков 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
иметь дело с.
Есть два условия для запуска данных запроса:
- При входе в список с других страниц (кроме страницы сведений) необходимо запрашивать данные.
- При возврате со страницы сведений на страницу списка, если страница сведений
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)
})
На этот раз проблем нет, файл можно нормально открыть, и содержимое нормальное, больше не искажено.
Решите, загружать ли файл в соответствии с содержимым фонового интерфейса
В авторском проекте большое количество страниц, на которых есть необходимость скачивания файлов, и эта потребность немного ненормальна.
Конкретные требования заключаются в следующем.
- Если объем данных загружаемого файла соответствует требованиям, загрузите его в обычном режиме (предел объема загружаемых данных различен для каждой страницы, поэтому он не может быть записан насмерть на внешнем интерфейсе).
- Если файл слишком большой, фон возвращается
{ 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.