该项目会生成从 Rust 后端到 TypeScript 和 Dart 客户端的 API 服务。
问题描述:
在跨 API 的团队中工作时,客户端和服务之间的对象、类型和类保持准确非常重要。
我从 Rest API 调用中发现的类型不正确、属性缺失等错误数量令人震惊。
我们研究了 GraphQL、tRPC、Rust Type Convert、gRPC 等多种技术,以确定哪种技术适合我们的需求。不幸的是,他们缺少自动客户端转换的一些重要功能,例如缺少错误类型、缺少返回类型、一切都是可选的(GraphQL ..)
在尝试了许多不同的 API 后,我们偶然发现了 Rust 的 Salvo。
使用 Salvo,您可以自动生成整个 Swagger 架构,其中包括返回类型、错误枚举等。
您无需执行任何手动工作,例如手动指定 OpenAPI 属性。它将结构转换为规格。
设置项目:
首先,在终端内创建一个项目目录
mkdir ./my-auto-gen-project cd my-auto-gen-project
|
创建我们的 Rust 项目
cargo new my_api cd my_api // Open in VSC code (optional) code .
|
将以下依赖内容添加到Cargo.toml
[dependencies] salvo = { version = "*", features = ["oapi", "cors"] } tokio = { version = "1", features = ["macros"] } tracing = "0.1.40" tracing-subscriber = "0.3"’
|
编码
打开src/main.rs并添加以下内容,这将设置我们的 API 和 UI
use salvo::cors::Cors;
use salvo::hyper::Method;
use salvo::oapi::extract::*;
use salvo::prelude::*;
#[endpoint]
async fn hello(name: QueryParam<String, false>) -> String {
format!("Hello, {}!", name.as_deref().unwrap_or("World"))
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().init();
let router = Router::new().push(Router::with_path("hello").get(hello));
let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
// Allow requests from origin
let cors = Cors::new()
.allow_origin("*")
.allow_methods(vec![Method::GET, Method::POST, Method::DELETE])
.into_handler();
let router = router
.push(doc.into_router("/api-doc/openapi.json"))
.push(SwaggerUi::new("/api-doc/openapi.json").into_router("ui"))
.hoop(cors);
let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
Server::new(acceptor).serve(router).await;
}
|
运行
现在在编辑器中打开终端并写入
cargo run
您应该能够访问http://127.0.0.1:5800/ui/
您将看到 Swagger UI
感谢 Salvo,我们的端点是自动生成的
正如您所看到的,我们的 API、文档和 Playground 完全是由上面一小段代码生成的,这真是太神奇了。
我们将使用它作为 TypeScript 和 Dart (Flutter) 项目的基础来自动生成我们的 API。
前端TypeScript客户端
现在我们已经完成了基本的 API 设置和 swagger 文档,接下来让我们从 TypeScript 开始生成前端客户端。
对于虚拟框架,我们将使用 SvelteKit,因为我们认为这是最短的工作量。
使用哪种框架(React、Vue 等)并不重要,它们都会起作用,因为我们正在生成可在任何 TypeScript 项目上使用的 API 服务(SSR 可能有一些修改)
生成 Svelte-kit 项目
// Svelte project
npm create svelte@latest web-frontend
(select SvelteKit demo app, Typescript, ESLint)
cd web-frontend
npm install
// Optional, we're opening in our editor VSC
code .
// Check that the project runs
npm run dev
//打开浏览器,您应该会看到一个 SvelteKit 示例项目。 http://localhost:5173/
|
生成有趣的 API
打开Svelte 项目中的package.json 。我们将添加以下“script”行,并将其命名为“gen-api”
// 您可以定义不同类型的请求者,例如 typescript-[client|axios|fetch] npx @openapitools/openapi-generator-cli generate -i http://127.0.0.1:5800/api-doc/openapi.json -g typescript-fetch -o ./src/api/
|
还有其他选项,例如基本路径等。它应该如下所示:
"scripts": {
"dev": "vite dev",
"build": "vite build",
"gen-api": "npx @openapitools/openapi-generator-cli generate -i http://127.0.0.1:5800/api-doc/openapi.json -g typescript-fetch -o ./src/api/",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
|
现在,如果我们想生成 API,我们只需运行
npm run gen-api
您应该会在 SRC 下看到生成的 api目录
让我们将 API 添加到页面并进行测试
转到src/routes/+page.svelte
将页面代码更改为如下所示:
<script>
import { Configuration, DefaultApi } from "../api";
import { onMount } from "svelte";
let test = "this is a test";
onMount(async () => {
console.log("onMount");
const config = new Configuration({
basePath: "http://localhost:5800",
});
const api = new DefaultApi(config);
test = await api.myApiHello({ name: "Olly" });
});
</script>
<svelte:head>
<title>Home</title>
<meta name="description" content="Svelte demo app" />
</svelte:head>
<section>
<h1>
{test}
</h1>
</section>
<style>
section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0.6;
}
h1 {
width: 100%;
}
</style>
|
浏览器打开http://localhost:5173
这里我们使用自动生成的 TypeScript API。它甚至带有自动完成、参数、错误类型和输入/输出。
API 100% 准确,不再需要手动编写服务、轻松更新等等。现在是 Flutter 应用程序和 Dart 代码
这也适用于外部服务的 Dart 后端。
我们尝试了一些 Dart 自动生成工具,但不幸的是,其中很多都被破坏或遗漏了 API 的关键部分(如返回类型!)。
最后,我们使用了一个名为 "swagger_dart_code_generator "的工具。
它似乎能完全工作,也就是说,它能生成类型、枚举、复杂对象等。实际上,官方的 swagger cli 在复杂对象方面是有问题的,所以我们不得不使用这个
再次进入项目根目录,生成 flutter 应用程序
flutter create mobile_app cd mobile_app // Optional opening the project code .
|
打开 pubspec.yaml 文件
确保它看起来像这样
name: mobile_app description: "My auto-gen project"
# pub.dev using 'flutter pub publish'. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment: sdk: '>=3.2.3 <4.0.0'
dependencies: flutter: sdk: flutter chopper: ^7.0.10 json_annotation: ^4.8.0 swagger_dart_code_generator: ^2.14.2
cupertino_icons: ^1.0.2
dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.3.3 chopper_generator: ^7.0.7 json_serializable: ^6.6.1 flutter_lints: ^3.0.1
flutter: uses-material-design: true
|
在移动项目根目录下创建 build.yaml,确保如下所示
targets: $default: sources: - lib/** - open_api/** builders: swagger_dart_code_generator: # https://pub.dev/packages/swagger_dart_code_generator options: input_folder: "open_api/" output_folder: "lib/generated_api/" add_base_path_to_requests: true input_urls: - url: "http://127.0.0.1:5800/api-doc/openapi.json"
|
它的作用是连接到 swagger 规范,并生成 dart 代码。
在应用程序项目根目录下创建一个 "open_api "文件夹
现在在终端运行
dart run build_runner build
发生两件事
- 1.应该已经下载了 openapi.json 规范
- 2.应已从中创建了生成的 API。
设置完成后,您就可以像使用 TypeScript 项目一样使用 API 了。转到 main.dart
我们将修改默认的计数器程序。当你按下 + 按钮时,它会发出请求并填写文本。
import 'package:flutter/material.dart'; import 'package:mobile_app/generated_api/client_index.dart';
void main() { runApp(const MyApp()); }
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } }
class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title});
final String title;
@override State<MyHomePage> createState() => _MyHomePageState(); }
class _MyHomePageState extends State<MyHomePage> { String _text = "";
void _incrementCounter() { _test(); }
@override void initState() { super.initState(); }
_test() async { print("test");
final Openapi api = Openapi.create( baseUrl: Uri.parse("http://localhost:5800"), );
try { final data1 = await api.helloGet(name: "Olly"); setState(() { _text = data1.body!; // << Is string, or can be complex object. }); } catch (e) { print(e); } }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(_text), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
|
点击 "加号 "按钮,它就会跳转到 API,然后再返回。你甚至可以在这里使用复杂类型。Rust API 还将支持复杂类型返回、验证等功能。我们的完整版还支持错误枚举和状态代码字符串,这样开发人员就能清楚地知道该如何处理。Github 演示项目链接