后端开发也要知道的Vue.js(下):实战篇

  在《准备篇》中,我们已经搭建好了Vue的开发和部署环境,并且尝试使用vue-cli脚手架构建了第一个Vue项目testvue。下面我们就在testvue的基础上进行完善,希望它能具备以下特性和功能:
1)整个页面布局由顶栏、侧边栏、主体区域组成。顶栏展示网站名称,当前用户昵称。昵称上鼠标悬停展示个人中心、退出登录等按钮。侧边栏显示当前用户可操作的菜单,刚登录进来右侧主体区域显示首页欢迎语,点击菜单后主体区域显示当前菜单内容。主体区域的第一行显示面包屑菜单。
2)用户未登录或登录已过期时,点击任何操作都跳转到登录页。登录成功将当前用户token,uuid保存到cookie,退出登录时清除上述cookie。
3)不同用户可操作的菜单不同,登录成功后调后端接口获取当前用户菜单。
4)订单页展示订单列表,点击订单号跳转到该订单详情页,列表页具有查询和重置功能。
5)由于本文只介绍Vue的相关知识,所有后端接口都采用node.js 来mock数据,可参考:https://www.jianshu.com/p/3becdd6ea766。 接口的数据结构统一采用如下结构,result中包含返回的数据信息:

{
	"errorCode":0,
	"errorMessage":"成功",
	"result":{	xxxxxx }
}

  效果图如下:
在这里插入图片描述
  本例中技术栈采用:Vue.js + vuex + vue-router + axios + element-ui
常用参考文档:
1) Vue.js:https://cn.vuejs.org/v2/api/
2) Vuex :https://vuex.vuejs.org/zh/guide/state.html
3) vue-router:https://router.vuejs.org/zh/
4) vue-axios:http://www.axios-js.com/zh-cn/docs/vue-axios.html
5) ES6:https://www.runoob.com/w3cnote/es6-tutorial.html
6) element-ui:https://element.eleme.cn/

一、 入口文件介绍:App.vue及main.js

  1. App.vue内容如下:
<template>
  <div id="app">
    <router-view/>
  </div>
</template>
  1. main.js内容如下:
//注释1:引入相关插件、组件等
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import util from '@/libs/util'

//注释2:引入element-ui
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)

Vue.config.productionTip = false

//注释3:创建Vue根实例
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

//注释4:路由拦截,权限验证
router.beforeEach((to, from, next) => {
  // 验证当前路由所有的匹配中是否需要有登录验证的
  if (to.matched.some(r => r.meta.auth)) {
    // 这里暂时将cookie里是否存有token作为验证是否登录的条件
    const token = util.cookies.get('token');
    if (token && token !== 'undefined') {
      next()
    } else {
      // 没有登录的时候跳转到登录页
      location.href = "/"
    }
  } else {
    // 不需要身份校验 直接通过
    next()
  }
})

router.afterEach(to => {
})

  App.vue是Vue应用程序的根实例,也是其核心所在。main.js作为入口js,与App.vue配合完成根实例的构建。
  main.js中有几点需要解释:

1)注释1处通过import引入了App.vue,由于Vue中引入文件、组件、插件等都可以省略后缀,import App from ‘./App’ 即引入当前目录下的App.vue 。另外需要注意的是import util from ‘@/libs/util’ 的路径中存在@符号,表示根目录,即src。因此“.”和“@”的写法可理为相对路径和绝对路径。
2)注释2处引入了element-ui的js和css,并通过Vue.use(ElementUI)调用。小伙伴可能存在疑问的地方是:为什么有的插件import了就可以直接用,而有的插件需要Vue.use()后才能使用呢?Vue.use()使用后调用的是组件的install 方法,所以引入前插件如果含install方就需要调用Vue.use(),否则可直接使用。
3)注释3处通过调用Vue的构造函数Vue()创建根实例,向该构造函数传入参数,提供构造应用程序所需要的的选项。其中选项el用来指定一个DOM元素,本例中即指定了App.vue中id为app的元素,vue会在这个元素上挂载当前应用程序。后面传入router和store, 用于在Vue实例中暴露这些属性或者方法,后续可直接在各组件中使用,使用的时候需增加$前缀,用于和用户定义属性区分开来,如this.$router。
  Vue对象参数的几个常用选项包括:挂载元素el、data函数成员、methods对象成员、模板template、生命周期钩子、props属性声明、computed计算成员、watch监视成员。此处只简要介绍,后面遇到时再详细说明。另外,一个组件通常需要导出为一个对象,供其他组件中通过import引用,其步骤通常如下:

var app= new Vue({
   xxxxxxxxxxxxxxxx
})
//默认输出,可在其他组件引用
export default app 

  也可以直接通过如下方法导出vue对象,下述方法貌似被使用的更广泛:

export default {
  name: '',
  components: {},
  data: () {}, // data函数成员
  watch: {}, // watch监视成员
  computed: {}// computed计算成员
  created: function () {},
  methods: {}, // methods对象成员
  actions: {}
}

  由于main.js中的根组件对应的Vue实例不需要给其他组件使用,因此不需要export。
