Spring Boot 与 Vue.js 整合实践

  一直都想尝试做前后端分离,我之前一直是学 Java 的,所以后端选择了 Spring Boot;前端选择了 Vue.js 这个轻量、易上手的框架。网上其实已经有了不少 Spring Boot 和 Vue.js 整合的资料,Github 上就有好多 repo,但是每当我指望按图索骥的时候就会出现各种各样奇怪的 bug,上 Stack Overflow 问了也没人搭理。前前后后研究了差不多三个星期,现在总算是理清楚了。

  本文重点介绍我在实践过程中的基本流程,以及我遇到的一个困扰了我好久的问题,就是如何 CORS。

框架版本

  • Spring Boot: 2.0.4.RELEASE(JDK 是1.8)
  • Vue.js: 2.x

基本流程

前端:编写 Vue 组件

  首先用 vue-cli 搭好脚手架,我这个 Demo 用到的第三方库有:

  • axios:负责 HTTP 请求
  • bootstrap-vue:Bootstrap 和 Vue.js 的整合,方便设计页面
  • vue-router:管理路由
  • qs:实现 CORS

然后写一个登录组件:

<!-- 下面是我直接从 bootstrap-vue 文档抄下来的模板  -->
<template>
  <div>
    <b-form @submit="onSubmit" @reset="onReset" v-if="show">
      <b-form-group id="exampleInputGroup1"
                    label="Username:"
                    label-for="exampleInput1">
        <b-form-input id="exampleInput1"
                      type="text"
                      v-model="form.username"
                      required
                      placeholder="Enter username">
        </b-form-input>
      </b-form-group>
      <b-form-group id="exampleInputGroup2"
                    label="Password:"
                    label-for="exampleInput2">
        <b-form-input id="exampleInput2"
                      type="text"
                      v-model="form.password"
                      required
                      placeholder="Enter password">
        </b-form-input>
      </b-form-group>
      <b-form-group id="exampleGroup4">
        <b-form-checkbox-group v-model="form.checked" id="exampleChecks">
          <b-form-checkbox value="me">Check me out</b-form-checkbox>
          <b-form-checkbox value="that">Check that out</b-form-checkbox>
        </b-form-checkbox-group>
      </b-form-group>
      <b-button type="submit" variant="primary">Submit</b-button>
      <b-button type="reset" variant="danger">Reset</b-button>
    </b-form>
  </div>
</template>

<script>
//...
</script>

  我现在想实现的就是用户登录成功之后导航到另一个组件,所以我就又写了一个欢迎组件:

<template>
    <div>
        <h1>Welcome!</h1>
    </div>
</template>

  记得配置路由:

// src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login.vue'
import Information from '@/components/Information.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Login',
      component: Login
    },
    {
      path: '/information',
      name: 'Information',
      component: Information
    }
  ]
})

后端:提供 RESTful API

  因为只有后端提供了接口,前端才能调用,所以现在要进行后端开发。RESTful 是现在很流行的 API 设计风格,所以我这里也实践了一下。下面是 controller 的代码,完整源码地址附在文末。

@RestController
@RequestMapping("/api")
public class LoginController {

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    @ResponseBody
    public String login(@RequestParam String username,
                        @RequestParam String password) {
        // 简单处理一下,实际开发中肯定是要用到数据库的
        if (username.equals("123") && password.equals("123")) {
            return "successful";
        } else {
            return "failed";
        }
    }
}

  后端的 API 现在有了,就差前端调用了。但是没这么简单,接下来就要解决我前面提到的问题。

实现 CORS

  在这个 Demo 中前端占用的端口是8080,后端是 8088。这就存在跨域的问题,如果不解决的话后端就没法接收前端的请求。

  我参考了这个例子,通过配置 Spring MVC 实现了 CORS:

@Configuration
public class CORSConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(ALL)
                .allowedMethods(ALL)
                .allowedHeaders(ALL)
                .allowCredentials(true);
    }
}

  后端配置好了还不行,前端也要有一些配置,要用 axios 顺利地发送请求并保证后端能接收到,需要对请求参数做处理。我参考这个回答用 qs 库对请求参数做了处理:

qs.stringify({
        'username': this.form.username,
        'password': this.form.password
      })

现在只需完善前端调用后端 API 的代码:

// Login.vue
<script>
export default {
  data () {
    return {
      form: {
        username: '',
        password: '',
        checked: []
      },
      show: true
    }
  },
  methods: {
    onSubmit (evt) {
      evt.preventDefault();
      
      // 关键就在于要对参数进行处理
      axios.post('http://localhost:8088/api/login',qs.stringify({
        'username': this.form.username,
        'password': this.form.password
      })).then((response) => {
        var status = response.data;
        if(status === 'successful') {
          this.$router.push('/information');
        } else {
          alert(response.data.message);
        }
        console.log(response);
      }).catch((error) => {
        console.log(response);
      });
    }
  }
}
</script>

至此,终于实现了前后端的分离,并且保证前后端能够顺利交互。

题外话

让 controller 能获取请求参数

  controller 可能无法获取请求参数,这篇文章提供了一种解决方案。我这个 Demo 中并没有出现 controller 收不到请求参数的问题,但也把这个问题记录下来,以后可能遇上也说不准。

axios 方法中的 this

  我这个 Demo 中还试着用 axios 发 GET 请求,然后获取后端响应的 JSON 数据。

// Information.vue
<template>
    <div>
        <h1>Welcome!</h1>
        <div>
            <b-button @click="getInfo()">Get your information</b-button>
            <h2 v-if="username !== ''">Your username is: {{ username }}</h2>
            <h2 v-if="email !== ''">Your email is: {{ email }}</h2>
        </div>
    </div>
</template>

<script>
import axios from 'axios'

export default {
    data () {
        return {
            username: '',
            email: ''
        };
    },
    methods: {
        getInfo () {
            axios.get('http://localhost:8088/api/information')
            .then(function(response) {
                this.username = response.data['username'];
                this.email = response.data['email'];
                console.log(response);
            }).catch(function(error) {
                console.log(error);
            });
        }
    }
}
</script>

  一开始我是这么写的,乍一看没什么问题,但是 JavaScript 就一直报错:

typeError: Cannot set property 'username' of undefined

  搞了很久都没有解决,直到看到这篇文章,才明白原来是this作用域的问题(JavaScript 的this是真的复杂啊!!!)。改成下面这样就没问题了:

            axios.get('http://localhost:8088/api/information')
            .then((response) => {
                this.username = response.data['username'];
                this.email = response.data['email'];
                console.log(response);
            }).catch((error) => {
                console.log(error);
            });

后来 Stack Overflow 上有人说不用箭头函数也行,只需提前把指向 Vue 实例的this保存在一个变量就行了:

            var vue = this;
            axios.get('http://localhost:8088/api/information')
            .then(function (response) {
                vue.username = response.data['username'];
                vue.email = response.data['email'];
                console.log(response);
            }).catch((error) => {
                console.log(error);
            });

经实践,这样也是可以的。

Demo 完整源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342