在做全栈架构设计的这几年里,我发现大家最容易争论、却也最容易陷入教条主义的技术决策之一,就是通信协议的选型。
很多团队在立项时,往往凭着直觉做决定:要么全盘 RESTful,不管内部调用链有多深、JSON 序列化有多耗 CPU;要么盲目上 gRPC,结果前端同学为了在浏览器里调试一个接口,不得不配一整套 Envoy 代理做 gRPC-Web 转换,开发体验苦不堪言。
技术选型没有银弹,本质上都是在开发效率、传输性能、调试成本与跨端生态之间做取舍。今天聊聊在全栈视角下,我们该如何划定通信边界,以及何时选择 REST、何时选择 RPC。
一、 先看边界:南北向流量 vs 东西向流量
讨论 REST 和 RPC 之前,必须先厘清两个网络边界:
- 南北向流量(Client to Server):通常指浏览器、App、小程序到网关或 BFF(Backend for Frontend)的通信。
- 东西向流量(Server to Server):微服务内部各模块、工作节点之间的协同通信。
[ Web / App / 小程序 ]
│ (南北向流量:要求兼容性高、网络异构、调试友好)
▼
[ API Gateway / BFF ]
┌────┴──────────────┐
│ (东西向流量:要求低延迟、高吞吐、强类型约束)
▼ ▼
[ User Service ] ◄──► [ Order Service ]
两者的网络环境与诉求截然不同。南北向面对的是不可控的公网环境与异构客户端,更注重缓存、穿透性与调试便利;东西向处于受控的内网环境,吞吐量和低延迟才是核心指标。
二、 REST:边缘接入与公共生态的坚守者
REST(通常基于 HTTP/1.1 或 HTTP/2 + JSON)依然是目前南北向通信的默认基准。
1. 优势与不可替代性
- 极致的调试体验:打开浏览器 DevTools 就能看 Payload,随时用
curl验证。 - 天然的基础设施支持:CDN 缓存、API 网关鉴权、WAF 防火墙对标准 HTTP 动词和状态码的支持非常成熟。
- 低集成门槛:作为对外开放的 Open API 时,第三方无需依赖特定的 SDK 或 IDL 文件。
2. 全栈视角的痛点
REST 的缺点同样明显:
- Over-fetching 与 Under-fetching:接口字段要么给多了浪费带宽,要么给少了需要前端发多次请求组合。
- 弱类型约束:虽然可以用 OpenAPI / Swagger 规范,但文档与代码容易脱节,前后端联调容易变成“对字段游戏”。
// 前端不得不手动维护类型定义,稍有不慎就会出现运行时 undefined
interface UserResponse {
id: string;
name: string;
avatarUrl?: string; // 后端改动字段名,前端编译期完全无感知
}
async function fetchUser(id: string): Promise<UserResponse> {
const res = await fetch(`/api/v1/users/${id}`);
if (!res.ok) throw new Error('Network error');
return res.json();
}
三、 RPC:微服务内部与确定性系统的肌肉
RPC(Remote Procedure Call)的核心理念是“像调用本地函数一样调用远程服务”。在现代全栈架构中,主流的方案可以分为两类:以 gRPC 为代表的基于二进制协议的强类型方案,以及在 TypeScript 全栈生态中兴起的 tRPC 等轻量方案。
1. 基于 Protobuf 的 gRPC(东西向主流)
gRPC 跑在 HTTP/2 之上,使用 Protocol Buffers 序列化,具有极高的压缩比与解析性能。
// user.proto
syntax = "proto3";
package user.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}
message GetUserRequest {
string user_id = 1;
}
message GetUserResponse {
string id = 1;
string name = 2;
string email = 3;
}
为什么东西向流量首选 gRPC?
- IDL 契约优先:通过
.proto强行约束上下游,生成各语言的 Stub 代码,接口变更在编译期就能发现破坏性改动(Breaking Changes)。 - 二进制序列化性能:相比 JSON 字符串解析,Protobuf 在序列化体积和 CPU 消耗上有数倍的优势,在高并发微服务调用下能节省大量服务器资源。
- 流式传输:原生支持 Server Streaming、Client Streaming 和双向流,适合长连接下的大数据量推送。
2. 基于 TypeScript 的 Code-first RPC(BFF 与全栈方案)
如果你的前端和 BFF 层(比如 Node.js / Next.js / NestJS)都是使用 TypeScript 构建,tRPC 提供了另一种维度的权衡:
// server.ts (BFF 层)
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
// 业务逻辑
return { id: input.id, name: 'LaoChen', role: 'admin' };
}),
});
export type AppRouter = typeof appRouter;
// client.ts (前端)
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCProxyClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })],
});
// 完全端到端的类型推导,无代码生成步骤,输入输出自动校验
const user = await client.getUser.query({ id: 'u_123' });
console.log(user.name); // 类型安全
tRPC 舍弃了 Protobuf 的跨语言能力,换取了前端与 BFF 层之间零构建生成、纯类型共享的极致开发体验。
四、 选型决策矩阵
为了方便在架构评审时做判断,我把常见的权衡维度整理如下:
| 维度 | REST (JSON) | gRPC (Protobuf) | tRPC (TS Ecosystem) |
|---|---|---|---|
| 首选场景 | 对外 Public API、移动端网关入口 | 微服务东西向调用、高并发后端服务 | TS 全栈 Monorepo、BFF 到前端 |
| 传输效率 | 中等(文本开销、无头压缩优化) | 极高(二进制、HTTP/2 多路复用) | 中等(底层仍为 HTTP + JSON) |
| 类型安全 | 弱(依赖 OpenAPI 规范维护) | 强(基于 IDL 生成多语言代码) | 极强(TS 编译器静态类型推导) |
| 浏览器直连 | 原生支持,调试极佳 | 较差(需 gRPC-Web / Envoy 桥接) | 原生支持(基于 Fetch) |
| 多语言生态 | 极佳 | 极佳 | 仅限 TypeScript / JavaScript |
五、 我的工程实践建议
在实际项目中,我倾向于采取分层治理的折中策略:
- 服务间通信(东西向):毫不犹豫选择 gRPC。用 Git Submodule 或私有 NPM/Buf 仓库统一管理
.proto文件,让接口契约成为各微服务团队的唯一交付物。 - 对外开放或多端接入(南北向):保留 RESTful API。遵循 OpenAPI 3.0 规范,配合网关做协议转换(如 gRPC-Gateway 直接将 gRPC 暴露为 REST JSON 接口)。
- 独立全栈应用(Next.js / Nuxt 架构):如果是前后端同构的单体或全栈项目,优先使用 tRPC / Server Actions。省去维护 API 文档的时间,把类型安全贯穿到 UI 组件级别。
架构设计的艺术在于知道边界在哪里。不要为了追求极致性能而在浏览器端硬啃二进制 RPC,也不要因为图省事就在高吞吐的后端集群里到处透传未经校验的 JSON。看清流量走向,技术选型自然水到渠成。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


