通用需求
axios 封装
封装 axios 请求
import axios from 'axios' // 导入 axios
// 第一种: 直接配置在 axios 上(无法配置多个)
axios.defaults.baseURL = 'http://ttapi.research.itcast.cn/'
// 第二种: 使用 create 方法创建一个 axios 实例化对象(能够配置多个)
// 1. 设置基准地址并暴露(request.js)
const request = axios.create({
baseURL: 'http://ttapi.research.itcast.cn/' // 基础路径
})
export default request
// 2. 针对不同组件暴露不同的方法(xxx.js)
import request from './request.js'
export const getUserInfoApi = () => {
return request({
method: 'GET',
url: '/user'
})
}
// 3. 集中暴露(index.js)
import {getUserInfoApi} from './xxx.js'
export default getUserInfoApi = getUserInfoApi
设置 axios 请求/拦截器
import axios from 'axios'
import router from '@/router'
import { Message } from 'element-ui'
import store from '@/store'
import { isTimeOut } from '@/utils/auth.js'
const timeOut = 3600 * 1000 // 秒
// 创建 axios 实例对象
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // 请求基准地址
timeout: 10 * 1000 // 请求超时(秒)
})
// 请求拦截器
request.interceptors.request.use(config => {
// 判断是否存在 token
if (store.getters.token) {
// 判断时间戳是否过期,如果过期则清空登录信息,返回登录页
if (isTimeOut(timeOut)) {
// 通过判断说明已超时,删除登录信息,主动跳转登录也
store.dispatch('user/logout') // 清空登录信息
router.push('/login') // 跳转登录页
return Promise.reject(new Error('登录超时'))
}
config.headers['Authorization'] = `Bearer ${store.getters.token}` // 将 token 添加到请求头中
}
return config // 必须返回请求对象
}, error => {
return Promise.reject(error) // 返回错误对象
})
// 响应拦截器
request.interceptors.response.use(response => {
const { success, message, data } = response.data // 解构数据(axios 默认包了一层data)
// 判断返回状态
if (success) {
return data // 业务成功,返回数据
}
Message.error(message) // 提示错误信息
return Promise.reject(new Error(message)) // 返回错误对象
}, error => {
// 判断错误对象的 token 是否超时
if (error.response && error.response.data && error.response.data.code === 10002) {
store.dispatch('user/logout') // 清空数据
router.push('/login')
} else {
Message.error(error.message) // 提示错误对象信息
}
return Promise.reject(error) // 返回错误对象
})
export default request
页面适配
移动端 rem
适配方案
PostCSS CSS处理器
PostCSS 是一个处理 CSS 的处理工具,本身功能比较单一,它主要负责解析 CSS 代码,再交由插件来进行处理,它的插件体系非常强大,所能进行的操作是多种多样的
PostCSS 一般不单独使用,而是与已有的构建工具进行集成
Vue CLI 默认集成了 PostCSS,并且默认开启了 autoprefixer 插件
Vue CLI 内部使用了 PostCSS,可以通过
.postcssrc
或任何 postcss-load-config 支持的配置源来配置 PostCSS。也可以通过vue.config.js
中的css.loaderOptions.postcss
配置 postcss-loader
默认开启了 autoprefixer。如果要配置目标浏览器,可使用package.json
的 browserslist 字段。
- Autoprefixer 插件可以实现自动添加浏览器相关的声明前缀
- PostCSS Preset Env 插件可以让你使用更新的 CSS 语法特性并实现向下兼容
- postcss-pxtorem 可以实现将 px 转换为 rem
- …
目前 PostCSS 已经有 200 多个功能各异的插件。开发人员也可以根据项目的需要,开发出自己的 PostCSS 插件
1. 利用 lib-flexible 动态设置 rem
基准值
npm i amfe-flexible // 安装
// 自行配置可以如此书写 scss
@function rem($px) {
@return calc($px / 37.5) + rem;
}
width: rem(100);
2. 使用 postcss-pxtorem 将 px
转为 rem
npm install postcss-pxtorem -D // 安装
.postcssrc.js // 配置文件
module.exports = {
plugins: {
'autoprefixer': { // 此配置 Vue/cli 默认配置,可以省略
browsers: ['Android >= 4.0', 'iOS >= 8']
},
'postcss-pxtorem': {
rootValue: 37.5, // 根据根字号配置
propList: ['*'] // 需要转换的属性: 所有属性
}
}
}
3. 如何解决自身设计稿与 UI 组件设置不同
因为 rem 适配方案本质上是根据设计稿像素的十分之一来进行设定,而 UI 组件是固定的 375 ==> 37.5,这样就会导致不匹配的情况
.postcssrc.js // 配置文件
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue({ file}) {
return file.indexOf('vant') !== -1 ? 37.5 : 75 // 根据判断是否为 UI 组件设置大小(vant 37.5; 自身: 75)
}
propList: ['*'] // 需要转换的属性: 所有属性
}
}
}
模拟数据
登录加密
1. 安装插件
npm install crypto-js
2. 创建工具类
import CryptoJS from 'crypto-js'
// 需要和后端一致
const KEY = CryptoJS.enc.Utf8.parse('1234567890123456')
const IV = CryptoJS.enc.Utf8.parse('1234567890123456')
export default {
/**
* 加密
* @param {*} word
* @param {*} keyStr
* @param {*} ivStr
*/
encrypt(word, keyStr, ivStr) {
let key = KEY
let iv = IV
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr)
iv = CryptoJS.enc.Utf8.parse(ivStr)
}
let srcs = CryptoJS.enc.Utf8.parse(word)
var encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
})
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext)
},
/**
* 解密
* @param {*} word
* @param {*} keyStr
* @param {*} ivStr
*/
decrypt(word, keyStr, ivStr) {
let key = KEY
let iv = IV
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr)
iv = CryptoJS.enc.Utf8.parse(ivStr)
}
let base64 = CryptoJS.enc.Base64.parse(word)
let src = CryptoJS.enc.Base64.stringify(base64)
let decrypt = CryptoJS.AES.decrypt(src, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
})
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
return decryptedStr.toString()
}
}
3. 引入并加密
import aes from '@/utils/aes'
let encryptionPassword = aes.encrypt(password)
Vue 脚手架代理
方案一: 简易配置
// vue.config.js 配置
devServer: {
proxy:"http://localhost:5000"
}
方案二: 具体配置
// vue.config.js 配置
module.exports = {
devServer: {
proxy: {
'/api1': {// 匹配所有以 '/api1'开头的请求路径
target: 'http://localhost:5000',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api1': ''}
},
'/api2': {// 匹配所有以 '/api2'开头的请求路径
target: 'http://localhost:5001',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api2': ''}
}
}
}
}
/*
changeOrigin设置为true时,服务器收到的请求头中的host为: localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为: localhost:8080
changeOrigin默认值为true
*/
后台管理系统需求
登录
import { getToken, setToken, removeToken, setTimestamp } from '@/utils/auth.js'
import { loginAPI, getUserInfoAPI, getUserDetailByIdAPI } from '@/api/index.js'
import { resetRouter } from '@/router'
const state = {
token: getToken(), // 用户 token
userInfo: {} // 用户基本信息(不能设置为null,映射时访问null中的属性将报错)
}
const mutations = {
// * 设置 token 并持久化
setToken(state, token) {
state.token = token // 保存状态
setToken(token) // 数据持久化
},
// * 删除 token 并重置持久化
removeToken() {
state.token = null // 重置状态
removeToken() // 清空 token 数据持久化
},
// * 设置用户信息
setUserInfo(state, data) {
state.userInfo = data // 保存数据
},
// * 设置用户信息
removeUserInfo() {
state.userInfo = {} // 清空数据
}
}
const actions = {
// * 异步登录
async login(context, data) {
const result = await loginAPI(data) // 调用后端接口请求数据
context.commit('setToken', result) // 保存token
// 此时已经获取到了 token ,加上时间戳,主动介入
setTimestamp() // 设置时间戳
},
// * 异步获取用户信息
async getUserInfo(context) {
const result = await getUserInfoAPI() // 获取用户基本信息
const baseInfo = await getUserDetailByIdAPI(result.userId) // 用户员工基本信息
const baseResult = { ...result, ...baseInfo } // 合并数据
context.commit('setUserInfo', baseResult) // 设置用户基本信息
return baseResult // 返回合并数据(为权限控制做准备)
},
// * 退出登录
logout(context) {
context.commit('removeToken') // 删除 token
context.commit('removeUserInfo') // 删除用户信息
resetRouter() // 重置路由表(去除动态路由权限)
context.dispatch('permission/setRoutes', [], { root: true }) // 情况权限路由数据
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
权限管理
权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源,而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发
触发有页面加载触发和页面上的按钮点击触发,总的来说所有的请求发起都触发自前端路由或视图,所以我们可以从这两方面入手,对触发权限的源头进行控制
最终实现
- 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转4xx提示页
- 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
- 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截
实现方案
- 接口权限: 接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录,具体实现在于后端
- 按钮权限:
- 方案一: 通过 v-if 来判断控制是否能够点击
- 方案二: 通过自定义指令通过权限鉴权,通过后端返回的权限数据判断对应的按钮是否可以点击
- 菜单权限:
- 方案一: 菜单与路由分离,菜单由后端返回,路由跳转时判断跳转信息是否在对应的权限菜单中来控制菜单访问权限
- 缺点1: 菜单需要与路由做–对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
- 缺点2: 全局路由守卫里,每次路由跳转都要做判断
- 方案二: 菜单和路由都由后端返回,前端统一定义路由组件,在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件
- 缺点1: 全局路由守卫里,每次路由跳转都要做判断
- 缺点2: 前后端的配合要求更高
- 方案一: 菜单与路由分离,菜单由后端返回,路由跳转时判断跳转信息是否在对应的权限菜单中来控制菜单访问权限
- 路由权限:
- 方案一: 初始化就挂载所有的路由,通过标识数据在每次路由跳转时判断用户是否有权限来访问此页面
- 缺点1: 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。
- 缺点2: 全局路由守卫里,每次路由跳转都要做权限判断。
- 缺点3: 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
- 缺点4: 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
- 方案二: 初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制,登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由
- 缺点1: 全局路由守卫里,每次路由跳转都要做判断
- 缺点2: 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
- 缺点3: 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
- 方案一: 初始化就挂载所有的路由,通过标识数据在每次路由跳转时判断用户是否有权限来访问此页面
路由权限设置
import router from '@/router' // 引入 router 实例
import store from '@/store' // 引入 vuex 实例
import NProgress from 'nprogress' // 引入一份进度条插件
import 'nprogress/nprogress.css' // 引入进度条样式
// 白名单路径
const whilePath = ['/login', '/404']
NProgress.configure({ showSpinner: false }) // NProgress 关闭顶部加载状态
// 路由前置守卫
router.beforeEach(async(to, from, next) => {
NProgress.start() // 开启进度条
// 判断有无 token
if (store.getters.token) {
// 有 token 做免登陆功能
if (to.path === '/login') {
next('/') // 免登录到主页
} else {
// 请求用户数据,但要先判断用户数据是否存在
if (!store.getters.userId) {
// 因为获取用户数据是异步任务,所有需要使用 await 暂停函数运行
const { roles } = await store.dispatch('user/getUserInfo') // 用户数据不存在获取用户数据
// 此时用户数据已经获取,获取路由权限(actions 是异步操作)
console.log(roles.menus)
const asyncRoute = await store.dispatch('permission/filterRoutes', roles.menus)
// 动态添加路由规则
router.addRoutes([...asyncRoute, { path: '*', redirect: '/404', hidden: true }])
// 使用动态添加路由规则后需要手动 next(地址) 不然会报错
next(to.path)
} else {
next() // 其余直接放行
}
}
} else {
// 判断路径是否在白名单中
if (whilePath.includes(to.path)) {
next() // 在白名单,直接放行
} else {
next('/login') // 不在白名单,强制调整登录页
}
}
NProgress.done() // 强制停止进度条,防止手动切换地址时不经过后置守卫而导致的错误
})
// 路由后置守卫
router.afterEach((to, from) => {
NProgress.done() // 关闭进度条
})