4)注释4为路由拦截(路由的内容在第三部分介绍,小伙伴可以后面返回再来读这部分)。路由切换时都会执行这段代码。router.beforeEach是指在路由跳转之前执行。三个参数的含义如下:
a)to: router即将进入的路由
b)from: 当前导航即将离开的路由
c)next: 是一个方法,进行管道中的一个钩子,如果执行完了,则导航的状态就是 confirmed (已确认);否则为false,即终止导航。
  需要解释的一句代码:if (to.matched.some(r => r.meta.auth)),to.matched表示即将进入的路由匹配上的所有路由记录,是一个数组。.some() 方法即测试该数组中的某些元素是否通过了指定函数的测试,这个指定函数即括号中的r => r.meta.auth,箭头函数,等价于:

function(r){
    return  r.meta.auth;
}

  举个例子,如果to(目的路由)为:

  {
        path: '/user',
        name: User,
        meta: { 
		title: '测试',
		auth: true
 }

  则if (to.matched.some(r => r.meta.auth))执行记结果即为true。总结来说,路径中的meta的auth属性为true的,都需要通过路由拦截,进行后续一系列关于token的判断,为false则直接通过。

二、整体布局:

  下面我们开始编写布局文件,包含顶栏、侧边栏、主体区域。首页仅显示简单的欢迎。配置/home路径与该布局文件绑定。

  1. 在src目录下新建layouts文件夹,新建layout.vue布局文件。内容如下:
<template>
  <div>
<el-container>
  <!--顶栏-->
      <el-header class="homeHeader">
        <div class="title">后台管理系统</div>
        <div>
      	  <!-- 注释1: @ command -->
          <el-dropdown class="userInfo" @command="commandHandler">
            <!--注释2{{}} -->
			<span class="el-dropdown-link">{{currentUser.name}}</span>
            <el-dropdown-menu slot="dropdown">
              <el-dropdown-item command="userinfo">个人中心</el-dropdown-item>
              <el-dropdown-item command="setting">设置</el-dropdown-item>
              <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </div>
      </el-header>
      <el-container>
        <!--侧边栏-->
        <el-aside width="200px">
          <el-menu router unique-opened>
            <!--注释3:index -->
            <el-submenu :index="index+''" v-for="(item,index) in routes" :key="index">
              <template slot="title">
                <i style="color: #409eff;margin-right: 5px" :class="item.iconCls"></i>
                <span>{{item.name}}</span>
              </template>
              <!--注释4:v-for -->
              <el-menu-item
                :index="child.path"
                v-for="(child,indexj) in item.children"
                :key="indexj"
              >{{child.name}}</el-menu-item>
            </el-submenu>
          </el-menu>
        </el-aside>
	    <!--主体部分-->
        <el-main>
          <!--面包屑菜单  注释5.:v-if -->
          <el-breadcrumb
            separator-class="el-icon-arrow-right" v-if="this.$router.currentRoute.path!='/home'">
            <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item>{{this.$router.currentRoute.name}}</el-breadcrumb-item>
          </el-breadcrumb>
		  <div class="homeWelcome" v-if="this.$router.currentRoute.path=='/home'">欢迎您!</div>
		  <!--路由容器-->
          <router-view class="homeRouterView" />
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script>
//注释6:mapState
import { mapState } from "vuex";
import util from "@/libs/util.js";
export default {
  name: "Home",
  //注释7:计算属性
  //使用vuex的辅助方法:mapState代替this.$store.state.routes的复杂写法
  //computed: mapState(["routes", "currentUser"]),
  computed: {
    ...mapState(["routes"]),
    currentUser() {
      return this.$store.state.currentUser;
    }
  },
  methods: {
    commandHandler(cmd) {
      if (cmd == "logout") {
        this.$confirm("此操作将注销登录, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning"
        })
          .then(() => {
            // 删除cookie
            util.cookies.remove("token");
            util.cookies.remove("uuid");
            // 清空 store 用户和菜单路由信息
            this.$store.commit("initUser", {});
            this.$store.commit("initRoutes", null);
            // 跳转路由
            location.href = "/";
          })
          .catch(() => {
            this.$message({
              type: "info",
              message: "已取消操作"
            });
          });
      } else if (cmd == "userinfo") {
        this.$router.push("/user/info");
      }
    }
  }
};
</script>

<style>
.homeRouterView {
  margin-top: 10px;
}
.homeWelcome {
  text-align: center;
  font-size: 30px;
  font-family: 华文行楷;
  color: #409eff;
  padding-top: 50px;
}
.homeHeader {
  background-color: #409eff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0px 15px;
  box-sizing: border-box;
}
.homeHeader .title {
  font-size: 30px;
  font-family: 华文行楷;
  color: #ffffff;
}
.homeHeader .userInfo {
  cursor: pointer;
}
.el-dropdown-link {
  display: flex;
  align-items: center;
}
</style>

  layout.vue是一个单文件组件(组件的知识后面再详细介绍),和App.vue一样,每个.vue文件都是一个Vue实例,包含三种类型的顶级语言块 <template>, <script> 和 <style>,这三个部分分别代表了 html、js、css。
  由于main.js中引入了element-ui,此处可以直接使用。Layout.vue中标签包裹的内容就是使用element-ui编写的html,几个重要组件已在代码上方注释。element-ui有非常丰富的组件库,详情可查看官方文档,例如布局部分的文档如下:

