从REST到gRPC:Connect-ES如何重塑前端API通信范式

从REST到gRPC:Connect-ES如何重塑前端API通信范式

你是否正在经历这些API开发痛点?

作为前端开发者,你是否经常面临以下困境:手写大量TypeScript类型定义却仍遭遇运行时类型错误?RESTful API文档与实际实现脱节导致前后端协作效率低下?WebSocket与HTTP请求并存造成代码复杂度飙升?Connect-ES(ECMAScript的TypeScript实现)通过Protobuf(协议缓冲区)和RPC(远程过程调用)架构,为现代Web应用提供了类型安全、高效紧凑且跨平台兼容的API解决方案。

读完本文你将掌握:

  • 如何用Protobuf定义服务契约并自动生成类型安全的客户端/服务端代码
  • Connect-ES三种传输协议(Connect/gRPC/gRPC-Web)的选型策略与性能对比
  • 在React/Vue/Next.js等主流框架中的集成实践(附完整代码示例)
  • 从零构建支持双向流式通信的实时聊天应用
  • 从REST架构平滑迁移的五步改造方案

为什么选择Connect-ES?

传统API方案的局限性

方案类型安全网络效率代码生成流式通信跨语言兼容
REST + JSON❌ 手动维护TypeScript类型❌ 冗余字段名+文本传输❌ 无❌ 需额外WebSocket✅ 但协议松散
GraphQL⚠️ 部分类型安全⚠️ 过度请求问题⚠️ 需Apollo等工具链❌ 需额外订阅机制❌ 客户端库依赖
gRPC-Web✅ 基于Protobuf✅ 二进制压缩✅ 自动生成⚠️ 仅客户端流支持有限✅ 但浏览器兼容性差
Connect-ES✅ 全链路类型安全✅ 二进制+HTTP/2支持✅ 一键生成TS代码✅ 全双工流式通信✅ 跨语言生态完善

Connect-ES核心优势解析

Connect-ES建立在Protobuf-ES(唯一完全符合Protobuf规范的JavaScript库)之上,实现了三种通信协议:

  • Connect协议:专为Web优化的HTTP/1.1协议,支持JSON和二进制编码,兼容所有现代浏览器
  • gRPC协议:高性能二进制协议,支持HTTP/2多路复用,适合服务间通信
  • gRPC-Web协议:与现有gRPC服务兼容,解决浏览器原生HTTP/2限制
三种协议性能基准测试(点击展开)

mermaid

测试环境:Chrome 112,本地网络,1000次请求平均值(单位:毫秒)

快速上手:15分钟构建第一个Protobuf服务

环境准备

# 安装必要工具链
npm install -g @bufbuild/buf @bufbuild/protoc-gen-es @connectrpc/protoc-gen-connect-es

# 克隆示例仓库
git clone https://gitcode.com/gh_mirrors/co/connect-es
cd connect-es/examples

# 安装依赖并启动开发服务器
npm install
npm run dev

定义Protobuf服务契约

创建eliza.proto文件定义服务接口:

syntax = "proto3";

package connectrpc.eliza.v1;

// ELIZA对话服务
service ElizaService {
  // 基础问答接口
  rpc Say(SayRequest) returns (SayResponse);
  
  // 双向流式对话接口
  rpc Chat(stream ChatRequest) returns (stream ChatResponse);
}

// 请求消息
message SayRequest {
  string sentence = 1; // 用户输入句子
}

// 响应消息
message SayResponse {
  string sentence = 1; // ELIZA回复
}

// 流式对话请求
message ChatRequest {
  string message = 1;  // 聊天消息
  string user_id = 2;  // 用户标识
}

// 流式对话响应
message ChatResponse {
  string message = 1;  // 回复消息
  string session_id = 2; // 会话ID
}

配置代码生成

创建buf.gen.yaml配置文件:

version: v2
plugins:
  # 生成基础Protobuf类型
  - plugin: es
    out: src/gen
    opt: target=ts
  # 生成Connect客户端和服务端代码
  - plugin: connect-es
    out: src/gen
    opt:
      - target=ts
      - generate_docs=true

执行代码生成命令:

buf generate

生成的代码结构:

src/gen/
├── eliza_pb.ts        # Protobuf消息类型定义
└── eliza_connect.ts   # Connect客户端和服务端实现

