用 Next.js, GraphQL, NestJS, MongoDB 写个博客网站吧

5,647 阅读9分钟

home page

折腾了大半年的业余时间, 我的新版博客网站于本月初终于上线了. 这次仍然由三个系统组成, 分别是:

新版博客最大的变化是使用 GraphQL 驱动数据, 因此整个项目也是对 GraphQL 的一次实践. 项目全部开源, 如果有兴趣可以点击 GitHub 查看, 如果有问题和建议可以直接提 issue 或 WeChat(Yancey_Studio) 与我一起交流. Blog 项目从我临毕业至今已经“折腾”了三个版本出来, 关于它的历史沿革都写在了 Yancey Blog 大事记 这篇文章, 有兴趣可以关注一下.

项目一览

What is GraphQL

在聊业务细节之前, 我们先了解一下 GraphQL.

GraphQL 是一种用于 API 的查询语言, 并且提供已有数据查询的运行时. 目前 Facebook, Twitter 已经在生产环境使用 GraphQL 了, GitHub API v4 也已全面使用 GraphQL, 相信它的前景不可限量.

Why GraphQL

GraphQL 的优势很多文章都已经介绍过, 这篇文章不讲 GraphQL 的具体用法, 有兴趣可以学习它的官方文档, 也可以配合我的博客项目源码一起食用, 下面跟据我的实际开发体验来简单谈一谈它的优势.

可精准控制接口返回字段

对于传统的 REST API, 接口的返回字段只能由后端控制, 如果接口字段存在冗余, 就会造成一定的性能问题. 而 GraphQL 允许你只获取想要的数据, 不存在数据的过度抓取或不足抓取的情况, 从而提高了应用程序的整体性能.

举个例子, 我的博客在 请求文章列表 时, 后端 sql 会返回当前页码的所有数据. 我们知道对于一篇文章来说, 最大的一部分就是 content 字段, 但对于博客卡片列表来说, 只需要渲染 meta 信息即可. 因此在定义相应的 typeDefs 时, 我们要求 GraphQL 不返回 content 字段. 这个 case 带来的好处是首屏加载的数据变少, 从而使加载速度变快.

博客卡片列表

