Vuejs+ElementUI搭建后台管理系统框架

文章目录

  • 1. Vue.js 项目创建
    • 1.1 vue-cli 安装
    • 1.2 使用 vue-cli 创建项目
    • 1.3 文件/目录介绍
    • 1.4 启动 Web 服务
  • 2. 集成 Vue Router 前端路由
    • 2.1 Vue Router 是什么
    • 2.2 集成 Vue Router 方法
    • 2.3 使 Vue Router 生效
  • 3. 集成 Vuex 组件
    • 3.1 Vuex 是什么
    • 3.2 集成 Vuex 方法
    • 3.3 使 Vuex 生效
    • 3.4 Vuex 使用方式
      • 3.4.1 设置 state 值
      • 3.4.2 获取 state 值
  • 4. 集成 ElementUI 组件
    • 4.1 Element UI 是什么
    • 4.2 集成 Element UI 方法
    • 4.3 使 ElementUI 生效
  • 5. 设计前端项目目录结构
  • 6. layout 设计
    • 6.1 Header 部分设计
    • 6.2 Side Menu 部分设计
    • 6.3 Main Content 部分设计
      • 6.3.1 面包屑功能
    • 6.4 Footer 部分设计
  • 7. 集成 Axios 组件
    • 7.1 Axios 是什么
    • 7.2 集成 Axios 方法
    • 7.3 Axios 初始化
  • 8. 权限拦截管理设计
  • 9. 菜单后台加载设计
  • 10. 前端路由设计
  • 11. 登录入口设计
  • 12. Mock 数据设计
    • 12.1 Mockjs 集成方法
    • 12.2 启用 Mockjs 服务
  • 13. 前端代理服务

1. Vue.js 项目创建

Vue.js 是一套用于构建用户界面的渐进式框架。在使用 Vue.js 框架之前,如果您还不能灵活的使用 HTML、CSS、JavaScript 语言,如果能抽空预习一下 HTML、CSS、JavaScript 这几门语言的用法,这样更利于您继续学习 Vue.js。

vue-cli 是开发 Vue.js 的一种官方标准工具,我们通过 vue-cli 4.1.2 版本(关于 vue-cli 版本问题,学习或者技术选型没有历史包袱时,建议使用最新版,4.1.2 是本场 Chat 时最新版本)来创建项目,当然没有 vue-cli 也能够使用 Vue.js 开发您的项目,但使用 vue-cli 可以更方便您使用 Vue.js 开发项目。

1.1 vue-cli 安装

 $ yarn global add @vue/cli
  • yarn

Facebook、Google 等公司推荐的一款 JavaScript 包管理工具,类似于 Java 界的 Maven、Gradle。当然您也可以继续使用 NPM 工具来管理 JavaScript 包。关于 Yarn 和 NPM 的对比不在此处介绍,有兴趣的朋友可以在网上搜索 NPM 了解其用法。

  • global

表示将需要下载的 JavaScript 包存储在全局目录中。这样我们就可以在命令行中直接使用 JavaScript 安装包内的程序。如 Vue 命令就来源于 vue/cli 包。

  • @vue/cli

scoped packages。@ 后边跟随的 Vue 是 scope,cli 是包名。意思是下载 Vue 这个组织下 cli 包。

1.2 使用 vue-cli 创建项目

我们使用 vue-cli 创建名称为 web-demo 的项目。操作命令如下所示:

 $ vue create web-demo

在创建项目的过程中,中途有一个选项操作,如下所示:

Vue CLI v4.1.2
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint) 
  Manually select features 

默认选择第一项。安心等待项目创建完成。当依赖文件下载完成后,进入项目所在目录,目录内文件如下所示:

README.md	babel.config.js	node_modules	
package.json	public		src		yarn.lock

1.3 文件/目录介绍

README.md

项目介绍说明信息。

babel.config.js

Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。babel.config.js 就是 babel 编辑器的配置文件。通常情况下,无需过多关注。

node_modules

项目依赖包目录,里边存储了当前 Web 项目依赖的 JavaScript 包。

package.json

项目配置信息,包括依赖包信息。

public

公共文件目录,如 HTML 文件、图片等。

src

当前项目开发的 Vue 代码。

yarn.lock

Yarn 工具自动生成的文件,里边包含每个依赖文件的确切版本等信息。注意 package.json 中也指定了项目依赖 JavaScript 包的版本信息,但 package.json 中可能对于同一个 JavaScript 包指定了多个版本(范围选择),而 yarn.lock 存储的是确切版本信息。

1.4 启动 Web 服务

下边来启动 Web 服务,操作步骤如下所示:

 $ cd web-demo
 $ yarn serve

Web 服务启动后,打开浏览器输入 Web 服务地址。

 http://localhost:8080/

8080 是 Web 服务的默认端口,如果在启动 Web 服务的时候,恰巧 8080 端口已经被占用,那么 Web 服务会默认启用 8081 端口。Web 服务启动成功后,至此,Vue.js 项目便创建完成。

2. 集成 Vue Router 前端路由

2.1 Vue Router 是什么

Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用(SPA)变得易如反掌。

为什么要引入 Vue Router 呢 ?

SPA(single page application)单一页面应用程序,即整个 Web 只有一个完整的页面,这个页面由非常多个组件组成。当 Web 加载时,不会一次性显示整个页面所有的组件,而是按需显示局部内容,也就是部分组件。SPA 页面中这么多个组件,究竟哪个组件要显示,哪个组件要隐藏。为了解决这个问题,我们引入一种前端路由的工具 Vue Router。

我们给每个组件定义一个 URL,从而形成一张前端路由表。当页面上发起请求跳转到指定 URL 时,这个 URL 对应的组件以及这个组件引用的组件就会被显示出来,不在这个范围内的组件就会被隐藏。

