从0到1 前后分离项目开发实战

前后端分离项目开发

文章目录

前言

前后端分离就是一个应用的前端代码和后端代码分开写,为什么这么做?

如果不使用前后端分离的方式,会有那些问题?

传统java web开发中,前端使用JSP开发,后端使用servlet。

前端 — 》HTML 静态文件 --》 后端 – 》 JSP

这种开发方式效率极低,可以使用前后端分离的方式进行开发,就可以完

美解决这一问题。

前端只需要独立编写客户端代码,后端也只需要独立编写服务端代码提供

数据接口即可。前端通过Ajax请求来访问后端的数据接口,将Model展示

到View中即可。

前后端开发者只需要提前约定好接口文档(URL、参数、数据类型…),然

后分别独立开发即可,前端可以造假数据进行测试,完全不需要依赖于后

端。完全不依赖于后端,前后端相互独立。前后端应用解耦,极大提升了开发效率。

单体 – 》 前端应用 + 后端应用

前端应用 : 负责数据展示和用户交互

后端应用: 负责提供数据处理接口。

前端HTML --》 Ajax --》 Restful 后端开发

从0到1 前后分离项目开发实战

技术栈

前端

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

从0到1 前后分离项目开发实战

可能出现的问题

vue ui 命令弹出空白页面

解决办法

在Chrome浏览器的设置中允许所有cookie

从0到1 前后分离项目开发实战

解决后执行命令

vue ui

出现的页面是如下所示:
从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

从0到1 前后分离项目开发实战

使用vscode打开创建好的vue项目;

从0到1 前后分离项目开发实战

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

访问

从0到1 前后分离项目开发实战

显示数据

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>

从0到1 前后分离项目开发实战

永远的CRUD

从上面的demo可以看出,数据是在前端页面构造的,而不是数据库获取的数据。因此,需要前端发送ajax请求得到数据.

安装axios

vue add axios

查看是否安装成功
从0到1 前后分离项目开发实战

安装element-ui

1. 在项目下 输入 npm install element-ui -S

2.查看配置文件package.json,是否有element-ui组件的版本号 如下图

从0到1 前后分离项目开发实战
4.在main.js文件中 引入 element 组件 :

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI)

从0到1 前后分离项目开发实战

5.使用elementui组件开发Student.vue页面

element-ui

demo展示
从0到1 前后分离项目开发实战

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发送请求保证每个接口正常执行

从0到1 前后分离项目开发实战

项目结构

从0到1 前后分离项目开发实战

(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>

这里面有很多细节,需要读者实操体会

项目结构

从0到1 前后分离项目开发实战
单文件上传 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

页面加载
从0到1 前后分离项目开发实战

编辑
从0到1 前后分离项目开发实战
添加
从0到1 前后分离项目开发实战

条件查询
从0到1 前后分离项目开发实战
删除看不到效果就不展示了。

上一篇:MySQL 视图


下一篇:Django框架之视图