首页
/ 在 Next.js 中集成 Knex 查询构建器操作 PostgreSQL:with-knex 官方示例全解析

在 Next.js 中集成 Knex 查询构建器操作 PostgreSQL:with-knex 官方示例全解析

2026-09-07 11:49:54作者:齐冠琰

导读:本篇文章以 next.js 官方仓库 examples/with-knex 为蓝本,完整演示如何让 Knex 查询构建器与 Next.js 协同工作——从环境变量加载、连接配置、数据库迁移,到通过 API Route 查询 PostgreSQL 数据并渲染到页面。阅读完成后,你将掌握一套可复制的「Next.js + Knex + Postgres」实战搭建流程,并理解其背后的源码细节(含开发模式热重载下的连接池复用技巧),且同一套连接代码可平滑迁移到 Knex 支持的其他数据库。

示例概览:Knex 在 Next.js 中的定位

Knex 是一个功能强大的 SQL 查询构建器,支持 Postgres、MySQL、SQLite、MSSQL、Oracle 等主流数据库。示例 README.md 的定位很清晰:它本身不强制绑定某一种数据库,而是演示如何用 Knex 作为统一的数据访问层去连接并查询 Postgres,其余 Knex 所支持的数据库也都可以按同样方式接入。

官方这个示例(位于 examples/with-knex)是一个完整可运行的最小应用,目录结构非常精简:

examples/with-knex/
├── .env.local.example     # 环境变量样例(含 PG_URI 占位值)
├── .gitignore             # 已忽略 .env*.local 等本地敏感文件
├── knexfile.js            # Knex CLI 配置文件(读取 PG_URI)
├── knex/
│   ├── index.js           # 连接实例模块(带全局缓存)
│   └── migrations/
│       └── 20201015140127_initial.js   # 示例迁移:创建 todos 表并写入种子数据
├── pages/
│   ├── api/
│   │   └── todos.js       # API Route:查询并返回 todos 列表
│   └── index.js           # 首页:客户端 fetch 并渲染 todos
└── package.json           # 脚本与依赖声明

从依赖清单(见 package.json)可以看出示例的技术栈组合:knex 负责查询构建,pg 作为 PostgreSQL 驱动,@next/env 用于在 Node 侧加载 Next.js 环境变量,再加上 nextreactreact-dom

快速启动:用 create-next-app 拉取并运行示例

官方提供了两种体验方式,最快捷的是直接使用 create-next-appwith-knex 作为模板引导一个全新项目(npm / Yarn / pnpm 命令任选其一):

npx create-next-app --example with-knex with-knex-app
yarn create next-app --example with-knex with-knex-app
pnpm create next-app --example with-knex with-knex-app

命令执行后,create-next-app 会把整个示例模板复制到新目录 with-knex-app 中。随后进入项目并安装依赖:

npm install
# 或
yarn
# 或
pnpm install

由于示例依赖清单中 next@next/env 均声明为 latest,安装时会拉取当前最新版本,而 knex@0.95.15pg@8.4.1 则为示例编写时锁定的稳定版本——如果你在自己的项目中使用,可以按需升级。

准备一个 Postgres 数据库

接下来需要准备数据库连接。既可以在本地安装并启动一个 Postgres 实例,也可以直接使用 AWS RDS、DigitalOcean Managed Database 等 DBaaS 云服务。无论哪种方式,最终你都需要得到一个形如 postgres://<user>:<password>@<host>:<port>/<dbname> 的连接串。

配置环境变量

环境变量是示例中数据库配置的唯一入口。示例附带了环境变量样例 .env.local.example,内容如下:

PG_URI=postgres://admin:pass@localhost:5432/mydb

将它复制为本地环境文件 .env.local(该文件已被 .gitignore 中的 .env*.local 规则忽略,不会提交到版本库):

cp .env.local.example .env.local

然后把 .env.local 里的 PG_URI 改成你真实数据库的连接串,例如:

PG_URI=postgres://myuser:mypassword@myhost.example.com:5432/mydb

