Vue + Spring Boot 项目实战(三):前后端结合测试(登录页面开发)

前面我们已经完成了前端项目 DEMO 的构建,这一篇文章主要目的如下:

一、打通前后端之间的联系,为接下来的开发打下基础
二、登录页面的开发(无数据库情况下)

本篇目录
前言:关于开发环境
一、后端项目创建
二、登录页面开发
1.关于前后端结合
2.前端页面开发
Login.vue
AppIndex.vue
3.前端相关配置
设置反向代理
配置页面路由
跨域支持
运行项目
4.后端开发
User 类
Result 类
LoginController
5.测试项目

第一篇文章也放上了 GitHub 的地址,有些小伙伴可能没看到,这里再放一遍:

https://github.com/Antabot/White-Jotter

一、后端项目创建

这个就很简单了。在 IDEA 中新建项目,选择 Spring Initializr,点击 Next

在这里插入图片描述
输入项目元数据,Next
在这里插入图片描述
选择 Web -> Web,Next
在这里插入图片描述
最后是项目名称和项目地址,Finish 后等待项目自动初始化即可。
在这里插入图片描述
运行 Application.java
在这里插入图片描述
访问 http://localhost:8080,发现弹出了错误页面,OK,这就对了,因为我们啥页面都没做啊。

在这里插入图片描述

二、登录页面开发

1.关于前后端结合

注意我们的项目是前后端分离的,这里的结合意思不是就不分离了,是如何把这俩分离的项目串起来用。

前面提到过前后端分离的意思是前后端之间通过 RESTful API 传递 JSON 数据进行交流。不同于 JSP 之类,后端是不涉及页面本身的内容的。

在开发的时候,前端用前端的服务器(Nginx),后端用后端的服务器(Tomcat),当我开发前端内容的时候,可以把前端的请求通过前端服务器转发给后端(称为反向代理),这样就能实时观察结果,并且不需要知道后端怎么实现,而只需要知道接口提供的功能,两边的开发人员(两个我)就可以各司其职啦。

艾玛做一个完整的教程真不容易,遇到的每个知识点感觉都能讲一堆。上次的文章被一位老哥反问是不是太着急了,也不知道是什么意思,我自己反思可能是讲的不够细吧,这里我就再啰嗦一下讲两句 正向代理反向代理。

正向代理就是,你要访问一个网站,比如“谷弟弟”,然后发现访问不到,于是你访问了一个能访问到“谷弟弟”的代理服务器,让它帮你拿到你想浏览的页面。

反向代理就是,你访问了一个网站,你以为它是“谷弟弟”,但其实它是“谷姐”,“谷姐”知道你其实是想找她弟,就取回“谷弟弟”的内容给你看。作为用户的你,是不知道有这个过程的,这么做是为了保护服务器,不暴露服务器的真实地址。

知乎上有张神图可以描述这两种过程

在这里插入图片描述

2.前端页面开发

Login.vue

首先我们开发登录页面组件,右键 src\components 文件夹,New -> Vue Component,命名为 Login,如果没有 Vue Component 这个选项,可以选择新建一个 File,命名为 Login.vue 即可。代码如下:

本篇代码为我自己写的,与原作者不同

<template>
<body id="bgdiv">
	<el-form class="login-container" label-position="left"
           label-width="0px">
		<h3 class="login_title">系统登录</h3>
		<el-form-item>
			<el-input type="text" v-model="loginForm.name" auto-complete="off" placeholder="请输入用户名"></el-input>
		</el-form-item>
		<el-form-item>
			<el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="请输入密码"></el-input>
		</el-form-item>
		<el-form-item>
			<el-button type="Info" style="width: 40%;background: #C0C4CC;border: none;float:left" v-on:click="goRegister">注册</el-button>
			<el-button type="primary" style="width: 40%;background: #409EFF;border: none;float:right" v-on:click="login">登录</el-button>
		</el-form-item>
	</el-form> 
</body>
</template>

<script>
  import UserRegister from "./UserRegister"

  export default {
    name: 'Login',
    data () {
      return {
        loginForm: {
          username: ' ',
          password: ''
        },
        responseResult: []
      }
    },
    methods: {
      login () {
		var _this = this
        console.log(_this.$store)
        this.$axios
          .post('/login', {
            name: this.loginForm.name,
            password: this.loginForm.password
          })
          .then(successResponse => {
		  console.log(successResponse.data.code)
            if (successResponse.data.code === 200) {
			  _this.$store.commit("login", _this.loginForm)
			  var path = this.$route.query.redirect
              this.$router.replace({path: path === '/' || path === undefined ? '/index' : path})
			  console.log(path)
			  //this.$router.push 跳转到指定url路径,并想history栈中添加一个记录,点击后退会返回到上一个页面
			  //this.$router.replace 跳转到指定url路径,但是history栈中不会有记录,点击返回会跳转到上上个页面
			  //this.$router.go(n) 向前或者向后跳转n个页面,n可为正整数或负整数
            }
			else{
				_this.$alert(successResponse.data.message, '提示', {
					confirmButtonText: '确定'
				})
			}
          })
          .catch(failResponse => {
          })
      },
	  goRegister(){
		var _this = this
		_this.$router.replace('/userRegister')
	  }
    }
  }