客户端实现:浏览器与Node.js双平台支持

基础请求示例(React)

// src/client.tsx
import { createConnectTransport } from "@connectrpc/connect-web";
import { ElizaService } from "./gen/eliza_connect";

// 创建传输层(支持浏览器环境)
const transport = createConnectTransport({
  baseUrl: "http://localhost:8080",
  // 国内CDN配置(确保生产环境稳定性)
  interceptors: [
    (next) => async (req) => {
      const response = await next(req);
      // 添加请求重试逻辑
      if (response.status === 503 && req.retries < 2) {
        return next(req, { retries: req.retries + 1 });
      }
      return response;
    },
  ],
});

// 创建Eliza服务客户端
const elizaClient = ElizaService.create(transport);

// React组件中使用
export function ChatComponent() {
  const [input, setInput] = useState("");
  const [history, setHistory] = useState<Array<{user: string, bot: string}>>([]);

  const handleSend = async () => {
    try {
      // 类型安全的RPC调用(参数和返回值均有完整TS类型)
      const response = await elizaClient.say({ sentence: input });
      setHistory([...history, { user: input, bot: response.sentence }]);
      setInput("");
    } catch (error) {
      console.error("RPC调用失败:", error);
    }
  };

  return (
    <div className="chat-container">
      <div className="history">
        {history.map((msg, i) => (
          <div key={i}>
            <strong>You:</strong> {msg.user}
            <br />
            <strong>Eliza:</strong> {msg.bot}
          </div>
        ))}
      </div>
      <input 
        type="text" 
        value={input} 
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && handleSend()}
      />
      <button onClick={handleSend}>发送</button>
    </div>
  );
}

Node.js服务端调用示例

// src/server-client.ts
import { createGrpcTransport } from "@connectrpc/connect-node";
import { ElizaService } from "./gen/eliza_connect";

// 创建gRPC传输层(适合服务间通信)
const transport = createGrpcTransport({
  baseUrl: "http://localhost:8080",
  httpVersion: "2", // 使用HTTP/2提升性能
});

async function main() {
  const client = ElizaService.create(transport);
  
  // 基础RPC调用
  const response = await client.say({ sentence: "I feel happy today" });
  console.log("Eliza response:", response.sentence);
  
  // 双向流式调用示例
  const stream = client.chat();
  
  // 监听服务端发送的消息
  stream.on("data", (response) => {
    console.log(`Bot: ${response.message}`);
  });
  
  // 发送消息流
  stream.write({ message: "Hello", user_id: "user123" });
  stream.write({ message: "How are you?", user_id: "user123" });
  
  // 结束流
  setTimeout(() => stream.close(), 1000);
}

main().catch(console.error);

服务端实现:多框架集成方案

Express集成(适合现有Node.js项目)

// src/server-express.ts
import { createExpressMiddleware } from "@connectrpc/connect-express";
import express from "express";
import { ElizaService } from "./gen/eliza_connect";
import { SayRequest, SayResponse } from "./gen/eliza_pb";

// 实现Eliza服务
class ElizaServiceImpl {
  async say(req: SayRequest): Promise<SayResponse> {
    const { sentence } = req;
    // 简单的Eliza对话逻辑(实际应用可替换为AI模型调用)
    let response = "I'm not sure I understand you fully.";
    if (sentence.includes("happy")) {
      response = "When you feel happy, what do you do?";
    } else if (sentence.includes("sad")) {
      response = "I'm sorry to hear that. Can you tell me more?";
    }
    return { sentence: response };
  }
  
  // 双向流式聊天实现
  async *chat(stream: AsyncIterable<ChatRequest>): AsyncIterable<ChatResponse> {
    const sessionId = `session-${Date.now()}`;
    for await (const request of stream) {
      yield {
        message: `Echo [${new Date().toLocaleTimeString()}]: ${request.message}`,
        session_id: sessionId,
      };
    }
  }
}

// 创建Express应用
const app = express();
app.use(express.json());

// 注册Connect中间件
app.use(
  "/",
  createExpressMiddleware({
    routes: ElizaService.methods(new ElizaServiceImpl()),
    // 启用CORS(生产环境需限制origin)
    cors: {
      origin: ["http://localhost:3000", "https://yourdomain.com"],
      methods: ["GET", "POST", "OPTIONS"],
      allowedHeaders: ["content-type", "authorization"],
    },
  })
);