在这里插入图片描述
  需要注意的是主体部分中的,router-view可以理解为一个容器,它渲染的组件是我们使用 vue-router 指定的,因此就可以方便的实现点击左侧菜单切换路由时,其他部分不变,只右侧主体部分(router-view标签所在位置)内容随之切换的效果。
  另外有几处需要稍加解释:
1)注释1的 @ command:由element-ui文档可知,command为点击Dropdown下拉菜单项事件。并且由Vue.js官方文档可知,@符号则是v-on指令的缩写,用于监听事件,文档如下:
在这里插入图片描述
  因此@command=“commandHandler” 即点击菜单项时触发commandHandler()函数。同理@click为点击事件,@select为选择事件等等。@后的事件名称可查找element-ui文档获得,如table组件的事件如下图所示:
在这里插入图片描述

2)注释2的{{}} :这种双花括号的写法是Vue借用了Mustache中的{{…}}语法来进行文本插值,但并不支持完整的Mustache定义。本例中是将currentUser的name属性显示在此: {{currentUser.name}}。
3)注释3处的:index:动态绑定index属性,是v-bind:index的缩写。
  同理<img v-bind:src=“imageSrc”>可缩写为:<img :src=“imageSrc”>。也就是当src的内容为变量时使用:src=“imageSrc”,src内容不变时则直接src=“xxx.png “即可,不用加冒号。
4)注释4处的v-for=”(child,index) in item.children”:即遍历item的children属性内容,其中child表示当前元素,index为当前元素的索引。
5)注释5处的v-if:根据v-if后的表达式有条件的渲染当前元素。

  至此,Layout.vue的html部分基本介绍完,接下来介绍下script部分中几个比较重要的知识点。
1)注释6处的import { mapState } from “vuex” 即引入vuex的辅助工具mapState。要使用mapState,首先要了解vuex是什么。从官方文档:https://vuex.vuejs.org/zh/ 可知,vuex是专门为Vue开发的状态管理模式,集中的存储整个应用程序的状态,一般应用在多个组件需要共享数据或状态的场景。本例中,我们希望当前用户的账号信息和菜单数据可以统一管理,然后在各个组价之间共享。实现方式是,在src目录下创建store目录,新建index.js文件,内容如下:

import Vue from 'vue'
import Vuex from 'vuex'
//引入api目录下封装的请求远程接口的方法
import { getData } from "@/api/request";
Vue.use(Vuex)
const store = new Vuex.Store({
    state: {
        routes: '',
        currentUser: ''
    },
    mutations: {
        initUser(state, user) {
            state.currentUser = user;
        },
        initRoutes(state, userId) {
            if (userId == null) {
                state.routes = [];
            } else {
                //根据userId获取菜单列表
                getData("/api/menus", userId).then(data => {
                    state.routes = data;
                });
            }
        },
    },
    actions: {
    }
})
export default store;

  Vuex有几个常用的概念:State、Getter、Mutation、Action。

  • State:存储状态数据。
  • Mutation:存储用于同步更改状态数据的方法,默认传入的参数为state。
  • Action:存储用于异步更改状态数据,但不是直接更改,而是通过触发Mutation方法实现。
      由于main.js中已近通过new Vue()的参数将store实例注入到了所有子组件中:
new Vue({
  el: '#app',
  router,
//把 store 的实例注入所有的子组件
  store,
  components: { App },
  template: '<App/>'
})

  调用方法如下:

  • state中状态的获取:this.$store.state.routes;
  • mutation中同步方法的调用:this.$store.commit(‘xxx’);
  • actions中异步方法的调用:this.$store.dispatch(‘xxx’);

  可以看出来上述写法比较繁琐,这也就是我们上文中提到的mapState要解决的问题。

import { mapState } from "vuex";
computed: mapState(["routes", "currentUser"])

  之后就可以直接使用routes和currentUser了,与如下写法有相同的效果:

 computed: {
    routes() {
      return this.$store.state.routes;
    },
    currentUser() {
      return this.$store.state.currentUser;
    }
  }

  与mapState 类似的还有mapMutations,、mapActions,分别用来完成mutations和actions中方法的辅助调用。
  小伙伴可能还有疑问的一点就是computed,前文中提到过computed是Vue的计算属性。当其依赖的属性的值发生变化时,计算属性会重新计算,反之则使用缓存中的属性值。不像methods里的方法,每次遇到都执行。
  此外,Layout.vue中<style>标签包裹的是样式,都比较简单,此处不赘述。

