抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

记录了这个 Vue + SpringBoot 前后端项目开发的一些知识点以及解决方案

GitHub 项目地址

基本介绍

SpringBoot

  • SpringBoot 是全新一代的 Spring 框架
  • 开箱即用、预定优于配置、更轻量级
  • 常用的第三方依赖整合(通过 Maven 子父工程的方式),简化 XML 配置,全部采用注解形式,内嵌 Web 应用容器
  • AOP 编程的支持(AOP 面向切面编程),功能增强,注解形式
  • IoC(Inversion of Control)控制反转
  • 启动顺序
    • 调用构造函数进行数据初始化
    • 加载配置环境及上下文环境,返回应用配置上下文

vue

  • MVVM 设计模式
  • V:视图 view
  • ViewModel:一个同步 View 和 Model 的对象,连接 Model 和 View
  • M:模型 model,在其中定义数据修改和操作的业务逻辑
  • 双向数据绑定,通过 ViewModel 进行交互,Model 和 ViewModel 之间的交互是双向的,因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上

MVC、MVVM 和三层架构

  • MVC(设计模式)
    MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新
  • MVVM
    Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步
  • 三层架构(体系架构设计)
    • 高内聚,低耦合
    • 表现层、业务逻辑层、数据访问层(SpringMVC、Spring、MyBatis)

项目介绍

Atlantis Racer JMVT 网站是由游戏爱好者组建的 Atlantis Racer JMVT 车队的网站。该网站项目采用 Vue + SpringBoot 前后端分离技术,从 0 开始开发,90% 的功能或组件自主实现,其他功能或组件采用了第三方方案,引用了 Element-UI, github-markdown-css, mavon-editor, vuex 等。由于第一次接触 Vue 和 SpringBoot 等技术,边学习、边练习、边实践,因此项目中代码有很多地方冗余、复杂或有更为简单的方案实现,还能够继续优化。网站部署地址:http://atlantisracer.tk(挂)。

开发环境:

Windows 11 + VSCode (前端、后端) + IDEA (后端) + Edge + Apifox (接口测试) + MySQL Workbench (数据库) + Git

前后端请求路径:

前端访问:localhost:8080

