前后端分离已经成为现今的主流开发模式,本篇将通过 demo 报道登记管理系统介绍如何搭建一个前后端分离的 web 应用。
1、报道登记管理系统介绍
后台技术栈:
spring + spring mvc + mybatis + mysql8.0
前台技术:
vue + vuex + element-ui
主要功能介绍
1、用户登录(包括自动登录)、退出、权限拦截
2、学生报道登记管理(列表、新增、编辑、删除、按条件查询、文件上传)
2、页面展示
3、前后端项目结构
目录及构图
前后端分离搭建项目时的要点:
4、解决前后端分离时的跨域问题
4.1、为什么会产生跨域问题
跨域问题是因为浏览器的同源策略引起的,所谓同源是指 " 协议 + 域名 + 端口 " 三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。同源策略 /SOP(Same origin policy)是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,现在所有支持 JavaScript 的浏览器都会使用这个策略。如果缺少了同源策略,浏览器很容易受到 XSS、 CSFR 等攻击。
4.2、如何解决跨域
4.2.1、前端解决
前端项目如果是 vue 项目,可以在 vue 的全局配置文件vue.config.js 中进行配置,将需要访问的后端地址进行代理,从安全方面考虑,推荐这样做,具体配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const define = require("./src/util/define")
module.exports = { lintOnSave: false, devServer: { proxy: { "/api": { target: define.APIURL, ws: true, changeOrigin: true, pathRewrite: { '^/api': '' } } } } }
|
4.2.2、后端解决
后端我使用的 spring 版本是 4.0 以上的,所以配置跨域很简单,只需要在相应的控制器或方法上加上注解 @CrossOrigin(origins = {"http://localhost:8080"})。
你也可以直接使用 @CrossOrgin 来进行配置,但是这样所有的访问地址都可以访问到你的服务,不安全。同时,这个注解也可以放在方法上,表示这个方法单独可以被访问。
关于后端解决跨域的方式还有其他途径,比如全局配置拦截器,这里再不做细讲。
5、ssm 重要的配置文件
Pom.xml 依赖配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion>
<groupId>com.cxsw</groupId> <artifactId>rmsService</artifactId> <version>1.0</version> <packaging>war</packaging>
<name>rmsService</name> <url>http://localhost:3000</url>
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties>
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.22</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.22</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.22</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.22</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.22</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.9.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.20</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.36</version> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>3000</port> <path>/</path> </configuration> </plugin> </plugins> </build> </project>
|
ApplicationContext.xml (spring 核心配置文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.cxsw.rms"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${druid.jdbc.driver}"/> <property name="url" value="${druid.jdbc.url}"/> <property name="username" value="${druid.jdbc.username}"/> <property name="password" value="${druid.jdbc.password}"/> </bean>
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis.xml"/> <property name="mapperLocations" value="classpath:/mappers/*.xml"/> </bean> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg ref="sqlSessionFactoryBean"/> </bean> <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.cxsw.rms.repository"/> </bean> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager"> <tx:attributes> <tx:method name="insert*"/> <tx:method name="delete*"/> <tx:method name="update*"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="pointCut" expression="execution(* com.cxsw.rms.service.impl.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/> </aop:config>
</beans>
|
SpringMVC.xml (springMVC 的核心配置文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.cxsw.rms.controller" /> <mvc:annotation-driven />
<bean id="dateConverter" class="com.cxsw.rms.convert.DateConverter" />
<bean class="org.springframework.context.support.ConversionServiceFactoryBean" id="conversionService"> <property name="converters" ref="dateConverter" />
</bean> <mvc:annotation-driven conversion-service="conversionService"/>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="52128800" /> <property name="maxUploadSizePerFile" value="52128800"/> <property name="defaultEncoding" value="UTF-8"/> </bean>
<mvc:default-servlet-handler />
</beans>
|
Mybatis 配置文件(ssm 集成之后可以不需要此配置文件,个人习惯而已)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases> <typeAlias type="com.cxsw.rms.entity.User" alias="user"/> <typeAlias type="com.cxsw.rms.entity.Student" alias="student"/> </typeAliases>
</configuration>
|
数据库的配置文件
1 2 3 4
| druid.jdbc.driver=com.mysql.cj.jdbc.Driver druid.jdbc.url=jdbc:mysql://localhost:3306/rms?useUnicode=true&characterEncoding=utf-8 druid.jdbc.username=root druid.jdbc.password=wb7446032001
|
Mapper 文件(示例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cxsw.rms.repository.StudentMapper">
<resultMap id="studentMap" type="student"> <id property="sid" column="SID" /> <result property="name" column="NAME" /> <result property="nation" column="NATION" /> <result property="idCard" column="ID_CARD" /> <result property="region" column="REGION" /> <result property="education" column="EDUCATION" /> <result property="school" column="SCHOOL" /> <result property="major" column="MAJOR" /> <result property="educationType" column="EDUCATION_TYPE" /> <result property="email" column="EMAIL" /> <result property="phone" column="PHONE" /> <result property="employeeUnit" column="EMPLOYEE_UNIT" /> <result property="registerBook" column="REGISTER_BOOK" /> <result property="diploma" column="DIPLOMA" /> <result property="creatTime" column="CREAT_TIME" /> </resultMap>
<insert id="insertStudent" parameterType="student"> INSERT INTO STUDENTS (`SID`, `NAME`, NATION, ID_CARD, REGION, EDUCATION, SCHOOL, MAJOR, EDUCATION_TYPE, EMAIL, PHONE, EMPLOYEE_UNIT, REGISTER_BOOK, DIPLOMA, CREAT_TIME) VALUE ( #{sid}, #{name}, #{nation}, #{idCard}, #{region}, #{education}, #{school}, #{major}, #{educationType}, #{email}, #{phone}, #{employeeUnit}, #{registerBook}, #{diploma}, #{creatTime} ) </insert>
</mapper>
|
6、后端三层架构示例
持久层和实体类
映射数据库字段的实体个人建议使用 Lombok 代码生成器,这样你在修改字段的时候就不用来回手动生成 get set tostring …… 等方法了。
用法如下:
1 2 3 4 5
| <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> </dependency>
|
通过上图就可以发现,在单独写了实体字段的情况下,通过注解就可以自动生成代码。
持久层的接口:这里以新增数据的某个接口示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public interface StudentMapper {
int insertStudent(Student student); }
|
对应的 mapper(由于新增字段有点多,没有使用 mybatis 的注解来写 sql)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cxsw.rms.repository.StudentMapper">
<insert id="insertStudent" parameterType="student"> INSERT INTO STUDENTS (`SID`, `NAME`, NATION, ID_CARD, REGION, EDUCATION, SCHOOL, MAJOR, EDUCATION_TYPE, EMAIL, PHONE, EMPLOYEE_UNIT, REGISTER_BOOK, DIPLOMA, CREAT_TIME) VALUE ( #{sid}, #{name}, #{nation}, #{idCard}, #{region}, #{education}, #{school}, #{major}, #{educationType}, #{email}, #{phone}, #{employeeUnit}, #{registerBook}, #{diploma}, #{creatTime} ) </insert>
</mapper>
|
业务层(由于业务不是很复杂,就是简单的数据处理,所以业务层就是调用持久层的接口返回数据)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public interface StudentService {
String insertStudent(Student student); }
|
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
@Service public class StudentServiceImpl implements StudentService {
@Resource private StudentMapper studentMapper;
@Override public String insertStudent(Student student) {
Date date = new Date(new java.util.Date(System.currentTimeMillis()).getTime()); student.setCreatTime(date); studentMapper.insertStudent(student); List<Student> students = studentMapper.listStudents(student.getName(), ""); User user = new User(); user.setUsername(student.getName()); user.setSid(students.get(0).getSid()); int res = userMapper.updateUserForeignKey(user); if(res == 0){ return " 新增失败 "; }else{ return " 新增成功 "; } } }
|
控制层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
@RestController @RequestMapping(value = "/student") public class StudentController {
@Resource private StudentService studentService;
@PostMapping public Result createStudentList(@RequestBody Student student){
if(student == null){ return new Result(500, " 上报信息不全 ", null); }else{ String msg = studentService.insertStudent(student); return new Result(200, " 操作成功 ", msg); }
}
}
|
✅ 😁这就完成后端的一个接口了,紧接着测试完就可以将接口交给前端了,在这里,个人建议写接口写类的时候一定要写好 javadoc 文档注释 ,这样方便后来者查看,我写代码的时候一定会这样想: 代码是写给别人看的,不是写给自己看的,所以一定要简洁明了。
7、前端使用接口
✅ 前端我使用的是 axios 发送请求,并对请求做了简单封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| import axios from "axios";
import { message } from "@/util/message"
axios.interceptors.response.use( response => { if (!response?.data) return; const res = response.data; if (res.code !== 200) { message({ message: res.msg || ' 请求出错,请重试 ', type: "error", duration: 1500, onClose: () => { } }) } else { return res; } }, error => { message({ message: " 请求出错,请重试 ", type: "error", duration: 1500 }) return Promise.reject(error) } )
const request = async ({ url, method, data, headers }) => {
const type = method.toLocaleUpperCase();
if (type === 'GET') { return await new Promise((res, rej) => { getRequest(url, data, res, rej) }) } else if (type === 'POST') { return await new Promise((res, rej) => { postRequest(url, data, headers, res, rej) }) } else if (type === 'PUT') { return await new Promise((res, rej) => { putReqeust(url, data, res, rej) }) } else { return await new Promise((res, rej) => { deleteRequest(url, res, rej) }) } }
const getRequest = (url, datas, res, rej) => { axios.get(url, { params: datas }).then((data) => { res(data) }).catch((error) => { rej(error) }) }
const putReqeust = (url, datas, res, rej) => { axios.put(url, datas).then((data) => { res(data) }).catch((error) => { rej(error) }) }
const deleteRequest = (url, res, rej) => { axios.delete(url).then((data) => { res(data) }).catch((error) => { rej(error) }) }
const postRequest = (url, datas, headers, res, rej) => { axios.post(url, datas, headers).then((data) => { res(data) }).catch((error) => { rej(error) }) }
export default request;
|
message 的封装如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { Message } from 'element-ui'
let messageInstance = null;
const resetMessage = (options) => { if (messageInstance) { messageInstance.close() } messageInstance = Message(options) }; let tipType = ["error", "info", "success", "warning"]; tipType.forEach(type => { resetMessage[type] = options => { if (typeof options === "string") { options = { message: options } } options.type = type return resetMessage(options) } }) export const message = resetMessage
|
在 src 下构建目录 api 用于统一管理接口:
1 2 3 4 5 6 7 8 9 10
| import request from "@/util/request";
export const addStudnetMsg = (data) => { return request({ url: "/api/student", method: "POST", data }) }
|
在相应的页面中调用接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import { updateStudentMsg, addStudnetMsg } from "@/api/student"
submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) {
const formMethod = this.ruleForm.sid ? updateStudentMsg : addStudnetMsg; this.ruleForm.region = this.ruleForm.region[0] + "-" + this.ruleForm.region[1] + "-" + this.ruleForm.region[2] formMethod(this.ruleForm).then((data) => { if (data?.code === 200) { message({ type: "success", message: data.msg, duration: 1500, onClose: () => { this.ruleForm.registerBook = '' this.ruleForm.diploma = '' this.$router.push("/"); if (!this.ruleForm.sid) { let params = { username: sessionUtils.get('token').username }; listUsers(params).then((data) => { if (data?.code === 200) { data.data[0].password = ""; sessionUtils.set("token", data.data[0], 1000 * 60 * 60 * 6) this.$store.dispatch("userInfo/getUserInfo", data.data[0]); } }) } } }) } })
} else { return false; } });
|
😜至此,一个接口通了。
8、源码地址
前端 gitee 地址:https://gitee.com/wenjingxin/rms.git
后端 gitee 地址:https://gitee.com/wenjingxin/ssm.git
😁 分享不易,希望多多支持 🤝