三、路由配置:

  我们在第二部分中编写了一个布局文件,包含顶栏侧边栏和主体等内容,那么如何将这个页面与浏览器中的请求路径联系起来呢?这就需要借助于路由,我们使用vue的官方路由管理器vue-router来管理路由。当URL发生变动时,Vue路由器会拦截请求请显示相应的路由。
  为了方便演示,我们在src目录下新建views文件夹,创建多个vue文件表示不同组件,内容可以只写简单的页面名称用以区分,如Auth.vue内容如下,其他几个文件内容都类似:
在这里插入图片描述在这里插入图片描述
  然后在src的router目录下的index.js文件中引入上述组件,并配置路由:

import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/layouts/layout'
import Auth from '@/views/Auth'
import Account from '@/views/Account'
import Order from '@/views/Order'
import Refund from '@/views/Refund'
import Login from '@/views/Login'
import Userinfo from '@/views/Userinfo'
Vue.use(Router)

export default new Router({
  //设置路由的 history 模式,解决路径中#问题
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Login',
      component: Login   
    },
    {
      path: '/home',
      name: 'Layout',
      component: Layout,
      meta: { auth: true }   
    },
    {
      path: '/user',
      name: '用户管理',
      component: Layout,
      meta: { auth: true },
      children: [
        {
          path: '/user/auth',
          name: '权限设置',
          component: Auth,
          meta: { auth: true }   
        }, {
          path: '/user/account',
          name: '账号管理',
          component: Account,
          meta: { auth: true }   
        },{
          path: '/user/info',
          name: '个人中心',
          component: Userinfo,
          meta: { auth: true }   
        }]
    }, {
      path: '/order',
      name: '订单管理',
      component: Layout,
      meta: { auth: true },
      children: [
        {
          path: '/order/myOrder',
          name: '我的订单',
          component: Order,
          meta: { auth: true }   
        }, {
          path: '/order/myRefund',
          name: '我的退换货',
          component: Refund,
          meta: { auth: true }   
        }]
    },
    {
      path: '*',
      redirect: '/home'
    }
  ]
})

  特别需要注意的是:第一部分main.js中介绍过路由拦截router.beforeEach()会对所有meta: { auth: true } 的路由进行拦截,未登录则跳转到根路由/,因此根路由“/”一定不要添加meta: { auth: true },否则会形成死循环,无法正常跳转。
  最后path: '*'的配置,是指之前的所有路由都没有匹配上时,会被该配置拦截,也就是路径不存在时跳转到home页,此处也可以配置跳转到错误页。需要注意的一点是,该配置需放在最后,不然所有路由都会被它拦截。
  这样,路由和组件之间的对应关系已经有了,下面就是编写登录页,登陆成功后调接口获取当前用户所有有权限的路由,并把路由列表渲染在左侧菜单中。
  在src目录下创建Login.vue文件,内容如下:

<template>
  <div>
    <el-form
      :rules="rules"
      ref="loginForm"
      v-loading="loading"
      element-loading-text="正在登录..."
      element-loading-spinner="el-icon-loading"
      element-loading-background="rgba(0, 0, 0, 0.8)"
      :model="loginForm"
      class="loginContainer"
    >
      <h3 class="loginTitle">登录</h3>
      <el-form-item prop="username">
        <el-input
          size="normal"
          type="text"
          v-model="loginForm.username"
          auto-complete="off"
          placeholder="请输入用户名"
        ></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          size="normal"
          type="password"
          v-model="loginForm.password"
          auto-complete="off"
          placeholder="请输入密码"
        ></el-input>
      </el-form-item>
      <el-button size="normal" type="primary" style="width: 100%;" @click="submitLogin">登录</el-button>
    </el-form>
  </div>
</template>

<script>
import { postData } from "@/api/request";
import { mapMutations } from "vuex";
import util from "@/libs/util.js";
export default {
  name: "Login",
  data() {
    return {
      loading: false,
      loginForm: {
        username: "test",
        password: "test123"
      },
      rules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" }
        ],
        password: [{ required: true, message: "请输入密码", trigger: "blur" }]
      }
    };
  },
  methods: {
    ...mapMutations(["initUser", "initRoutes"]),
    submitLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true;
          postData("/api/login", this.loginForm).then(
            resp => {
              this.loading = false;
              if (resp) {
              //将登录后返回的用户信息存储到state中
              //this.$store.commit("initUser", resp);
              //使用vuex的辅助方法mapMutations代替:this.$store.commit("initUser", resp)的复杂写法
           this.initUser(resp.userInfo);

                //将uuid和token存入cookie
                util.cookies.set("uuid", resp.uuid);
                util.cookies.set("token", resp.token);

                //获取该用户权限菜单并初始化路由
                this.$store.commit("initRoutes", resp.userInfo.id);
                //跳转到home页
                let path = this.$route.query.redirect;
                this.$router.replace(path == "/" || path == undefined ? "/home" : path);
              } else {
                alert("登陆失败");
              }
            }
          );
        } else {
          return false;
        }
      });
    }
  }
};
</script>