所以:前端路由通俗地讲,就是给组件取个名字,这个名字就是 URL。当页面上发生 URL 跳转时,显示这个 URL 对应的组件以及这个组件引用的组件。

{ 
  path: '/user/details', 
  component: User 
}

上边例子是前端路由的简单定义。path 对应的便是 URL,component 对应的便是组件。当页面上发生 /user/details 跳转时,便会显示 User 组件。

2.2 集成 Vue Router 方法

首先进入我们在第一步中创建的项目 web-demo 目录中。然后使用 Yarn 工具在线下载 vue-router 包,执行下边命令如下所示:

yarn add vue-router

vue-router 包下载完成后,将会被存储到当前项目 node_modules 目录下。

2.3 使 Vue Router 生效

Vue Router 引入项目后,使用 Vue.use(plugin) 方法添加插件。在 main.js 内导入 Vue Router 对象关联前端路由组件。

// main.js
import Vue from 'vue'
import VueRouter from 'vue-router';
import router from '@/router/router.js'

Vue.use(VueRouter);

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

3. 集成 Vuex 组件

3.1 Vuex 是什么

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。SPA 由多个组件组成,组件与组件之间需要共享状态时,怎么来实现呢?浏览器上常见的几种存储方式如下所示:

  • HTML5 的 localStorage。主要通过本地存储的方式存储数据,浏览器关闭后,数据仍在在本地。
  • HTML5 的 sessionStorage。主要通过会话保存数据,当浏览器关闭后,数据丢失。
  • Cookies。每次 HTTP 请求时,都会将 Cookies 信息发送给服务端。Cookies 存储大小有限制,否则每次 HTTP 请求发送给服务器的数据会占用过多带宽。
  • Vuex 通过内存存储数据。页面刷新时,state 状态丢失

localStorage、sessionStorage 更多的时候用于页面之间数据传递。Cookies 更多用于存储 Web 前端与后台服务端之间的数据。Vuex 更适合于组件之间的状态管理。上述几种存储方式可在一个项目中同时存在,主要取决于需求,不同的场合,选用不同的数据存储方式。

Vuex 模块核心概念如下所示:

  • State 存储的状态变量;
  • Getter 获取状态变量;
  • Mutation 修改状态变量的值;
  • Action 提供入口方法,修改存储状态的值;
  • Module 状态模块化管理,即我们将几百个状态分布到多个文件中,每个文件中对应的是一个模块。

修改状态的流程:action -> mutation -> state 修改状态。

获取状态的流程:getter -> state 获取状态。

3.2 集成 Vuex 方法

使用 Yarn 工具在线下载 Vuex 包,下载命令如下所示:

yarn add vuex

Vuex 包下载完成后,将会被存储到当前项目 node_modules 目录下。

3.3 使 Vuex 生效

Vuex 组件引入项目后,使用 Vue.use(plugin) 方法添加插件。在 main.js 导入 Vuex 的对象来关联状态管理组件。

// main.js
import Vue from 'vue'
import store from '@/store/index.js';

Vue.use(Vuex);

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

3.4 Vuex 使用方式

3.4.1 设置 state 值

this.$store.dispatch({action 名字},{state 新的值})

第一个参数是 action 中定义的方法。第二个参数是 state 新的值。如我们需要调整 height 这个变量的值为 100px。假设 action 中定义了一个方法 autoHeight。则使用命令如下所示:

this.$store.dispatch('authHeight', '100px')

3.4.2 获取 state 值

通过 mapGetters 方法获取到 state 中 height 这个变量,或者从 data 中读取 clientHeight 变量。

import { mapGetters } from "vuex";

export default {
  computed: {
    // 从 vuex 中获取浏览器高度,实时更新,保持左侧菜单栏高度与浏览器高度一致,保持垂直方向 100% 高度
    ...mapGetters(["height"])
  },
  data(){
    return {
      // 从 vuex 读取 state 状态的第二种方式
      clientHeight: this.$store.getters.height
    }
  },
}

4. 集成 ElementUI 组件

4.1 Element UI 是什么

Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。这个库集成了大量漂亮的组件,如选择框组件、弹框组件、输入框组件、按钮组件等等。集成了 ElementUI 后,可以方便我们更快的开发出更漂亮的 Web 页面。

4.2 集成 Element UI 方法

使用 Yarn 工具在线下载 element-ui 包,下载命令如下所示:

yarn add element-ui

element-ui 包下载完成后,将会被存储到当前项目 node_modules 目录下。

4.3 使 ElementUI 生效

使用 Vue.use(plugin) 方法添加插件,将 ElementUI 添加到项目中。

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI, {
  size: 'small' // set element-ui default size
});

5. 设计前端项目目录结构

项目目录结构:

-- public
-- src
---- api
---- assets
---- components
------ layout
------ pagination
------ Breadcrumb.vue
---- layout
------ BaseLayout.vue
------ EmptyRouter.vue
---- mock
------ mock.js
------ data
---- router
---- store
---- utils
---- views
---- App.vue
---- main.js
---- permission.js

6. layout 设计

在前端页面开发时,第一步先规划整体布局,如将整个 Web 划分为几个大的区域,每个区域负责特定的显示。我们将后台管理系统划分为如下四个部分,分别是:

  • Header 头部信息显示栏
  • Side Menu 左侧菜单显示栏目
  • Main Content 内容显示区域
  • Footer 页脚显示信息

6.1 Header 部分设计

头部显示栏主要显示整个系统重要信息,如 LOGO 信息、主菜单信息、登录用户信息,以及其他重要信息。实现 Header 部分代码如下所示:

<template>
  <div class="wisrc-header">
    <el-row>
      <el-col :span="24">
        <!-- Logo 显示-->
        <Logo />
        <!--一级主菜单区域-->
        <MainMenu />
        <!-- 右侧工具栏(包含用户登录信息) -->
        <Tools />
      </el-col>
    </el-row>
  </div>
</template>

<script>
import Logo from "@/components/layout/Logo.vue";
import MainMenu from "@/components/layout/MainMenu.vue";
import Tools from "@/components/layout/Tools.vue";

export default {
  name: "WisrcHeader",
  components: {
    Logo,
    MainMenu,
    Tools
  }
};
</script>

<style scoped>
.wisrc-header {
  height: 60px;
  width: 100%;
  text-align: left;
  padding: 0px;
  line-height: 60px;
  background-color: rgb(84, 92, 100);
  color: #ffffff;
}
</style>

6.2 Side Menu 部分设计

Side Menu 部分由两个组件组成,分别是:

  • Side.vue
  • SideChildrenMenu.vue

Side 组件主要实现左侧菜单栏主要布局,建构一个紧靠浏览器左侧,且横向宽度为 260 像素,垂直方向自适应浏览器高度的区域。布局构建完成后,通过嵌套 SideChildrenMenu 组件显示具体的菜单信息。Side 组件的代码如下所示:

<template>
  <div class="wisrc-side">
    <el-menu
      default-active="2"
      :style="{height: (height-60)+'px'}"
      class="wisrc-side-menu"
      background-color="#545c64"
      text-color="#fff"
      active-text-color="#ffd04b"
    >
      <el-scrollbar style="height: 100%">
        <SideChildrenMenu  v-bind:children="sideMenuList" />
      </el-scrollbar>
    </el-menu>
  </div>
</template>

<script>

import { mapGetters } from "vuex";
import { getMenu } from '@/api/menu.js';
import SideChildrenMenu from '@/components/layout/SideChildrenMenu.vue';

export default {
  name: "wisrc-side",
  computed: {
    // 从 vuex 中获取浏览器高度,实时更新,保持左侧菜单栏高度与浏览器高度一致,保持垂直方向 100% 高度
    ...mapGetters(["height"])
  },
  components: {
    SideChildrenMenu
  },
  data() {
    return {
      sideMenuList: [],
    };
  },
  methods: {
    openPage(url) {
      this.$router.push(url);
    }, 
  },
  mounted(){
    // 定时获取后台菜单信息
    getMenu().then(resp => {
      this.sideMenuList = resp
    })
  }
};
</script>

<style scoped>
.wisrc-side {
  width: 260px;
  height: 100%;
  float: left;
  text-align: left;
}
.wisrc-side-menu {
  overflow-y: auto;
}
.el-scrollbar__wrap {
  overflow-x: hidden;
}
</style>

SideChildrenMenu 组件用来渲染树形层级菜单。树形菜单采用递归渲染的方式实现,即在 SideChildrenMenu 组件内嵌套使用组件自身,从而实现递归渲染树形菜单的效果。Vue.js 中组件采用递归时,该组件一定要设置 name 属性。否则递归无效。

SideChildrenMenu 组件的代码实现如下所示:

<template>
<div>
    <div v-for="(item, index) in children" :key="index">
          <!-- 目录菜单 -->
          <el-submenu v-if="item.menuType == 1" :index="index">
            <!-- 目录菜单名称 -->
            <template slot="title">
              <i :class="item.iconClass"></i>
              <span>{{item.menuName}}</span>
            </template>
            <!-- 目录下子菜单 -->
            <SideChildrenMenu  v-bind:children="item.children" />
          </el-submenu>
          <!-- 叶子菜单 -->
          <el-menu-item v-if="item.menuType == 2" :index="item.menuId" @click="openPage(item.path)">
            {{item.menuName}}
          </el-menu-item>
    </div>
</div>
</template>

<script>
export default {
    props: ["children"],
    name: 'SideChildrenMenu',
    methods: {
        openPage(url) {
            this.$router.push(url);
        }
    }
}
</script>

自此,左侧菜单组件开发完毕,我们通过 Axios 组件向后台服务发起请求,获取左侧菜单栏信息。菜单信息数据格式见下边 9.1 节。

6.3 Main Content 部分设计

Main Content 用来渲染具体的业务信息。例如当我们在左侧菜单栏点击打开“字段管理”页面,字段管理页在前端路由表中配置了相应的组件 Column.vue。也就是点击“字段管理”页面,将会打开 Column.vue 组件以及这个组件中引入的其他组件。那问题来了, 待显示的组件在哪里显示呢?

答案就是

我们在 Main Content 中加入了 DOM 元素。当左侧菜单栏点击打开“字段管理”页面后,其对应的组件 Column.vue 将会在 Main Content 中 DOM 元素内显示。

<template>
  <div>
    <el-card>
      <Breadcrumb></Breadcrumb>
    </el-card>
    <div class="wisrc-content" :style="{height: (height-138)+'px'}">
      <section style="overflow: auto !important">
        <transition name="fade" mode="out-in">
          <keep-alive>
            <el-card
              style="overflow: auto !important; text-align: left"
              :style="{height: (height-140)+'px'}"
            >
              <router-view></router-view>
            </el-card>
          </keep-alive>
        </transition>
      </section>
    </div>
  </div>
</template>
<script>
import { mapGetters } from "vuex";
import Breadcrumb from "@/components/Breadcrumb";

export default {
  name: "WisrcContent",
  components: {
    Breadcrumb
  },
  computed: {
    ...mapGetters(["height"])
  }
};
</script>

