前面我们已经完成了前端项目 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版权协议,转载请附上原文出处链接及本声明。