</script>
<style>
.login-container{
	background-clip: padding-box;
    margin: 170px auto;
	padding: 35px 35px 15px 35px;
	width: 350px;
	background: #fff;
	border-radius: 1em; 
	border: 1px solid #eaeaea;
	box-shadow: 0 0 25px #cac6c6;
}
.login_title {
    margin: 0px auto 40px auto;
    text-align: center;
    color: #505458;
  }
#bgdiv{
 background:url("../assets/bg.jpg") no-repeat;
    background-position: center;
    height: 100%;
    width: 100%;
    background-size: cover;
    position: fixed;
 }
 body{
    margin: 0px;
  }
</style>

标签中随便写了一个登录的界面, methods 中定义了登录按钮的点击方法,即向后端 /login 接口发送数据,获得成功的响应后,页面跳转到 /index。因为之前我们设置了默认的 URL,所以请求实际上发到了 http://localhost:8443/api/login。

AppIndex.vue

右键 src\components 文件夹,新建一个 directory,命名为 home,再在 home 下新建一个 Appindex.vue ,即首页组件,这里暂时不做过多开发,先随便写个 Hello World。

<template>
  <div style="overflow:-Scroll;overflow-x:hidden;border:none">
		<div class="block">
			<el-carousel trigger="click" style="margin-top:24px;">
			  <el-carousel-item v-for="item in imgsURL" :key="item.index">
				<h3>{{ item.imgItem }}</h3>
				<div class="cover">
					<img :src="item.url" class="imgs" alt="封面">
				</div>
			  </el-carousel-item>
			</el-carousel>
		</div>
  </div>
</template>

<script>
  export default{
    name: 'Appindex',
	data(){
		return{
			imgsURL:[
				{name:"book1",url:require('../../assets/book1.jpg'),imgItem:"如果时间已经不够"},
				{name:"book2",url:require('../../assets/book2.jpg'),imgItem:"何不静下心来读书"},
				{name:"book3",url:require('../../assets/book3.jpg'),imgItem:"未来已在你手中"}
			]
		}
		
	}
	
  }

</script>

<style scoped>
.cover{
	cursor: pointer;
	border:none;
}
.imgs{
	//background-size: 300px 200px;
	width:98%;
	height:80%;
	
}
.block{
	margin-top:22px;
}


</style>

<style>

.el-carousel__item h3{
	color:white;
	font-size:14px;
	opacity: 0.75;
	position: absolute;
	line-height: 0px;
	margin-top:37%;
	margin-left:46.6%;
	border-radius:20px;
}

.el-carousel__container{
	position: relative;
	height:600px;
}
</style>

3.前端相关配置

设置反向代理
修改 src\main.js 代码如下:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import store from './store'

//设置反向代理,前端请求默认发送到 hhtp://localhost:8080/api
var axios = require('axios')
axios.defaults.baseURL = 'http://localhost:8080/api'

//跨域访问前端能够带上 cookie
axios.defaults.withCredentials = true

//全局注册,之后可在其他组件中通过this.$axios 发送数据

Vue.prototype.$axios = axios
Vue.config.productionTip = false
Vue.use(ElementUI);
/* eslint-disable no-new */

/*每个钩子方法接收三个参数: 
* to: Route: 即将要进入的目标 路由对象 
* from: Route: 当前导航正要离开的路由 
* next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。 
* next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。 
* next(false): 中断当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。 
* next(‘/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。

确保要调用 next 方法,否则钩子就不会被 resolved。

其中,to.meta中是我们自定义的数据,其中就包括我们刚刚定义的requireAuth字段。通过这个字段来判断该路由是否需要登录权限。需要的话,
同时当前应用不存在token,则跳转到登录页面,进行登录。登录成功后跳转到目标路由。*/


