前后端分离项目开发
文章目录
前言
前后端分离就是一个应用的前端代码和后端代码分开写,为什么这么做?
如果不使用前后端分离的方式,会有那些问题?
传统java web开发中,前端使用JSP开发,后端使用servlet。
前端 — 》HTML 静态文件 --》 后端 – 》 JSP
这种开发方式效率极低,可以使用前后端分离的方式进行开发,就可以完
美解决这一问题。
前端只需要独立编写客户端代码,后端也只需要独立编写服务端代码提供
数据接口即可。前端通过Ajax请求来访问后端的数据接口,将Model展示
到View中即可。
前后端开发者只需要提前约定好接口文档(URL、参数、数据类型…),然
后分别独立开发即可,前端可以造假数据进行测试,完全不需要依赖于后
端。完全不依赖于后端,前后端相互独立。前后端应用解耦,极大提升了开发效率。
单体 – 》 前端应用 + 后端应用
前端应用 : 负责数据展示和用户交互
后端应用: 负责提供数据处理接口。
前端HTML --》 Ajax --》 Restful 后端开发
技术栈
前端
1.vue.js
2.axios
3.element-ui
后端
1.springboot
2.mybatis-plus
数据库
mysql
安装 前端环境
node.js 查看是否安装成功
node -v
没有就得安装
安装淘宝镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org
npm 查看是否安装成功
npm -v
安装vue vue-cli 脚手架
cnpm install vue-cli 或者安装最新版 cnpm i -g @vue/cli
查看 vue-cli是否安装成功
vue --version
可能出现的问题是
’Set-ExecutionPolicy’ 不是内部或外部命令,也不是可运行的程序
解决办法
用管理员的身份打开 power shell,执行以下命令:
set-ExecutionPolicy RemoteSigned
get-ExecutionPolicy
创建vue项目
vue ui
可能出现的问题
vue ui 命令弹出空白页面
解决办法
在Chrome浏览器的设置中允许所有cookie
解决后执行命令
vue ui
出现的页面是如下所示:
使用vscode打开创建好的vue项目;
1)Book.vue
在 src/views 下创建 Book.vue
<template>
<div>
<table>
<tr>
<td>编号</td>
<td>书名</td>
<td>作者</td>
</tr>
<tr>
{{msg}}
</tr>
</table>
</div>
</template>
<script>
export default {
name: "Book",
data(){
return{
msg: 'Hello'
}
}
}
</script>
<style scoped>
</style>
2)index.js配置
打开 src/router 下的 index.js
index.js
- 导入Book.vue
import Book from “…/views/Book”
- 配置(一定要在前带 逗号(,) )
,{ path: ‘/book’, component: Book }
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Book from '../views/Book.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/book',
name: 'Book',
component: Book
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
3)测试
Terminal 中输入 npm run serve
访问
显示数据
Book.vue
-
遍历
books:被遍历的数组 ,item:每次遍历的对象
< tr v-for="item in books ">
< td >{{item.id}}< /td >
< td >{{item.name}}< /td >
< td >{{item.author}}< /td >
< /tr > -
books中添加数据
books: [ { id: 1, name: ‘Java’, author: ‘哈哈’ }, { id: 2, name: ‘C++’, author: ‘啦啦’ }, { id: 3, name: ‘Python’, author: ‘嘿嘿’ } ]
<template>
<div>
<table>
<tr>
<td>编号</td>
<td>书名</td>
<td>作者</td>
</tr>
<!--books:被遍历的数组 ,item:每次遍历的对象-->
<tr v-for="item in books">
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.author}}</td>
</tr>
</table>
</div>
</template>
<script>
export default {
name: "Book",
data() {
return {
msg: 'Hello',
books: [
{
id: 1,
name: 'Java',
author: '哈哈'
},
{
id: 2,
name: 'C++',
author: '啦啦'
},
{
id: 3,
name: 'Python',
author: '嘿嘿'
}
]
}
}
}
</script>
<style scoped>
</style>
永远的CRUD
从上面的demo可以看出,数据是在前端页面构造的,而不是数据库获取的数据。因此,需要前端发送ajax请求得到数据.
安装axios
vue add axios
查看是否安装成功
安装element-ui
1. 在项目下 输入 npm install element-ui -S
2.查看配置文件package.json,是否有element-ui组件的版本号 如下图
4.在main.js文件中 引入 element 组件 :
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
5.使用elementui组件开发Student.vue页面
demo展示
6.此时使用axios发送http请求访问后台页面数据会因为浏览器的同源策略造成跨域问题
解决办法
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET","HEAD","POST","DELETE","OPTIONS","PUT")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
开发步骤
(1)创建数据库 test 和表 student
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `test`;
/*Table structure for table `student` */
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'ID自增',
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
`gender` tinyint(1) DEFAULT NULL COMMENT '性别 0女1男',
`birthday` date DEFAULT NULL COMMENT '生日',
`headImageFilePath` varchar(100) DEFAULT NULL COMMENT '头像路径 学生头像相对路径',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8mb4;
/*Data for the table `student` */
insert into `student`(`id`,`name`,`gender`,`birthday`,`headImageFilePath`) values
(3,'wang',0,'2002-03-21','https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'),
(25,'cs',1,'2021-12-15','https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'),
(49,'15',0,'2021-12-14','');
(2)构建springboot项目 并按照mvc模式开发好 curd接口,使用postman发送请求保证每个接口正常执行
项目结构
(3)前端页面对应操作放置 对应的后端接口请求。
for example
后端对应
@RequestMapping("/selectAll")
public List<Student> selectAll(){
return studentService.selectAll();
}
由前台页面发送查询请求
loadData(){
const _this = this;
axios.get('http://127.0.0.1:8081/selectAll').then(function(resp){
_this.students = resp.data;
}).catch((err) =>{
console.log(err);
});
},
源码 :页面Student.vue
<template>
<div>
<el-dialog title="学生信息" :visible.sync="dialogFormVisible">
<el-form :model="form">
<el-form-item label="名称" :label-width="formLabelWidth">
<el-input v-model="form.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="性别" :label-width="formLabelWidth">
<el-select v-model="form.gender" placeholder="请选择性别">
<!-- 通过循环的形式展示出下拉菜单 key必须添加,否则可能会出错,相当于唯一性标识 -->
<el-option v-for="sextype in sexType"
:key="sextype.type"
:label="sextype.name"
:value="sextype.type">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="生日" :label-width="formLabelWidth">
<el-date-picker type="date" placeholder="日期" value-format="yyyy-MM-dd" v-model="form.birthday"></el-date-picker>
</el-form-item>
<el-form-item label="头像" :label-width="formLabelWidth">
<!-- <el-input v-model="form.headImageFilePath"></el-input> -->
<!-- 文件上传 -->
<single-upload v-model="form.headImageFilePath"></single-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="update">确 定</el-button>
</div>
</el-dialog>
<el-form :model="queryParam" ref="form" label-width="100px" class="demo-ruleForm" size="mini">
<el-row>
<el-col :span="8">
<el-form-item label="姓名">
<el-input v-model="queryParam.name"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="性别">
<el-select v-model="queryParam.gender" placeholder="性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="0"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item>
<el-button type="primary" @click="add">新增</el-button>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="16">
<el-form-item label="生日">
<el-date-picker type="date" placeholder="开始日期" value-format="yyyy-MM-dd" v-model="queryParam.startDate"></el-date-picker>
--
<el-date-picker type="date" placeholder="结束日期" value-format="yyyy-MM-dd" v-model="queryParam.endDate" ></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item>
<el-button type="primary" @click="searchQuery" icon="el-icon-search">查询</el-button>
<el-button type="warning" @click="resetForm" icon="el-icon-search" plain>重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-table
:data="students"
style="width: 100%">
<el-table-column
prop="id"
label="ID"
v-if="false"
width="180">
</el-table-column>
<el-table-column
prop="headImageFilePath"
label="头像"
width="180">
<template slot-scope="scope">
<img :src="scope.row.headImageFilePath" min-width="70" height="70" />
</template>
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="gender"
label="性别"
width="180">
<template slot-scope="scope">
<!-- 通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据
用|分割,前面获取传给后面,然后通过filters获取 -->
<span>{{scope.row.gender | sexTypeFilter}}</span>
</template>
</el-table-column>
<el-table-column
prop="birthday"
value-format="yyyy-MM-dd"
label="生日">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
// @ is an alias to /src
import SingleUpload from '@/components/upload/singleUpload.vue'
// 过滤器的数据写在data外面,因为过滤器不能调用this.
const sexType = [
{ "type":1, "name":'男'},
{ "type":0, "name":'女'}
]
export default {
name: "Student",
components:{
SingleUpload
},
data(){
return {
sexType,
input: '',
students: [
],
fits: ['fill'],
url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
imageUrl: '',
dialogFormVisible: false,
formLabelWidth: '80px',
flag: '', // 因为编辑和新增共用一个弹框元素,因此用flag加以区分
form: {
name: '',
gender: '',
birthday:'',
headImageFilePath:''
},
queryParam: {
name: '',
gender: '',
birthday:'',
starDate: '',
endDate: ''
}
}
},
//过滤器,数字类型和汉字的转换
//两种写法都可以,type是传入的数字,与过滤器数据对比
// sexType.find(obj=>{
// return obj.type === type})
filters:{
sexTypeFilter(type){
const sexTy = sexType.find(obj => obj.type === type)
return sexTy ? sexTy.name : null
},
},
created(){
// this.loadData();
this.searchQuery();
},
methods: {
loadData(){
const _this = this;
axios.get('http://127.0.0.1:8081/selectAll').then(function(resp){
_this.students = resp.data;
}).catch((err) =>{
console.log(err);
});
},
add(){
this.flag = 1;
this.form = {
name: '',
gender: '',
birthday:'',
headImageFilePath:''
},
// 设置点击按钮之后进行显示对话框
this.dialogFormVisible = true;
},
cancel(){
this.dialogFormVisible = false;
},
update(){
if(this.flag == 1){
// 将我们添加的信息提交到总数据里面
this.students.push(this.form);
this.save();
}else{
this.updateOne();
}
this.dialogFormVisible = false;
},
save(){
axios.post('http://127.0.0.1:8081/add',this.form).then((resp) =>{
}).catch((err) =>{
console.log(err);
});
},
updateOne(){
var s = this.form;
axios.post('http://127.0.0.1:8081/update',this.form).then((resp) =>{
}).catch((err) =>{
console.log(err);
});
},
searchQuery(){
var that = this;
axios.post('http://127.0.0.1:8081/selectAllbyCondition',this.queryParam).then((resp) =>{
that.students = resp.data;
}).catch((err) =>{
console.log(err);
});
},
resetForm(){
this.flag = '';
this.queryParam = {};
},
handleEdit(index, row) {
this.flag = 2;
// 将数据的index传递过来用于实现数据的回显
this.form = this.students[index];
// 设置对话框的可见
this.dialogFormVisible = true;
},
handleDelete(index, row) {
var id = row.id;
axios.delete('http://127.0.0.1:8081/delete'+'/'+id)
.then(() => {
this.students.splice(index, 1);
this.$message({
type: "success",
message: "删除成功!"
});
}).catch((err) =>{
console.log(err);
});
}
},
}
</script>
<style>
</style>
这里面有很多细节,需要读者实操体会
项目结构
单文件上传 singleUpload.vue
<template>
<el-upload
class="avatar-uploader"
action="https://jsonplaceholder.typicode.com/posts/"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</template>
<script>
export default {
data() {
return {
imageUrl: '',
file:'',
headers: {
'Content-Type': 'multipart/form-data', // 默认值
},
// 图片上传参数
// actionUrl: this.$axios.defaults.baseURL + '/file/uploadImage',
}
},
methods: {
handleAvatarSuccess(res, file) {
debugger;
this.imageUrl = URL.createObjectURL(file.raw)
},
beforeAvatarUpload(file) {
debugger;
var s = file;
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
}
return isJPG && isLt2M
},
},
}
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
Show
页面加载
编辑
添加
条件查询
删除看不到效果就不展示了。