<style>
.loginContainer {
  border-radius: 15px;
  background-clip: padding-box;
  margin: 120px auto;
  width: 350px;
  padding: 15px 35px 15px 35px;
  background: #fff;
  border: 1px solid #eaeaea;
  box-shadow: 0 0 25px #cac6c6;
}

.loginTitle {
  margin: 15px auto 20px auto;
  text-align: center;
  color: #505458;
}

.loginRemember {
  text-align: left;
  margin: 0px 0px 15px 0px;
}
.el-form-item__content {
  display: flex;
  align-items: center;
}
</style>

  调接口获取到的菜单数据是router中路由数据的子集,如后端返回如下数据,则左侧菜单只展示“权限设置”和“我的订单”2个菜单:

{
    path: '/user',
    name: '用户管理',
    children: [
        {
            path: '/user/auth',
            name: '权限设置',
        }
    ]
}, {
    path: '/order',
    name: '订单管理',
    children: [
        {
            path: '/order/myOrder',
            name: '我的订单',
        }
    ]
}

  需要注意的一点是调用接口的方法。Vue官方推荐使用axios请求后台资源。我们上文中提到的getData,postDate等方法都是对axios接口的封装,分别用来发送get请求和post请求。本例中我们在src目录下创建plugins/axios文件夹,在该文件夹下创建index.js文件,其内容如下:包含创建axios实例,请求和响应的拦截。

import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import util from '@/libs/util'

// 创建一个错误
function errorCreate(msg) {
    const error = new Error(msg)
    errorLog(error)
    throw error
}

// 记录和显示错误
function errorLog(error) {
    // 打印到控制台
    if (process.env.NODE_ENV === 'development') {
        console.log(error)
    }
    // 显示提示
    Message({
        message: error.message,
        type: 'error',
        duration: 5 * 1000
    })
}

// 创建一个 axios 实例
const service = axios.create({
    baseURL: process.env.VUE_APP_API,
    timeout: 5000 * 3 // 请求超时时间
})

// 请求拦截器
service.interceptors.request.use(
    config => {
        return config
    },
    error => {
        // 发送失败
        console.log(error)
        Promise.reject(error)
    }
)

// 响应拦截器
service.interceptors.response.use(
    response => {
        // dataAxios 是 axios 返回数据中的 data
        const dataAxios = response.data
        // 这个状态码是和后端约定的
        const { errorCode } = dataAxios
        // 根据 code 进行判断
        if (errorCode === undefined) {
           // 如果没有 code 代表这不是项目后端开发的接口
            return dataAxios
        } else {
            // 有 code 代表这是一个后端接口 可以进行进一步的判断
            switch (errorCode) {
                case 0:
                    // [ 示例 ] code === 0 代表没有错误
                    return dataAxios.result
                case -1:
                    // [ 示例 ] 其它和后台约定的 code
                    errorCreate(`[ code: -1 ] ${dataAxios.errorMessage}: ${response.config.url}`)
                    break
                case 302:
                    MessageBox({
                        title: '跳转提示',
                        message: `${dataAxios.errorMessage}`,
                        confirmButtonText: '登录',
                        callback: () => {
                            location.href = '/'
                        }
                    })
                    break
                default:
                    // 不是正确的 code
                    util.message.error(`${dataAxios.errorMessage}`)
                    break
            }
        }
    },
    error => {
        if (error && error.response) {
            switch (error.response.status) {
                case 400: error.message = '请求错误'; break
                case 401: {
                    MessageBox({
                        title: '跳转提示',
                        message: '未授权,请登录',
                        confirmButtonText: '登录',
                        callback: () => {
                            location.href = '/'
                        }
                    })
                    break
                }
                case 403: error.message = '拒绝访问'; break
                case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
                case 408: error.message = '请求超时'; break
                case 500: error.message = '服务器内部错误'; break
                case 501: error.message = '服务未实现'; break
                case 502: error.message = '网关错误'; break
                case 503: error.message = '服务不可用'; break
                case 504: error.message = '网关超时'; break
                case 505: error.message = 'HTTP版本不受支持'; break
                default: break
            }
        }
        errorLog(error)
        return Promise.reject(error)
    }
)
export default service

  其中响应拦截器主要针对后端返回的错误码来决定后续处理,该错误码只要和当前项目的后端开发约定好即可,没有统一的规定。如本例中约定返回302表示未授权,需跳转到登录页。下面进行axios中各种类型请求的封装:get、post、put、delete等。在src目录下创建api目录,该目录下创建request.js文件,内容如下:

import request from '@/plugins/axios'

// 获取数据
export function getData(url, params) {
    return request({
        url,
        method: 'get',
        params
    })
}

// 提交数据
export function postData(url, data) {
    return request({
        url,
        method: 'post',
        data
    })
}

// 提交数据
export function postFormData(url, data, headers) {
    return request({
        url,
        method: 'post',
        data,
        headers
    })
}

// 更新数据
export function putData(url, data) {
    return request({
        url,
        method: 'put',
        data
    })
}