连接串的组成部分含义如下:

部分 示例值 说明
user myuser 数据库用户名
password mypassword 对应用户密码
host myhost.example.com 数据库主机地址,本地可为 localhost
port 5432 Postgres 默认端口
dbname mydb 要连接的数据库名

为什么 knexfile 能直接读到 Next.js 的环境变量

这里有一个值得注意的实现细节:Knex CLI(如 knex migrate:latest)本身并不认识 Next.js 的 .env.local 约定,因此示例的 knexfile.js 在文件最顶部手动调用了 @next/env 来加载环境变量:

const { loadEnvConfig } = require("@next/env");

const dev = process.env.NODE_ENV !== "production";
const { PG_URI } = loadEnvConfig("./", dev).combinedEnv;

module.exports = {
  client: "pg",
  connection: PG_URI,
  migrations: {
    directory: "./knex/migrations",
  },
  seeds: {
    directory: "./knex/seeds",
  },
};

各字段作用如下:

  • loadEnvConfig("./", dev):让 Knex 运行时也能解析 Next.js 项目根目录下的 .env.localdev 为真时还会一并加载 .env.development.local 等开发环境文件),combinedEnv 是合并后的环境对象;
  • client: "pg":声明使用 PostgreSQL 驱动,若改用 MySQL、SQLite 等,仅需更换 client 值并安装对应驱动即可,这是 Knex 多数据库支持的关键;
  • connection: PG_URI:直接以连接串形式传入连接配置;
  • migrations.directory:指向迁移文件所在目录;
  • seeds.directory:指向种子数据目录。

同时注意,这个配置文件以 CommonJS(require/module.exports)导出,因此可以被 knex CLI 以及 ESM 代码两种模式共同消费。

数据库迁移与种子数据:理解示例的 todos 表结构

迁移脚本做了什么

Knex 的迁移机制用于以可版本化的方式演进数据库结构。示例迁移文件 knex/migrations/20201015140127_initial.js 完成两件事:建表 + 插入初始数据。

up 方法先通过 knex.schema.createTable 创建名为 todos 的表,定义了三个字段:

exports.up = async function (knex) {
  await knex.schema.createTable("todos", (table) => {
    table.increments("id");            // 自增主键
    table.string("text").notNullable(); // 待办内容,非空字符串
    table.boolean("done").notNullable(); // 是否完成,非空布尔
  });

  await knex("todos").insert([
    { text: "Buy milk", done: true },
    { text: "Wash car", done: false },
  ]);
};

接着用查询构建器的 .insert() 接口直接写入两条演示数据(Buy milk / Wash car),让应用一启动就有内容可展示。down 方法则定义了回滚逻辑——用 knex.raw 执行原生 SQL DROP TABLE todos CASCADE 将表彻底删除,CASCADE 会一并清除依赖该表的外键约束等对象。

从这份迁移可以看出 Knex 的双层能力:knex.schema 负责 Schema 级 DDL(建表、改列),而 knex("表名") 负责面向数据行的 CRUD;当构建器语法无法覆盖需求时,还可退回到 knex.raw 写原生 SQL。

迁移相关命令一览

示例在 package.json 中预置了完整的迁移命令,覆盖了「创建、应用、回滚、查看状态」全生命周期:

命令 底层执行 用途
npm run migrate:make knex migrate:make 新建一个空的迁移文件
npm run migrate:latest knex migrate:latest 应用所有尚未执行的迁移
npm run migrate:up knex migrate:up 只执行下一个待执行的迁移
npm run migrate:down knex migrate:down 回滚最近一次执行的迁移
npm run migrate:status knex migrate:status 查看迁移执行状态
npm run seed:create knex seed:make 创建种子文件
npm run seed:run knex seed:run 执行全部种子文件

对当前示例,只需应用一次即可完成建表与数据初始化:

npm run migrate:latest
# 或
yarn migrate:latest
# 或
pnpm migrate:latest