export const POSTS = gql`
  query Posts($input: PaginationInput!) {
    posts(input: $input) {
      total
      page
      pageSize
      items {
        _id
        title
        summary
        ...

        # 我不需要 content 字段!
        # content
      }
    }
  }

只需向一个接口发送请求

传统的 REST API 需要定义路由以区分每个接口, 这意味着前端需要请求多个 URL 以获取资源. 虽然 HTTP2 增加了多路复用的功能(同域名下所有通信都在单个连接上完成, 消除了因多个 TCP 连接而带来的延时和内存消耗; 单个连接上可以并行交错的请求和响应, 之间互不干扰), 但对于移动端来说仍然是一个不小的考验. 下面的代码描述了一个经典的 RESTful 风格的增删改查.

GET /api/v1/posts
GET /api/v1/post/:id
POST /api/v1/post
PUT /api/v1/post/:id
DELETE /api/v1/post/:id

而 GraphQL 可以在一个接口中获得应用程序所需的所有数据, 比如我的 Blog 都是向 https://api.yanceyleo.com/graphql 发送请求. 此外, 适时地使用 batch request 可以将多个 GraphQL Query 或 Mutation 合并, 这些措施都可以提高移动端网站或应用的响应速度, 从而提升用户体验.

graphql

强类型约定的 schema

每个 GraphQL module 都要精准定义接口返回值的类型, 下面这个例子是短信验证码的返回值. 它定义的 verificationCode 必须是一个不为空的纯数字字符串, 且该字符串的长度必须是 6.

这种约束的好处是: 后端接口的返回值必须满足 Model 的定义, 字段不能多也不能少, 类型不能有一点偏差, 否则开发环境就会报错(保证了后端接口的质量); 对于前端来讲, Model 就是一份 API 文档, 请求的字段只能是 Model 存在的字段, 如果请求额外的字段也会报错.

在某种程度上, GraphQL 能避免前后端一些不必要的扯皮.

import { Field, ObjectType } from '@nestjs/graphql'
import { IsNumberString, IsUUID, IsPhoneNumber, Length, IsDate } from 'class-validator'

@ObjectType()
export class SMSModel {
  @Field({ nullable: false })
  @IsUUID()
  public _id: string

  @Field({ nullable: false })
  @IsPhoneNumber('zh-CN')
  public phoneNumber: string

  @Field({ nullable: false })
  @IsNumberString()
  @Length(6)
  public verificationCode: string

  @Field({ nullable: false })
  @IsDate()
  public createdAt: Date

  @Field({ nullable: false })
  @IsDate()
  public updatedAt: Date
}

GraphQL 生态

对于后端来讲, GraphQL 支持了大多数主流后端语言, 你可以通过 GraphQL Code 这个网站检查它是否支持你喜爱的语言. 我的 Blog 后端服务使用 NestJs 搭建, 除了它能够跟 GraphQL 很好的搭配, 其 AOP 思想也能大大提高代码的质量和可维护性(虽然得多写不少样板文件).

对于前端来讲, 主流的库有 apollo client, 它能和三大框架完美结合; 此外 Facebook 自己出品的 Relay 也是不错的选择, 不过 FB 的产品你懂的, 都是先服务自家, 作为开源框架还是稍微有一定的学习成本.

前台项目

前台项目使用 next.js 做服务端渲染(我心心念念的 Twitter 大图分享终于回来了); 使用 apollo client 驱动 GraphQL 数据; 使用 styled-components 处理样式, 可以很方便的做主题切换(当然 css 原生的变量也是不错的选择).

线上地址是 Yancey Official Blog, 项目已开源, GitHub 仓库为 blog-fe-v2, 如果你对 GraphQL 或者其他技术栈有兴趣, 可以做一个参考.

因为这个项目大家都可以访问到, 这篇文章就不贴图了. 目前项目只支持桌面端展示, 后续会做成响应式布局. 此外下面是 lighthouse 的一个跑分, 除了 a11y, 首屏加载看起来还凑合, 后续也会对 a11y 做一个优化.

lighthouse

后台管理系统

后台管理系统因为没做多权限管理, 暂时不开放注册, 后续等复合式权限管理, 角色管理等功能完善了, 将会开放注册功能, 抽时间我做个预览版的放到线上给大家玩. 项目使用 CRA + apollo client 驱动 GraphQL 数据; 使用 Material UI 提供 UI 组件层.

项目已开源,如果有兴趣可以 pull 下来跑一跑, GitHub 仓库为 blog-cms-v2.

Dashboard

展示服务器的实时和近期状态, 展示 Top PV 和点赞的文章, 展示文章的标签云, 展示博客行为的热图.

Material 风格的 Dashbord

博客行为统计

在触发博客创建/修改/草稿 or 发布等行为时, 会将这些行为存储到数据库, 并可视化展示在热图中(算是个埋点吧).

blog statistics

增删改查

很多模块的配置管理都是使用如下的形式, 主要做增删改查和批量删除, 为灵活控制展示顺序, 通过权重的方式增加了上移/下移/置顶的功能, 当然拖拽也不不错的思路, 不过刚需不是特别大, 我的博客可能在后期运营的时候需要大量用到置顶功能.

Create or Update an item

支持上移/下移/置顶

Agenda

增加了 Agenda 模块, 可以做些简单的日程管理.

Agenda 模块

Profile

这次后台对 Setting 模块对了较大的补充, 样式借(chao)鉴(xi) 的 Google 用户管理的设计. 首先是 Profile 模块, 可修改姓名, 地区, 组织, 网站, Bio, 头像, 修改后会和左边 Drawer 实时同步.

修改基本信息

Account

支持修改用户名和邮箱, 它俩是一个用户独有的标识, 如果被占用将会报错. 此外项目还支持删除账号的功能, 这是一个不可逆的操作. 私以为, 自由的修改用户名, 邮箱以及删除账号是一个网站必备的素质. 嗯, 说的就是你, 微 x 和 微 x.

Account

Security

Security 设计了较多的模块: 支持修改密码, 基于 TOTP 的二步验证, 还可以绑定手机号(用的是阿里的 SMS). 此外, 在 TOTP 和 SMS 不可用的情况下, 你还可以使用提前申请的 Recovery Codes. 目前所有的功能均已实现, 但还没和用户登录结合在一起, 后续如果用户绑定了二步验证, 登录时会走二步验证逻辑, 先列个 TODO 占坑.

修改密码

二步验证 - 选择手机系统

二步验证 - 扫码

二步验证 - 手动录入

二步验证 - code 验证

绑定手机号

Recovery Codes

后端

后端主要使用了 NestJs + GraphQL + MongoDB, 使用 JWT 做 Auth 权限. 如果把 express.js, koa 称为 http 库的话, NestJs 才配得上真正意义的后端框架.

NestJs 使用了 AOP 思想, 被称为 Node 版的 Spring Boot, 控制反转和依赖注入的思想能够让 module, resolver(controller) 和 service 各司其职, 虽然会增加一些样板文件, 但会使你的项目变得清晰可维护; 此外, NestJs 官方文档质量非常高, 有很多的实践案例, 有兴趣可以学习一下(我成功把 NestJs 安利给了我们部门后端, 他们的所有项目目前都在使用 NestJs 重构).

这个项目还做了 e2e 测试, 目前测试覆盖率是 74%, 话说写个 e2e 比业务代码都多...

e2e

项目已开源, GitHub 仓库为 blog-be-next. 可以按照 README 上面讲的配置好环境变量, 在 MongoDB 建好数据库名, 项目就可以跑起来了.

运维相关

项目运行在一台 Debian 系统的 vps 上, 服务器在米国, 有时比较慢; 前台和后端使用 pm2 守护进程, 其中后端开启了集群模式, 效果还行; 项目扔到了 Cloudfare 的 CDN 上, 配上 http2 + brotli 爽歪歪; 三个项目都用 Travis CI 做了 CI 和 CD, 只要 master 有了变化, 代码更新分分钟的事儿, 妈妈再也不用担心我手动上线了; 用 Caddy 做反向代理(没用 nginx 主要是懒得手动申请 https 证书), 后期考虑试试 docker.

最后

整个博客项目大抵就是这些, 欢迎大家 fork 和 star, 关于 GraphQL 技术栈, 还有很多好玩的地方没有一一列出, 有兴趣可以跟我交流. 欢迎关注我的微信公众号:进击的前端.

进击的前端