// 删除数据
export function deleteData(url, data) {
    return request({
        url,
        method: 'delete',
        data
    })
}

  使用时按需引入,如只引入getDate和postData:

import { getData, postData } from "@/api/request";

  至此,基本的框架已经搭建起来,具备了登录,动态菜单渲染,路由切换等功能。也引入了element-ui,vue-router,vuex,axios等,辅助我们编写页面及实现一些特定的功能。

四、高级组件:

  在Vue.js中,组件是一个非常强大的概念,它可以减少或简化代码。运用组件,可以提取出代码的重复部分,并在整个应用程序中复用。本例中我们采用组件的方式编写订单页面。在views目录下创建Order文件夹,下面包含index.vue文件和conponents文件夹,conponents文件夹下是各组件所在的文件夹,如本例中的SearchBlock和SearchTableBlock,分别为订单页面的顶部搜索部分和表格部分,如下图红框所示:
在这里插入图片描述

  1. 在SearchBlock文件夹下创建index.vue,内容如下:
<template>
  <div class="search-block">
    <el-form ref="form" :model="form" size="mini"  label-position='left' label-width="80px">
      <el-row :gutter="30">
        <el-col :span="6">
          <el-form-item label="订单ID">
            <!--注释1:v-model -->
            <el-input v-model="form.orderId" width="100%" clearable></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="6">
          <el-form-item label="订单状态">
            <el-select v-model="form.orderStatus" placeholder="不限"  width="100%" clearable style="width: 100%;">
              <el-option
                v-for="item in orderStatusOptions"
                :key="item.value"
                :label="item.name"
                :value="item.value"
              ></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="筛选时间">
            <el-date-picker
              width="100%"
              v-model="form.date"
              type="daterange"
              range-separator="至"
              start-placeholder="开始时间"
              end-placeholder="结束时间"
            ></el-date-picker>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </div>
</template>

<script>
import { getData } from "@/api/request";
export default {
  name: "SearchBlock",
  //注释2:props父子组件间传递数据
  props: {
    // 筛选条件
    form: Object
  },
  //注释3:created生命周期钩子
  created() {
    //获取订单状态列表
    this.getOrderStatusOptions();
  },
  data() {
    return {
      orderStatusOptions: []
    };
  },
  methods: {
    getOrderStatusOptions() {
      getData(`/api/order-status`).then(data => {
        this.orderStatusOptions = data || [];
      });
    }
  }
};
</script>

<style scoped>
.search-block {
  margin-top: 20px;
}
</style>
  1. 在SearchTableBlock文件夹下创建index.vue,内容如下:
<template>
  <!--表格展示-->
  <div>
    <!-- 操作 -->
    <div class="table-action">
      <div>
        <el-button size="mini" type="primary" @click="handleSearch()">查询</el-button>
        <!-- 注释4:$emit 向父组件传递数据 -->
        <el-button size="mini" type="primary" @click="$emit('reset')">重置</el-button>
      </div>
    </div>
    <el-table
      :data="data"
      border
      style="width: 100%"
      size="mini"
      v-loading="loading"
      header-align="center"
    >
      <el-table-column prop="orderId" label="订单号">
        <template slot-scope="scope">
          <router-link type="text" :to="`/order/detail/${scope.row.orderId}`">{{scope.row.orderId}}</router-link>
        </template>
      </el-table-column>
      <el-table-column prop="orderStatus" label="订单状态"></el-table-column>
      <el-table-column prop="receiver" label="收件人"></el-table-column>
      <el-table-column label="编辑">
        <!--  注释5:slot-scope -->
        <template slot-scope="scope">
          <div class="action_cell">
            <el-button
              round
              type="primary"
              size="mini"
              slot="reference"
              @click="handleEdit(scope.row)"
              :disabled="!scope.row.canEdit"
            >编辑</el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页 -->
    <el-pagination
      class="table-pagination"
      layout="total, prev, pager, next"
      @current-change="handleChange"
      :current-page="currentPage"
      :page-size="size"
      :total="total"
    ></el-pagination>
  </div>
</template>

<script>
export default {
  name: "SearchTableBlock",
  components: {},
  props: {
    data: {
      type: Array,
      default: []
    },
    page: {
      type: Number,
      default: 1
    },
    total: {
      type: Number,
      default: 0
    },
    loading: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      // 每页几条
      size: 15,
      // 当前页
      currentPage: 1
    };
  },
  methods: {
    //查询
    handleSearch() {
      //子组件向父组件传值
      this.$emit("search", 1);
    },
    handleChange() {}
  }
};
</script>

<style scoped>
.table-action {
  display: flex;
  flex-direction: row;
  justify-content: end;
  padding: 9px 14px;
  border: 1px solid #EBEEF5;
  border-bottom: 0px;
}
.table-pagination.el-pagination {
  text-align: right;
  margin-top: 20px;
}
</style>
  1. Order文件夹下的index.vue是上述2组件的父组件,内容如下:
