Skip to content

Latest commit

 

History

History
 
 

sct-app

Spring Cloud Template 分布式微服务系统 -- 前端

注意

请仔细阅读vue-admin-template项目官方文档:传送门

如何食用 vue-admin-template 前端模板?

环境准备

因为该模板用mock模拟了前端所需数据,首先要删除mock相关的配置

  • 删除mock文件夹
  • 删除/src/main.js下这段代码:
import { mockXHR } from '../mock'
if (process.env.NODE_ENV === 'production') {
  mockXHR()
}
  • 删除vue-config.js下这段代码:
after: require('./mock/mock-server.js') 

全局配置后端接口URL,以下配置文件中都存在一个VUE_APP_BASE_API配置,他指定了后端请求的URL根路径。比如我们后端的请求都是http://localhost:9999/api/xx的,所以可设置VUE_APP_BASE_API='http://localhost:9999/api/',其中/xx具体的接口请求放在/src/api/*.js中。

  • .env.development: 即dev开发环境
  • .env.production: 即prod生产环境
  • .env.staging: 即mock模拟环境

注意:在SpringCloud微服务项目中,前端的所有请求都应该走Gateway网关服务的URL地址。

取消ESLint校验

在你开发项目中可能遇到前端莫名其妙报错语法不对,比如多一个空格、冒号啥的,这都是因为vue-admin-template这个模板启用了ESLint最严格模式,其实我们关闭ESLint检查即可:

修改vue.config.js中如下代码:

修改前:
  lintOnSave: process.env.NODE_ENV === 'development',
  
修改后:
  lintOnSave: false,

登录功能

拿到的项目模板,首先需要解决的就是登录功能。按照 vue-admin-template 官方文档的描述,所有的请求都将经过如下流程:

1. `.vue` 首先是Vue组件内部发送了请求
2. `src/utils/request.js` 作者对 axios 请求全局的封装
3. `src/api/xx.js` vue组件使用的接口地址,配合`request.js`完成axios请求与相应
4. `src/store/modules/user.js` 这个尤为重要,登录接口不同于其他接口,当登录成功后,需要使用vuex将登录接口响应的数据保存,以便维持与后端的会话通信。

那么,登录功能不同于其他的CRUD业务流程,在vue-admin-template中登录需要后台提供:

  1. 登录接口
  2. 获取用户信息接口

这刚好符合了我们使用的Security-OAuth2框架。在Security-OAuth2框架汇总,默认提供了获取Token的接口(登录接口),我们仅需要调用这个接口即可实现登录。

登录接口

如果你对Security-OAuth2还不熟悉,建议看下我之前写的文档:

  1. Spring Security OAuth2概念引入
  2. Spring Security OAuth2实战
  3. Spring Security OAuth2数据持久化

Security-OAuth2中提供的默认获取Token的接口:/oauth/token,下面是使用Postman工具模拟请求的示例图:

/oauth/token接口是谁提供的?

​ 切记,/oauth/token接口是Security-OAuth2内部提供的获取Token的接口,这个接口不需要我们手动定义,并且即使使用了Spring Security,/oauth开头的接口也应为是内置的不会被拦截,所以我们也无需特殊配置Spring Security 不拦截这个接口。

/oauth/token登录请求需要传入什么参数?

​ 关于这点可以看下我的 博客 中之前介绍OAuth2的文章。我们需要手动提供:

  1. username 登录账户
  2. password 登录密码。这并不是必须的,但由于我们使用的OAuth的密码模式,所以需要定义
  3. grant_type 因为我们使用的OAuth2的密码模式,可直接定义为grant_type=password
  4. Request Headers >> Authorization 注意这个是客户端账户密码信息,对应了后端ClientDetails中查询的数据

/oauth/token 响应什么数据?

​ 如上图,请求一般响应如下信息:

{
    "access_token": "f59359c1-86c0-48a3-b060-ff97e5163bb2",
    "token_type": "bearer",
    "expires_in": 22531,
    "scope": "app"
}

其中access_token 尤为重要,后面所有的请求都需要携带这个Token值才能正常访问,否则就403拒绝。所以,在vue-admin-template项目中,一旦登录接口响应成功,会将返回的Token信息全局设置再请求头中,这样以后所有的请求中都携带这个请求都信息。具体可以看:src/utils/request.js中这段代码:

