shadowfish和他的代码

vuePress-theme-reco shadowfish    2020 - 2023
shadowfish和他的代码

Choose mode

  • dark
  • auto
  • light
时间轴

shadowfish

49

Article

42

Tag

时间轴

我是如何构建并维护亦习校园的——总结亦习校园技术栈

vuePress-theme-reco shadowfish    2020 - 2023

我是如何构建并维护亦习校园的——总结亦习校园技术栈

shadowfish 2022-02-01 架构

# 前言

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文档的自动更新。