刘果 | Guo Liu
刘果 | Guo Liu

“To change something, build a new model that makes the existing model obsolete.”

Matters 的架构与技术栈


photo credit: Ray Wenderlich

随着开源计划的启动,马特市市民们可以直接看到马特市的所有机制和逻辑。全面开放代码仓库后,任何人都可以提出建议和想法、提交功能和优化,也可以自行建立像马特市一样的平台,参与到马特市生态的演进中。

过去两年的持续迭代后,马特市有了越来越多的功能,也有了越来越大的容量。这使得整个系统变得越来越复杂,即使是职业的软件开发者,也需要花上不少精力才能使用和参与。

之前,我们曾介绍过马特市API 的文档与测试环境,现在也有了一个专门的仓库用于技术文档协作文档issue 提交。之后,我们将会继续撰写一系列文章,介绍马特市整个系统的不同侧面。

本文是这个系列的第一篇,介绍整个系统大致的结构与思路。一部分涉及的代码仓库尚未公开,如果你想抢先尝试,可以报名马特市开源计划招募

前端

马特市的网页前端是一个Progressive Web App ,采用响应式设计适配不同的设备,并让用户在添加到桌面之后能够获得类似原生应用的体验。前端与后端通过GraphQL来调用数据、定义数据结构,并均以TypeScript写成。

相比后端,前端更容易上手,也可能是社区设计者和开发者最能够发挥想像力的地方。本地开发时,我们可以将前端指向马特市的生产环境、及时看到改动在真实数据上的效果,也可以通过Apollo Playground查看API文档、直接测试query 语句。

借鉴JAMstack 架构,马特市网页的渲染大致分为两步:当用户访问马特市的一个网页时,会先从服务器的缓存中调取网页的公开版本;在送达用户的浏览器后,网页会根据用户的登录状态向后端请求个人数据,并更新网页中个性化的部分。

网页的服务端渲染由Next.js实现,文档结构也受到Next.js 影响。每个网页的入口位于src/pages中,通过Dynamic Routes将文档路径映射为用户使用的url。 src/pagessrc/views中调用可复用的视图逻辑, src/views又再调用位于src/components的组件库。

前端组件库由React写成,遵循马特市的设计系统,并包含了很多通用的context (例如当前用户信息、全局语言设定)与强大的hooks (例如响应式设计、下拉更新)。后续我们将引入Storybook等工具,让组件库更加一目了然,方便开发者直接修改和使用。

在React 代码风格上,我们大量使用函数式编程,借助functional component让代码结构更加简洁明了。需要调用数据的组件都有一个fragments栏位,包含了描述数据需求的GraphQL fragment 。这样,父组件可以不必考虑子组件的具体数据需求,直接在query 中调用fragment 即可。

正如React 组件的相互调用形成了一个树形结构,GraphQL fragment 的层层调用也形成了这样的树形结构,与React 树相互贴合。在fragment 树的顶端,是整合之后的query与mutation ,均通过Apollo Client发起。

Apollo Client 的配置位于src/common/utils/withApollo.ts中,由不同的Apollo Link组成,包含了服务器API 地址、身份校验、 persisted queries等逻辑。同时,里面还有一些 客户端的GraphQL schemaresolver ,让我们也能通过GraphQL 读写客户端本地的数据,例如首页文章瀑布流的选择、文章评论的草稿。

文章编辑器单独作为一个项目,位于matters-editor中,基于Quill.js搭建。这一部分是前端交互最为复杂的地方,也是最需要优化与改进之处,之后我们会专门撰文进行介绍。目前编辑器有很多bug 没有办法复现,我们随后也会专门邀请马特市的市民们和我们一起来抓bug。

后端

马特市的后端依赖不少服务,结构相对复杂,我们在GitHub 上绘制了简化的架构图。本地启动时,用docker 来安装和管理不同的服务会方便一些。

后端的GraphQL API 基于Apollo Server ,提供了数据读写的入口,也定义了前后端共享的数据结构。决定API结构的GraphQL schema位于src/types路径下,其中的备注则会作为文档出现在Apollo Playground里。

我们通过GraphQL directives来实现一些schema 层面的通用逻辑,例如权限管理、缓存、操作频率限制,位于src/types/directives路径下。 GraphQL directives 并不是一个非常常见的功能,但其实非常强大,能够通过对声明式的方式控制schema 的解析过程,也能够简化代码结构,我们后续也会增加对它的使用。