后端请求:

  • localhost:8081/api
  • localhost:8082/api(于 utils/baseUrl.jscontroller/*/application.yml 下修改)

文件保存路径和数据库:

保存路径:

项目根目录 /Data/{photos|article_pictures}/(于 com/atlantis/common/ProjectPath.java 下修改)

测试用数据库:

TestSamples_DB.sql

实际使用数据库:

ForActualUse_DB.sql

实现的功能包括:

数据库表关系

路由关系

总结

这次的项目开发,我们学习到了很多如:前端 Web 语言如 HTML、JavaScript、CSS 等;后端的语言 Java;前端的主流框架 Vue 和后端的主流框架 SpringBoot 相结合进行前后端的分离开发等。这些都是在理论课的基础上,进行知识的拓展和应用,让我们更进一步地了解企业中开发一个项目的大体流程,是宝贵的经验。

项目遇到的问题及解决方案

组件固定于浏览器某一位置:

利用 position: fixed 属性固定。

禁止页面文字被复制:

设置css作用范围:

(未解决)获取当前路由路径并映射为中文:

路由跳转后,页面不刷新:

router-view 设置 :key 属性,路由改变,就会重新渲染。

子组件(子路由)向父组件(父路由)传递参数:

通过事件进行绑定,发生某个自定义事件,绑定一个方法,参数通过方法传递。

兄弟组件(兄弟路由)数据通信:

使用 vuex 即可。需要注意 vuex 在页面刷新后数据丢失,因此不要存储登录相关数据。

手动刷新子组件(子路由):

给子组件(子路由)设置 v-if 属性。自刷新可利用 provide, inject 注入函数依赖。刷新函数利用 this.$nextTick(function() {}) 改变绑定 v-if 的boolean变量的值。

cors跨域:

后端设置Config类,或者设置过滤器类。

// Config class
@Configuration
public class CorsConfig {

    // 当前跨域请求最大有效时长。1天
    private static final long MAX_AGE = 24 * 60 * 60;

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
        // corsConfiguration addAllowedOriginsPattern("*");
        corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
        corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
        // corsConfiguration.setAllowCredentials(true); // 解决前后的session对象不一致问题
        corsConfiguration.setMaxAge(MAX_AGE);
        source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
        System.out.println("doing CORS config...");
        return new CorsFilter(source);
    }
}

// filter class. Need to add 'ServletComponentScan' at Application class
@WebFilter(filterName = "requestFilter", urlPatterns = {"/*"})
public class RequestFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain  filterChain)
        throws IOException, ServletException {

        System.out.println("doing request filter...");

        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        // 此处 setHeader、addHeader 方法都可用。但 addHeader时写多个会报错:“...,but only one is allowed”
        response.setHeader("Access-Control-Allow-Origin", "*");
        // response.addHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
        // 解决预请求(发送2次请求),此问题也可在 nginx 中作相似设置解决。
        response.setHeader("Access-Control-Allow-Headers",
                "x-requested-with,Cache-Control,Pragma,Content-Type,Token, Content-Type");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        String method = request.getMethod();
        if (method.equalsIgnoreCase("OPTIONS")) {
            servletResponse.getOutputStream().write("Success".getBytes("utf-8"));
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void destroy() {
    }
}

cors跨域后session id不一致:

http://localhost:8080http://172.16.12.103:8080 只能成功一个。在 application.yml 中配置 Tomcat 监听ip地址。

模块开发:

利用maven的继承和聚合。建立父工程,只包含 pom.xml,修改打包方式为 pom、声明子工程、统一设置依赖版本号以及子工程版本号。子工程内将 <parent> 设为父工程,<dependency> 中的依赖具有传递性。

代码重复,进行复用:

这个项目里,Java使用继承和泛型来减少代码重复;vue内主要重复的是axios请求,采用字符串拼接减少重复。

// 关系:泛型ServiceImpl类实现泛型接口Service类。接口AdminService类继承泛型泛型接口Service类。AdminServiceImpl类继承泛型ServiceImp类,同时实现接口AdminService类。
// BaseService<T>
public interface BaseService<T> {
    public T getById(Integer id);
    public List<T> getAll();
    public boolean insert(T obj);
    public boolean update(T obj);
    public boolean updateInfo(T obj);
    public boolean updatePwd(T obj);
    public boolean delete(Integer id);
    public T login(T login);
}
// BaseServiceImpl<T>
public abstract class BaseServiceImpl<T> implements BaseService<T> {
    @Autowired
    // 代理对象自动装配,SpringBoot无法识别
    // userDao实例可能会报错,修改检查配置
    // protected 子类可继承
    protected BaseMapper<T> baseMapper;

    @Override
    public T getById(Integer id) {
        return baseMapper.getById(id);
    }

    @Override
    public List<T> getAll() {
        return new ArrayList<T>(baseMapper.getAll());
    }

    @Override
    public boolean insert(T obj) {
        return false;
    }

    @Override
    public boolean update(T obj) {
        return (baseMapper.update(obj) == 1);
    }

    @Override
    public boolean updateInfo(T obj) {
        return false;
    }

    @Override
    public boolean updatePwd(T obj)
    {
        return false;
    }

    @Override
    public boolean delete(Integer id) {
        return (baseMapper.delete(id) == 1);
    }

    @Override
    public T login(T login) {
        return null;
    }
}
// AdminService
public interface AdminService extends BaseService<Admin> {
}
// AdminServiceImpl
// IoC容器管理对象
@Service
public class AdminServiceImpl extends BaseServiceImpl<Admin> implements AdminService {

    @Override
    public boolean updateInfo(Admin admin) {
        return (baseMapper.updateInfo(admin) == 1);
    @Override
    public boolean updatePwd(Admin admin) {
        return (baseMapper.updatePwd(admin) == 1);
    }

    @Override
    public boolean insert(Admin admin) {
        // 查询数据库发现没有同名用户
        if (baseMapper.getByUsername(admin.getUsername()) == null)
        {
            return (baseMapper.insert(admin) == 1);
        }
        else
        {
            return false;
        }
    }

    @Override
    public Admin login(Admin login)
    {
        return (baseMapper.getByUsernameAndPassword(login.getUsername(), login.getPassword()));
    }
}

多个页面共用一个组件:

通过路径的变化,props 进行传参。props 内的参数JavaScript内无法直接获取,可以通过 watch 监听获得。

setTimeOut() 延迟执行:

用户头像上传下载:

利用 ElementUI 上传控件,并修改样式。

// basePath定义在 application.yml 配置文件中
// 用户的头像上传
@PostMapping("/upload/{id}")
public Result upload(MultipartFile file, @PathVariable Integer id)
{
    System.out.println("uploading...");
    // 获取的file是临时文件,需要转存

    File filePath = new File(basePath);
    // 若目录不存在,创建
    if (!filePath.exists())
    {
        filePath.mkdirs();
    }

    // 获取原始文件名后缀
    // String postfix = Objects.requireNonNull(
    //                    file.getOriginalFilename()).substring(
    //                            (file.getOriginalFilename().lastIndexOf(".")));

    // 根据id重新生成用户名
    // 文件上传加入后缀 '.png'
    String fileName = "userId_" + id.toString() + ".png";

    // 转存文件
    try {
        file.transferTo(new File(basePath + fileName));
    }
    catch (IOException e) {
        throw new SystemException("upload failed", e, Code.SYS_ERR);
    }

    return new Result(Code.UPLOAD_OK, (String)fileName, "upload succeeded");
}

// 用户头像下载
@GetMapping("/download/{id}")
public Result download(HttpServletResponse response, @PathVariable Integer id)
{
    try
    {
        // 输入流读取文件内容
        FileInputStream fis = new FileInputStream(basePath + "userId_" + id.toString() + ".png");
        // 输出流写回浏览器
        ServletOutputStream sos = response.getOutputStream();
        response.setContentType("image/png");

        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes)) != -1)
        {
            sos.write(bytes, 0, len);
            sos.flush();
        }

        //close
        fis.close();
        sos.close();
    }
    catch (Exception e)
    {
        e.printStackTrace();
        return new Result(Code.DOWNLOAD_ERR, null, "no user photo");
    }

    return new Result(Code.DOWNLOAD_OK,
                    basePath + "userId_" + id.toString() + ".png",
                    "download succeeded");
}

分页查询:

利用 ElementUI 简化前端分页组件的开发;后端利用 page-helper 进行分页查询的业务层和控制层处理。

前端:

后端:

搜索结果实时显示:

使用 watch:<input> 输入值进行监听。

浮动元素导致父元素高度塌陷:

添加以下css:

localStorage过期时间:

封装了localStorage,在存储的同时将时间存进去。

解析 Markdown 字符串:

使用 vue-markdown 插件,并引入 github-markdown-css 文件。

mavon-editor 图片上传、回显:

mavon-editor 使用方法:mavon-editor

mavon-editor 图片上传:pictures-upload

自定义 imgAdd 事件,后端设置上传接口;回显时设置 URL 磁盘映射,直接通过 URL 访问磁盘文件。

<div> 元素内超过范围文字显示省略号:




本站采用 Volantis 主题设计