Laravel + Vue (Inertia) + gRPC:构建一个与 gRPC 用户服务通信的简单 BFF
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 服务器。
流程
- Vue 页面 (
UserShow.vue) 发送GET /api/users/{id}请求。 - Laravel API 路由 调用
App\Services\UserGrpc。 UserGrpc使用生成的 gRPC 客户端调用UserService.GetUser。- 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.yaml、buf.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 端点。 |
| 6 | Inertia/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 仍可能更简单。