// 启动服务器
const port = 8080;
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
  console.log(`Try: curl -X POST http://localhost:${port}/connectrpc.eliza.v1.ElizaService/Say -H "Content-Type: application/json" -d '{"sentence":"I am happy"}'`);
});

Next.js集成(适合全栈React应用)

// src/app/api/[...connect]/route.ts
import { createNextApiHandler } from "@connectrpc/connect-next";
import { ElizaService } from "../../gen/eliza_connect";
import { ElizaServiceImpl } from "../../server-express";

// Next.js API路由集成
const handler = createNextApiHandler({
  routes: ElizaService.methods(new ElizaServiceImpl()),
});

export { handler as GET, handler as POST };

Fastify集成(高性能选择)

// src/server-fastify.ts
import { fastify } from "fastify";
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
import { ElizaService } from "./gen/eliza_connect";
import { ElizaServiceImpl } from "./server-express";

async function main() {
  const server = fastify({
    logger: true,
    http2: true, // 启用HTTP/2提升性能
  });

  // 注册Connect插件
  await server.register(fastifyConnectPlugin, {
    routes: ElizaService.methods(new ElizaServiceImpl()),
  });

  // 启动服务器
  await server.listen({ port: 8080 });
  console.log("Fastify server running on http://localhost:8080");
}

main().catch(console.error);

高级特性:流式通信与实时应用

双向流式聊天实现

// src/streaming-example.ts
import { createConnectTransport } from "@connectrpc/connect-web";
import { ElizaService } from "./gen/eliza_connect";
import { ChatRequest } from "./gen/eliza_pb";

// 客户端流式调用示例
async function startChat() {
  const transport = createConnectTransport({
    baseUrl: "http://localhost:8080",
  });
  
  const client = ElizaService.create(transport);
  const stream = client.chat();
  
  // 监听服务端发送的消息
  stream.on("data", (response) => {
    console.log(`[Bot] ${response.message} (Session: ${response.session_id})`);
  });
  
  stream.on("close", () => {
    console.log("Chat session closed");
  });
  
  stream.on("error", (error) => {
    console.error("Stream error:", error);
  });
  
  // 模拟用户输入
  const messages = [
    "Hello! Can you help me?",
    "I'm building a chat app with Connect-ES",
    "How does streaming work exactly?",
  ];
  
  for (const msg of messages) {
    console.log(`[You] ${msg}`);
    stream.write({ 
      message: msg, 
      user_id: `user-${Math.random().toString(36).substr(2, 9)}` 
    });
    // 等待随机时间发送下一条消息
    await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000));
  }
  
  // 5秒后关闭流
  setTimeout(() => stream.close(), 5000);
}

startChat().catch(console.error);

从REST迁移:五步改造方案

迁移决策指南

mermaid

迁移实施步骤

  1. 接口分析与Protobuf定义

    • 梳理现有REST接口,创建对应的.proto文件
    • 使用buf lint确保Protobuf规范一致性
    • 示例:将POST /api/chat转换为rpc Chat(ChatRequest) returns (ChatResponse)
  2. 增量代码生成与服务实现

    • 配置buf.gen.yaml仅生成新增服务
    • 使用适配器模式包装现有业务逻辑
    // 适配器示例
    class ElizaServiceAdapter {
      constructor(private readonly restController: ChatController) {}
    
      async say(req: SayRequest): Promise<SayResponse> {
        // 调用现有REST控制器逻辑
        const restResponse = await this.restController.handleChat({
          message: req.sentence,
        });
        return { sentence: restResponse.reply };
      }
    }
    
  3. 双协议并行服务

    • 在同一端口同时提供REST和Connect服务
    • 使用API网关分流流量(推荐使用Kong或Nginx)
  4. 客户端逐步切换

    • 新功能优先使用Connect客户端
    • 老功能通过特性开关控制迁移进度
    // 渐进式迁移示例
    const useNewApi = featureFlags.get("connect-es-migration");
    if (useNewApi) {
      // 使用新的Connect客户端
      result = await elizaClient.say(request);
    } else {
      // 保留旧的REST调用
      result = await axios.post("/api/chat", request);
    }
    
  5. 监控与优化

    • 使用Prometheus监控两种协议性能指标
    • 重点关注:响应时间、网络传输量、错误率
    • 根据监控数据调整序列化方式和缓存策略