config.headers['Authorization'] = getToken()

这是全局配置axios实例,因为所有的API请求都需要经过这个request.js文件,所以其中的配置项对所有的请求都有效。

vue-admin-template中如何处理登录接口响应的数据?

​ 看完上面的配置,你觉得已经能完成前端的登录功能了?那你就错了。上面仅仅介绍了使用Postman工具模拟测试,而在vue-admin-template项目中,如果请求/oauth/token接口正常响应数据,需要将响应的数据储存到vuex中。那么主要涉及src/store/modules/user.js中的代码:

const actions = {
  // user login
  login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => {
        commit('SET_TOKEN', response.access_token)
        setToken(response.token_type + ' ' + response.access_token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // get user info
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.token).then(response => {
        const { data } = response

        if (!data) {
          reject('Verification failed, please Login again.')
        }

        const { name, avatar } = data

        commit('SET_NAME', name)
        commit('SET_AVATAR', avatar)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // user logout
  logout({ commit, state }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        commit('SET_TOKEN', '')
        removeToken()
        resetRouter()
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // remove token
  resetToken({ commit }) {
    return new Promise(resolve => {
      commit('SET_TOKEN', '')
      removeToken()
      resolve()
    })
  }
}

这段代码才是登录、获取用户信息、注销功能实现的核心代码。其实就是利用src/api/xx.js中定义的接口方法发送axios请求。然后将响应response数据处理一下。

比如上面代码中,login()方法就是登录接口放他,它实际请求了/oauth/token接口,前面提到每次请求都需要携带access_token,所以需要vuex储存token信息(setToken(token)),以便在request.js中使用config.headers[]全局定义请求头信息。

在Postman中设置的Basic Auth,在此项目中在哪体现呢?

​ 当然,按照OAuth2协议的规定,想要获取应用信息必须先请求/oauth/token获取令牌Token值,而想要获取令牌Token除了username password grant_type信息,还要告诉OAuth2这是哪个客户端的请求,所以在请求/oauth/token接口时需要携带客户端信息。

​ 这一点在 Spring Security OAuth2实战 一文中我有详细介绍过。所以在vue-admin-template前端项目中,想要实现所有请求都携带客户端信息,就需要全局设置请求头参数,所以,我们直接在 src/main.js 中全局设置Axios 默认请求头参数:

import axios from 'axios'
axios.defaults.headers.post['Authorization'] = 'Basic Y2xpZW50OnNlY3JldA==';

上面设置了一个请求头参数Authorization,他的值是对username: client, password: 123456client:123456按照Base64加密后的值。因为整个项目仅仅是个人使用的,所以这个写死也并无大碍,毕竟数据库中写死了客户端信息。

测试

上面基本介绍了登录请求的流程和注意事项,下面使用浏览器F12看一下实际的请求信息:

如果登录请求响应成功,想要进入系统的第二关就是调用获取用户信息的接口,全局设置用户信息(用户名、头像…) 。所以,vue-admin-template会立即再请求获取用户信息的接口:

可看到,如果登录成功,可携带access_token访问应用的其他接口,只需要在请求时将请求头Authorization设置为access_token信息即可。

CRUD业务

一旦解决了登录功能,相信你对vue-admin-template这个前端模板项目有一定理解了,后端其他的业务也相对简单很多了。涉及Axios请求部分,只需要关注:

  1. src/views 下定义 .vue 组件
  2. src/api 下定义API接口信息

栗子

举例:根据ID获取用户信息的功能

  1. src/api/user.js中定义根据ID获取用户信息的API接口
export function findById(id) {
  return request({
    url: '/admin/user/' + id,
    method: 'get'
  })
}
  1. .vue组件中使用这个API接口
<el-table-column align="center" label="Actions">
	<template slot-scope="scope">
		<el-button type="danger" @click="handleDel(scope.row.id)" icon="el-icon-delete" size="mini">删除</el-button>
	</template>
</el-table-column>
<script>
  import { findById } from '@/api/user'

  export default {
    components: {Pagination, Save},
    data() {
      return {
        form: null,
      }
    },
    methods: {
      handleEdit(id) {
        findById(id).then(response => {
          this.form = response.data;
        })
      },
    }
  }
</script>

以上即可实现根据ID查询用户信息的功能,是不是很简单呢?

分页查询

如果没有使用Vue+ElementUI实现分页查询经验的朋友可以先看下我的这篇文章:

Vue+ElementUI+SpringMVC实现分页

vue-admin-template的作者其实提供好了一个pagination分页组件,是对Element-UI的<el-pagination>控件的封装。作者封装的这个组件是通用的,可以在项目的任何需要分页的位置使用,非常方便。如何食用呢?

  1. src/components下引入该组件

  1. src/api/user.js中定义分页查询接口
export function getList(query, data) {
  return request({
    url: '/admin/user/list?pageCode=' + query.page + '&pageSize=' + query.limit,
    method: 'post',
    data
  })
}

传递三个参数:pageCode当前页码、pageSize每页多少记录、data查询条件

  1. 在需要使用分页的组件中引入该分页组件
<pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit"
            @pagination="fetchData"></pagination>

<script>
  import {getList} from '@/api/user'
  import Pagination from '@/components/Pagination'

  export default {
    components: {Pagination},
    data() {
      return {
        list: null,
        search: {},
        listQuery: {
          page: 1,
          limit: 20,
          importance: undefined,
          title: undefined,
          type: undefined,
          sort: '+id'
        },
        total: 0,
      }
    },
    created() {
      this.fetchData()
    },
    methods: {
      fetchData() {
        getList(this.listQuery, this.search).then(response => {
          this.list = response.data.rows
          this.total = response.data.total
        })
      },
    }
  }
</script>

可以看到上面传递了两个参数:listQuery分页条件、search查询条件。

也就是说分页查询:

  • 前端需要传递的参数:pageCode: 当前页码、pageSize:每页多少条记录。如果需要条件查询再传递查询条件
  • 后端需要返回的参数:total:数据库总记录数、List<T>:封装了查询到的数据集合

可以看下后端的代码实现:

// Controller
@PostMapping("/list")
@ResponseBody
public Result<Map> list(SysUser user, QueryPage queryPage) {
    return new Result<Map>(this.selectByPageNumSize(queryPage, () -> sysUserService.list(user)));
}

// QueryPage
@Data
@ToString
public class QueryPage implements Serializable {

    private int pageCode; //当前页
    private int pageSize; //每页显示的记录数
}

组件传值

实际项目中,经常使用组件传值。比如:用户管理模块中,编辑功能通常需要一个弹出窗,而这个弹出窗通常是抽取在另外一个组件中,这样就涉及到了父组件 ( 用户管理组件 ) 和子组件 ( 编辑功能组件 ) 的通信;简单来说,在用户管理组件中需要控制编辑功能弹出框的弹出和关闭等操作。

概念引入

父组件向子组件传值

比如在父组件中定义子组件:

<div id="app">
    <son :info="msg"></son>
</div>

那么在组件中获取到这个info中的数据:

data() {
    return {}
},
methods: {},
props: ['info']

即可获取到父组件传递来的数据,注意:这个props属于new vue()根路径下的属性,不属于data。如果获取父组件传进来的多个擦书,使用逗号隔开即可获取。

子组件向父组件传值

在父组件中定义:

<son @func="getMsg"></son>

//Vue实例
methods: {
  //父组件注册的方法,子组件通过`this.$emit()`的方式调用这个方法将参数传递给父组件的val。
  getMsg(val) {
      console.log("这是子组件传递来的数据:" + val);
  }
}

那么在子组件中通过this.$emit('方法名', 要传递的数据)的方式调用父组件中的方法,传递数据。

<input type="button" value="向父组件传值" @click="sendMsg">

//Vue实例
methods: {
  sendMsg() {
      this.$emit('func', '我是来自子组件的数据');
  }
}

$refs

this.$refs可以获取元素和组件(以及组件中的元素)。

  • 如果在HTML中定义了 ref="xx" 那么在Vue实例中通过this.$refs.xx就能获取到当前定义ref="xx"的DOM元素。
  • 如果在组件引用上(比如<son ref="xx">)上使用了ref,那么在父组件Vue实例中通过this.$refs获取到的是整个子组件的对象,可以通过.的方式调用子组件datamethods中绑定数据。

栗子

  1. 除了创建用户管理的组件,新增一个封装了用户信息编辑框的组件:

其中的save.vue就是封装了用户信息编辑框的组件。

  1. 在用户管理组件中引入用户信息编辑框的组件
<save :sonData="form" @sonStatus="status"></save>
<script>
  import Save from './save'

  export default {
    components: {Save},
    data() {
      return {
        form: null,
      }
    },
    created() {
      this.fetchData()
    },
    methods: {
      fetchData() {
        getList(this.search).then(response => {
        })
      },
      handleEdit(id) {
        findById(id).then(response => {
          this.form = response.data;
        })
      },

      //子组件的状态Flag,子组件通过`this.$emit('sonStatus', val)`给父组件传值
      //父组件通过`@sonStatus`的方法`status`监听到子组件传递的值
      status(data) {
        if (data) {
          this.fetchData();
        }
      },
    }
  }
</script>

如上,如果父组件index.vue想要给子组件save.vue传值,比如修改信息会在父组件index.vue中触发编辑按钮,触发事件去根据ID查询该用户信息,让后将信息绑定到子组件save.vue模态框上,最后在编辑模态框上修改完了用户数据,点击确定按钮提交修改后的数据,如此编辑功能就实现了。

So

父组件index.vue给子组件save.vue传递值,仅需要在子组件的实例上写 :aa="bb" 即可,其中:

  • aa是传递的数据的key值,在子组件save.vue中可用props: ['aa']接收到。( 实时监听 )
  • bb是value值,可以是任意对象。( 实时更新,实时传递给子组件 )

子组件可通过props: ['aa']得到父组件传递的数据,但是要实时绑定到save.vue修改信息的表单上还需要监听一下,所以在子组件上:

//`props`不属于data,但是`props`中的参数可以像data中的参数一样直接使用
props: ['sonData'],
    
watch: {
  'sonData': function (newVal, oldVal) {
    this.form = newVal
    this.dialogVisible = true
    if (newVal.id != null) {
      this.dialogTitle = 'Edit'
    }
  },
},

如果父组件传递的值sonData改变了,就证明用户点击了编辑/新增按钮,立即将传递来的值绑定到表单对象form上,并打开模块框diaalogVisible=true

如果子组件save.vue编辑完用户数据并更新了,按照常理,此时应该立即刷新用户列表数据以获取最新的数据。那么就涉及到子组件save.vue给父组件index.vue传值。

So

子组件save.vue给父组件index.vue传递值,仅需要在父组件index.vue上定义的子组件save.vue实例上写: @sonStatus="status",这就实现子组件给父组件传递值status

  • 在子组件save.vue中写this.$emit('sonStatus', val)会立即改变父组件index.vue中绑定的对象status
  • 在父组件index.vue中子组件传递值实际绑定的是一个方法status,这个方法传递一个值也是子组件传递来的值

子组件通过this.$emit('sonStatus', val)会强制父组件index.vue中改变绑定的值,是实时监听的。而父组件index.vue对应绑定的是一个方法对象status(),不同于父组件给子组件传值,子组件给父组件传值通过方法对象绑定,这个方法就相当于watch实时监听值的改变。

status(data) {
  if (data) {
    this.fetchData();
  }
},

如上,一旦子组件告诉父组件,我已经修改了数据,你需要更新了。此时父组件的status方法会立即鉴定到值的变化并立即触发传方法fetchData()刷新表格数据。

左侧路由导航

vue-admin-template中作者也封装了路由和侧边栏,具体介绍看作者的这篇文章:路由和侧边栏

如果我们想增加组件 ( 页面 ),需要以下几步即可实现:

  1. src/views/下新增一个.vue组件

  1. src/router/index.js下新增一条路由:
{
  path: '/admin',
  component: Layout,
  redirect: '/admin/user',
  name: '权限管理',
  meta: { title: '权限管理', icon: 'example' },
  children: [
    {
      path: 'user',
      name: '用户管理',
      component: () => import('@/views/user/index'),
      meta: { title: '用户管理', icon: 'table' }
    },
  ]
},

如上,一旦在router/index.js中新增了一条路由导航,侧边栏会自动渲染一个导航。其中:

  • path 该路由的URL相对地址
  • component 展示在哪个组件上,src/layout是整个前端项目的布局骨架,所以所有的子组件都要显示在该骨架上
  • children 二级导航目录,如果不需要二级目录,就无需定义此节点
  • component => import 该路由对应的组件位置