项目配置
配置文件入口: vue.config.js
做一些基础配置:指定应用上下文、端口号、title
// vue.config.js const port = 7070 const title = 'vue项目最佳实践' module.exports = { publicPath: '/best-practice', // 部署应⽤用包时的基本 URL devServer: { port }, configureWebpack: { // 向index.html注⼊入标题 name: title } } // index.html <title><%= webpackConfig.name %></title>
命令:vue inspect 可以拿到整个webpack的全部配置
命令:vue inspect --rules 查看当前项目下所有的规则
命令:vue inspect --rule vue 查看vue的规则(也可以查看其他的规则,例如 svg)
链式操作:演示webpack规则配置
范例:项目要使用icon,传统方案是图标字体(字体文件+样式文件),不便维护;svg方案采用 svg-sprite-loader 自动加载打包,方便维护
使用icon前先安装依赖:svg-sprite-loader
npm i svg-sprite-loader -D
下载一个svg图标, 存入 /src/icons/svg 中,例如: /src/icons/svg/wx.svg
修改规则和新增规则,vue.config.js
const path = require('path') // 将传入的相对路径转换为绝对路径 function resolve(dir){ return path.join(__dirname, dir) } chainWebpack(config) { // 配置svg规则排除icons⽬目录中svg⽂文件处理理 config.module.rule('svg') .exclude.add(resolve('src/icons')) // 新增icons规则,设置svg-sprite-loader处理理icons⽬目录中的svg config.module.rule('icon') //新增icons规则 .test(/.svg$/) // 设置test选项 .include.add(resolve('src/icons')) // 加入include,include选项是数组 .end() //add完上下⽂是include数组不是icons规则,使⽤end()回退 .use('svg-sprite-loader') // 添加loader .loader('svg-sprite-loader') // 切换上下文为 svg-sprite-loader .options({ symbolId: 'icon-[name]' }) // 指定选项,如果到时候图片名称是 wx.svg,这里就是 icon-wx .end() // 回退 }
在组件中使用
<svg> <use xlink:href="#icon-wx"></use> </svg> import '@/icons/svg/wx.svg'
我们发现使用方式比较繁琐:
1、将<svg>建一个组件
2、自动导入
图标自动导入
// src/icons/index.js // 利用webpack的require.context 自动导入 // 返回的req是只去加载svg目录中的模块的函数 // 参数2:不要继续往下递归,参数3:匹配的规则 const req = require.context('./svg', false, /.svg$/) // 遍历加载上下文中所有项 req.keys().map(req) // main.js import './icons'
封装组件
创建组件 Icon /components/Icon.vue
<template> <svg :class="svgClass" aria-hidden="true" v-on="$listeners"> <use :xlink:href="iconName" /> </svg> </template> <script> export default { name: 'SvgIcon', props: { iconClass: { type: String, required: true }, className: { type: String, default: '' } }, computed: { iconName() { return `#icon-${this.iconClass}` }, svgClass() { if (this.className) { return 'svg-icon ' + this.className } else { return 'svg-icon' } } } } </script> <style scoped> .svg-icon { 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } </style>
修改 /icon/index.js 文件,加入以下代码
import Vue from 'vue'; import Icon from '@/components/Icon' // Icon组件全局注册一下 Vue.component('Icon', Icon)
组件中修改使用方法
<Icon icon-class="qq" /> <Icon icon-class="wx" />
权限控制
权限控制分为constantRoutes 和 asyncRoutes
定义路由 route/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(/* webpackChunkName: "home" */ "@/views/About.vue"), name: "about", meta: { title: "About", icon: "qq", roles: ['admin', 'editor'] }, } ] } ]; export default new Router({ mode: "history", base: process.env.BASE_URL, routes: constRoutes });
布局页面 :layout/index.vue
<template> <div class="app-wrapper"> <!-- 导航菜单 --> <!-- <sidebar class="sidebar-container" /> --> <div class="main-container"> <router-view /> </div> </div> </template>
用户登录页面:views/Login.vue
<template> <div> <h2>⽤用户登录</h2> <div> <input type="text" v-model="username"> <button @click="login">登录</button> </div> </div> </template> <script> export default { data() { return { username: "admin" }; }, methods: { login() { this.$store.dispatch("user/login", { username: this.username }) .then(() => { this.$router.push( { path: this.$route.query.redirect || "/" } ); }) .catch(error => { alert(error) }); } } }; </script>
路由权限守卫:src/permission.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中加入 permission
import './permission'
utils/auth.js : npm i js-cookie -S
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); }
实现 store : 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 } }) export default store
user模块:用户数据、用户登录, store/modules/user
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); 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); resolve({ roles }); }, 1000); }); }, // remove token resetToken({ 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 filterAsyncRoutes(routes, roles) { const res = []; routes.forEach(route => { // 复制一份 const tmp = { ...route }; // 如果用户有访问权则加入结果路由表 if (hasPermission(roles, tmp)) { // 如果存在子路由则递归过滤之 if (tmp.children) { tmp.children = filterAsyncRoutes(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 = filterAsyncRoutes(asyncRoutes, roles); } commit("SET_ROUTES", accessedRoutes); resolve(accessedRoutes); }); } }; export default { namespaced: true, state, mutations, actions };
getters编写: store/index.js
Vue.use(Vuex) const store = new Vuex.Store({ modules: { permission, user }, // 全局定义getters便于访问 getters: { roles: state => state.user.roles, } })
数据交互
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, // 跨域时若要发送cookies需要设置该选项 timeout: 5000 // 超时 }) // 请求拦截 service.interceptors.request.use( config => { // do something if(store.getters.token){ // 设置令牌请求头 config.headers["Authorization"] = 'Bearer ' + getToken() } return config }, error => { // 请求错误预处理 // console.log(error) return Promise.reject(error) } ) // 响应拦截 service.interceptors.response.use( // 通过自定义code判定响应状态,也可以通过HTTP状态码判定 response => { // 仅返回数据部分 const res = response.data // code不为 1 判定为失败 if(res.code !== 1){ console.log('请求发生错误') // 假设:10008-非法令牌;10012-其他客户端已登录;10014-令牌过期 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 || '服务器错误')) }else{ return res } }, error => { console.log('err:' + error) Message({ message: error.message, type: 'error', duration: 2 * 1000 }) return Promise.reject(error) } ) export default service
设置 VUE_APP_BASE_API 环境变量,创建 .env.development 文件
# 在development 时才会生效 # base api VUE_APP_BASE_API = '/dev-api'
创建API: @/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' }) }
请求封装全过程:
第一步:request.js 封装axios(处理拦截和token等)
第二步:封装 api/user.js 将api和request结合
第三步:在 store/getter.js 中调用接口,并返回数据
解决跨域
如果请求的接口在另一台服务器上,开发时则需要设置代理避免跨域问题:
添加代理配置: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, pathRewrite: { ["^" + process.env.VUE_APP_BASE_API]: "" } } } }