我是如何构建并维护亦习校园的——总结亦习校园技术栈
# 前言
2019年底爆发的新冠疫情改变了很多事情,我觉得对我来说,它改变了我大学的走向。恰好在武汉上大学,我竟然喜提寒暑假直通券一张,寒假回家,暑假放完才回去!当然课还是要上的。前所未有的纯线上授课模式带来了很多问题,对于拖延癌晚期的我来说最明显的就是一点:作业太多,老忘记交。高中的代码经验告诉我,这个时候就需要用代码来解决问题了。我想要构建一个允许班委发布作业,同学在上面提交作业,最后班委可以一键收集的网站。
我尝试了之前完全没有接触过的Web开发,用PHP+Mysql搭建了第一代亦习校园(当时叫“学生学习平台”)。感谢班长的大力支持,它在我们班稳定运行了线上教学的整个学期,并屡收好评。此时我已经发现,前后端耦合的PHP屎山代码已经越来越不可维护了,想要将其彻底重写,前后端分离。
于是,靠着机缘巧合认识了志同道合的伙伴,一起为亦习校园添砖加瓦。随着其他教务查询功能的迭代,亦习校园居然慢慢被设计成了一个挺完善的软件系统,且听我慢慢道来......
感谢亦习校园团队每一位成员的辛勤付出。他们分别是:
- 张谦煜(本文作者,创始人)
- 李子健(后端开发、爬虫开发)
- 孙龙飞(后端开发)
- 黄杨(前端开发)
- 张丽艳(UI设计、LOGO设计)
- 滕坤宇(UI设计、宣传图设计)
- 洪锦辉(后端开发)
- 张可(后端开发)
- 廖宜蕾(文案编辑)
- 葛宇凡(前端开发)
同时感谢韩班长、涂班长等同学对亦习校园的支持,感谢辅导员程老师、曹老师、于老师的支持,很幸运,在武汉遇到最好的你们。
感谢湖工大信息中心给予的支持和帮助。
这里要特别感谢我的女朋友,“亦习校园”是她命名的,在产品推广和研发过程中给了我非常大的帮助。如果没有她,亦习校园不会有现在的成就。
# 前端技术栈
亦习校园目前在Web、微信小程序、Android App三大平台上线。
# Web
亦习校园第一代Web是采用PHP+笔下光年样式模板库开发的。主要是复用模板库提供的一些样式,然后自己做一些微调和布局,直接用PHP插值在里面呈现数据。
亦习校园第二代Web开始,采用前后端分离架构。我采用Vue2和Element UI完全重构代码,并尽量保证整体视觉效果和原来一致。一开始还好,这代码越开发越复杂混乱,充斥大量的重复逻辑代码:
由于我们设计了权限控制,不同用户看到的东西不一样,很多地方都需要重复判断逻辑,开始的时候高强度写代码是写爽了,后面慢慢维护,想着改一下权限判断逻辑时候简直是苦上了天,改了一处发现还有其他地方也需要改,每次发版都没有自信,瑟瑟发抖,生怕哪里改漏了。
不过,代码可维护性其实相比第一代PHP时代已经好很多了,我们顺利完成了大量功能迭代,大量用户也从这个版本开始接触、使用亦习校园服务。
亦习校园第三代Web没有带来颠覆性改动,从第三代开始,我们正式从“学生学习平台”更名“亦习校园”,并加入了第三方登录相关的逻辑,这里直接和第二代一起总结技术栈。
- 基于Vue CLI生成的项目骨架,我尝试将一些可复用组件抽提,并在多处进行复用。
- 使用Vuex做全局状态管理,主要存储用户信息。
- 使用Vue Router做页面路由逻辑,页面URL参考Restful思想,像作业ID这种参数,直接作为路径的一部分传递。
- 编写全局路由守卫做用户登录态拦截鉴权。
- 浏览器刷新时,由于Vuex中存储的数据会丢失,而有些页面中的组件需要依赖Vuex中一些用户数据才能呈现,为了减少白屏加载和偶尔出现的异常报错现象,使用localstorage暂存用户信息,页面加载时若存在缓存,则直接加载,然后再异步请求后端接口更新数据。
- 网络请求使用Axios实现,用户登录态采用Cookie存储。
- 网站部署域名和接口域名不一样,会遇到CORS跨域问题,这里采用的是反代的解决方法,开发时指定Vue的devServer来帮忙反代请求,部署时使用Nginx实现反代。通过反代,还能刚好解决测试和生产环境分离的问题,做到同一套代码灵活更改接口调用环境(路径)。
总的来说,随着项目逻辑的复杂和功能的增加,第二/三代亦习校园Web也逐渐展现了代码的坏味道。部分页面逻辑过多且没有组件化开发(当时觉得只有可复用的组件才需要组件化开发),比如登录页面,实现了普通登录、教务登录、注册、第三方登录、忘记密码、各种小提示弹窗等等功能,Vue2的Options API确实让代码非常混乱,可读性很差,代码逻辑连贯性不强。
亦习校园第四代使用Vue3完全重写了代码,同时重新设计了界面。
- 原来一些逻辑耦合性强的代码,通过组合式API的思想进行抽提合整,变得更加易读、容易维护。
- 更多地去抽提组件,我觉得按逻辑来抽提组件,能起到很不错的效果。
- 使用Vuex和Vue Router进行全局状态管理和路由服务。
- 由于此时接入第二代API,登录态由之前的Cookie变为基于token传递,需要在每次发送请求前附加token字段,并处理token刷新逻辑,因此编写了Axios拦截器实现这一目的。
- 尝试在拦截器中对业务接口调用错误的情况进行统一GUI处理,利用接口
userMsg
字段返回的用户展示文本,直接在拦截器中执行弹窗报错。
# 微信小程序
微信小程序使用uni-app进行开发。由于其开发模式非常类似Vue,上手很快,只需要学习部分不兼容的特性和差别,了解其API的使用就快速上手了。项目开发起来基本和开发Vue项目没有太大区别。由于uni-app自带的网络请求不支持编写拦截器,我直接自己封装了拦截器调用方法来实现token相关逻辑。微信小程序同样注重代码复用,进行了较细粒度的组件抽提,比如,作业列表的每一项作业都会抽提出单独的组件。
# Android APP
安卓APP是我之前从未涉及的领域。原生安卓开发之前接触过一些,但是感觉和Web开发的思路完全不一样,有些一下子学不进去,遂放弃。不过,我基于Angular和ionic(使用cordova),顺利用写Web的方式开发出了不错的安卓APP。Angular和ionic国内市占率都不高,中午教程和博客都比较少,学习过程中也是顺便培养了自己的英语阅读能力:
学Angular,直接照着官方英文文档看,顺着它的案例写一遍,有Vue的基础,就大概能明白Vue里的一些基本概念和语法在Angular中的对应形式。学ionic,同样照着官方英文文档看,用CLI把项目框架生成好,搭好adb环境跑起来安卓实时调试,基本就预示着我基本入门,可以开始业务逻辑开发了。遇到问题,直接英文google。
Angular相比于Vue,是一种全新的前端编程模式。它把前端编程变得更像是Spring框架下井井有条的后端编程模式。它采用的发布订阅思想也是着实让我眼前一亮。
- 简单学习了Rxjs的基本内容,但仅仅是会用一些皮毛。
- 使用了Angular自带的HTTPClient发送网络请求,编写拦截器处理HTTP异常处理、token逻辑和请求缓存机制。其中,请求缓存机制是对于指定了需要缓存的请求,每次请求前先访问缓存,是否存在不超过十分钟前发送的相同请求。若有,直接从缓存读取,若没有,发送请求,并更新/写入缓存。
- 引入了Lottie Splash Screen实现了动画loading效果,非常不戳。
# 后端技术栈
亦习校园后端主要使用Java(spring boot)编写,辅以使用PHP编写的邮件发送服务(部署于阿里云函数计算)和NodeJs编写的Websocket、文件管理中间件。
亦习校园主API(Java编写)突出实现了以下特性:
- 参考阿里云JAVA开发规约,严格统一返回值格式。接口返回值均为JSON字符串,有
taskId
(接口任务号)、code
(错误码)、data
(返回的业务数据)、userMsg
(要回显用户的文本信息)、errMsg
(便于开发人员定位错误,但不包含隐私数据的文本信息)四大字段。 - 参考阿里云JAVA开发规约,统一规范返回值的错误码。成功码定义为
00000
,客户端、服务器、第三方服务错误分别以A
、B
、C
开头。错误码通过常量在程序内使用。 - 用户登录态鉴权采用JWT(JSON Web Tokens)方案,通过与Shiro配合使用,实现通过注解指定需要登录态的接口,统一优先校验token是否有效,并通过token中存储的用户ID信息,从数据库取出当前用户的基本信息,通过全局
UserUtil
可直接访问到当前登录用户的基本信息。同时,也实现了token自动刷新机制。 - 完善设计的程序log规范。基于logback,配置了日志按类型、按日格式化滚动存储,同时通过
ThreadLocal
实现同session唯一的接口任务号,该任务号将在日志和接口返回中体现,方便追踪、排错。 - 完善设计的接口调用日志持久化存储。通过自定义注解,基于
AOP
的方式,在每个接口调用后向数据库写入一条信息丰富(包含taskId、入参、返回值、调用URL、错误信息、版本信息、响应时间等)的日志记录,便于后续统计整理、跟踪排错。 - 自行设计的权限判断逻辑和工具类。亦习校园将用户权限按项进行拆分(如查看作业权限、发布作业权限),同时每个用户的每项权限可对应多个权限范围。这支持了非常灵活的用户权限分配,除班级内分工外 ,还能支持团委统一管理一系列班级等情景。在数据库设计上,用户与权限一对多映射,用户权限与该权限范围一对多映射。在程序设计上,通过构建权限
enum
实现程序内权限的可读引用,通过对权限判断逻辑的抽象,设计了可横向扩展的通用权限判断类,将具体权限的判断逻辑(如是否为班长)封装在单独类中,后续需要判断权限时,可以直接实例化类进行判断,在需要验证多个权限时,可通过链式调用灵活进行验证。 - 实现第三方接入逻辑。允许外部应用作者向我们申请接口访问权限,通过分配
clientId
、clientSecret
实现接口调用者鉴权。对于不需要用户token的接口,通过传递clientId、时间戳、签名(使用clientId、clientSecret、时间戳、参数进行计算得到)确定外部应用作者;对于需要用户token的接口,直接通过token中存储的client信息确定调用的外部应用作者。 - 实现Oauth2协议的第三方登录。第三方网站可以基于Oauth2协议,跳转到亦习校园第三方登录页进行用户安全登录。目前,湖北工业大学物理实验系统已接入亦习校园第三方登录。
- 完善的接口集成测试。基本所有接口都编写了集成测试,并将配合下文提到的CI\CD流程,充分发挥测试带来的好处。
- 部分常用查询接口使用Redis进行缓存,提高速度。部分具有时效性的数据直接利用Redis可设置过期时间的特性进行存储。
- 使用Swagger维护接口文档。
邮件发送服务(PHP编写)由于其执行需要资源较多,且执行次数较少、和其他服务无强关联等特点,我将其部署在阿里云函数计算,充分利用serverless带来的低成本、运维方便的特点。
NodeJs主要编写简单的中间件服务。其中,websocket服务主要用来实现实时统计Web站点同时在线人数。文件管理中间件更加复杂一些,是供亦习校园主API内部调用的文件相关服务,它主要有以下特性:
- 使用express编写,划分model、router、controller层,便于维护。
- 完善的log存储机制,同请求下基于
AsyncLocalStorage
实现全局taskId唯一共享。 - 基于
cluster
实现node调用系统多核能力,提高性能。 - 压缩队列功能,采用Redis作为轻量式消息队列实现,通过
lPush
入队压缩请求,brPop
获取列头压缩请求,执行完毕后再获取或等待下一个请求。
# 运维技术栈
亦习校园部署有生产、测试环境,不同环境的数据独立,互不干涉。由于学生服务器性能不够强,我们之前购买了多台学生服务器,分别部署不同环境在上面。
- 一直采用CentOS作为服务器操作系统。
- 曾采用宝塔更加高效方便地管理和配置服务器资源。
- 使用Nginx作为网页服务器,通过自定义配置,实现反向代理,以解决CORS问题,实现映射内网端口等功能。
- 数据库使用Mysql和Redis
- 我后续将所有服务迁移到了单台性能强劲的服务器上,所有后端服务均使用Docker进行打包,使用Docker-Compose迅速拉起运行环境。利用Docker-Compose,可以实现在同一台机器上同时部署多套环境且互不干扰,且运维简单,一句
docker-compose up -d
即可。在这台服务器上我没有使用宝塔面板,纯bash进行维护操作,基本了解Linux的使用方法。
# DevOps技术栈
上文提到,亦习校园有许多子项目,每个子项目都有两套环境。如果每次发版都需要登陆服务器执行一系列操作,显然是效率极低的。我们先后使用阿里云效、自建Jenkins、Coding进行CI\CD研发,最终选定Coding作为我们的DevOps平台。在DevOps上,我做了这些:
- 对于Java程序,编写Jenkins流水线脚本,每次提出合并请求、推送分支时都会运行测试,只有测试通过才能合并请求。每次更新git tag时,也会运行测试,测试通过后执行部署。
- 对于其他没有测试的前后端程序,默认在develop分支的提交会自动执行CI\CD流程,打包静态文件(前端)/构建docker镜像(后端)后自动部署到服务器上的测试环境,master分支的提交会部署到生产环境。
- 我们是团队开发,需要把控整体代码质量,减少每次提交混入的不必要的BUG,因此我们采用代码评审机制,各开发成员开发完毕功能后需提出合并请求,审核人员评审代码通过后才可以合并分支。
- 利用Coding提供的OpenApi渲染API文档的功能,在CI/CD过程中集成了API文档的自动更新。