使用 Rust Tonic 和 React 构建和部署 gRPC-Web 应用程序
gRPC 是一种现代的高性能远程过程调用(RPC)框架,可在任何环境中运行。gRPC 基于 protobufs 构建,可扩展且高效,并在多种语言和运行时中提供广泛支持。
在本教程中,我们将介绍如何将一个基于 Rust 的 gRPC API 支持的 React 应用程序部署到 Koyeb。演示应用程序是一个电影数据库网站,其功能是展示一系列电影及其关联的元数据。您可以在此处找到两个应用程序组件的源代码:
准备条件
为了按照本教程进行操作,您需要以下内容:
- 一个用于部署 Rust 和 React 服务的 Koyeb 帐户。Koyeb的免费套餐允许您每月免费运行两个服务。
- 安装在本地计算机上的 protobuf 编译器。我们将使用它从我们的数据格式生成特定于语言的 subs。
- Rust 和
cargo
安装在本地计算机上以创建 gRPC API 服务。 - Node.js 和
npm
安装在本地计算机上以创建基于 React 的前端。
满足要求后,请继续开始。
创建 Rust API
我们将首先为后端创建 Rust API 和 protobuf 文件,该文件定义了我们两个服务将用于通信的数据格式。
创建一个新的 Rust 项目并安装依赖项
首先定义一个新的 Rust 项目。
使用该 cargo
命令生成使用预期的包文件初始化的新项目目录:
cargo new movies-back
接下来,cd 到新的项目目录并安装项目的依赖项:
cd movies-back
cargo add tonic@0.9.2 tonic-web@0.9.2
cargo add prost@0.11 prost-types@0.11
cargo add --build tonic-build@0.8
cargo add tonic-health@0.9.2
cargo add tower-http@0.2.3
cargo add --features tokio@1.0/macros,tokio@1.0/rt-multi-thread tokio@1.0
默认情况下,Web 浏览器不支持 gRPC,但我们将使用 gRPC-web 来实现它。
定义数据格式
接下来,通过创建 protobuf 定义文件来创建数据格式。
创建一个名为 proto
的新目录:
mkdir proto
在里面,创建一个以以下内容命名的新 proto/movie.proto
文件:
syntax = "proto3";
package movie;
message MovieItem {
int32 id = 1;
string title = 2;
int32 year = 3;
string genre = 4;
string rating = 5;
string starRating = 6;
string runtime = 7;
string cast = 8;
string image = 9;
}
message MovieRequest {
}
message MovieResponse {
repeated MovieItem movies = 1;
}
service Movie {
rpc GetMovies (MovieRequest) returns (MovieResponse);
}
该文件 proto/movie.proto
使用 protobuf 格式定义我们的数据格式。它指定了一个数据结构来保存有关电影的所有数据,并概述了对该数据的请求和响应的外观。此定义将用于定义我们服务之间的 API。
创建后端服务
现在我们有了数据格式,我们可以创建后端服务。
首先配置 Rust 构建过程来编译 protobuf 文件。创建一个包含以下内容的调用 build.rs
文件:
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("./proto/movie.proto")?;
Ok(())
}
现在,我们可以使用 GetMovies
端点实现实际的 API 服务器。打开 src/main.rs
文件并将内容替换为以下内容:
use std::env;
use tonic::{transport::Server, Request, Response, Status};
pub mod grpc_movie {
tonic::include_proto!("movie");
}
use grpc_movie::movie_server::{Movie, MovieServer};
use grpc_movie::{MovieRequest, MovieResponse};
#[derive(Debug, Default)]
pub struct MovieService {}
#[tonic::async_trait]
impl Movie for MovieService {
async fn get_movies(
&self,
request: Request<MovieRequest>,
) -> Result<Response<MovieResponse>, Status> {
println!("Got a request: {:?}", request);
let mut movies = Vec::new();
movies.push(grpc_movie::MovieItem {
id: 1,
title: "Matrix".to_string(),
year: 1999,
genre: "Sci-Fi".to_string(),
rating: "8.7".to_string(),
star_rating: "4.8".to_string(),
runtime: "136".to_string(),
cast: "Keanu Reeves, Laurence Fishburne".to_string(),
image: "http://image.tmdb.org/t/p/w500//aOIuZAjPaRIE6CMzbazvcHuHXDc.jpg".to_string(),
});
movies.push(grpc_movie::MovieItem {
id: 2,
title: "Spider-Man: Across the Spider-Verse".to_string(),
year: 2023,
genre: "Animation".to_string(),
rating: "9.7".to_string(),
star_rating: "4.9".to_string(),
runtime: "136".to_string(),
cast: "Donald Glover".to_string(),
image: "http://image.tmdb.org/t/p/w500//8Vt6mWEReuy4Of61Lnj5Xj704m8.jpg".to_string(),
});
movies.push(grpc_movie::MovieItem {
id: 3,
title: "Her".to_string(),
year: 2013,
genre: "Drama".to_string(),
rating: "8.7".to_string(),
star_rating: "4.1".to_string(),
runtime: "136".to_string(),
cast: "Joaquin Phoenix".to_string(),
image: "http://image.tmdb.org/t/p/w500//eCOtqtfvn7mxGl6nfmq4b1exJRc.jpg".to_string(),
});
let reply = grpc_movie::MovieResponse { movies: movies };
Ok(Response::new(reply))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let port = env::var("PORT").unwrap_or("50051".to_string());
let addr = format!("0.0.0.0:{}", port).parse()?;
let movie = MovieService::default();
let movie = MovieServer::new(movie);
let movie = tonic_web::enable(movie);
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
health_reporter
.set_serving::<MovieServer<MovieService>>()
.await;
println!("Running on port {}...", port);
Server::builder()
.accept_http1(true)
.add_service(health_service)
.add_service(movie)
.serve(addr)
.await?;
Ok(())
}
该应用程序使用 tonic gRPC 实现,根据 protobuf 文件定义的结构和接口为我们的后端构建和提供 API。在实际场景中,这通常由包含电影数据的数据库支持,但为了简化实现,我们只包含一些内联电影的数据。
默认情况下,该服务将在端口 50051 上运行(可使用 PORT
环境变量进行修改),并将响应使用 protobuf 文件定义的响应对象的电影请求。
测试 API 后端
API 后端现已完成,因此我们可以通过键入以下内容在本地启动服务器以测试其功能:
cargo run
API 服务器将构建并开始在端口 50051 上运行。可以使用所选的 gRPC 客户端(如 grpcurl
gRPC 客户端)或 postman 来测试功能。
例如,您可以通过在运行 Rust 服务的项目根目录中键入以下内容 grpcurl
来请求电影列表:
grpcurl -plaintext -import-path proto -proto movie.proto 127.0.0.1:50051 movie.Movie/GetMovies
该服务应按预期返回电影列表:
{
"movies": [
{
"id": 1,
"title": "Matrix",
"year": 1999,
"genre": "Sci-Fi",
"rating": "8.7",
"starRating": "4.8",
"runtime": "136",
"cast": "Keanu Reeves, Laurence Fishburne",
"image": "http://image.tmdb.org/t/p/w500//aOIuZAjPaRIE6CMzbazvcHuHXDc.jpg"
},
{
"id": 2,
"title": "Spider-Man: Across the Spider-Verse",
"year": 2023,
"genre": "Animation",
"rating": "9.7",
"starRating": "4.9",
"runtime": "136",
"cast": "Donald Glover",
"image": "http://image.tmdb.org/t/p/w500//8Vt6mWEReuy4Of61Lnj5Xj704m8.jpg"
},
{
"id": 3,
"title": "Her",
"year": 2013,
"genre": "Drama",
"rating": "8.7",
"starRating": "4.1",
"runtime": "136",
"cast": "Joaquin Phoenix",
"image": "http://image.tmdb.org/t/p/w500//eCOtqtfvn7mxGl6nfmq4b1exJRc.jpg"
}
]
}
创建 Dockerfile
当我们将后端部署到 Koyeb 时,应用程序将在容器中运行。我们可以为我们的项目定义一个 Dockerfile
来描述项目的代码应该如何打包和运行。
在项目的根目录中,创建一个包含 Dockerfile
以下内容的新文件:
FROM rust:1.64.0-buster as builder
# install protobuf
RUN apt-get update && apt-get install -y protobuf-compiler libprotobuf-dev
COPY Cargo.toml build.rs /usr/src/app/
COPY src /usr/src/app/src/
COPY proto /usr/src/app/proto/
WORKDIR /usr/src/app
RUN rustup target add x86_64-unknown-linux-musl
RUN cargo build --target x86_64-unknown-linux-musl --release --bin movies-back
FROM gcr.io/distroless/static-debian11 as runner
# get binary
COPY --from=builder /usr/src/app/target/x86_64-unknown-linux-musl/release/movies-back /
# set run env
EXPOSE 50051
# run it
CMD ["/movies-back"]
完成后,将 API 的所有文件添加到新的 GitHub 存储库,以便我们稍后可以将其部署到生产环境。
现在,我们已准备好创建将使用 API 的 React 应用程序。
创建 React 应用程序
现在后端已经完成,我们可以开始为我们的应用程序开发 React 前端服务了。
生成一个新的 React 项目
创建基本 react 应用程序的最快方法是使用 create-react-app
.检查以确保您不在 Rust 服务的目录中,然后键入:
npx create-react-app movies-front
这将为您的前端创建一个新的项目目录,并生成一些基本文件来帮助您入门。
移动到新目录并启动服务以验证是否正确安装了所有内容:
cd movies-front
npm run start
开发服务器将在端口 3000 上打开,React 将尝试打开一个新的浏览器窗口来查看它。如果您没有被自动定向到那里,您可以访问 localhost:3000
。默认的 React 开发页面应该出现:
完成后按 Ctrl - c 停止开发服务器。
配置 Tailwind CSS 框架
我们的 React 应用程序将在页面上显示电影列表。为了加快样式设置过程,我们将使用 Tailwind CSS。安装必要的软件包并通过键入以下内容初始化 Tailwind:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
现在 Tailwind 已经安装完毕,我们需要配置 React 应用程序来支持它。
首先,打开 tailwind.config.js
文件。修改 content
属性以选取所有预期的 CSS 内容:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
}
我们将直接使用 Sass 而不是 CSS,因此接下来我们需要安装 sass
软件包:
npm install sass
删除 src/App.css
该文件并将其替换为 src/App.scss
.在里面,我们导入了我们需要的所有顺风代码:
@tailwind base;
@tailwind components;
@tailwind utilities;
我们可以通过更改主 React 应用程序文件来测试 Tailwind。更改 src/App.js
文件的内容,如下所示:
import './App.scss';
import Movie from './Movie';
function App() {
// for now we will
let movies = [
{
id: 1,
title: 'The spiderman across the spider verse',
year: 2023,
genre: 'animation',
rating: 'L',
starRating: '4.9',
runtime: '2h 22min',
cast: 'Donald Glover',
image: 'http://image.tmdb.org/t/p/w500//8Vt6mWEReuy4Of61Lnj5Xj704m8.jpg'
}
]
return (
<div className="App">
{movies.map((movie) => (
<Movie key={movie.id} details={movie} />
))}
</div>
);
}
export default App;
在这里,我们创建一个应用程序,该应用程序将在请求页面时为我们的电影列表提供服务。此时,我们只需模拟单个电影,以便测试我们的 CSS 样式。
创建一个 src/Movie.js
文件来定义影片的样式和显示方式:
export default function Movie(props) {
let movie = props.details
return (
<article className="flex items-start space-x-6 p-6">
<img src={movie.image} alt="" width="60" height="88" className="flex-none rounded-md bg-slate-100" />
<div className="min-w-0 relative flex-auto">
<h2 className="font-semibold text-slate-900 truncate pr-20">{movie.title}</h2>
<dl className="mt-2 flex flex-wrap text-sm leading-6 font-medium">
<div className="absolute top-0 right-0 flex items-center space-x-1">
<dt className="text-sky-500">
<span className="sr-only">Star rating</span>
<svg width="16" height="20" fill="currentColor">
<path d="M7.05 3.691c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.372 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.363-1.118L.98 9.483c-.784-.57-.381-1.81.587-1.81H5.03a1 1 0 00.95-.69L7.05 3.69z" />
</svg>
</dt>
<dd>{movie.starRating}</dd>
</div>
<div>
<dt className="sr-only">Rating</dt>
<dd className="px-1.5 ring-1 ring-slate-200 rounded">{movie.rating}</dd>
</div>
<div className="ml-2">
<dt className="sr-only">Year</dt>
<dd>{movie.year}</dd>
</div>
<div>
<dt className="sr-only">Genre</dt>
<dd className="flex items-center">
<svg width="2" height="2" fill="currentColor" className="mx-2 text-slate-300" aria-hidden="true">
<circle cx="1" cy="1" r="1" />
</svg>
{movie.genre}
</dd>
</div>
<div>
<dt className="sr-only">Runtime</dt>
<dd className="flex items-center">
<svg width="2" height="2" fill="currentColor" className="mx-2 text-slate-300" aria-hidden="true">
<circle cx="1" cy="1" r="1" />
</svg>
{movie.runtime}
</dd>
</div>
<div className="flex-none w-full mt-2 font-normal">
<dt className="sr-only">Cast</dt>
<dd className="text-slate-400">{movie.cast}</dd>
</div>
</dl>
</div>
</article>
)
}
如果再次启动开发服务器,您应该能够看到模拟的电影:
npm run start
从 API 获取电影列表
接下来,我们将修改应用程序以从 gRPC API 提取数据,而不是显示硬编码的电影。
首先,为 React 安装 protobuf 和 gRPC Web 支持:
npm install google-protobuf@~3.21.2 grpc-web@~1.4.1
接下来,我们需要基于我们为后端 Rust 应用程序定义的相同 protobuf 文件在 JavaScript 中生成电影实体。为此,我们将使用先决条件中的 protobuf 安装中包含的 protoc
命令。
假设 和 movies-back
movies-front
彼此相邻,您可以通过在 movies-front
目录中运行以下命令来生成具有 protobuf 定义的相应 JavaScript 文件:
# change the proto_path to the correct place where movie.proto is.
protoc --proto_path=../movies-back/proto/ movie.proto --grpc-web_out=import_style=commonjs,mode=grpcweb:src --js_out=import_style=commonjs,binary:src
这将从您的 protobuf 文件生成适当的 JavaScript 存根,以便 React 应用程序了解如何与 API 通信。
现在我们可以将 src/App.js
文件更改为使用 gRPC 而不是提供硬编码电影:
import './App.scss';
import Movie from './Movie';
import { useState, useEffect} from 'react';
const proto = {};
proto.movie = require('./movie_grpc_web_pb.js');
function App() {
let url = process.env.REACT_APP_BACKEND_URL;
if(url == null){
url = "http://localhost:50051"
}
const client = new proto.movie.MovieClient(url, null, null);
let [movies, setMovies] = useState([])
useEffect(() => {
const req = new proto.movie.MovieRequest();
client.getMovies(req, {}, (err, response) => {
if(response == null){
return
}
if (response.getMoviesList().length === 0) {
return
}
let m = []
response.getMoviesList().forEach((movie) => {
console.log(movie.toObject())
m.push(movie.toObject())
})
setMovies(m)
})
}, [])
return (
<div className="App">
{movies.map((movie) => (
<Movie key={movie.id} details={movie} />
))}
</div>
);
}
export default App;
再次启动开发服务器以测试新更改:
npm run start
这一次,您应该会看到从 API 后端提供的完整电影列表:
确认应用程序按预期工作后,将 React 应用程序的文件添加到新的 GitHub 存储库并推送它们。我们将在下一阶段将应用程序部署到生产环境。
将应用程序部署到科耶布
现在我们的前端和后端按预期工作,我们可以将这两个服务部署到 Koyeb。
部署 API 服务
首先,我们将 Rust 应用程序部署到 Koyeb。
在 Koyeb 控制面板中,单击“创建应用程序”按钮以开始使用。选择 GitHub 作为部署方法,然后从存储库列表中选择后端 API 的存储库。或者,可以使用此服务的示例存储库,其中包含我们通过放入 https://github.com/filhodanuvem/movies-rust-grpc
公共 GitHub 存储库字段来讨论的相同代码。
在下一页上,选择 Dockerfile 作为构建器:
单击高级以展开其他设置。将 PORT
环境变量的值更改为 50051。在“公开服务”部分中,将端口也修改为 50051。从协议下拉列表中选择 HTTP/2
,并将路径设置为 /api
。
完成后,单击“部署”按钮以部署 API 后端。
在 API 的服务页面上,复制“公共 URL”的值。在配置 React 应用程序时,我们将需要此值。
部署 React 应用程序
现在 API 正在运行,我们可以部署 React 应用程序。与 API 不同,我们将为这个项目使用 buildpack 构建器,而不是从 Dockerfile 构建。
在 Koyeb 控制面板中,单击包含您刚刚部署的 API 服务的应用程序。在服务的索引页面中,单击“创建服务”按钮以在同一 Koyeb 应用程序的上下文中部署其他服务。
在以下屏幕上,选择 GitHub 作为部署方法,然后从存储库列表中单击前端 React 应用程序的存储库。或者,可以使用此服务的示例存储库,其中包含我们通过放入 https://github.com/filhodanuvem/movies-react
公共 GitHub 存储库字段来讨论的相同代码。
在下一页上,选择“构建包”作为构建器:
单击高级以展开其他设置。单击添加变量按钮并创建一个名为 REACT_APP_BACKEND_URL
的新变量。使用您复制的 API 服务的公有 URL 作为值。它应如下所示:
REACT_APP_BACKEND_URL=https://<YOUR_APP_NAME>-<KOYEB_ORG_NAME>.koyeb.app/api
完成后,单击“部署”按钮开始生成和部署应用程序。部署完成后,使用 React 应用程序的公共 URL 访问电影数据库站点。
结论
在本指南中,我们创建并部署了一个由 Rust 后端和 React 前端组成的端到端应用程序。这两个服务使用 gRPC 进行通信。我们将这两项服务部署到 Koyeb 以利用其本机 gRPC 支持。这些服务能够安全地通信,以满足用户对电影数据库站点的请求。