Laravel + Vue (Inertia) + gRPC:构建一个与 gRPC 用户服务通信的简单 BFF

发布: (2026年1月4日 GMT+8 04:49)
8 min read
原文: Dev.to

I’m happy to translate the article for you, but I need the full text of the post. Could you please paste the content you’d like translated (excluding the source line you’ve already provided)? Once I have the article text, I’ll translate it into Simplified Chinese while preserving the formatting, markdown, and any code blocks or URLs.

为什么在 Laravel + Vue 项目中使用 gRPC?

如果你正在构建一个现代的 Laravel + Vue 应用,默认的直觉通常是 REST/JSON
gRPC 则不同:你在 .proto 文件中定义 API 合约,然后生成 强类型的客户端/服务器代码(存根)。它运行在 HTTP/2 上,并使用 Protocol Buffers 进行紧凑的序列化。

注意: 浏览器中的 Vue 应用不能直接使用 gRPC,除非额外搭建基础设施(gRPC‑Web + 如 Envoy 的代理)。
一个实用的模式是:

✅ Vue (浏览器) → Laravel (HTTP/JSON) → gRPC 微服务 (内部)

Laravel 成为你的 BFF(Backend‑For‑Frontend)。本仓库正是演示了这一点。

目标

在 Vue 页面上显示用户,但通过 Laravel 获取数据,Laravel 调用 gRPC 服务器。

流程

  1. Vue 页面 (UserShow.vue) 发送 GET /api/users/{id} 请求。
  2. Laravel API 路由 调用 App\Services\UserGrpc
  3. UserGrpc 使用生成的 gRPC 客户端调用 UserService.GetUser
  4. Node gRPC 服务器 返回用户数据(演示读取 Laravel 使用的同一个 SQLite 数据库)。

文件布局(重点)

proto/user/v1/user.proto          → contract
generated/App/Grpc/...           → generated PHP stubs (client classes)
grpc-server/                     → demo gRPC server (Node)
app/Services/UserGrpc.php         → Laravel gRPC client wrapper
resources/js/Pages/UserShow.vue  → Vue / Inertia page

对于全新项目,Laravel 的 Vue 入门套件(Inertia)是一个很好的基础。

1️⃣ 定义合约 (proto/user/v1/user.proto)

syntax = "proto3";

package user.v1;

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  string id   = 1;
  string name = 2;
}

.proto 文件是 唯一可信来源

2️⃣ 生成 PHP 客户端存根

buf 标准化了 Protobuf 工作流和生成配置(buf.yamlbuf.gen.yaml)。

npx buf generate

生成的文件位于 generated/App/Grpc/... 目录下。

提示: 许多团队 提交生成的代码(他们在 CI 中生成)。
提交这些代码对于演示和快速搭建是可以接受的。

重要: 生成 PHP 客户端存根并 不会 创建服务器。你仍然需要一个 gRPC 服务器实现(Go、Node、Java、PHP,……)。在此仓库中,演示服务器位于 grpc-server/

Source:

3️⃣ Demo gRPC 服务器(Node)

cd grpc-server
npm install

如果出现类似 Cannot find module 'dotenv'Cannot find module 'better-sqlite3' 的错误,请安装缺失的依赖:

npm install dotenv better-sqlite3 @grpc/grpc-js @grpc/proto-loader

grpc-server/server.js

require("dotenv").config();
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

const PROTO_PATH = __dirname + "/../proto/user/v1/user.proto";
const packageDef = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const userProto = grpc.loadPackageDefinition(packageDef).user.v1;

// Demo handler
function GetUser(call, callback) {
  const { id } = call.request;

  // Minimal demo response
  callback(null, { id, name: `User #${id}` });
}

function main() {
  const server = new grpc.Server();
  server.addService(userProto.UserService.service, { GetUser });

  const addr = process.env.GRPC_ADDR || "0.0.0.0:50051";
  server.bindAsync(
    addr,
    grpc.ServerCredentials.createInsecure(),
    () => {
      console.log(`gRPC server listening on ${addr}`);
      server.start();
    }
  );
}

main();

启动服务器:

node server.js

如果 Laravel 报告 Connection refused,说明 gRPC 服务器没有运行或没有在预期的地址/端口监听。

4️⃣ Laravel gRPC 客户端包装器 (app/Services/UserGrpc.php)