router.beforeEach((to, from, next) => {
		//这里是加载时机的的问题
		//不应该是在admin页面的时候才index,应该是index时就初始化
		if (store.state.user.username && to.path.startsWith('/index')) {
			initAdminMenu(router, store)
		}
		// 已登录状态下访问 login 页面直接跳转到后台首页
		if (store.state.user.username && to.path.startsWith('/login')) {
		  next({
			path: 'admin/dashboard'
		  })
		}
		if (store.state.user.username && to.path.startsWith('/admin')) {
		  next({
			path: 'index'
		  })
		}
		if(to.meta.requireAuth){ // 判断该路由是否需要登录权限
			//if(store.state.user.username){ // 通过vuex state获取当前的token是否存在
			//2020.04.23 跨域访问修改
			//这里与原作者有出入,原作者是写store.state.user,直接用user判断,但如果写成这样,会造成白页面,不报错的情况,很奇怪的情况
			//登陆后进入方法,确实打印了“进来了语句”,估计程序中还有问题
			//alert(store.state.user)
			if(store.state.user.username){
				axios.get('/authentication').then(resp => {
				  if (resp.data) next( )
				})
			}else{
				next({
					path: 'login',
					query: {redirect: to.fullPath}  // 将跳转的路由path作为参数,登录成功后跳转到该路由
				})
			}
		} else {
			next()
		}
	}
)

const initAdminMenu = (router, store) => {
	
  store.state.adminMenus = []
  // 防止重复触发加载菜单操作
  if (store.state.adminMenus.length > 0) {
    return
  }
  axios.get('/getMenu').then(resp => {
    if (resp && resp.status === 200) {
      var fmtRoutes = formatRoutes(resp.data)
      router.addRoutes(fmtRoutes)
      store.commit('initAdminMenu', fmtRoutes)
    }
  })
}

const formatRoutes = (routes) => {
  let fmtRoutes = []
  routes.forEach(route => {
    if (route.children) {
      route.children = formatRoutes(route.children)
    }

    let fmtRoute = {
      path: route.path,
      component: resolve => {
        require(['./components/admin/' + route.component + '.vue'], resolve)
      },
      name: route.name,
      nameZh: route.nameZh,
      iconCls: route.iconCls,
      meta: {
        requireAuth: true
      },
      children: route.children
    }
    fmtRoutes.push(fmtRoute)
  })
  return fmtRoutes
}


new Vue({
  el: '#app',
  render: h => h(App),
  router,
  // 注意这里 使用钩子函数判断是否拦截
  store,
  components: { App },
  template: '<App/>'
})

因为使用了新的模块 axios,所以需要进入到项目文件夹中,执行 npm install --save axios,以安装这个模块。

配置页面路由
修改 src\router\index.js 代码如下

import Vue from 'vue'
import Router from 'vue-router'
import Login from '../components/login'
import Appindex from "../components/home/Appindex";
import Home from "../components/Home"
import LibraryIndex from '../components/library/LibraryIndex'
import UserRegister from '../components/UserRegister'
import AdminIndex from '../components/admin/adminIndex'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
	  path: '/home',
      name: 'Home',
	  component:Home,
	  redirect: '/index',
	  children:[
	   {
		 path:'/index',
         name:'AppIndex',
	     component:Appindex,
		 meta:{
			 requireAuth:true
		 }
	   },
	   {
		 path:'/library',
         name:'Library',
	     component:LibraryIndex,
		 meta:{
			 requireAuth:true
		 }
	   }
	  ]
	},
    {
      path: '/Login',
      name: 'Login',
      component: Login
    },
    {
      path:'/index',
      name:'AppIndex',
      component: Appindex,
	  meta: {
        requireAuth: true
      }
    },
	{
	  path: '/',
      name: 'index',
      redirect: '/index',
      component: Appindex,
      meta: {
        requireAuth: true
      }
	},
	{
	  path: '/userRegister',
      name: 'userRegister',
      component: UserRegister
	},
	{
	  path:'/admin',
	  name:'admin',
	  component:AdminIndex,
	  meta: {
        requireAuth: true
      }
	}
  ]
})


// 用于创建默认路由
export const createRouter = routes => new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Default',
      redirect: '/home',
      component: Home
    },
    {
      // home页面并不需要被访问,只是作为其它组件的父组件
      path: '/home',
      name: 'Home',
      component: Home,
      redirect: '/index',
      children: [
        {
          path: '/index',
          name: 'AppIndex',
          component:Appindex
        },
        
        {
          path: '/library',
          name: 'Library',
          component: LibraryIndex
        }
      ]
    },
    {
      path: '/login',
      name: 'Login',
      component:Login
    },
    {
      path: '/userRegister',
      name: 'userRegister',
      component: UserRegister
    },
    {
      path: '/admin',
      name: 'Admin',
      component: AdminIndex,
      meta: {
        requireAuth: true
      },
      children: [
        {
          path: '/admin/dashboard',
          name: 'Dashboard',
          component: index,
          meta: {
            requireAuth: true
          }
        }
      ]
    }
  ]
})

跨域支持

为了让后端能够访问到前端的资源,需要配置跨域支持。

在 config\index.js 中,找到 proxyTable 位置,修改为以下内容