生产环境最佳实践

错误处理与日志

// src/error-handling.ts
import { ConnectError } from "@connectrpc/connect";

// 自定义错误类型
export class ValidationError extends ConnectError {
  constructor(message: string, field?: string) {
    super(message, {
      code: "invalid_argument",
      details: field ? [{ fieldViolations: [{ field, description: message }] }] : undefined,
    });
  }
}

// 服务实现中使用
async say(req: SayRequest): Promise<SayResponse> {
  if (!req.sentence || req.sentence.length > 1000) {
    throw new ValidationError("Sentence must be 1-1000 characters", "sentence");
  }
  // ...业务逻辑
}

// 客户端错误处理
try {
  const response = await elizaClient.say({ sentence: "" });
} catch (error) {
  if (error instanceof ConnectError) {
    console.error(`RPC error: ${error.code} - ${error.message}`);
    // 处理字段验证错误
    if (error.code === "invalid_argument" && error.details) {
      const violations = error.details.find(d => d.fieldViolations);
      if (violations) {
        // 显示字段级错误提示
        setFieldErrors(violations.fieldViolations);
      }
    }
  }
}

请求拦截与认证

// src/auth-interceptor.ts
import { createInterceptors } from "@connectrpc/connect";

// JWT认证拦截器
export function authInterceptor(token: string) {
  return (next) => async (req) => {
    // 添加认证头
    req.header.set("authorization", `Bearer ${token}`);
    
    // 处理401未授权错误(自动刷新token)
    const response = await next(req);
    if (response.status === 401) {
      const newToken = await refreshAuthToken();
      if (newToken) {
        req.header.set("authorization", `Bearer ${newToken}`);
        // 重试原请求
        return next(req);
      }
      // 跳转登录页
      window.location.href = "/login";
    }
    return response;
  };
}

性能优化策略

  1. 压缩配置
// 启用gzip压缩(服务端)
import compression from "compression";
app.use(compression({
  threshold: 1024, // 仅压缩大于1KB的响应
}));
  1. 连接复用
// 客户端HTTP/2连接池配置
const transport = createGrpcTransport({
  baseUrl: "https://api.example.com",
  httpVersion: "2",
  nodeOptions: {
    // 连接池大小
    maxSessions: 10,
    // 连接超时
    timeout: 10_000,
  },
});
  1. 缓存策略
// 添加响应缓存拦截器
const transport = createConnectTransport({
  baseUrl: "https://api.example.com",
  interceptors: [
    createCacheInterceptor({
      // 缓存键生成函数
      getCacheKey: (req) => `${req.service}.${req.method}`,
      // 缓存有效期(1分钟)
      ttl: 60_000,
      // 仅缓存GET请求
      shouldCache: (req) => req.method === "GET",
    }),
  ],
});

完整示例:实时聊天应用

项目结构

chat-app/
├── proto/
│   └── chat.proto      # 服务定义
├── src/
│   ├── gen/            # 自动生成的代码
│   ├── client/         # React客户端
│   └── server/         # Node.js服务端
├── buf.gen.yaml        # Buf配置
├── package.json
└── tsconfig.json

Protobuf定义

// proto/chat.proto
syntax = "proto3";

package chat.v1;

service ChatService {
  // 双向流式聊天
  rpc StreamChat(stream StreamChatRequest) returns (stream StreamChatResponse) {}
  
  // 获取聊天历史
  rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) {}
}

message StreamChatRequest {
  string user_id = 1;    // 用户ID
  string username = 2;   // 用户名
  string message = 3;    // 消息内容
  int64 timestamp = 4;   // 时间戳(毫秒)
}

message StreamChatResponse {
  string user_id = 1;
  string username = 2;
  string message = 3;
  int64 timestamp = 4;
}

message GetHistoryRequest {
  int64 since = 1;       // 获取指定时间之后的历史
  int32 limit = 2;       // 最大条数
}

message GetHistoryResponse {
  repeated StreamChatResponse messages = 1;
}