<style scoped>
.wisrc-content {
  margin-left: 260px;
  border: #f6f3f3 solid 1px;
  background-color: #f6f3f3;
  overflow-y: auto;
  padding: 6px 6px;
}
</style>

通过使用 渲染待刷新显示的内容,实现了局部页面刷新的功能。如果没有 ,那么前端路由还能渲染出来吗?

答案是:不行,router-view 就像是个容器,没有容器,待显示的组件无处安放。

6.3.1 面包屑功能

此处的面包屑不是吃了一半的面包渣渣。面包屑导航(Breadcrumbs)是一种基于网站层次信息的显示方式。前端路由往往由多层路由组成,在页面跳转过程中,可能点着点着就不知道现在处于哪个页面中(除非每个页面中都设计了标题)。引入面包屑导航功能后,我们可以很方便的知道当下所在的页面。面包屑实现方式如下所示:

<template>
  <div>
    <el-breadcrumb separator="/">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item
        v-for="(item,index) in breadcrumb"
        :to="{path: item.path}"
        :key="index"
      >{{item.title}}</el-breadcrumb-item>
    </el-breadcrumb>
  </div>
</template>
<script>
export default {
  data() {
    return {
      breadcrumb: []
    };
  },
  watch: {
    $route(to) {
      // 监听路由跳转,每次发生路由跳转时,$route 中的值都会发生变化。
      // to.matched 用来获取匹配成功的路由信息,
      const routers = to.matched;
      this.breadcrumb = [];
      if (routers && routers.length > 0) {
        for (let i = 1; i < routers.length; i++) {
          this.breadcrumb.push({
            title: routers[i].meta.title,
            path: routers[i].path
          });
        }
      }
    }
  }
};
</script>

前端路由往往由多个层级组成,例如现在有如下几个路由信息:

  • /modeller
  • /modeller/column
  • /modeller/column/add

路由 /modeller/column/add 的上一层级路由是 /modeller/column,/modeller/column 的上一层路由是 /modeller。 当我们跳转到 /modeller/column/add 时,这个组件的所有直系父组件都会被显示。所以 to.matched 返回上边三个值。我们通过这三个返回值,便可描绘出路由的层级结构。从而生成面包屑导航。

6.4 Footer 部分设计

Footer 主要显示一些版权信息,没有具体的业务逻辑。此部分通常位于系统底部,实现代码如下所示:

<template>
  <div class="wisrc-footer">
    <el-footer style="margin-left: -260px; font-size: 12px">Copyright © 2019 基于 Vue.js + ElementUI 后台管理系统</el-footer>
  </div>
</template>
<script>
export default {
  name: "WisrcFooter"
};
</script>

<style scoped>
.wisrc-footer {
  position: fixed;
  text-align: center;
  width: 100%;
  height: 24px;
  line-height: 24px;
  bottom: 0px;
  background-color: #f6f4f4;
  border-top: #cccccc solid 1px;
  margin-left: 260px;
}
</style>

7. 集成 Axios 组件

7.1 Axios 是什么

Axios 是一个基于 Promise 的 HTTP 库。HTTP 库提供了浏览器通过 HTTP 协议与后台服务通信的能力。由于网络带宽、后台服务等可能存在不稳定的情况,所以有时候请求后台服务很快,有时候请求后台服务很慢,那当服务请求很慢时,前端是否有必要卡着不动,等待请求完成呢?

如果 Web 页面卡着不动,那用户体验一定很糟糕。众所周知 JavaScript 运行在单线程上,现在很多语言都讲究多线程、高并发,从而实现任务异步处理。那么单线程运行的 JavaScript 怎么来实现异步处理呢?主要有两种方法:

  • 基于事件监听实现异步处理
  • 基于回调函数实现异步处理

基于事件监听实现异步处理存在一定的限制,HTML 事件属性有范围,如 Window 事件、Form 事件、Keyboard 事件、Mouse 事件、Media 事件。当异步处理的业务不在 HTML 事件属性范围内时,将无法使用事件监听来实现异步处理任务。

基于回调函数的异步处理,将会吞噬掉 return 值。常见的回调函数使用方法是:将需要执行的任务放进 setInterval 或 setTimeout 内来实现任务异步处理。


setTimeout(function(){
	// 异步处理任务,5秒后执行
	console.log("异步处理任务执行");
},5000)

setInterval(function(){
    // 异步处理任务,每 5 秒执行一次
    console.log("每 5 秒钟执行一次异步处理任务");
}, 5000)

由于 setInterval 和 setTimeout 无法保证执行顺序,所以,当项目中大量使用回调函数时,几十个不知道顺序的任务即将执行,不知道哪个先运行,哪个后运行,谁又在谁前边运行,这样对于后期代码维护,将会是一场巨大的灾难。

有没有办法既能实现异步处理,又能解决顺序问题呢?

Axios 库有一个非常有效的特性,就是支持 Promise API。使用 Promise 既能实现任务的异步处理,又能实现任务的顺序执行。Promise 将多个需要异步处理的任务队列化,队列有效的保证了任务执行的有序性。队列中的异步任务顺序执行,上一个任务的执行返回值作为下一个异步任务的输入值,这样又解决了异步任务吞噬 return 返回值的问题。

但与传统的函数调用使用 return 返回值不同的是,Promise 使用 resolve 函数传递正确处理的返回值,使用 reject 函数传递错误处理的返回值。举个例子:

function asyncDemo(){
    return new Promise((resolve, reject) => {
        axios.get('/api/demo').then(response => {
            if (response) {
            	// 请求 API 成功,将返回结果传递到下一个任务
            	resolve('请求 API 成功');
            } else {
                // 请求 API 失败,终止异步处理任务
                reject('请求 API 失败')
            }
        })
    })
}

