webapck配置文件 vue.config.js
// commonjs nodejs
const port = 7070
const title = 'vue的最佳实践'
module.exports = {
// 程序的上下文
publicPath: 'best-practice',
devServer: {
port
},
configureWebpack: {
name: title
}
}
index.html
<%= htmlWebpackPlugin.options.title %>
vue inspect --rules 查看webapck中的rules规则模块
vue inspect --rule vue 查看单个模块中的规则
vue inspect --plugins 可以查看配置中的所有插件
vue inspect --plugin vue-loader 单个插件的配置项
图标的使用
项目要使用icon,传统方案图标字体(字体文件+样式文件),不便维护,
svg方案采用svg-sprite-loader 自动加载打包,方便维护
使用icon前先安装依赖:svg-sprite-loader
npm i svg-sprite-loader -D
下载图标,存入src/icons/svg中 修改规则和新增规则,vue.confifig.js
// resolve定义一个绝对路径获取函数
const path = require('path')
function resolve (dir) {
return path.join(__dirname, dir)
}
// commonjs nodejs
const port = 7070
const title = 'vue的最佳实践'
module.exports = {
// 程序的上下文
//publicPath: 'best-practice',
devServer: {
port
},
configureWebpack: {
name: title
},
// 链式webapck的配置 对config进行链式操作
chainWebpack (config) {
// module中是所有的规则 获取svg
// svg规则配置一下,排除icons目录
config.module.rule('svg')
.exclude.add(resolve('src/icons'))
.end()
// 如果遇到icons目录 就退回上一级
// 新增icons规则 设置svg-sprite-loader
config.module
.rule('icons')// 定义一个规则
.test(/\.svg$/) // 什么文件的后缀名
.include.add(resolve('src/icons')) // 规则对应的测试的地址
.end()
.use('svg-sprite-loader') // 应用一下svg-sprite-loader
.loader('svg-sprite-loader') // 找到它的loader 给他做进一步的配置
.options({
symbolId: 'icon-[name]'
}) // 选项配置 symbolId表示将来以什么方式使用图标
.end()
}
}
封装成组件 components/SvgIcon.vue
在svg同文件夹下创建一个index.js
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon.vue'
// 全局引入SvgIcon组件
Vue.component('svg-icon', SvgIcon)
// require.context webapck中的方法
// 参数1:指定目录 以它为上下文 去自动加载
//参数2: false, 表示svg文件夹下不会再嵌套别的文件夹了
//参数3;加个正则,让它去匹配
const req = require.context('./svg', false, /\.svg$/)
// ['qq.svg', 'wx.svg']
req.keys().map(req)
main.js中全局引入index.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './icons'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
直接全局使用
项目开发阶段
动态路由
路由定义
路由分为两种 constantRouters 和 asyncRouters
**constantRoutes:代表那些不需要动态判断权限的路由,如登录、看板、404等通用页面
asyncRoutes:代表那些需要判断权限并通过addRoutes动态添加的页面**
权限控制+动态路由 定义路由,router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/layout' // 布局页
Vue.use(Router)
// 通用页
export const constRoutes = [
{
path: '/login',
component: () => import('@/views/Login'),
hidden: true// 导航菜单忽略该项
}, {
path: '/',
component: Layout, // 应用布局
redirect: '/home',
children: [
{
path: 'home',
component: () => import(/* webpackChunkName:"home" */'@/views/Home.vue'),
name: 'home',
meta: {
title: 'Home', // 导航菜单项标题
icon: 'qq' // 导航菜单项图标
}
}
]
}
]
export const asyncRoutes = [
{
path: '/about',
component: Layout,
redirect: '/about/index',
children: [
{
path: 'index',
component: () => import(/* webapckChunkName:'home' */ '@/views/About.vue'),
name: 'about',
meta: {
title: 'About',
icon: 'qq',
roles: ['admin', 'editor']
}
}
]
}
]
export default new Router({
mode: 'history',
base: process.env.BABEL_URL,
routes: constRoutes
})
创建布局页面,layout/index.vue
创建用户登录页面,views/Login.vue
用户登录
添加动态路由
asyncRoutes中定义的路由需要在用户登录后获取其角色并过滤出有权访问的部分,最后动态添加至router
添加动态路由定义,router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/layout' // 布局页
Vue.use(Router)
// 通用页
export const constRoutes = [
{
path: '/login',
component: () => import('@/views/Login'),
hidden: true// 导航菜单忽略该项
}, {
path: '/',
component: Layout, // 应用布局
redirect: '/home',
children: [
{
path: 'home',
component: () => import(/* webpackChunkName:"home" */'@/views/Home.vue'),
name: 'home',
meta: {
title: 'Home', // 导航菜单项标题
icon: 'qq' // 导航菜单项图标
}
}
]
}
]
export const asyncRoutes = [
{
path: '/about',
component: Layout,
redirect: '/about/index',
children: [
{
path: 'index',
component: () => import(/* webapckChunkName:'home' */ '@/views/About.vue'),
name: 'about',
meta: {
title: 'About',
icon: 'qq',
roles: ['admin', 'editor']
}
}
]
}
]
export default new Router({
mode: 'history',
base: process.env.BABEL_URL,
routes: constRoutes
})
路由守卫,创建./src/permission.js,并在main.js中引入
// 路由全局守卫
// 权限控制逻辑在这里
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import { getToken } from '@/utils/auth' // 从cookie获取令牌
const whiteList = ['/login']// 无需令牌白名单
router.beforeEach(async (to, from, next) => {
// 获取令牌判断用户是否登录
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 若已登录重定向至首页
next({ path: '/' })
} else {
// 若用户角色已附加则说明动态路由已添加
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next() // 继续即可
} else {
try {
// 先请求获取用户信息
const { roles } = await store.dispatch('user/getInfo')
// 根据当前用户角色动态生成路由
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// 添加这些路由至路由器
router.addRoutes(accessRoutes)
// 继续路由切换,确保addRoutes完成
next({ ...to, replace: true })
} catch (error) {
// 出错需重置令牌并重新登陆(令牌过期、网络错误等原因)
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
}
}
}
} else {
// 用户无令牌
if (whiteList.indexOf(to.path) !== -1) {
// 白名单路由路过
next()
} else {
// 重定向至登陆页
next(`/login?redirect=${to.path}`)
}
}
})
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/router'
import store from './store'
import './icons'
// 路由守卫
import './permission'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
添加缺少组件:
- element-ui: vue add element
- js-cookie: npm i js-cookie -S
3. 添加utils/auth.js
import Cookies from 'js-cookie'
const Token = 'token'
export function getToken () {
return Cookies.get(Token)
}
export function setToken (token) {
return Cookies.set(Token, token)
}
export function removeToken () {
return Cookies.remove(Token)
}
vuex相关模块实现,创建store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import permission from './modules/permission'
import user from './modules/user'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: { permission, user },
// 全局定义getters便于访问
getters: {
roles: state => state.user.roles
}
})
export default store
user模块:用户数据、用户登录等,store/modules/user.js
import { getToken, setToken, removeToken } from '@/utils/auth'
const state = {
token: getToken(),
roles: []
// 其他用户信息
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}
const actions = {
// user login
login ({ commit }, userInfo) {
const { username } = userInfo
return new Promise((resolve, reject) => {
setTimeout(() => {
if (username === 'admin' || username === 'jerry') {
// 保存状态
commit('SET_TOKEN', username)
// 写入cookie
setToken(username)
// 信息返回
resolve()
} else {
reject('用户名、密码错误')
}
}, 1000)
})
},
// get user info
getInfo ({ commit, state }) {
return new Promise((resolve) => {
setTimeout(() => {
const roles = state.token === 'admin' ? ['admin'] : ['editor']
commit('SET_ROLES', roles)
}, 1000)
})
},
removeToken ({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
permission模块:路由配置信息、路由生成逻辑, store/modules/permission.js
import { asyncRoutes, constRoutes } from '@/router'
/**
* 根据路由meta.role确定是否当前用户拥有访问权限
* @roles 用户拥有角色
* @route 待判定路由
*/
function hasPermission (roles, route) {
// 如果当前路由有roles字段需判断用户访问权限
if (route.meta && route.meta.roles) {
// 若用户拥有的角色有被包含在待判定路由角色表中的则拥有访问权
return roles.some(role => route.meta.roles.includes(role))
} else {
// 没有设置roles则无需判定即可访问
return true
}
}
/**
* 递归过滤AsyncRoutes路由表
* @routes 待过滤路由表 首次传入的就是AsyncRoutes
* @roles 用户拥有角色
*/
export function filtersAsyncRoutes (routes, roles) {
const res = []
routes.forEach(route => {
// 复制一份
const tmp = { ...route }
// 如果用户有访问权 则加入结果路由表
if (hasPermission(roles, tmp)) {
// 如果存在子路由则递归过滤
if (tmp.children) {
tmp.children = filtersAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [], // 完整路由表
addRoutes: [] // 用户可访问路由表
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constRoutes.concat(routes)
}
}
const actions = {
generateRoutes ({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
// 用户是管理员则拥有完整的访问权限
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
// 否则需要根据角色做过滤处理
accessedRoutes = filtersAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
导航菜单⽣成
导航菜单是根据路由信息并结合权限判断⽽动态⽣成的。它需要⽀持路由的多级嵌套,所以这⾥要⽤到 递归组件。
菜单结构是典型递归组件,
数据准备,添加getter⽅法,store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import permission from './modules/permission'
import user from './modules/user'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: { permission, user },
// 全局定义getters便于访问
getters: {
roles: state => state.user.roles,
permission_routes: state => state.permission.routes
}
})
export default store
SideMenu/index.vue
-
按钮权限
import store from '@/store'
const permission = {
// 指令已经添加到元素上 el-指令相关的Dom元素,binding - 对象
// {}
inserted (el, binding) {
// 获取指令的值:按钮要求的角色数组
// 结构value 并取别名pRoles
const { value: pRoles } = binding
// 获取用户角色
const roles = store.getters && store.getters.roles
if (pRoles && pRoles instanceof Array && pRoles.length > 0) {
// 判断用户角色中是否有按钮要求的角色
const hasPermission = roles.some(role => {
return pRoles.includes(role)
})
// 如果没有权限则删除当前dom
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`需要指定按钮要求角色数据,如v-permission="['admin','editor']"`)
}
}
}
export default permission
mian.js中引入
// 路由守卫
import './permission'
面包屑组件
/components/Breadcrumb
{{item.meta.title}}
{{item.meta.title}}
使用 layout/index
数据交互
数据交互流程
api service => request => local mock /easy-mock/server api
*主要问题分析:
1、有时需要对请求头、响应进行统一预处理
2、请求不同数据源时url会变化,需要根据环境自动修改url
3、可能出现跨域问题*
封装request
*解决前两个问题需要统一封装请求代码
安装axios: npm i axios -S
创建@/utils/request.js*
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url基础地址,解决不同数据源url变化问题
// withCredentials:true,//跨域时若要发送cookie需设置该选项
timeout: 5000// 超时
})
// 请求拦截
service.interceptors.request.use(
config => {
// do something
if (store.getters.token) {
// 设置令牌请求头
config.headers["Authorization"] = 'Bearer' + getToken()
}
return config
},
error => {
// 请求错误预处理
// console.log(error) //for debug
return Promise.reject(error)
}
)
// 响应拦截
service.interceptors.response.use(
// 通过自定义code判定响应状态,也可以通过HTTP状态码判定
response => {
// 仅返回数据部分
const res = response.data
// code不为1则判定为一个错误
if (res.code !== 1) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 10
})
// 假设: 10008-非法令牌;10012-其他客户端已登录;100140-令牌过期
if (res.code === 10008 || res.code === 10012 || res.code === 10014) {
// 重新登录
MessageBox.confirm(
'登录状态异常,请重新登录',
'确认登录信息',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
store.dispatch("user/resetToken").then(()=>{
location.reload()
})
})
}
return Promise.reject(new Error(res.message||"Error"))
} else {
return res
}
},
error => {
//console.log("err"+error)//for debug
Message({
message: error.message,
type:"error",
duration: 5*1000
})
return Promise.reject(error)
}
)
export default service
设置VUE_APP_BASE_API环境变量,创建.env.development⽂件
base api, 前缀VUE_APP是vue-cli要求的
VUE_APP_BASE_API = '/dev-api'
添加token的getter⽅法
import Vue from 'vue'
import Vuex from 'vuex'
import permission from './modules/permission'
import user from './modules/user'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: { permission, user },
// 全局定义getters便于访问
getters: {
roles: state => state.user.roles,
token: state => state.user.token,
permission_routes: state => state.permission.routes
}
})
export default store
测试代码
创建@/api/user.js
import request from "@/utils/request"
export function login(data){
return request({
url:'/user/login',
method:"post",
data
})
}
export function getInfo(){
return request({
url:'/user/info',
method:'get'
})
}
要测试需要接⼝
数据mock
*数据模拟常见的两种方式,本地mock和线上easy-mock
本地mock修改vue.confifig.js,给devServer添加相关代码:
post请求需额外安装依赖: npm i body-parser -D*
const bodyParser = require('body-parser')
module.exports = {
// 程序的上下文
// publicPath: 'best-practice',
devServer: {
// node服务器代码 基于express
before:app =>{
// bodyParser用來解析post请求中的json数据
app.use(bodyParser.json())
app.use(
bodyParser.urlencoded({
extended: true
})
)
app.post("/dev-api/user/login",(req,res)=>{
const { username} = req.body
if(username === "admin"||username === "jerry"){
res.json({
code:1,
data: username
})
}else{
res.json({
code:10204,
message:"用户名或密码错误"
})
}
})
app.get("/dev-api/user/info",(req,res)=>{
//从请求头中获取令牌
//afdfsg;gasjhjgakgka;gjshgjakjgkajkjdka
// 令牌头 令牌体 哈希
//加密算法 用户信息:有效期
const auth = req.headers["authorization"]
const roles = auth.split('')[1] === "admin"?["admin"]:["editor"]
res.json({
code:1,
data:roles
})
})
}
}
调用接口 @/store/modules/user.js
import { login, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
const actions = {
login ({ commit }, userInfo) {
// 调用并处理结果,错误处理已拦截无需处理
return login(userInfo).then((res) => {
commit('SET_TOKEN', res.data)
setToken(res.data)
})
},
// get user info
getInfo ({ commit, state }) {
return getInfo(state.token).then(({ data: roles }) => {
commit('SET_ROLES', roles)
return { roles }
})
}
}
esay-mock
使用步騶:
- 登录easy-mock⽹站(https://easy-mock.com/login)
- 创建一个项目
- 创建需要的接口
- 调用base_url,.env.development
VUE_APP_BASE_API = ' https://easy-mock.com/mock/5e...'
解决跨域
*如果请求的接口在另一台服务器上,开发时则需要设置代理避免跨域问题
添加代理设置 vue.config.js*
devServer:{
port :port,
proxy:{
//代理/dev-api/user/login到http://127.0.0.1:3000/user/login
[process.env.VUE_APP_BASE_API]:{
target:`http://127.0.0.1:3000`,
changeOrigin:true,//要不要变更origin头
pathRewrite:{//地址重写:http://127.0.0.1:3000/user/login
["^"+process.env.VUE_APP_BASE_API]:""
}
}
}
},
创建⼀个独⽴接⼝服务器,~/test-server/index.js
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
app.use(bodyParser.json())
app.use(
bodyParser.urlencoded({
extended: true
})
)
app.post('/user/login', (req, res) => {
const {
username
} = req.body
if (username === 'admin' || username === 'jerry') {
res.json({
code: 1,
data: username
})
} else {
res.json({
code: 10204,
message: '⽤户名或密码错误'
})
}
})
app.get('/user/info', (req, res) => {
const roles = req.headers.authorization.split(' ')[1] ? ['admin'] : ['editor']
res.json({
code: 1,
data: roles
})
})
app.listen(3000)
git hooks
利用git hooks在每次提交代码时执行lint 执行过程如下:commit => git hooks => test&&lint 完成这两项任务需要安装:husky和lint-staged
npm install husky lint-staged -D
配置,package.json
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
}