proxyTable: {
      '/api': {
        target: 'http://localhost:8443',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }

注意如果不是在最后的位置,大括号外需要添加一个逗号。

运行项目

执行 npm run dev,或双击 dev(start 也一样)脚本,查看登录页面效果。

注意地址是 localhost:8080/#/login ,中间有这个 # 是因为 Vue 的路由使用了 Hash 模式,是单页面应用的经典用法,但连尤雨溪本人都觉得不太好看,所以可以在路由配置中选择使用 History 模式,但会引发一些问题,需要在后端作出处理,所以这里先不更改,之后我单独写一篇关于这个的文章。

在这里插入图片描述

这是我最终的界面,与原作者不一样,大概也差不多,图片是小米官网找的。

在这里插入图片描述
呃,总之这个页面的功能都是一样的。

4.后端开发

User 类

在 Login.vue 中,前端发送数据的代码段为

.post('/login', {
            username: this.loginForm.username,
            password: this.loginForm.password
          })

后端如何接收这个 JS 对象呢?我们很自然地想到在需要创建一个形式上一致的 Java 类。

打开我们的后端项目 wj,首先在 src\main\java\com\lihao\onenote 文件夹(就是你自己的 web 项目的包)下,新建一个 pojo 包(package),然后新建 User类,代码如下

package com.lihao.onenote.pojo;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;


import javax.persistence.*;

@Entity
@Table(name = "user")
@JsonIgnoreProperties({"handler","hibernateLazyInitializer"})
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    int id;
    //用户名
    String name;
    //密码
    String password;



    //盐
    String salt;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getSalt() { return salt;}

    public void setSalt(String salt) { this.salt = salt; }
}

Result 类

Result 类是为了构造 response,主要是响应码。新建 result 包,创建 Result 类,代码如下

package com.lihao.onenote.result;

public class Result {
    /**
     * 响应状态码
     */
    //响应码 实际上由于响应码是固定的,code 属性应该是一个枚举值,这里作了一些简化。
    private int code;
    /**
     * 响应提示信息
     */
    private String message;
    /**
     * 响应结果对象
     */
    private Object data;

    public Result(int resultCode, String message, Object data) {
        this.code = resultCode;
        this.message = message;
        this.data = data;
    }


    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Result(int code) {
        this.code = code;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

实际上由于响应码是固定的,code 属性应该是一个枚举值,这里作了一些简化。

LoginController

Controller 是对响应进行处理的部分。这里我们设定账号是 admin,密码是 123456,分别与接收到的 User 类的 username 和 password 进行比较,根据结果返回不同的 Result,即不同的响应码。前端如果接收到成功的响应码(200),则跳转到 /index 页面。

在 wj 下新建 controller 包,新建 LoginController 类,代码如下

package com.lihao.onenote.controller;

import com.lihao.onenote.pojo.User;
import com.lihao.onenote.result.Result;
import com.lihao.onenote.service.UserService;

import com.lihao.onenote.util.ResultFactory;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;

import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;

import org.springframework.beans.factory.annotation.Autowired;
import org.apache.shiro.session.Session;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.HtmlUtils;


import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;


@Controller
public class LoginController {

    @Autowired
    UserService userService;

    //解决跨域访问
    @CrossOrigin
    @PostMapping(value = "/api/login")
    @ResponseBody
    public Result login(@RequestBody User requestUser, HttpSession session){
        String userName = requestUser.getName();
        // 对 html 标签进行转义,防止 XSS 攻击
        userName = HtmlUtils.htmlEscape(userName);
         if (!Objects.equals("admin", username) || !Objects.equals("123456", requestUser.getPassword())) {
            String message = "账号密码错误";
            System.out.println("test");
            return new Result(400);
        } else {
            return new Result(200);
        }
    }
}

这里只是为了演示前后端的交互过程,真正的登录验证要考虑更多因素,后面的文章会有详细介绍。另外教程初期对项目结构做了一些简化,实际上在 controller 里写这么多逻辑是不合理的,要尽量封装到 service 里面去。

最后,在 src\main\resources 文件夹下找到 application.properties 文件配置端口,即加上 server.port=8443(初始应该是空白的,后期还要配置数据库等)

5.测试项目

同时运行前端和后端项目,访问 localhost:8080/#/login,输入用户名 admin,密码 123456
在这里插入图片描述
点击确定,成功进入 localhost:8080/#/index

在这里插入图片描述
通过这篇文章,希望大家可以直观地感受到前后端分离项目中前后端的过程,之后的功能开发基本思路就是在后端开发 Controller,在前端开发不同的组件,这个顺序可以随意。实际的项目应该是前后端人员根据功能需求约定好接口,然后齐头并进,以提高开发效率。

接下来一段时间需要写的内容大概有以下这些:

  • 数据库的引入
  • 后端拦截器的配置
  • 部署项目时会遇到的一些坑
  • 使用 Element 辅助前端开发
  • 公共组件的开发

版权声明:本文为CSDN博主「Evan-Nightly」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值