需要说明:种子目录 ./knex/seeds 在迁移脚本中虽已被配置,但示例当前并未提供实际的种子文件(examples/with-knex 目录下只有 migrations 目录),因此日常演示中用不到 seed:run;它属于预置好、方便你扩展的脚手架能力。

代码深读:连接实例缓存与 API Route 查询链路

knex/index.js:解决开发热重载下连接泄漏的关键

在 Next.js 开发模式下,代码会随着编辑而被反复重新执行(Fast Refresh)。如果每次模块重新加载都新建一个 Knex 连接实例而不做清理,就会造成连接不断累积,最终达到数据库连接上限。示例在 knex/index.js 中通过挂在 global 对象上做缓存来规避这一经典问题:

import knex from "knex";
import config from "../knexfile.js";

/**
 * Global is used here to ensure the connection
 * is cached across hot-reloads in development
 */
let cached = global.pg;
if (!cached) cached = global.pg = {};

export function getKnex() {
  if (!cached.instance) cached.instance = knex(config);
  return cached.instance;
}

理解这段代码的关键在于模块作用域与全局作用域的区别:

  • 普通模块级变量在每次模块热替换/重新求值后都会重新初始化,导致连接被反复重建;
  • global 是 Node.js 的进程级全局对象,其内容不会因模块重载而丢失
  • 因此把连接实例挂在 global.pg(代码注释甚至给出了相关的 Next.js discussion 链接作参考)上,就能保证整个开发进程生命周期内只创建一次 Knex 实例,后续任何模块、任何请求都复用它。

getKnex() 采用惰性初始化模式:首次调用时才执行 knex(config),之后直接返回缓存实例。从源码结构可以推断,这种「模块只导出函数、连接由函数按需提供」的写法,同时兼顾了两点:一是在生产环境避免在模块加载阶段就建立数据库连接(此时环境变量可能尚未就绪或不需要连接),二是让所有调用方共享同一连接池,从而复用 Knex 内置的连接池能力。在你自己的 Next.js + 数据库项目(无论 Knex、Prisma 还是其他客户端)中,这一「global 缓存」写法都值得直接借鉴。

API Route:一条极简的查询链

数据出口是 Next.js Pages Router 的 API Route pages/api/todos.js,它只有短短几行:

import { getKnex } from "../../knex";

export default async function handler(req, res) {
  const knex = getKnex();
  const todos = await knex("todos");
  res.status(200).json(todos);
}

执行流程解读:

  1. ../../knex(即 knex/index.js)导入 getKnex
  2. 调用 getKnex() 获取(首次创建后缓存的)Knex 实例;
  3. await knex("todos") 等价于 SELECT * FROM todos,Knex 会基于迁移文件里定义的表自动映射出行数据;
  4. 通过 res.status(200).json(todos) 以 JSON 返回查询结果。

由于 API Route 在服务端执行,pg 驱动直连数据库的过程对浏览器完全不可见,浏览器最终拿到的只是干净的 JSON 数据——这正是「服务端持有数据库凭证、客户端零暴露」的安全边界。

前端页面:客户端数据获取

首页 pages/index.js 是一个经典的客户端渲染取数模式:组件挂载后(useEffect 且依赖数组为空,仅执行一次)向 /api/todos 发起 fetch 请求,把返回的数组存入 state 后再渲染:

const [todos, setTodos] = useState();
useEffect(() => {
  async function loadTodos() {
    const resp = await fetch("/api/todos");
    const data = await resp.json();
    setTodos(data);
  }
  loadTodos();
}, []);

渲染逻辑利用 state 的「未定义」状态做加载提示,收到数据后逐条渲染待办文本,done 字段为真时追加 (complete) 后缀:

{!todos && <p className="todos-loading">Todos loading...</p>}
{todos &&
  todos.map((todo) => {
    return (
      <p className="todos-item" key={todo.id}>
        {todo.text} {todo.done && "(complete)"}
      </p>
    );
  })}

这里的 key={todo.id} 正是对应迁移中定义的 id 自增主键。从示例可见一条完整的「数据库 → API Route → 浏览器」数据流:迁移建好的数据,经 Knex 查询、API Route 序列化,最终以列表形式呈现在页面上。