<template>
  <div>
    <!--条件筛选组件-->
    <SearchBlock :form="form" />
    <!-- 表格组件 -->
    <SearchTableBlock
      :data="tableData"
      :page="pageNo"
      :total="total"
      :loading="loading"
      @search="handleSearch"
      @reset="handleReset"
    />
  </div>
</template>

<script>
import { getData } from "@/api/request";
import cloneDeep from "lodash/cloneDeep";
import SearchBlock from "./components/SearchBlock";
import SearchTableBlock from "./components/SearchTableBlock";

// 筛选条件 字段
const formData = {
  orderId: "",
  orderStatus: "",
  date: []
};

export default {
  name: "Order",
  components: {
    SearchBlock,
    SearchTableBlock
  },
  data() {
    return {
      form: {
        ...formData
      },
      // 当前页
      pageNo: 1,
      // 总数
      total: 0,
      // table数据
      tableData: [],
      // loading
      loading: false
    };
  },
  created() {
    this.handleSearch(1);
  },
  methods: {
    // 查询接口
    handleSearch(page) {
      this.loading = true;
      let startDate, endDate;
      if (this.form.date != null) {
        startDate = this.form.date[0];
        endDate = this.form.date[1];
      }

      //拼接请求参数
      let param = {
        orderId: this.form.orderId,
        orderStatus: this.form.orderStatus,
        startDate: startDate,
        endDate: endDate,
        pageNum: page,
        pageSize: 15
      };

      // 获取table数据
      getData("/api/order-list", param).then(data => {
        this.tableData = data.resultList;
        this.total = data.totalNum;
        this.loading = false;
      });
    },
    // 重置
    handleReset() {
      this.form = cloneDeep(formData);
    }
  }
};
</script>

  需要注意的几点如下:
1)注释1:SearchBlock中的v-model:在表单中使用,实现数据的双向绑定。
<el-input v-model=“form.orderId” width=“100%” clearable>
  所谓“双向”,大概可以理解为:数据的变化会反映到组件上,组件的输入改变时也会反映到其绑定的数据上,这也就是其与v-bind(缩写是“:”)的不同之处。
2)注释2:使用props实现父子组件间传递数据。在父组件中使用SearchBolck时:
<SearchBlock :form=“form” /> 传入了form,即SearchBlock中的props里定义的内容:

props: {
    form: Object
}

3)注释3:created生命周期钩子。从官方文档可知,Vue的生命周期如下图,当执行到不同的阶段,会触发相应阶段的钩子函数。本例中的created,会在实例创建完成后被调用,即实例创建完成后调用“获取订单状态列表”的方法,初始化订单状态下拉框的选项数据。
在这里插入图片描述
4)SearchTableBlock中的注释4:$emit(‘reset’),即向父组件的reset传递数据,此处数据为空,不为空时可写成:$emit(‘reset’,data),将data数据传递给父组件的reset事件函数。如本例中,父组件使用SearchTableBlock时:即SearchTableBlock中点击重置按钮时,会调用其父组件的handleReset方法。

<SearchTableBlock
      :data="tableData"
      :page="pageNo"
      :total="total"
      :loading="loading"
      @search="handleSearch"
      @reset="handleReset"
/>

5)SearchTableBlock中的注释5:slot-scope,为作用域插槽。它就像一个临时变量,包含从组件传入的属性,如本例中,使用slot-scope获取当前行的数据。除了这里介绍的作用于插槽外,插槽的种类还有很多,用法也不尽相同,小伙伴们可以自行查阅资料了解更多内容。

<template slot-scope="scope">
          <div class="action_cell">
            <el-button
              round
              type="primary"
              size="mini"
              @click="handleEdit(scope.row)"
              :disabled="!scope.row.canEdit"
            >编辑</el-button>
          </div>
</template>

四、其他:

  除了上述几部分比较大的知识模块外,还有些零散的小功能,下面我们介绍下其实现方法。

1.动态路由:
  第三部分中已经完成了订单列表页,我们希望点击订单号时跳转到该订单的详情页。该功能可通过匹配带参数的动态路由来实现,如订单号为:234567890,则详情页的路径为:/order/detail/234567890。在router文件中,动态路由的配置是通过冒号(:)来实现的,如下:

{
     path: '/order/detail/:id',
     component: OrderDetail,
     meta: { title: '订单详情' }
}

  详情页Orderdetail.vue内容如下:

<template>
    <div>
        订单详情:{{this.$route.params.id}}
    </div>
</template>
<script>
    export default {
        name: "Orderdetail"
    }
</script>

  this.$route.params.id获取的即路径中的参数,即orderId。
  订单列表页中的跳转是通过如下方式实现的:

<el-table-column prop="orderId" label="订单号">
  <template slot-scope="scope">
  <router-link type="text" :to="`/order/detail/${scope.row.orderId}`">{{scope.row.orderId}}</router-link>
  </template>
</el-table-column>