// 运行异步处理任务
asyncDemo().then(response => {
    // 请求成功后,上一个任务通过 resolve 函数传递的参数作为当前的输入
    console.log(response);
    // 输出信息是: 请求 API 成功
}).then(error => {
    // 请求 API 失败,上一个任务通过 reject 函数传递的参数作为当前的输入
    console.log(error)
    // 输出信息是: 请求 API 失败
)

7.2 集成 Axios 方法

使用 Yarn 工具在线获取 Axios 包,并安装到当前项目中。

yarn add axios

Axios 包下载好之后,被存放到当前项目 node_modules 目录中。

7.3 Axios 初始化

项目中使用 Axios 组件请求 API 服务,通常需要设置一些统一处理逻辑,如异常信息全局处理、token 信息注入等等。那么如何初始化 Axios 组件呢?代码示例如下所示:

// utils/request.js
import axios from 'axios';
import { Message } from 'element-ui';

// 设置远程服务器 IP 地址
// axios.defaults.baseURL = process.env.VUE_APP_BASE_API;

// 设置请求远程服务器时携带的 token 值信息
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;

// 设置 POST 方法请求时默认的请求头信息
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';


// 拦截请求发生前,可以在此修改请求参数
axios.interceptors.request.use(config => {
    return config;
}, error => {
    Message.error(error)
})

// 拦截请求完成后,可以全局处理异常信息
axios.interceptors.response.use(response => {
    if (response && response.status == 200) {
        if (response.data.statusCode == "200") {
            // 请求成功
            return response.data.data;
        } else {
            Message.error(response.data.statusMessage)
            return false;
        }
    }
    Message.error('请求API失败');
}, error => {
    Message.error(error)
    return false;
})
  • axios.interceptors.request:浏览器向后台服务发起请求之前的处理逻辑。
  • axios.interceptors.response:浏览器向后台服务发起请求之后,拦截响应值,判断是否存在异常,如果存在异常,拦截处理结果,提示请求错误信息。如果请求成功,则将请求结果返回给具体的发起请求的函数。

8. 权限拦截管理设计

管理系统通常会针对不同的用户开发不同的权限,如超级管理员可以使用管理系统中所有的功能,财务部门只能使用财务相关的功能,人事部门只能使用员工管理相关功能。为了实现这个需求,我们在设计后台管理系统中需要引入权限管理功能。

权限管理通常分为几个形式:

  • API 权限控制
  • 数据权限控制
  • 前端页面权限控制
  • 页面按钮权限控制

API 权限控制

通常 API 的权限管理在后台进行,后台服务的入口处会拦截所有的请求,当用户发起 API 请求后,校验用户是否被授予访问这个 API 的权限,用户被授权,则允许访问,若用户未被授权,则用户被拒绝访问。

数据权限控制

数据权限往往基于角色、岗位等维度进行管理。往往针对的是数据的访问控制,如哪些角色、岗位的用户能够访问哪些数据。这块的权限控制往往由后台服务控制。

前端页面权限控制

前端页面权限,指用户登录系统之后,哪些页面可见,哪些页面不可见。与数据权限有一点相似之处。通常前端读取后台服务关于用户的权限信息后,在渲染菜单的时候,将无权限的菜单屏蔽。

页面按钮权限控制

页面按钮控制权限,指用户打开页面之后,哪些页面按钮可见,哪些页面按钮不可见。通常前端读取后台服务关于用户的权限信息后,在渲染页面的时候,将用户没有权限的按钮屏蔽。

用户在浏览器中打开页面后,怎么在每个页面中都加入校验,用来拦截判断用户是否有权限访问这个页面呢?

  • 在每个页面中加入判断,校验用户是否登录
  • 全局校验用户是否登录

在每个页面中加入判断用户是否登录的代码,显然这种方式对于业务的侵入太大,过于笨拙,显然不适合。那么怎么设置全局的用户登录校验呢?

Vue Router 导航守卫功能。导航守卫主要在如下几个阶段来拦截请求。分别是:

  • beforeEach 全局前置守卫,在导航路由触发前拦截
  • beforeRouteUpdate
  • beforeEnter
  • beforeRouteEnter
  • beforeResolve
  • afterEach

上述几种导航守卫,我们使用 beforeEach 实现登录校验的判断。实现代码如下所示:

import router from '@/router/router.js';
import store from '@/store/index.js';

function checkLogin() {
    // todo 校验用户是否登录,以及用户 token 令牌是否过期
    return !store.getters.loginStatus;
}

// @param to 到哪里去,跳转到哪个路由
// @param from 从哪里来,从哪个路由跳转过来的
// @param next 执行跳转
router.beforeEach((to, from, next) => {
    // 判断用户是否登录,
    // 如果用户已经登录,执行 next() 方法,
    // 如果用户未登录,则跳转到登录页面
    if (to.path != '/login' && checkLogin()) {
        next({ path: '/login' })
    } else {
        next()
    }

})

每次触发路由导航时,都会执行这个判断逻辑,当发现用户未登录,或登录的 token 失效后,将会被引导进入登录页面。注意一定要判断目标地址是否为登录地址,否则出现死循环

checkLogin 方法现在只是读取了保存在 vuex 中的用户登录状态,这种处理方式并适合生产环境。主要原因是:在浏览器执行 F5 刷新时,Vuex 存储的状态值会丢失,那么用户又要重新登录。

通常生产环境中,会将 token 信息存储到 Cookies 中,登录状态存储在 Vuex 中,判断用户是否登录要结合 Cookies 中的 token 与 Vuex 中的登录状态一起判断。如用户登录状态为 false 时,从 Cookies 中读取用户 token 值,向后台服务请求验证 token 有效性,如果 token 有效,则设置 Vuex 中用户登录状态为 true,然后跳转到目标路由地址。否则跳转到登录页面。

9. 菜单后台加载设计

字段介绍

  • menuId 菜单编码,每个菜单必须对应一个唯一的菜单编码。
  • menuName 菜单名称。
  • menuType 菜单类型。1 表示目录类型,2 表示叶子菜单,目录类型菜单,表示其下还有菜单信息,叶子类型菜单,表示达到树底部。
  • path 前端路由值,叶子菜单被点击时,将会跳转到该路由。目录菜单设置 path 值将会被忽略。
  • iconCLass 目录类型菜单前边的小图标。
  • children 表示目录类型菜单下的子菜单信息。

假设我们要创建一个目录菜单,目录菜单下边挂载两个叶子菜单,菜单数据格式如下所示:

{
  menuId: "1-1",
  menuType: 1,
  menuName: '业务规则',
  iconClass: 'el-icon-location',
  children: [
     {
          menuId: "1-1-1",
          menuType: 2,
          menuName: '数据集管理',
          path: '/modeller',
     },
     {
          menuId: "1-1-2",
          menuType: 2,
          menuName: '内容管理',
          path: '/modeller',
     }
  ]
}

Web 中向后台请求菜单信息的方式:

// api/menu.js
import axios from "axios"

export function getMenu(){
    return axios.get('/menu')
}

10. 前端路由设计

前端路由,粗鲁的解释,就是给组件取个名字,这个名字采用 URL 的形式来命名。前端路由格式如下所示:

      path: '/foo',
      name: 'foo',
      component: Foo,
      meta: { title: 'foo' }
      children: [
         {
            path: '/foo',
            name: 'foo',
            component: Foo,
            meta: { title: 'foo' }
         }
      ]
  • path 路由地址
  • name 路由名称
  • component 路由对应的组件
  • meta 路由元数据,如路由标签等
  • children 子路由信息

定义好路由信息后,我们在程序中便可以调用路由跳转方法进行路由切换操作。具体方法如下所示:

// 字符串
router.push('/foo')

// 对象
router.push({ path: '/foo' })

// 命名的路由
router.push({ name: 'foo', params: { userId: '123' }})

// 带查询参数,变成 /register?plan=private
router.push({ path: '/foo', query: { plan: 'private' }})

在 HTML 页面元素中实现路由跳转的方法:

<router-link to="/foo">Go to Foo</router-link>

在浏览历史记录中切换跳转方法:

router.go(n)
// n 表示第几个历史记录,n 必须是整数

前端路由往往嵌套多层。下边我们来模拟一个复杂的场景,构建路由信息,详细路由表如下所示:

import VueRouter from "vue-router"
import BaseLayout from '@/layout/BaseLayout'
import EmptyLayout from '@/layout/EmptyRouter'
import Dashboard from '@/views/Dashboard'
import Login from '@/views/Login'

import Modeller from '@/views/modeller/Modeller'
import ModelUpdate from '@/views/modeller/ModelUpdate'
import ModelColumn from '@/views/modeller/column/ModelColumn'
import ModelColumnUpdate from '@/views/modeller/column/ModelColumnUpdate'

// 前端路由表
const routes = [{
    path: '',
    component: EmptyLayout,
    redirect: 'dashboard',
    children: [{
        path: '/login',
        component: Login,
        name: 'login',
        meta: {
            title: '登录'
        }
    }]
}, {
    path: '',
    component: BaseLayout,
    redirect: 'dashboard',
    children: [{
        path: 'dashboard',
        component: Dashboard,
        name: 'dashboard',
        meta: {
            title: '首页'
        }
    }]
}, {
    path: '/',
    component: BaseLayout,
    children: [{
        path: 'modeller',
        component: Modeller,
        meta: {
            title: '数据集管理'
        }
    }, {
        path: 'modeller',
        component: EmptyLayout,
        meta: {
            title: '数据集管理'
        }, children: [
            {
                path: 'add',
                name: 'add',
                component: ModelUpdate,
                meta: {
                    title: '新增数据集',
                }
            }, {
                path: 'column',
                name: 'column',
                component: ModelColumn,
                meta: {
                    title: '字段管理'
                }
            }, {
                path: 'column',
                component: EmptyLayout,
                meta: {
                    title: '字段管理'
                },
                children: [
                    {
                        path: 'add',
                        name: 'modeller-column-add',
                        component: ModelColumnUpdate,
                        meta: {
                            title: '新增字段'
                        }
                    }
                ]
            }
        ]
    }]
}]

var router = new VueRouter({
    scrollBehavior: () => ({ y: 0 }),
    routes
})

export default router;

路由组件之间嵌套层级比较多,例如当用户请求 /modeller/column 时,页面渲染顺序是:

  • 先打开 BaseLayout 组件;
  • 然后在这个组件中找到
  • 接着在打开 EmptyLayout 组件,EmptyLayout 组件被嵌入 BaseLayout 组件的 内;
  • 最后打开 ModelColumn 组件,此时 ModelColumn 的上级组件即 EmptyLayout 组件内查找 ,然后将 ModelColumn 组件嵌入到 EmptyLayout 组件的 内。

前端路由定义时,如果路由中包含了 children 属性,那么这个组件内一定带有 DOM 元素 ,否则 children 内的组件无处安放。我们可以查看一下整个项目的入口组件 App.vue。里边就是一个

<!--App.vue-->

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "app",
  methods: {
    autoHeight() {
      // 初始化浏览器高度
      this.$store.dispatch("autoHeight");
    }
  },
  beforeCreate() {
    this.$store.dispatch("autoHeight");
  },
  mounted() {
    window.onresize = () => {
      // 浏览器
      this.autoHeight();
    };
  }
};
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
body {
  padding: 0px;
  margin: 0px;
}
</style>