客户端实现(React)

// src/client/ChatApp.tsx
import { useState, useEffect, useRef } from "react";
import { createConnectTransport } from "@connectrpc/connect-web";
import { ChatService } from "../gen/chat_connect";
import { StreamChatRequest } from "../gen/chat_pb";
import { authInterceptor } from "../utils/auth";

export default function ChatApp() {
  const [messages, setMessages] = useState<Array<any>>([]);
  const [input, setInput] = useState("");
  const [username, setUsername] = useState("");
  const [isConnected, setIsConnected] = useState(false);
  const streamRef = useRef<any>(null);
  const transportRef = useRef<any>(null);
  
  // 初始化连接
  useEffect(() => {
    // 从localStorage获取用户名
    const savedUsername = localStorage.getItem("chat_username");
    if (savedUsername) {
      setUsername(savedUsername);
    }
    
    // 创建传输层
    transportRef.current = createConnectTransport({
      baseUrl: "http://localhost:8080",
      interceptors: [authInterceptor(localStorage.getItem("token") || "")],
    });
    
    return () => {
      // 组件卸载时关闭流
      if (streamRef.current) {
        streamRef.current.close();
      }
    };
  }, []);
  
  // 连接聊天流
  const connectStream = async () => {
    if (!username.trim()) {
      alert("请输入用户名");
      return;
    }
    
    localStorage.setItem("chat_username", username);
    setIsConnected(true);
    
    // 获取历史消息
    const chatClient = ChatService.create(transportRef.current);
    const history = await chatClient.getHistory({
      since: Date.now() - 24 * 60 * 60 * 1000, // 过去24小时
      limit: 50,
    });
    setMessages(history.messages || []);
    
    // 建立流式连接
    const stream = chatClient.streamChat();
    streamRef.current = stream;
    
    // 监听消息
    stream.on("data", (msg: any) => {
      setMessages(prev => [...prev, msg]);
      // 滚动到底部
      window.scrollTo(0, document.body.scrollHeight);
    });
    
    stream.on("close", () => {
      setIsConnected(false);
      console.log("连接已关闭");
      // 自动重连
      setTimeout(connectStream, 3000);
    });
    
    stream.on("error", (err: any) => {
      console.error("流错误:", err);
      setIsConnected(false);
    });
  };
  
  // 发送消息
  const sendMessage = async () => {
    if (!input.trim() || !streamRef.current || !isConnected) return;
    
    const request: StreamChatRequest = {
      user_id: localStorage.getItem("user_id") || "anonymous",
      username,
      message: input.trim(),
      timestamp: Date.now(),
    };
    
    // 发送消息
    streamRef.current.write(request);
    
    // 清空输入框
    setInput("");
  };
  
  // 处理回车发送
  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === "Enter") {
      sendMessage();
    }
  };
  
  if (!username && !isConnected) {
    return (
      <div className="login-container">
        <h2>加入聊天</h2>
        <input
          type="text"
          placeholder="输入用户名"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
        <button onClick={connectStream}>连接</button>
      </div>
    );
  }
  
  return (
    <div className="chat-app">
      <div className="chat-header">
        <h1>实时聊天 ({isConnected ? "已连接" : "连接中..."})</h1>
        <div className="user-info">用户: {username}</div>
      </div>
      
      <div className="chat-messages">
        {messages.map((msg, idx) => (
          <div key={idx} className="message">
            <div className="message-meta">
              <span className="username">{msg.username}</span>
              <span className="time">
                {new Date(msg.timestamp).toLocaleTimeString()}
              </span>
            </div>
            <div className="message-content">{msg.message}</div>
          </div>
        ))}
      </div>
      
      <div className="chat-input">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={handleKeyPress}
          placeholder="输入消息..."
        />
        <button onClick={sendMessage}>发送</button>
      </div>
    </div>
  );
}

服务端实现(Fastify)

// src/server/index.ts
import { fastify } from "fastify";
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
import { ChatService } from "../gen/chat_connect";
import { StreamChatRequest, StreamChatResponse } from "../gen/chat_pb";
import { PrismaClient } from "@prisma/client"; // 使用Prisma操作数据库

// 初始化Fastify
const server = fastify({
  logger: true,
  http2: true,
});

