vue-element-admin 学习记录

2021 年 11 月 5 日 星期五(已编辑)
/
10
这篇文章上次修改于 2024 年 7 月 20 日 星期六,可能部分内容已经不适用,如有疑问可询问作者。

阅读此文章之前,你可能需要首先阅读以下的文章才能更好的理解上下文。

vue-element-admin 学习记录

vue-element-admin 是一个后台前端解决方案,它基于 vueelement-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来动态挂载路由。

实现步骤

  1. 在创建vue实例时,将vue-router挂载,但这个时候挂载的只是一些通用的路由,比如登录,或者不用权限的页面。
  2. 登陆成功后,根据token来获取role,并将role中的权限与路由表中的权限一一比对,生成最终用户可以访问的路由表。
  3. 调用router.addRoutes(store.getters.addRouters)来添加用户可以访问的路由。
  4. 使用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;

前后端交互

实现思路

  1. UI 组件交互操作;
  2. 调用统一管理的 api service 请求函数;
  3. 使用封装的 request.js 发送请求;
  4. 获取服务端返回;
  5. 更新 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中的proxyafter

新增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文件输出目录,可在configoutputDir中进行配置。

环境变量

环境变量都保存在.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})
  }
}

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...