vue-element-admin 学习记录
vue-element-admin 是一个后台前端解决方案,它基于 vue 和 element-ui实现。
项目目录结构
├── build // 构建相关
├── config // 配置相关
├── src // 源代码
│ ├── api // 所有请求
│ ├── assets // 主题 字体等静态资源
│ ├── components // 全局公用组件
│ ├── directive // 全局指令
│ ├── filters // 全局 filter
│ ├── icons // 项目所有 svg icons
│ ├── lang // 国际化 language
│ ├── mock // 项目mock 模拟数据
│ ├── router // 路由
│ ├── store // 全局 store管理
│ ├── styles // 全局样式
│ ├── utils // 全局公用方法
│ ├── vendor // 公用vendor
│ ├── views // view
│ ├── App.vue // 入口页面
│ ├── main.js // 入口 加载组件 初始化等
│ └── permission.js // 权限管理
├── static // 第三方不打包资源
│ └── Tinymce // 富文本
├── .babelrc // babel-loader 配置
├── eslintrc.js // eslint 配置项
├── .gitignore // git 忽略项
├── favicon.ico // favicon图标
├── index.html // html模板
└── package.json // package.json
src目录
api和views
所有的api和views都一一对应,从而方便维护。
components
放一些全局公用的组件。
store
统一管理如登录token,用户信息,或者是一些全局个人偏好设置等data。
alias
为路径取别名,理清文件与文件之间的引用关系。
ESLint
规范代码格式,提高代码可读性。
登录权限
不同权限对应不同的路由,不同的路由异步生成对应不同的侧边栏。
实现思路
登录: 当用户填写完账号和密码后,向服务端验证是否正确,验证如果通过,则会返回一个token,拿到token后,将该token贮存在cookie中,保证浏览器刷新后仍然能记录状态,此时前端再根据token向服务器请求用户的详细信息。
权限验证:通过获取的token请求对应的role,根据role中的角色权限并通过router.addRoutes来动态挂载路由。
以上的所有数据由vuex统一管控,再刷新数据后,vuex的数据将会丢失,所以需要重复上述的步骤。
实现步骤
登录获取token
登录页
//vuex中存储登录数据
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push({ path: '/' }); //登录成功之后重定向到首页
}).catch(err => {
this.$message.error(err); //登录失败提示错误
});
action
//定义登录方法
LoginByUsername({ commit }, userInfo) {
//去一下空格
const username = userInfo.username.trim()
return new Promise((resolve, reject) => {
loginByUsername(username, userInfo.password).then(response => {
const data = response.data
Cookies.set('Token', response.data.token) //登录成功后将token存储在cookie之中
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
});
});
}
获取用户信息
用户登陆成功后,在router.beforeEach中拦截路由,判断是否以获取token,判断之后开始获取用户信息。javascript
//router.beforeEach
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取user_info
const roles = res.data.role;
next();//resolve 钩子
})
根据权限动态加载侧边栏
实现思路:前端有一份路由表,它表示了每一个路由可以访问的权限,当用户登录后,通过token来获取对应的role,根据role的权限来动态算出对应的路由,再通过router.addRoutes
来动态挂载路由。
实现步骤
- 在创建vue实例时,将vue-router挂载,但这个时候挂载的只是一些通用的路由,比如登录,或者不用权限的页面。
- 登陆成功后,根据token来获取role,并将role中的权限与路由表中的权限一一比对,生成最终用户可以访问的路由表。
- 调用
router.addRoutes(store.getters.addRouters)
来添加用户可以访问的路由。 - 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。
具体实现
router.js
// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Login from '../views/login/';
const dashboard = resolve => require(['../views/dashboard/index'], resolve);
//使用了vue-router的懒加载
//所有权限通用路由表 如首页和登录页和一些不用权限的公用页面
export const constantRouterMap = [
{ path: '/login', component: Login },
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: '首页',
children: [{ path: 'dashboard', component: dashboard }]
},
]
//实例化vue的时候只挂载constantRouter
export default new Router({
routes: constantRouterMap
});
//异步挂载的路由
//动态需要根据权限加载的路由表
export const asyncRouterMap = [
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: { role: ['admin','super_editor'] }, //页面需要的权限
children: [
{
path: 'index',
component: Permission,
name: '权限测试页',
meta: { role: ['admin','super_editor'] } //页面需要的权限
}]
},
{ path: '*', redirect: '/404', hidden: true }
];
根据 vue-router官方推荐的方法通过meta标签来标示改页面能访问的权限有哪些。如meta: { role: ['admin','super_editor'] }表示该页面只有admin和超级编辑才能有资格进入。
main.js
// main.js
router.beforeEach((to, from, next) => {
if (store.getters.token) { // 判断是否有token
if (to.path === '/login') { //如果到登录页
next({ path: '/' }); //回到首页
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取info
const roles = res.data.role;
store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch(err => {
console.log(err);
});
} else {
next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next();
} else {
next('/login'); // 否则全部重定向到登录页
}
}
});
在导航守卫中判断用户权限,同时根据用户信息动态添加当前用户可访问的路由表。
store/permission.js
// store/permission.js
import { asyncRouterMap, constantRouterMap } from 'src/router'; //引入通用路由表和异步路由表
function hasPermission(roles, route) { //鉴权
if (route.meta && route.meta.role) {
return roles.some(role => route.meta.role.indexOf(role) >= 0) //如果当前路由中有role的信息,那就看看在roles是否有这个role,如果有返回true
} else {
return true
}
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers;
state.routers = constantRouterMap.concat(routers); //把有权限的路由和通用路由拼接
}
},
actions: {
GenerateRoutes({ commit }, data) { //根据data的信息生成可以访问的路由表
return new Promise(resolve => {
const { roles } = data;
const accessedRouters = asyncRouterMap.filter(v => {
if (roles.indexOf('admin') >= 0) return true; //如果roles里是admin,那就返回这个roles
if (hasPermission(roles, v)) {
if (v.children && v.children.length > 0) {
v.children = v.children.filter(child => { //如果有子路由,那就把子路由一个个拿来看有没有权限
if (hasPermission(roles, child)) {
return child //把有权限的给他返回
}
return false; //去掉roles没有权限的路由
});
return v
} else {
return v
}
}
return false;
});
commit('SET_ROUTERS', accessedRouters);
resolve();
})
}
}
};
export default permission;
前后端交互
实现思路
- UI 组件交互操作;
- 调用统一管理的 api service 请求函数;
- 使用封装的 request.js 发送请求;
- 获取服务端返回;
- 更新 data;
封装axios
封装步骤:
//引入axios
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
//引入token获取方法
import { getToken } from '@/utils/auth'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(config => {
//判断store中token是否存在
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 在请求头中塞入token
}
return config
}, error => {
// 请求错误时Promise返回reject
console.log(error) // for debug
Promise.reject(error)
})
// respone拦截器
service.interceptors.response.use(
//意外登出时 判断返回的错误码
response => response,
const res = response.data;
if (res.code !== 20000) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
});
// 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
//当前页面弹出警告消息框
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload();// 为了重新实例化vue-router对象 避免bug
});
})
}
return Promise.reject('error');
} else {
return response.data;
}
error => {
console.log('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
})
export default service
封装后使用步骤:
//引入封装好的axios请求
import request from '@/utils/request'
//使用
export function getInfo(params) {
//入参为config
return request({
url: '/user/info',
method: 'get',
params
});
}
跨域问题纯前端解决方案
开发环境可以通过配置 vue.config.js来解决
devServer: {
port: 2021, // 设置端口号
host: '10.32.120.69', // ip
disableHostCheck: true, //是否关闭用于 DNS 重绑定的 HTTP 请求的 HOST 检查
hotOnly: false, // 热更新
https: false, // https:{type:Boolean}配置前缀
open: false, //配置自动启动浏览器
proxy: null, //设置代理
proxy: {
'/api': { //代理标识,请求接口时遇到此路径则代理到该地址
target: 'http://localhost:2022/', //指向的实际地址
changeOrigin: true, //是否跨域
ws: true, //如果要代理 websockets,配置这个参数
secure: false, // 如果是https接口,需要配置这个参数
// 标识替换
// 原请求地址为 /api/getData 将'/api'替换''时,
// 代理后的请求地址为: http://xxx.xxx.xxx/getData
// 若替换为'/other',则代理后的请求地址为 http://xxx.xxx.xxx/other/getData
pathRewrite: { // 标识替换
'^/api': '/api/mock' //
}
}
}
生产环境可以通过配置 nginx 来解决
Mock 数据
使用mock数据可以分离前后端开发,通过预先与后端约定的接口来模拟数据请求,让前端开发更加独立自主,不被后端开发进度所阻塞。
Easy-Mock
线上免费服务很不稳定。
Mockjs
实现机制中会重写XMLHttpRequest
对象,导致progress
或一些底层依赖XMLHttpRequest
的库都会不兼容,而且调试不方便。
新方案
在4.0版本之后,在本地会启动一个mock-server
来模拟数据。
好处:在保留mockjs优势的同时,解决之前的痛点。因为mock-server
是完全基于webpack-dev-server
来实现的,所以在启动前端服务时,mock-server
会自动启动,而且使用chokidar
来观察mock文件夹内容的变化,支持热更新。并且可以通过控制台中的network
来观察接口返回的数据结构。
为了方便在不同的环境使用不同的api,所有的请求都设置了一个baseURL
,这个baseURL可以通过读取process.env.VUE_APP_BASE_API
这个环境变量来动态设置。
移除mock-server
如果不需要使用mock-server
可以在vue.config.js
中移除webpack-dev-server
中的proxy
和after
。
新增mock数据
在根目录下找到mock文件,添加对应的路由,对其进行拦截和模拟数据。
使用第三方库
在Vue.js项目中使用第三方库的一个优雅的方式是将其代理到Vue的原型对象上去 比如说引入Moment库。
main.js
import moment from 'moment'
Object.defineProperty(Vue.prototype, '$moment', { value: moment })
为什么不直接Vue.prototype.$moment = moment
呢? 因为使用Object.defineProperty
能够配置对象内置属性,比如是否可被迭代,是否只读等,用来防止出现意料之外的操作导致他的数据被修改。
构建
当项目开发完毕 运行npm run build:prod
打包正式环境,运行npm run build:stage
打包预发布环境。如需要自定义构建,如dist文件输出目录,可在config
中outputDir
中进行配置。
环境变量
环境变量都保存在.env.xxxx
文件中,他们都会通过webpack.DefinePlugin插件注入到全局。
> 一定要注意的是,环境变量必须要以VUE_APP_
开头,如:VUE_APP_API等
获取环境变量使用如下方式:
> javascript
> console.log(process.env.VUE_APP_xxxx)
>
分析构建文件体积
通过webpack-bundle-analyzer
命令分析代码体积分布来优化代码
npm run preview -- --report
文件命名规则
Component
所有的Component
文件都以大写开头,除了index.vue。。
如:
> @/components/BackToTop/index.vue
> @/components/Charts/Line.vue
> @/views/example/components/Button.vue
JS文件
所有的.js文件都遵循横线连接。
如:
> @/utils/open-window.js
> @/views/svg-icons/require-icons.js
> @/components/MarkdownEditor/default-options.js
Views
在views文件下的.vue文件都代表一个路由,遵循横线连接。
如:
> @/views/svg-icons/index.vue
> @/views/svg-icons/require-icons.js
为什么呢要这样命名?因为横线链接连接也是命名规范之一,views下的.vue文件代表的是一个路由,所以要和component区分(component都是以大写开头),还有页面的URL也是用横线连接的,比如https://www.xxx.admin/export-excel
,所以对应的view也要保持统一,也没有大小写敏感的问题。
路由懒加载
把不同的路由组件分割成不同的代码块,当路由被访问的时候才加载对应的组件。
const Foo = () => import('./Foo.vue')
// 一来可以聚合webpackChunkName名字一样的为一个模块,也是当前版本推荐的加载姿势
const Home = () =>
import(/* webpackChunkName: "HomePage" */ "@/views/home/index.vue");
Echarts
按需引入
// 按需引入 引入 ECharts 主模块
var echarts = require('echarts/lib/echarts')
// 引入柱状图
require('echarts/lib/chart/bar')
// 引入提示框和标题组件
require('echarts/lib/component/tooltip')
require('echarts/lib/component/title')
全部引入
var echarts = require('echarts')
初始化
因为ECharts初始化必须绑定dom,所以只能在mounted生命周期里进行初始化。
mounted() {
this.initCharts(); //初始化ECharts
},
methods: {
initCharts() {
this.chart = echarts.init(this.$el);
this.setOptions();
},
setOptions() { //配置
this.chart.setOption({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
})
}
}
如果ECharts的data是远程获取的,那么可以用watch
来触发setOptions
方法。
//第一种 watch options变化 利用vue的深度 watcher,options 一有变化就重新setOption
watch: {
options: {
handler(options) {
this.chart.setOption(this.options)
},
deep: true
},
}
//第二种 只watch 数据的变化 只有数据变化时触发ECharts
watch: {
seriesData(val) {
this.setOptions({series:val})
}
}