11. 登录入口设计

登录入口主要负责接收用户账号和密码,校验用户身份。当用户身份校验通过后,进入系统。现在流行的登录方式非常多。常见的几种登录方式如下所示:

  • 微信扫码登录
  • 支付宝扫码登录
  • 手机验证码登录
  • 用户账号密码登录
  • 微博授权登录

上边的几种登录方式,从技术实现的角度来看,主要为下边几种:

账号密码登录

最常见的登录方式是用户账号密码登录。后台校验用户账号和密码是否正确,如果正确,则返回有效的 token 信息, Web 端上携带 token 信息请求后台服务。

短信验证码登录

Web 端向后台服务发起请求获取短信验证码,短信运营商将验证码发送到用户手机上。用户通过手机号 + 短信验证码请求后台短信登录接口。后台服务使用手机号 + 短信验证码到短信运营商(或者自己开发的验证接口)请求验证,如果验证通过,则用户使用短信登录成功,后台服务向 Web 端下发 token 信息,Web 端携带 token 信息进入系统。

二维码扫码登录

实质上是通过手机上已经登录的账号来获取新的登录令牌。二维码通常分为活码和静态码表。

  • 静态码是将文字信息翻译成二维码,这种二维码存储信息量有限,且不能动态改变。如我们将 hello 这个单词做成一个静态的二维码。这样不论是谁来扫描,得到的都是 hello 这个词。
  • 活码是将一个 URL 地址做成二维码。扫描二维码时得到这个 URL 地址,然后跳转到这个 URL 地址获取真正的内容。