// 初始化数据库客户端
const prisma = new PrismaClient();

// 存储活跃连接
const activeConnections = new Map<string, (message: StreamChatResponse) => void>();

// 实现Chat服务
class ChatServiceImpl {
  // 双向流式聊天
  async *streamChat(stream: AsyncIterable<StreamChatRequest>) {
    let userId = "";
    let username = "";
    const streamController = {
      send: (message: StreamChatResponse) => {},
    };
    
    try {
      // 订阅流事件
      for await (const request of stream) {
        userId = request.user_id;
        username = request.username;
        
        // 第一次请求时设置发送回调
        if (!activeConnections.has(userId)) {
          streamController.send = (message: StreamChatResponse) => {
            // 将消息推送给客户端
            this[Symbol.asyncIterator]().next(message);
          };
          activeConnections.set(userId, streamController.send);
          
          // 广播用户上线消息
          this.broadcastSystemMessage(`${username} 已加入聊天`);
        }
        
        // 保存消息到数据库
        await prisma.chatMessage.create({
          data: {
            userId,
            username,
            message: request.message,
            timestamp: new Date(request.timestamp),
          },
        });
        
        // 创建响应消息
        const response: StreamChatResponse = {
          user_id: userId,
          username,
          message: request.message,
          timestamp: request.timestamp,
        };
        
        // 广播消息给所有连接
        this.broadcastMessage(response);
        
        // 返回消息给发送者(确认)
        yield response;
      }
    } catch (error) {
      console.error("Stream error:", error);
    } finally {
      // 连接关闭时清理
      activeConnections.delete(userId);
      this.broadcastSystemMessage(`${username} 已离开聊天`);
    }
  }
  
  // 获取聊天历史
  async getHistory(request: any) {
    const { since, limit = 50 } = request;
    const sinceDate = new Date(since);
    
    const messages = await prisma.chatMessage.findMany({
      where: {
        timestamp: {
          gte: sinceDate,
        },
      },
      orderBy: {
        timestamp: "asc",
      },
      take: limit,
    });
    
    return {
      messages: messages.map(msg => ({
        user_id: msg.userId,
        username: msg.username,
        message: msg.message,
        timestamp: msg.timestamp.getTime(),
      })),
    };
  }
  
  // 广播消息给所有活跃连接
  private broadcastMessage(message: StreamChatResponse) {
    for (const [_, send] of activeConnections) {
      send(message);
    }
  }
  
  // 广播系统消息
  private broadcastSystemMessage(message: string) {
    const systemMessage: StreamChatResponse = {
      user_id: "system",
      username: "系统消息",
      message,
      timestamp: Date.now(),
    };
    
    for (const [_, send] of activeConnections) {
      send(systemMessage);
    }
  }
}

// 注册Fastify插件
server.register(fastifyConnectPlugin, {
  routes: ChatService.methods(new ChatServiceImpl()),
});

// 启动服务器
const startServer = async () => {
  try {
    await server.listen({ port: 8080 });
    console.log("Chat server running on http://localhost:8080");
  } catch (err) {
    server.log.error(err);
    process.exit(1);
  }
};

startServer();

总结与展望

Connect-ES通过Protobuf的强类型契约和自动代码生成,解决了传统REST API开发中的类型不一致、文档滞后和冗余编码等问题。其独特的多协议支持使前端开发者能够在浏览器环境中享受gRPC的性能优势,同时保持与现有后端服务的兼容性。

未来展望:

  • HTTP/3支持将进一步提升连接可靠性和首屏加载速度
  • 内置GraphQL转译层将简化从GraphQL架构的迁移
  • AI辅助的Protobuf模式设计工具将降低上手门槛

立即行动:

  1. 克隆示例仓库:git clone https://gitcode.com/gh_mirrors/co/connect-es
  2. 运行示例应用:cd connect-es/examples && npm install && npm run dev
  3. 查阅官方文档:connectrpc.com/docs

通过Connect-ES,前端开发者可以专注于业务逻辑实现,而非重复的API通信细节。这种"一次定义,到处使用"的开发模式,正在成为现代API开发的新范式。

提示:关注仓库的"MIGRATING.md"文档,获取从v1版本迁移到v2版本的详细指南,以及最新功能更新日志。

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

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值