本地运行与预期结果

一切配置就绪后启动开发服务器:

npm run dev
# 或
yarn dev
# 或
pnpm dev

应用默认运行在 http://localhost:3000。正常工作时,页面标题下方会展示 "Todos" 区块,依次列出两条来自数据库的数据:

Buy milk (complete)
Wash car

(第二行 donefalse,故无 (complete) 标记。)这正是 API Route 从 Postgres 取回并返回的数据;作为验证,你也可以直接在浏览器访问 http://localhost:3000/api/todos 查看接口返回的原始 JSON。

如果页面加载异常,请优先按「数据库连接是否正确可达 → PG_URI 是否已写入 .env.local → 迁移是否已成功执行」的顺序排查。

部署到 Vercel:环境变量的正确姿势

方式一:本地项目推送后导入

先在本地把项目推送至 GitHub / GitLab / Bitbucket 任一 Git 托管平台,再在 Vercel 上「Import Project」导入该仓库,触发自动化部署。

关键一步:导入项目时,务必在 Vercel 的 Environment Variables 面板中手动添加与本地 .env.local 中完全一致的变量(即 PG_URI),否则云端无法获知数据库连接信息,应用会因缺少配置而无法工作。注意 .env.local 中的本地值若指向 localhost,部署后必须替换为云数据库实例可公网访问的连接串(并确保数据库的网络访问策略放行了 Vercel 的出口 IP / 服务)。

方式二:直接使用模板一键部署

with-knex 示例还配置了「一键部署」按钮,点击后会引导你基于该模板在 Vercel 创建新项目,并在创建向导中直接填写 PG_URI(官方按钮的 env 参数里同时携带了 envDescription 提示该变量用于连接 Postgres)。

由于本仓库当前仅提供源码级示例,以上部署均要求你自己持有可访问的 PostgreSQL 实例;Vercel 本身不托管 Postgres 数据,数据库仍需依赖外部 DBaaS 或自建服务。

进阶延展:把示例改造成生产可用的注意点

结合 knexfile.jsknex/index.js迁移脚本 的实现,从示例走向生产环境时有几点值得关注:

  1. 连接池与并发安全:示例演示用的是默认连接池行为。global.pg 缓存保证了单实例,但在 Serverless 部署(如 Vercel 的 Serverless Function)下,每个并发实例都是独立进程,各自持有一份连接,因此生产部署时应关注数据库的最大连接数,并按需配置 Knex 连接池参数(pool.min / pool.max)。
  2. 凭证安全:数据库连接串属于敏感信息,务必遵循示例的 .env*.local 忽略规则,绝不提交到 Git;云端则通过平台的环境变量能力注入。
  3. 迁移纳入发布流程:生产环境部署前,应把 knex migrate:latest 纳入 CI/CD 发布流程,保证数据库 Schema 与应用版本同步演进(示例中迁移目录与脚本已就绪)。
  4. 统一的数据访问层:示例把 getKnex 收敛在 knex/index.js 单点导出,所有页面与 API 都经由它取连接。这种「单一入口」便于统一管理查询与事务,适合作为你项目数据访问层的起点。
  5. 替换数据库只需改两处:得益于 Knex 的多方言设计,要换到 MySQL 等数据库,仅需修改 knexfile.js 中的 client 值、安装对应驱动、并把 PG_URI 换成相应连接串(可能需要微调迁移中的字段类型),业务代码基本不用变动。

从源码结构看,with-knex 示例刻意保持了最小化:它没有引入 ORM 模型层、没有封装 Repository,而是把 Knex 查询构建器最原生的用法直接摆在 pages/api/todos.js 中,让你在理解全链路的同时,可以按需叠加更复杂的抽象。以此为基础,你可以继续阅读仓库内其他数据库相关示例(例如 with-mongodbwith-prisma 同类生态)做横向对比,选择最适合自己数据模型与团队习惯的方案。

登录后查看全文
热门项目推荐
相关项目推荐