☰
Midway 与 Next.js 集成指南:基于 Functional API 的类型安全 Bridge 客户端
2026/9/28 2:17:07 网站建设 项目流程
  • 后端
  • 微服务
  • 云原生

【免费下载链接】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. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

@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 实现。其核心逻辑是:

  1. 遍历命名空间映射,把userApi中每条路由编译为一条ApiBridgeOperation(含operationId、method、path、fullPath);
  2. operationId采用${namespaceKey}.${routeKey}的命名规则,例如user.getUser;
  3. fullPath由basePath(/api)+ 模块前缀(/users)+ 路由路径(/:id)拼接得到/api/users/:id;
  4. 每个命名空间下的方法会被包装为(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 给出的完整流程可以概括为四步:

  1. 定义 API:在src/server/api中用defineApi定义接口(如user.api.ts),并在index.ts统一导出;
  2. 保留 Next 路由:把 HTTP 入口放在src/app/api/**/route.ts,由 Next.js 完成 URL 匹配;
  3. 创建桥接客户端:在src/app/lib/api-client.ts中createClient({ user: userApi }, { basePath: '/api' });
  4. 复用类型化方法:在 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. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

相关推荐

上一篇:推荐开源项目:AndroidProcesses - 获取运行中的应用程序进程库
下一篇:【亲测免费】 探索未来软件分发模式:snapd

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询