2.退出登录:
  在顶栏的右侧点击用户名可看到包含“退出登录”的下拉菜单。退出登录的实现主要是清除store中的全局用户状态数据,清除cookie中的token和uuid,然后跳转到登录页。内容如下(上文中layout.vue中也有这段代码):

commandHandler(cmd) {
  	  //退出登录
      if (cmd == "logout") {
        this.$confirm("此操作将注销登录, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning"
        })
          .then(() => {
            // 删除cookie
            util.cookies.remove("token");
            util.cookies.remove("uuid");
            // 清空 store 用户和菜单路由信息
            this.$store.commit("initUser", {});
            this.$store.commit("initRoutes", null);
            // 跳转路由
            location.href = "/";
          })
          .catch(() => {
            this.$message({
              type: "info",
              message: "已取消操作"
            });
          });
      } else if (cmd == "userinfo") {
        this.$router.push("/user/info");
      }
    }

3.watch的用法:
  watch是Vue中的一个重要概念,我们在第一部分介绍Vue构造函数的参数时有提及,用来监测Vue实例中的数据变动。watch的一个简单应用如下:
1)订单列表页的编辑按钮改成组件的形式:

<el-table-column label="编辑">
        <template slot-scope="scope">
          <div class="action_cell">
            <edit-dialog
              :row="scope.row"
              :canEdit="scope.row.canEdit"
              @updateOrder="updateOrder"
            >
          </div>
        </template>
</el-table-column>

2)编辑组件的内容如下:

<template>
  <div :style="style.editDialog">
    <el-button
      round
      @click="handleColumnClick()"
      type="primary"
      size="mini"
      :disabled="!canEdit"
    >编辑</el-button>
    <el-dialog title="编辑" :visible.sync="dialogFormVisible">
      <el-form :model="formRow" :rules="rules" ref="useMatrixForm">
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="订单号" :label-width="formLabelWidth" prop="orderId">
              <el-input v-model="formRow.orderId"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="订单状态" :label-width="formLabelWidth" prop="orderStatus">
              <el-select v-model="formRow.orderStatus" placeholder="请选择">
                <el-option
                  v-for="item in orderStatusOptions"
                  :key="item.value"
                  :label="item.name"
                  :value="item.value"
                ></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="收件人" :label-width="formLabelWidth" prop="receiver">
              <el-input v-model="formRow.receiver"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="handleSubmit('useMatrixForm')">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { getData, postFormData } from "@/api/request";
import util from "@/libs/util";
export default {
  //注释1:监听dialogFormVisible数据变化
  watch: {
    dialogFormVisible: function(val) {
      if (val) {
        this.formRow = {
          ...this.row
        };
      }
    }
  },
  created() {
    //获取订单状态列表
    this.getOrderStatusOptions();
  },
  data() {
    return {
      style: {
        editDialog: {
          display: "inline-block",
          marginRight: "5px"
        }
      },
      dialogFormVisible: false,
      formRow: {},
      //订单状态选项
      orderStatusOptions: [],
      rules: {
        orderId: [{ required: true, message: "订单号必填", trigger: "blur" }],
        orderStatus: [
          { required: true, message: "订单状态必填", trigger: "blur" }
        ],
        receiver: [{ required: true, message: "收件人必填", trigger: "blur" }]
      },
      formLabelWidth: "110px"
    };
  },
  props: {
    // 每一列的数据
    row: {
      type: Object,
      default: () => {}
    },
    canEdit: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    getOrderStatusOptions() {
      getData(`/api/order-status`).then(data => {
        this.orderStatusOptions = data || [];
      });
    },
    handleColumnClick() {
      this.dialogFormVisible = true;
    },
    // 提交
    handleSubmit(formName) {
      this.$refs[formName].validate(valid => {
        if (!valid) {
          util.message.success("校验失败");
          return false;
        }
        // 触发父组件提交函数
        this.$emit("updateOrder", this.formRow);
        this.dialogFormVisible = false;
      });
    }
  }
};
</script>

  注释1即监控dialogFormVisible的变化,为true即编辑弹窗显示时,将父组件传递过来的row赋值给formRow,用于在表单中显示。

  综上,我们基本实现了本文开头提出的目标,并在实现功能的同时将Vue.js中一些常用知识点给大家做了简单的介绍。本文涉及的内容比较浅显,但相对广泛,目的是让小伙伴们对Vue项目有个全局的认识,对Vue中的一些知识点有个大概的印象,其中可能也不乏错误和疏漏,仅供入门学习使用。正规项目中,可以通过飞冰、beeworks等搭建,也可去GitHub上下载一些美观且完善的前端项目代码。最后,欢迎小伙伴们批评指正,共同学习。

附件:(还在研究如何上传附件ing)
a) 前端代码:testvue
b) mock接口代码:app.js
c) nginx配置:

server {
        listen       8081;
        server_name  localhost;
	
        location / {
           root   E:/testvue/dist;  
		   try_files $uri $uri/ /index.html;	
           index  index.html index.htm;
        }
		
	    location ^~/api {
            proxy_pass http://127.0.0.1:3000;
            expires -1;
        }
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值