client = new UserServiceClient(
            env('USER_SVC_ADDR', '127.0.0.1:50051'),
            ['credentials' => ChannelCredentials::createInsecure()]
        );
    }

    public function getUser(string $id): array
    {
        $req = new GetUserRequest();
        $req->setId($id);

        /** @var \App\Grpc\User\V1\GetUserResponse $resp */
        [$resp, $status] = $this->client->GetUser($req)->wait();

        if ($status->code !== STATUS_OK) {
            throw new \RuntimeException($status->details, $status->code);
        }

        return [
            'id'   => $resp->getId(),
            'name' => $resp->getName(),
        ];
    }
}

常见问题:Grpc\ChannelCredentials 未找到

这通常意味着 gRPC PHP 扩展 没有在你运行的 PHP 环境中启用(CLI 与 FPM 可能使用不同的配置)。

php -m | grep grpc   # 应该会列出 "grpc"
php --ini            # 显示加载了哪个 php.ini

在正确的 php.ini 中启用该扩展:

extension=grpc.so

5️⃣ 暴露经典 JSON API 端点

routes/api.php

use Illuminate\Support\Facades\Route;
use App\Services\UserGrpc;

Route::get('/users/{id}', function (string $id, UserGrpc $userGrpc) {
    return response()->json($userGrpc->getUser($id));
});

routes/web.php(Inertia 页面路由)

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/users/{id}', function (string $id) {
    return Inertia::render('UserShow', [
        'id' => $id,
    ]);
});

Source:

6️⃣ Vue 页面 (resources/js/Pages/UserShow.vue)

一个符合 Inertia 规范的简洁实现使用 props(不需要 Vue Router)。

<script setup lang="ts">
import { computed, onMounted, ref } from "vue";

type User = { id: string; name: string };

const props = defineProps<{ id: string }>();
const id = computed(() => props.id);

const loading = ref(true);
const error = ref<string | null>(null);
const user = ref<User | null>(null);

onMounted(async () => {
  try {
    const resp = await fetch(`/api/users/${id.value}`);
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    user.value = await resp.json();
  } catch (e: any) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <div>
    <h2>User {{ id }}</h2>

    <div v-if="loading">Loading…</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <p><strong>ID:</strong> {{ user?.id }}</p>
      <p><strong>Name:</strong> {{ user?.name }}</p>
    </div>
  </div>
</template>

回顾

步骤你需要做的
1编写 .proto 合约。
2生成 PHP 客户端存根(buf generate)。
3运行 gRPC 服务器(Node 示例)。
4将生成的客户端包装在 Laravel 服务中(UserGrpc)。
5暴露调用该服务的 JSON API 端点。
6Inertia/Vue 页面获取该 JSON 端点。

通过此设置,你将获得:

  • 强类型 跨服务(感谢 Protobuf)。
  • 高效的二进制传输 在你的后端内部(HTTP/2 + protobuf)。
  • 干净的 BFF,为浏览器屏蔽 gRPC‑Web 的复杂性。

编码愉快! 🚀

附加 Vue 示例(替代实现)

<script setup lang="ts">
import { ref, onMounted } from "vue";

const id = ref(/* route param or similar */);
const user = ref<{ id: string; name: string }>({ id: "", name: "" });
const loading = ref(true);
const error = ref<string | null>(null);

onMounted(async () => {
  try {
    const res = await fetch(`/api/users/${encodeURIComponent(id.value)}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    user.value = await res.json();
  } catch (e: any) {
    error.value = e?.message ?? "Unknown error";
  } finally {
    loading.value = false;
  }
});
</script>

<template>
  <div>
    <h2>User</h2>

    <div v-if="loading">Loading…</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>
      <p><strong>ID:</strong> {{ user.id }}</p>
      <p><strong>Name:</strong> {{ user.name }}</p>
    </div>
  </div>
</template>

运行服务

终端 1 — gRPC 服务器

cd grpc-server
node server.js

终端 2 — Laravel + Vite

composer install
cp .env.example .env
php artisan key:generate

npm install
npm run dev

php artisan serve

何时使用此设置

  • 您希望在内部服务之间拥有类型化的契约。
  • 您拥有使用不同语言的多个后端。
  • 您需要一个在团队之间共享的稳定接口(proto)。
  • 您希望获得性能和流式特性(gRPC 支持流式)。

如果您的应用是一个小型单体,且唯一关注的是面向公众的 API,REST 仍可能更简单。

Back to Blog

相关文章

阅读更多 »

使用 DataBlock 探索真实世界 API

比较 Symfony 和 Laravel 使用 GitHub 与 Packagist 数据 在第一篇文章《Handling Nested PHP Arrays Using DataBlock》中,我们探索了 DataBlock 与一个 s...