- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
@midwayjs/nextjs是 Midway 为 Next.js 场景提供的 Bridge 辅助包,它让开发者可以用 Midway Functional API(defineApi)定义服务端接口,再通过一个类型安全的桥接客户端在 Next.js 的 Route Handler、Server Component 中直接调用这些接口。本文将基于该包在仓库中的文档与源码实现,完整讲解安装方式、推荐目录结构、服务端 API 定义、桥接客户端创建、App Router 集成以及底层运行原理,帮助你在一小时内把一个 Next.js + Midway 的全栈应用跑起来。
运行时边界:Bridge 不接管 Next.js 路由
在开始写代码之前,首先要明确@midwayjs/nextjs的职责边界。README 中给出了三条核心约定:
- Next.js 保留自己完整的文件路由体系(
app/api、pages/api); @midwayjs/nextjs不会替换 Next 的路由匹配逻辑;- 该包只提供类型化的桥接客户端辅助工具(typed bridge client helpers)。
换句话说,src/app/api/**/route.ts依然是请求的真正入口,Next.js 负责把/api/users/1这样的 URL 匹配到对应的 Route Handler;而@midwayjs/nextjs解决的是另一个问题——如何让服务端 API 的定义(src/server/api)在前后端之间共享,并生成类型安全的调用方法,避免手写 URL 拼接和any类型。
从源码结构看,这一边界也非常清晰:包内 src/bridge.ts 全部是对@midwayjs/web-bridge的薄封装,负责导出createClient、createNextjsApiClient等桥接工具;而 src/middleware.ts 与 src/configuration.ts 则提供了另一条可选路线——在 Koa/Express 等 Midway 应用中直接托管 Next.js(见下文“基于中间件托管 Next.js”一节)。
安装
npm i @midwayjs/nextjs对应package.json:
{ "dependencies": { "@midwayjs/nextjs": "^4.0.0-beta.11" } }该包依赖@midwayjs/web-bridge(见 package.json 中的dependencies),而 bridge 的类型与运行时能力实际来自@midwayjs/api-bridge,因此只需安装这一个包即可获得完整的桥接能力。包名关键字为midway、nextjs、ssr,说明它同样服务于 SSR 场景。当前仓库中的开发版本为4.2.3,要求 Node.js>= 20,并在开发环境使用 Next.js~16.3.0(均以 package.json 实际声明为准)。
推荐项目结构
README 给出了一个前后端共置(co-located)的推荐布局:
src/ app/ page.tsx api/ users/ [id]/ route.ts lib/ api-client.ts server/ api/ user.api.ts index.ts这个结构的关键点在于:
src/app/是 Next.js App Router 的工作区:page.tsx是页面(Server Component),api/users/[id]/route.ts是 API Route Handler;src/server/api/是 Midway Functional API 的定义区,只存放 API 描述,不参与 Next.js 的构建路由;src/app/lib/api-client.ts是桥接客户端实例,它同时引用src/server/api的类型定义,从而获得完整的类型提示。
仓库测试夹具packages/nextjs/test/fixtures/full-flow/中还有一套更完整的多模块示例(user、order、system、account、profile五个 API 模块),可作为实际工程中 API 拆分的参考。
服务端 API 定义(defineApi)
在src/server/api/user.api.ts中,用defineApi定义一组 REST 风格接口:
// src/server/api/user.api.ts import { defineApi } from '@midwayjs/core/functional'; export const userApi = defineApi('/users', api => ({ getUser: api .get('/:id') .handle(async ({ input }) => { return { id: input.params?.id, name: 'harry', }; }), }));defineApi('/users', ...)中的/users是模块前缀,api.get('/:id')定义了一个带路径参数:id的 GET 接口,handler 通过input.params读取路径参数并返回业务数据。
然后在统一出口src/server/api/index.ts中汇总:
// src/server/api/index.ts export { userApi } from './user.api';仓库中的多模块夹具展示了defineApi的更多进阶能力,这里先给出两个值得注意的点(详见下文“版本与全局前缀”小节):
- 模块级元信息:
defineApi第三个参数可以传入{ version, versionType, versionPrefix, ignoreGlobalPrefix }等元数据,例如 account.api.ts 通过version: '2'生成了/api/v2/accounts/:id这样的版本化路径; - 路由级元信息:
api.get('/public').meta({ ignoreGlobalPrefix: false })可以覆盖模块级配置,例如 system.api.ts 中health走/system/health,而publicInfo则保留全局前缀/api/system/public。
创建 Bridge 客户端
在src/app/lib/api-client.ts中创建一个类型安全的客户端:
// src/app/lib/api-client.ts import { createClient } from '@midwayjs/nextjs'; import { userApi } from '@/server/api'; export const api = createClient( { user: userApi, }, { basePath: '/api', } );createClient的第一个参数是命名空间映射:user是命名空间名,userApi是 API 定义模块。第二个参数是配置,其中basePath: '/api'表示所有请求会拼接在/api前缀之下。
由此生成的api.user.getUser(...)是一个类型化方法。从 bridge.ts 的源码看,createClient直接透传给@midwayjs/web-bridge的createBridgeClient,最终由 api-bridge 的 createClient 实现。其核心逻辑是:
- 遍历命名空间映射,把
userApi中每条路由编译为一条ApiBridgeOperation(含operationId、method、path、fullPath); operationId采用${namespaceKey}.${routeKey}的命名规则,例如user.getUser;fullPath由basePath(/api)+ 模块前缀(/users)+ 路由路径(/:id)拼接得到/api/users/:id;- 每个命名空间下的方法会被包装为
(input) => Promise,入参结构统一为{ params, query, body, headers }。
从仓库测试 bridge.test.ts 可以看到,使用自定义 adapter 时,api.user.getUser({ params: { id: 'u-1' } })会以fullPath: '/api/users/:id'发起调用,这正是上面拼路径规则的验证。
单操作客户端与底层工具
@midwayjs/nextjs还导出了几个底层工具,适用于更细粒度的控制:
createNextjsApiClient(definition, options):基于一个ApiClientDefinition(含operations映射)创建客户端,通过client.call('getUser', input)调用;createNextjsApiClientFromOperations(operations, options):直接从ApiBridgeOperation[]数组创建;resolveNextjsApiBridgeOptions(options):解析桥接选项,默认transport为http,adapter为空。
对应测试见 bridge.test.ts,其中验证了默认 transport 为http、自定义transport: 'trpc'与 adapter 会被原样保留。
App Router Route Handler(推荐用法)
在src/app/api/users/[id]/route.ts中编写 Next.js 路由处理器:
// src/app/api/users/[id]/route.ts import { NextResponse } from 'next/server'; import { api } from '@/app/lib/api-client'; export async function GET( _req: Request, context: { params: { id: string } } ) { const user = await api.user.getUser({ params: { id: context.params.id }, }); return NextResponse.json(user); }这段代码体现了整套方案的精髓:HTTP 入口由 Next.js 接管(/api/users/[id]匹配到本文件),而业务参数与返回类型来自 Midway 的 API 定义。context.params.id被直接透传给api.user.getUser,返回的user拥有与 handler 一致的推断类型,最后用NextResponse.json输出。
Server Component 中使用
在src/app/page.tsx(服务端组件)中,可以绕过 Route Handler、直接在页面里调用 API 客户端:
// src/app/page.tsx import { api } from '@/app/lib/api-client'; export default async function Page() { const user = await api.user.getUser({ params: { id: 'u-1' } }); return <pre>{JSON.stringify(user, null, 2)}</pre>; }Server Component 运行在服务端,天然具备发起 HTTP 请求或直接访问数据层的能力,因此这里直接await api.user.getUser(...)是安全的。README 特别强调:
For Client Components (
'use client'), call Next API routes (or pass data from server components) instead of importing server API definitions directly.
即客户端组件不要直接 import 服务端 API 定义(避免把服务端代码打进客户端 bundle),而应该调用 Next 的 API 路由,或者由 Server Component 取数后通过 props 传给客户端组件。这也是推荐把页面放在 Server Component、把 API 调用收敛在 Route Handler 层的原因。
端到端集成步骤
README 给出的完整流程可以概括为四步:
- 定义 API:在
src/server/api中用defineApi定义接口(如user.api.ts),并在index.ts统一导出; - 保留 Next 路由:把 HTTP 入口放在
src/app/api/**/route.ts,由 Next.js 完成 URL 匹配; - 创建桥接客户端:在
src/app/lib/api-client.ts中createClient({ user: userApi }, { basePath: '/api' }); - 复用类型化方法:在 Route Handler / Server Component 中调用
api.user.getUser(...),获得类型推导与运行时 URL 拼接。
传输层与自定义 adapter
默认的 HTTP 传输使用全局fetch(transport: 'http')。当需要更换传输通道时,在createClient(..., { adapter })中提供自定义 adapter:
const api = createClient( { user: userApi }, { basePath: '/api', adapter: myCustomAdapter, // axios / tRPC / 自定义通道 } );adapter 的类型签名(见 api-bridge 源码)为:
type ApiBridgeTransportAdapter = <TInput = unknown, TOutput = unknown>( request: ApiBridgeTransportRequest<TInput> ) => Promise<TOutput>;其中request形如{ operation: { operationId, method, path, fullPath }, input }。这意味着你完全可以用 axios、tRPC 或任意自定义协议替换底层传输,而调用方api.user.getUser(...)的写法保持不变。仓库还提供了createAxiosAdapter(axiosInstance)工厂函数(见 api-bridge 源码),用于接入 axios 风格实例。
从源码看默认 fetch adapter 的行为(api-bridge 源码):
- 按
:key模板替换路径参数,并做encodeURIComponent编码; - query 通过
URLSearchParams序列化,过滤undefined/null值,数组值会展开为多个同名参数; - GET/HEAD 不带 body;其余方法在
body存在时自动JSON.stringify,并补充content-type: application/json(除非已显式设置);URLSearchParams、FormData、Blob、ArrayBuffer等原生 body 原样透传; - 响应按
content-type决定json()还是text();非 2xx 时抛出包含方法、URL、状态码与响应体的错误信息。
底层实现剖析:桥接与托管两条路线
Bridge 客户端:@midwayjs/web-bridge的薄封装
src/bridge.ts 的职责非常单一:把@midwayjs/web-bridge(其再导出@midwayjs/api-bridge)的能力以nextjs命名空间重新导出,并补充NextjsApiBridgeOptions、NextjsCreateClientOptions等类型别名。运行时与类型逻辑全部复用底层桥接层,这也解释了为什么 README 说“No Vite plugin is required”——因为这里不依赖 Vite 插件注入,只需要运行时模块映射与类型推导。
中间件托管:在 Midway 应用中直接运行 Next.js
除了“Bridge 客户端 + 独立 Next.js”这种模式,@midwayjs/nextjs还支持把 Next.js 作为中间件挂载到 Koa/Express 等 Midway 应用中(这也是该包作为“component”存在的原因):
- src/configuration.ts:声明
@Configuration({ namespace: 'nextjs' }),在onReady阶段把NextJSMiddleware注册到koa、faas、express、egg应用上; - src/middleware.ts:
@Init阶段调用next({ dev, dir: appDir, ...this.nextConfig })并app.prepare()初始化 Next;请求进来时先通过webRouterService.getMatchedRouterInfo(path, method)判断是否命中 Midway 路由——命中则交给 Midway 处理,否则把请求交给 Next 的getRequestHandler()渲染页面/API。
其中间件核心逻辑(middleware.ts)可概括为:
return async (ctx: Context, next: NextFunction) => { const routeInfo = await this.webRouterService.getMatchedRouterInfo(ctx['path'], ctx['method']); if (routeInfo) { return await next(); // Midway 路由优先 } else { ctx['res'].statusCode = 200; await nextHandler(ctx['req'] || ctx, ctx['res']); // 否则交给 Next.js } };next的配置通过 Midway 配置项next注入(@Config('next')),对应的类型声明在 index.d.ts:
declare module '@midwayjs/core' { interface MidwayConfig { next?: NextServerOptions; } }测试夹具 base-app/src/index.ts 展示了如何在配置中设置next: { dev: false },并在imports: [Koa, Next]中同时引入 Koa 与 Next 组件。对应集成测试 index.test.ts 验证了:启动该夹具后请求/,返回text/html的 Next.js 渲染页面且状态码为 200。
此外,inject.js 提供了一个可选的启动注入脚本:通过 Proxy 拦截next/dist/server/lib/start-server的startServer,在 Next 启动前先Bootstrap.run()初始化 Midway,适用于需要把 Next 独立进程与 Midway 服务同时拉起的部署场景。
进阶能力:版本、全局前缀与 Manifest
版本与全局前缀
从 e2e 测试 bridge.e2e.test.ts 可以观察到完整的路径行为(由 api-bridge 的 createClient 实现resolveVersionedPrefix):
userApi(无版本、无 ignoreGlobalPrefix):GET /users/:id→ fullPath/api/users/:id;accountApi(version: '2'、versionType: 'URI'、versionPrefix: 'v'):fullPath/api/v2/accounts/:id;systemApi(模块ignoreGlobalPrefix: true):health→/system/health(无/api前缀);publicInfo通过路由级meta({ ignoreGlobalPrefix: false })覆盖 →/api/system/public;profileApi(versionType: 'HEADER'):版本不进入 URL 路径。
这些路径全部在createClient创建阶段静态计算进fullPath,运行时只需按模板做参数替换,因此请求路径的生成是确定且可测试的。e2e 测试中的createInMemoryAdapter正是利用这一点,通过RouterInfo表把每个fullPath映射回真实的 Functional API handler 执行,验证了从类型化调用到业务返回值的完整链路。
Manifest 运行时模式
createClient的第二个参数还支持manifest选项(见 api-bridge 源码),可传入:
false:关闭 manifest 运行时,完全基于模块映射生成的 operation;- 字符串路径:动态
import()一个ApiRouteManifestLike[]默认导出; - 函数:运行时调用返回 manifest;
- 数组或 Promise:直接提供 manifest 数据。
其作用是以服务端实际注册的路由表(含operationId、method、path、fullPath)为最终依据解析 operationId,避免客户端拼出的 operation 与服务端不一致。未显式传入时,默认的virtual:midway-route-manifest仅在解析失败且非严格模式下静默降级到模块映射,保证开发与生产的行为一致性。
开发与构建
README 对 Dev & Build 的说明如下:
- 开发:直接运行
next dev(单进程,Next 负责路由与 HMR,bridge 的类型仍来自src/server/api定义); - 生产:Next 构建 web 输出;bridge 的类型提示依旧来自
src/server/api; - 无需 Vite 插件:
@midwayjs/nextjs不依赖 Vite 插件。
值得注意的是,createClient是纯运行时构造(遍历模块生成 operations),不依赖构建期代码注入,因此开发与生产环境的行为一致;类型安全完全由 TypeScript 对userApi模块结构的推导保证。
如果采用中间件托管模式(Koa + Next 组件),则需要在 Next 的next.config.ts中把 Midway 相关模块加入服务端externals,避免 Webpack 打包冲突,参考夹具 base-app/next.config.ts:
webpack: (config, { isServer }) => { if (isServer) { const externals = ['@midwayjs/core', '@midwayjs/core/functional']; config.externals = [...externals, ...(Array.isArray(config.externals) ? config.externals : [])]; } return config; },结语
@midwayjs/nextjs用一个轻量的桥接层解决了 Next.js 与 Midway 之间的“类型共享”与“调用类型化”问题:Next.js 保留自己的文件路由与渲染体系,Midway Functional API 负责业务定义,createClient在两者之间架起一条带完整类型推导的调用通道。对于希望复用已有 Midway 业务模块、同时享受 Next.js App Router 生态的团队,这是一条低侵入、可渐进落地的集成路线;而中间件托管模式则为需要在 Koa/Express 应用中直接嵌入 Next.js 的场景提供了另一种选择。你可以从 packages/nextjs/README.md 开始,再对照本仓库 bridge.ts、middleware.ts 以及 bridge.e2e.test.ts 与 bridge.test.ts 等测试用例,深入验证每一步的行为与边界。
- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
相关推荐
Next.js与TRPC客户端:构建类型安全API调用的终极指南
Next.js与TRPC客户端:构建类型安全API调用的终极指南 在现代Web开发中,类型安全已成为提高开发效率和代码质量的关键因素。Next.js作为Reac
前端后端Web框架SSR前端构建openapi-fetch 集成 SvelteKit:端到端类型安全的 API 客户端实战指南
openapi fetch 集成 SvelteKit:端到端类型安全的 API 客户端实战指南 本指南基于 openapi fetch 仓库中的 SvelteK
开发工具代码生成后端FastAPI:基于 OpenAPI 自动生成类型安全的客户端 SDK 完整指南
FastAPI:基于 OpenAPI 自动生成类型安全的客户端 SDK 完整指南 本篇指南讲解如何利用 FastAPI 内置的 OpenAPI 生成能力,把后端
后端Web框架API设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考