NextJS 热重载保留状态

虽然 Next.js 不仅是一个不错的前端服务端框架,在极端情况下也可以用作后端服务。通过 API Router,可以在 pages/api/ 目录下,添加相应的代码。对于没必要前后端分离的小型项目,前后端合在一起在部署可以提升开发效率。

众所周知,动态语言做开发,由于热重载的存在,每次改动代码时服务端会自动重启,会为开发者带来极佳的开发体验。通常而言,对于前端项目,由于 Web 的特性,页面本身不存在状态,因此热重载基本不存在副作用(Next.js 对于已经加载后的页面,目前也已经可以保留上次渲染的 state)。
而对于后端而言,服务往往是有状态的。比如后端服务需要与数据库建立连接,如果每次热重载都新建一条连接,并且由于热重载缺乏析构函数,相当于每次代码变动都会建立一条不会析构的数据库连接,这些连接以类似野指针的形式存在,会占用大量的资源

zhblogs 的开发中,使用了 LokiJS 作为内嵌数据库,以确保整个系统可以结合成为一个整体,方便部署和开发。需要确保数据可持久化更可控,因此这里需要自己实现的定时函数执行数据持久化。在热重载的加成下,5 min 一次的数据持久化飞速就变成了每秒一次,并且由于每个定时任务对应的实际上是不同的 LokiJS 实例,因此真正的数据修改可能会被之前已成为野指针的内存数据覆盖掉,导致真正的改动无法生效。

确定原因后,可以从两个方向解决

  • 实现一个缓存保存数据库实例,确保程序中只有一个在运行
  • 实现析构函数,在重载时销毁上一个实例

由于 JavaScript 本身不存在析构函数(甚至搜这个概念很多人都在讨论 GC,很少有生命周期的讨论),因此这里没法实现。那么另一个思路就是使用一个缓存机制。热重载并不是重启服务,而是重新加载用到的代码,因此在这个过程中 global 是不变的,因此只需要在 global 上写入一个全局变量,在初始化数据库前检查是否已设置该变量。如果未设置则正常初始化,否则返回之前的变量内容。
采用这个机制,也可以实现析构函数,不过这里不进一步讨论

相关代码如下

declare module globalThis {
  let loki: DatabaseLoki;
}
export function newLokiCached(path: string){
  /**
     * using cache in development mode
     * see https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
     */
  if (!globalThis.loki) { 
    globalThis.loki = new DatabaseLoki(path);
  } else {
    log.log("使用已连接的 loki 数据库缓存");
  }
  
  return globalThis.loki;
}