人一旦闲下来,是十分可怕的,就比如我,自从上了大学,每年国庆都能整出点骚活来:去年国庆,用 Jetpack Compose 搓了一个课程表 Android App,而到了今年,我直接搓了一个网站前后端出来……
其实很早以前我就想开发一套面向我校学生的匿名树洞网站了,早在半个月前,我就已经开始研究如何将自己的服务接入学校的 CAS 统一认证系统里,正好十一闲着没事儿干,遂说干就干,就开始了开发。
因为先前有过相关的学习和开发经验,因此我毫不犹豫地选择了前后端分离的开发模式:前端采用 Vue 3 作为 JavaScript 框架,Vuetify 作为 UI 框架;后端采用 Java 开发 Spring Boot 框架应用。
虽然说是毫不犹豫,但其实前端和后端选型的时候,我还是有一些调整和妥协。前端方面,其实直到现在,Vuetify 的 Vue 3 适配版本 Vuetify Titan 仍处于 Beta Live 状态,RC 版本可能仍需要几个月的时间才会产生,但是因为 Vuetify 提供的组件和其他 API 相比其他 UI 框架实在不知道高到哪里去了,又因为个人也非常喜欢 Material Design,遂仍旧采用了 Vuetify。
而后端方面,作为一个 Kotlin 爱好者,刚开始我其实是打算用 Kotlin 开发后端的,但是又考虑到这套代码可能可以供学校的学生在入门 Java 或是 Spring Boot 开发的时候能作为参考学习(当然,这是我的一厢情愿),遂决定改用 Java 开发。
接下来,让我们谈谈详细的实际开发内容部分:
先来谈谈前端。前端开发上,我采用了 vite 作为构建工具,使用 yarn 作为包管理器,除了 vue 和 vuetify 以外,我还主要引入了这些依赖:
vue-router
(Vue 官方开发的路由系统)vue-showdown
(一套对 Markdown 解析库 showdown.js 的 Vue 封装)typescript
(由于 Vuetify 的引导式命令行新建项目向导默认初始化的项目没有 typescript,因此我手动引入了,但是不知道是不是我的配置问题,这导致 IDE 导入在 ts 文件中声明的函数时,导入的文件雷静总是错误的变为 js 而不是 ts)我想得到的一个成品是:
最后,我大差不差的把这些页面的原型都开发了出来,在后端开发完成后,我又成功完成了与后端的对接,不过,与期望不同的是一些小问题导致的差异:
开发前端期间,还遇到了许多疑难问题,比如组件中使用 this
作用域在开发环境可以工作,但是在生产环境无法工作的问题,又比如 Vue 3 新的组合式 API 和 setup 函数与先前使用方式不同导致差异的问题,又比如使用异步 fetch API 的问题。不过好在这些问题最后都有惊无险的化解了。不过在这里,必须特别感谢 GitHub 上 这位老兄的 Gist 提供了一套在 Vue 上使用异步 computed 属性的方式,简直是救了我的命(我在这个一年前的 Gist 下面回复,作者竟然还回我了,在交谈中,他建议我在现在最好使用 VueUse 提供的 computedAsync 功能,不过因为我懒得调整了所以最后没用)。
前端部分的原型和主要框架开发大概花费了 5 天时间(9/30/2022 —— 10/4/2022),之后,我便开始着手开发后端。比起略显生涩的前端,早已驾轻就熟的后端才是我的大本营,因此开发时间也很快。
后端主要引入的开发依赖有:
org.springframework.boot:spring-boot-starter-data-jpa
, org.springframework.boot:spring-boot-starter-data-jdbc
, mysql:mysql-connector-java
ORM,数据库连接桥和数据库驱动;org.springframework.boot:spring-boot-starter-web
Spring Boot Web 开发 Starter;org.springframework.boot:spring-boot-starter-cache
, org.springframework.boot:spring-boot-starter-data-redis
, org.springframework.session:spring-session-data-redis
, Spring Boot 数据和会话 Redis 缓存 Starter;org.springframework.boot:spring-boot-starter-mail
Spring Boot 邮件管理 Startercn.dev33:sa-token-spring-boot-starter
SA Token 的 Spring Boot Starter 封装com.google.code.gson:gson
Google 的 Json 解析库com.squareup.okhttp3:okhttp
一个 Kotlin 开发的 HTTP 客户端com.fasterxml.jackson.dataformat:jackson-dataformat-xml
Jackson 的 XML 解析模块(引入这个本来是为了识别 CAS 统一认证系统返回的 XML 信息)cn.hutool:hutool-all
一个功能及其丰富和强大的 Utils 库com.ramostear:Happy-Captcha
一个使用简单,功能强大的验证码模块org.projectlombok:lombok
Lombok(其实我是不想用 Lombok 的,但是奈何 Getter 和 Setter 太多了看得我眼花缭乱,不得已还是得把 Lombok 请回来)采用了这些数据结构:
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "users")
public class UserEntity {
@Id
@Column(nullable = false)
private long id;
private String email;
private String password;
@OneToMany(mappedBy = "poster")
private List<PostEntity> createdPosts;
@OneToMany(mappedBy = "poster")
private List<CommentEntity> createdComments;
@ManyToMany
@JoinTable(name = "STARRED_POSTS")
private List<PostEntity> starredPosts;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "posts")
public class PostEntity {
@Id
@Column(nullable = false)
@SequenceGenerator(name = "post_id_seq", sequenceName = "post_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_id_seq")
private long id;
@ManyToOne
private UserEntity poster;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "post")
private List<CommentEntity> comments;
@ManyToMany(mappedBy = "starredPosts")
private List<UserEntity> starredUsers;
@Column(nullable = false)
private Date postTime;
@Column(nullable = false, length = 65535, columnDefinition = "Text")
private String content;
@ElementCollection
private List<String> attributes;
@ElementCollection
private List<String> tags;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "comments")
public class CommentEntity {
@Id
@Column(nullable = false)
@SequenceGenerator(name = "comment_id_seq", sequenceName = "comment_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "comment_id_seq")
private long id;
@ManyToOne
private UserEntity poster;
@ManyToOne
private PostEntity post;
@Column(nullable = false)
private Date postTime;
@Column(nullable = false)
private String content;
}
添加了这些 Controller:
AuthController
登录和注册相关接口CaptchaController
验证码相关接口PostController
树洞发布和回复相关接口UserController
用户信息相关接口WebVpnController
WebVPN 相关接口(不过最后没用上)这期间也遇到了一些新的坑:
jackson-dataformat-xml
导致 RestController 默认返回 XML 数据而不是 Gson(通过在 Spring Application 配置文件设置 spring.mvc.converters.preferred-json-mapper
,且在前端请求时显式指定 Content-Type 解决)(另外,HuTool 真是太好用了,我已经无法想象没有 HuTool 的 Java 开发了)
生产站点: XAUFEHole – 西财树洞 (minecraft.kim)
其实可能用手机看起来效果会更好些:
个人感觉还是做了个很棒的工作的,并且最后的效果也很符合我的预期(除了人流量以外)。
这些代码也开源到了 GitHub 上(还没来得及设定一个开源许可证),有兴趣的可以参考看看:
shaokeyibb/XAUFEHoleFrontend: 西财树洞前端程序,Made by Vue3 && Vuetify (github.com)
shaokeyibb/XAUFEHoleBackend: 西财树洞后端程序,Made by SpringBoot (github.com)
那么,就这样吧。