扫码登录的常见流程如下(大概流程,中间省略一些验证细节):

  • Web 页面上选择扫码登录,Web 上显示出二维码,此时的二维码中包含一个唯一 ID 信息,且这个 ID 信息会被记录到后台服务中。
  • APP 上(假设这个 APP 是自己公司开发,且支持扫码登录功能)扫描二维码,读取到二维码中包含的内容(包含了第一步中的唯一 ID)。
  • APP 点击确认登录,此时手机 APP 向后台发送自己的登录信息(也就是 APP
    扫码登录的前提是 APP 在手机上已经登录),并带上第二步读取到的唯一 ID 信息。
  • 后台服务收到用户在手机上的登录信息以及唯一 ID 信息。后台服务开始校验用户登录信息是否有效,如果有效,则修改服务端存储的唯一 ID 对应的状态为 true。
  • Web 端通过唯一 ID 查询到后台服务对应的状态为 true 时,后台返回新的登录 token 信息。此时 Web 端携带 token 信息进入系统。

第三方授权登录

第三方授权也是一种比较常见的登录方式,如微博账号登录、微信账号登录、QQ 账号登录等等。第三方授权登录流程是:

  • 用户点击第三方授权登录请求,跳转到微博、QQ、微信的登录页面。微博、QQ、微信校验用户登录信息,如果用户登录信息校验通过,则回调我们的后台服务地址,告诉我们的服务器授权登录成功,并返回 token 信息给我们的后台服务;
  • 我们的服务获取到 token 信息后,可以根据这个 token 与微博、QQ或微信建立绑定关系。如果此时的 token 权限过大,则后台服务可以根据这个 token 来操作用户微博、QQ、微信部分功能(一些小网站上,建议不要轻易在上边使用进行第三方授权的方式登录,万一不法网站获取到权限过大的授权,此时可能存在信息泄露的风险)。
  • 我们的服务器收到微博、QQ或微信的回调后。我们的后台服务生成的 token 信息,并下发给 Web 端,Web 端携带我们服务器下发的 token 信息进入系统。

下边我们来实现一个简单的账号密码登录页面,示例代码如下所示:

<template>
  <div>
    <el-row style="margin-top: 80px;">
      <el-col :span="8" :offset="8">
        <el-form>
          <el-form-item prop="username" label="用户名:">
            <el-input name="username" v-model="username" placeholder="用户账号" />
          </el-form-item>
          <el-form-item prop="password" label="密码">
            <el-input type="password" name="passwrod" v-model="password" placeholder="用户密码" />
          </el-form-item>
        </el-form>
        <el-button @click="loginSubmit">&nbsp;&nbsp;</el-button>
      </el-col>
    </el-row>
  </div>
</template>
<script>

import { login } from '@/api/login.js';

export default {
  name: "login",
  data() {
    return {
      username: "admin",
      password: "123456"
    };
  },
  methods: {
    loginSubmit() {
      login(this.username, this.password).then(resp => {
          if (resp) {
             // 修改登录状态
             this.$store.dispatch('loginStatus', true);
             this.$router.push('/dashboard')
          }
      });
    }
  }
};
</script>

当用户在 Web 上输入用户名和密码后,点击登录按钮。此时将会执行 loginSubmit 方法。这个方法负责调用后台服务登录接口。登录成功后跳转到系统首页。

12. Mock 数据设计

当我们在开发前端 Web 服务时,可能对应的后台服务尚未开发完成,此时,我们可以借助于 Mock 服务来模拟后台服务,当 Web 端使用 Axios 向后台服务发起请求时,请求将会被 Mock 服务拦截,Mock 服务返回预先定义的数据。当后台服务开发完成后,只需关闭对应的 Mock 服务,此时请求将会被转发到真正的后台服务。所以,Mock 服务给我们开发前端 Web 提供了挡板功能,在后台服务尚未开发完成的情况下,通过使用 Mock 服务,从而保障前端开发进度。

12.1 Mockjs 集成方法

使用 Yarn 工具在线下载 mockjs 工具包。下载命令如下所示:

yarn add mockjs

mockjs 包下载完成后,将会被存储在当前项目 node_modules 目录下。

12.2 启用 Mockjs 服务

// mock/mock.js
import Mock from 'mockjs'

const mock_source = ['biz.js', 'sys.js']

function load(mock_source) {
    for (let i = 0; i < mock_source.length; i++) {
        let file = import('./data/' + mock_source[i])
        file.then(content => {
            if (content && content.default) {
                // 找到目标
                initMock(content.default)
            }
        })
    }
}

function initMock(rules) {
    for (let [rule, resp] of Object.entries(rules)) {
        const element = rule.split(" ")
        if (element && element.length == 2) {
            const rtype = element[0].trim()
            const rurl = element[1].trim()
            Mock.mock(rurl, rtype.toLowerCase(), resp)
        } else {
            Mock.mock(rule, resp)
        }
    }
}

if (mock_source && mock_source.length > 0) {
    load(mock_source)
}

注意事项

Mock 服务启动需要时间,有可能首页打开时,Mock 服务尚未启动,可能出现请求 Mock 服务时出现 404 的情况。比如从 Mock 服务中加载菜单信息。如果加载菜单的时候,Mock 服务未初始化完成,则很有可能请求菜单时出现 404。

解决办法:在 Mock 数据初始化完成后,将 Mock 服务状态保存到 store 中存储。

在 store 中 定义 Mock 服务初始化状态的变量,变量名称为 mockInitFinished,变量定义方式如下所示:

// store/modules/basic.js
const basic = {
    state: {
    	// mock 服务初始化状态
        mockInitFinished: false,
    },
    mutations: {
        MOCK_INIT_FINISHED: (state, status) => {
            // mock 初始化完成
            state.mockInitFinished = status;
        },
    },
    actions: {
        mockInitFinished({ commit }, status) {
        	// 修改 mock 服务初始化状态为已完成
            commit('MOCK_INIT_FINISHED', status)
        },
    }
}

export default basic;

当我们初始化 Mock 服务完成后,通过如下方式更新 store 中 Mock 服务的状态为 true。具体方式如下所示:

// mock/mock.js
import store from '@/store/index.js';

if (mock_source && mock_source.length > 0) {
    load(mock_source)
    // 将 Mock 状态保存到 store 中存储。
    store.dispatch('mockInitFinished', true)
}

从后台 API 获取菜单信息方法修改成如下所示:

	// components/layout/Side.vue
    // 定时获取后台菜单信息
    const timer = setInterval(()=> {
      if (this.mockInitFinished){
        getMenu().then(resp => {
          this.sideMenuList = resp
          clearInterval(timer)
        })
      }
    },1000)

setInterval 方法每隔一个固定的频率执行一次任务。上边的代码中,我们每隔 1 秒钟获取一次后台 API 中的菜单信息。为什么要在此使用定时循环执行的方式获取菜单信息呢? 主要原因是 Mock 服务启动可能晚于我们在 Side 组件中查询后台 API /menu 的时间,这样必然会出现请求 404 的错误。

为了解决这个问题,我们可以通过引入定时器,每隔 1 秒钟定时查询一次 Mock 服务的状态,当 Mock 服务启动后,再去请求获取 Mock 服务的数据。如果我们请求的是真实的后台环境,那么我们就没必要引入 setinterval 这种方式来查询后台服务的菜单信息

Mock 服务初始化完成后,我们开始根据具体的 API 需求编写 Mock 服务。比如编写一个菜单查询的 Mock 服务。详细信息如下所示:

// mock/data/sys.js
const menu = {
    'GET /menu': {
        statusCode: "200", statusMessage: "succcess", data: [
        {
          menuId: "2",
          menuType: 1,
          menuName: '系统管理',
          iconClass: 'el-icon-location',
          children: [
            {
              menuId: "2-1",
              menuType: 2,
              menuName: '数据集管理',
              path: '/modeller',
            },
            {
              menuId: "2-2",
              menuType: 2,
              menuName: '内容管理',
              path: '/modeller',
            }
          ]
        },
        {
          menuId: "3-1",
          menuType: 2,
          menuName: '数据集管理',
          path: '/modeller',
        }
      ]
    }
}

export default menu;

13. 前端代理服务

在前后端分离模式开发中,后台服务往往部署在某一个 K8s 集群中,Web 与后台服务可能不在一个网络(尤其是开发阶段),此时 Web 跨域请求后台服务可能出现跨域失败的问题。可以通过设置前端代理的方式来解决这个问题。

在项目根目录创建 vue.config.js 文件。然后添加如下代码:

// vue.config.js
module.exports = {

    devServer: {
        port: 8080,
        proxy: {
            '/': {
                // process.env.VUE_APP_BASE_API 替换成对应的后台服务地址
                target: process.env.VUE_APP_BASE_API,
                changeOrigin: true,
                secure: false,
                pathRewrite: {
                    '^/': '/'
                }
            }
        }
    }
}

Web 端通过 Axios 发起后台服务请求,都会被转发到 process.env.VUE_APP_BASE_API 这个地址,通过设置 changeOrigin 为 true,有效的解决跨域请求失败的问题。

本篇 Chat 代码地址

https://github.com/hzwy23/vue-admin

Vue.js 官方网站地址

https://cn.vuejs.org/

Vue Cli 官网网站地址

https://cli.vuejs.org/zh/

Vue Router 官方网站地址

https://router.vuejs.org/zh/

Vuex 官方网站地址

https://vuex.vuejs.org/zh/

Element UI 官方网站地址

https://element.eleme.cn/#/zh-CN

你可能感兴趣的:(前端开发,vue.js,elementui,前端)