有句谚语说,计算机科学中最难的事莫过于缓存清理和命名;命名实在很难,不过我们花了不少精力调试缓存,并把实战测试后的逻辑和代码单独抽到了一个仓库之中。里面有一个plugin和对应的几个directives ,实现了简单的缓存与清理。 GraphQL 服务器端缓存的精确清理一直比较薄弱,所以我们之后专门撰文介绍马特市的解决方案,以方便其他项目直接使用。

GraphQL schema 的根节点分为query 与mutation ,query 用于读取数据,而mutation 用于写入数据。两者的执行逻辑都由resolver定义,分别位于src/querysrc/mutation中。 resolver 在执行的时候,从context中调用data source ,向数据库等服务发起具体的请求、进行计算。

不同的data source 由src/connector中的文件定义,其中也包含了其他对接服务所需要的接口,比如s3、Google 翻译、ElasticSearch 等。其中, queue路径下存放了基于Redis 的队列操作,包括定期执行的操作(如数据库更新)、限制并行的操作(如赞赏、支持与提现)等。随着马特市容量的扩大,未来会有越来越多的操作在队列中异步完成。

在收到请求时,Apollo Server 将所有data source 注入到context 中,由resolver 进行调用。最顶层的这部分逻辑由src/routes中的文件定义,与oauthpay两个用于第三方认证和支付接入的endpoint 并列。

排序算法与数据库

马特市里内容的呈现,是用户创作与行为涌现的结果,而内容排序的逻辑则是涌现的规则,直接决定了什么样的内容被读者看见。这些逻辑既是『好内容』的定义,也是『公共空间』的质地。

排序使用的数据来源于每一个用户的行为:对于文章,是赞赏、支持、评论、关联、阅读、收藏和精选;对于标签,是编辑、精选和追踪;对于作者,则是追踪,以及对作者文章或标签的所有行为。每一项行为都是一条时间序列,包含无数种切分时间的窗口;同时,每一位用户又有不同的加权方式,比如追踪者数、收到支持数、给出支持数,能够给行为赋予不同的权重。

排序算法需要利用这些多维的数据,涌现出受到认可的内容和作者,既要保证一定频率的更新,又要避免恶意用户的刷屏攻击。这使得排序算法变得复杂而精细,也使得我们需要不断地以简洁易懂的方式沟通和改善排序逻辑,让公共空间的质地真正成为社区的共识。之后我们也会专门撰文介绍排序算法的思路,方便马特市市民们的参与。

这些不同的排序都以materialized view的形式存储与数据库中,通过cron jobs 进行定期更新。数据库的迁移、配置和seeding 等文件存储在db路径下,与src路径平行。排序方式的相关代码则在db/migrations中,对应的materialized view 经由不同的resolver 调用呈现在API 的返回结果中。

数据库结构相对复杂,难以很快理解和上手。为此,我们制作了数据库的文档与结构图,下载文档之后可以点开网页,直观地理解目前的数据库结构。

开发环境与部署方式

因为马特市只有很小的工程师团队,所以我们尽力标准化本地开发规范、自动化DevOps 操作,以便提升开发效率。

不管是前端后端,GraphQL 类别均会在开发与建构时生成对应的TypeScript 类别,实现数据结构校验:后端采用graphql-schema-typescript 项目,调用npm run gen来生成;前端则采用apollo-tooling项目实现,调用npm run gen:type来生成。本地开发时,这些类别都会及时自动生成。

前后端在开发工具配置上也大致相同: Prettier用于自动规范代码格式, Commitzen用于规范git commit 格式, Jest则用于单元测试。前端仓库bdd路径下还有cucumber文档,既可以作为产品功能的文档,也作为前后端整合测试的脚本;不过这部分尚未发挥出潜力,还有待开发和完善。

新版本的部署通过GitHub action 完成,一方面将新版本的git commit 自动整合为release note,另一方面将新版代码上传到服务器中。随着马特市依赖的服务越来越复杂,我们开始尝试采用Terraform自动化基建的更改与调度,这一部分的进展会在之后更新。


以上便是马特市目前的大致结构与思路,其中还有很多细节与侧面,留待以后的文章介绍。欢迎你来提出自己的看法,不管是发现了什么问题,还是有想进一步了解的部分。也欢迎报名马特市开源计画的第一步,抢先进入代码仓库玩玩看。

Happy Hacking ❤️

CC BY-NC-ND 2.0 版权声明

喜欢我的文章吗?
别忘了给点支持与赞赏,让我知道创作的路上有你陪伴。

加载中…
加载中…

发布评论