本文为公司内小组分享的文本内容。
今天分享的内容来源于自己去年开发的一个项目。它是一个图片社区微信小程序,类似于 Instagram,并且采用了类似于抖音的交互形式,上下滑动切换作品,单屏只展示一条作品,你可以在上面分享自己日常拍的照片和生活。
先说一下这个项目的来源,去年一年和朋友一起做了两个项目,都是为了把自己的想法实现成可用的产品,产品设计、UI 设计、开发都是自己完成的。
第一个项目是上茶小商店,一个电商小程序的 SaaS 类产品,用户无需任何开发就可以搭建一个自己的电商小程序,可以在后台管理商品、订单等数据。但是在我们产品开发完成时,微信推出了自己的微信小商店,功能和理念与我们的高度重合,且完全免费,开通流程更简单(因为一些功能依靠微信已开放的 API 无法实现),于是就有了第二个产品。
第二个项目是 FUTAKE,一个类似于 Instagram 的图片社区,以微信小程序的形式呈现。采用小程序的形式而不是独立的 App 有两方面的原因,一是小程序开发成本低,有助于快速实现出可用的产品;二是 App 推广难度大,获客成本高。
先来看一下这个小程序长什么样。小程序主要包含首页、广场、消息和我的四个页面。
首页可以看到你关注的人的作品,以作品发布时间排序;广场可以看到所有人的作品,看到作品都是在一定的规则下随机展示的,并不会根据你的喜好进行算法推荐;消息页面是点赞、评论通知和系统消息等;我的页面就是你的个人主页了,展示你的作品和你喜欢的作品。
开发时最先遇到的问题两个非技术性的问题。
第一个是实名制。现在凡是用户可发布内容的产品都要求绑定手机号,其实这是因为网络安全法的关于要求用户提供真实身份信息的规定,提供手机号实质上是一种变通的实名制,对用户来说门槛较低。我们也不例外,在注册时需要填写手机号,但不会在任何地方展示。
第二个是内容安全。根据微信小程序的运营规范,UGC 类的产品需要对用户发布的内容进行审核后才能展示出来,包括作品、评论、昵称、头像和个性签名。微信也提供了免费的内容审核接口,可以审核文本和图片,直接调用就可以了。
下面是技术方面的内容。
除了小程序,还包含管理后台和官网。小程序是基于 Taro 开发的;管理后台是使用 React 开发的单页面应用,比较简单;官网是使用 Next.js 开发的,性能比较好,且对 SEO 友好。
不过今天分享的内容不包含这些,今天分享的是后端相关的内容,后端是基于 Node.js 开发的,技术栈主要为 NestJS、PostgreSQL、Redis 和 Docker。
详细说一下后端技术选型的原因。
Web 框架方面,目前 Node 生态中可选项其实并不多,生态和社区比较成熟的有 Egg.js 和 NestJS,并且在上茶小商店的开发中使用过了 Egg.js,体验一般,并且对 TypeScript 支持较差;Express 和 Koa 偏底层,与其说是框架,不如说是库,尤其是 Koa 仅提供了一套中间件机制,如果直接使用,需要自己补齐大量功能,而 NestJS 是一个功能完备的框架,开箱即用,同时 NestJS 引入的依赖注入和模块机制等相当于提供了一个灵活且稳定的架构。
数据库方面,没有使用知名度相对较高的 MySQL,而是使用了 PostgreSQL,一是因为 PostgreSQL 是最先进的开源数据库(大雾);二是因为 PostgreSQL 提供了一些 MySQL 所不具备而我们又需要的功能,比如 JSON 数据格式和地理位置索引等。
下面主要讲几个值得分享的技术要点。
首先是项目结构,上图是上茶小商店的项目结构,同时也是比较典型的 Node Web 应用的结构。大致等同于 MVC 结构,但由于没有 View 层,所以分为了 Model、Service 和 Controller 三层,Model 层负责与数据的交互,Service 层主要为业务逻辑,Controller 层用于接收和响应 HTTP 请求。
类似与前端的 MVVM 架构,都是通过分层对项目进行一定程度上的解耦,每一层只做自己的事,有利于代码的维护。分层的结构简单明了,也适合大部分的项目,但是由于每个层级内的不同业务模块耦合较严重,文件之间互相导入,当项目变大以后,很难从垂直方向(也就是按业务)进行拆分。
NestJS 引入了依赖注入和模块机制,对于不同层和不同模块的划分相对更灵活更严谨。
NestJS 的模块机制可以更灵活的对项目进行分层和分模块,同时依赖注入可以降低各个模块之间的耦合,可以很简单的将任意一个模块拆分为一个应用进行独立开发独立部署。
如上图是 FUTAKE 的结构,在水平方向上进行了分层,同时垂直方向也进行了模块划分,每一个黑色方框内为一个 NestJS 模块。目前整体为一个单体应用,但在这种结构下也可以比较方便地将每个模块拆分为一个独立应用,当用户量大了以后(希望会),单体应用无法支撑的时候可以将每个模块拆分为独立的应用,也就是所谓的微服务。比如 UserService 和 UserModel 组成的模块可以拆分为一个独立应用,只用来处理用户相关的逻辑。
只看图可能有点抽象,可以看一下代码的目录结构。可以看到除了各个模块外,还有 main.ts
和 worker.ts
两个文件,其分别为 HTTP 进程的入口和 Worker 进程的入口,也就是后面要讲到的内容。
我们知道 JavaScript 是单线程的,在处理 HTTP 请求时需要及时做出响应,不然会阻塞后面的请求,而且有些情况需要一些比较慢的操作,比如用户发布内容时进行审核,这就不能等所有任务处理完成后再进行 HTTP 响应了,这时就需要用到消息队列了。
所以项目就有了两个入口,一个是用来 HTTP 服务的 main.ts
,一个是处理消息队列的 worker.ts
,同时 worker.ts
还会承担一些定时任务,比如每小时刷新一次微信的 Access Token,每天对日志文件进行归档。
开发环境会直接启动两个 Node 进程,在生产环境会使用 pm2 进行进程管理和守护。
消息队列除了用来内容安全检测外,还会承担处理信息流的任务。信息流也就是首页和广场页可以无限向下刷的作品列表。
信息流实现最基本的模式有两种,一是推模式,二是拉模式。
推模式实现起来比较简单,发布作品时向所有粉丝推送数据,同时在关注新用户和取消关注时需要进行处理,所有的信息流数据都写入到 feed 表中,当用户刷新列表时,直接从 feed 表中读取数据即可。
推模式在实现上比较简单,但当数据量很大时就会出现延迟,比如某个用户有十万粉丝,在他发布作品时要写入十万条数据,从写入第一条到最后一条会有一定的时间差。
小程序首页的信息流采用的就是推模式,所有作品都按照作品发布时间进行排序,但由于关注用户和取消关注时需要对 feed 表进行增删,所以写入 feed 表的写入时间顺序并不等同于作品的发布时间顺序。
为了方便查询,作品 ID 需要具有自增性,并且不能使用数据库的自增 ID,因为这样会暴露我们的数据量,并且会方便爬虫爬取数据,所以借鉴了 Twitter 的雪花算法,在它的基础上进行了简化,实现了一个用于生成 ID 的方法。
拉模式则不依靠消息队列,但是查询逻辑比较复杂,用户刷新列表时需要先去查询他所关注的所有用户,然后再去查询这些用户的作品列表,并且要根据作品发布时间排序,当关注的用户数较多时,单纯的依靠数据库是无法实现的,需要引入 Elasticsearch 或类似的搜索服务。
小程序的广场页面采用的拉模式,但由于广场页的信息流是展示所有用户的作品,并不需要关心关注逻辑,所以查询逻辑相对简单一些。
内容安全没有太多可讲的内容,主要是状态流转逻辑,上图中有一个审核失败的状态,是指请求内容审核接口失败的情况,微信的内容审核接口对图片有大小限制,所以可能会出现审核失败的情况,这时就会流转到人工审核流程。
感兴趣的话,可以扫码体验一下,也可以邀请朋友一起来玩!