"imports": {
"@std/path": "jsr:@std/path@^1.0.6",
"@trpc/client": "npm:@trpc/client@^11.0.0-rc.593",
"@trpc/server": "npm:@trpc/server@^11.0.0-rc.593",
"zod": "npm:zod@^3.23.8"
}
}

設(shè)置 tRPC 服務(wù)器

構(gòu)建我們的 tRPC 應(yīng)用程序的第一步是設(shè)置服務(wù)器。我們將從初始化 tRPC 和創(chuàng)建基礎(chǔ)路由器和過(guò)程構(gòu)建器開(kāi)始。這些將是我們定義 API 端點(diǎn)的基礎(chǔ)。

創(chuàng)建一個(gè)?server/trpc.ts?文件:

// server/trpc.ts
import { initTRPC } from "@trpc/server";

/**
* Initialization of tRPC backend
* Should be done only once per backend!
*/
const t = initTRPC.create();

/**
* Export reusable router and procedure helpers
* that can be used throughout the router
*/
export const router = t.router;
export const publicProcedure = t.procedure;

這初始化了 tRPC 并導(dǎo)出了我們將用于定義 API 端點(diǎn)的路由器和過(guò)程構(gòu)建器。publicProcedure?允許我們創(chuàng)建不需要身份驗(yàn)證的端點(diǎn)。

接下來(lái),我們將創(chuàng)建一個(gè)簡(jiǎn)單的數(shù)據(jù)層來(lái)管理我們的恐龍數(shù)據(jù)。創(chuàng)建一個(gè)?server/db.ts?文件:

// server/db.ts
import { join } from "@std/path";

type Dino = { name: string; description: string };

const dataPath = join("data", "data.json");

async function readData(): Promise<Dino[]> {
const data = await Deno.readTextFile(dataPath);
return JSON.parse(data);
}

async function writeData(dinos: Dino[]): Promise<void> {
await Deno.writeTextFile(dataPath, JSON.stringify(dinos, null, 2));
}

export const db = {
dino: {
findMany: () => readData(),
findByName: async (name: string) => {
const dinos = await readData();
return dinos.find((dino) => dino.name === name);
},
create: async (data: { name: string; description: string }) => {
const dinos = await readData();
const newDino = { ...data };
dinos.push(newDino);
await writeData(dinos);
return newDino;
},
},
};

這創(chuàng)建了一個(gè)簡(jiǎn)單的基于文件的數(shù)據(jù)庫(kù),它將恐龍數(shù)據(jù)讀取和寫入到一個(gè) JSON 文件中。在生產(chǎn)環(huán)境中,你通常會(huì)使用一個(gè)合適的數(shù)據(jù)庫(kù),但這對(duì)我們的演示來(lái)說(shuō)已經(jīng)足夠了。

??? 在本教程中,我們硬編碼數(shù)據(jù)并使用基于文件的數(shù)據(jù)庫(kù)。然而,你可以連接到各種數(shù)據(jù)庫(kù)并使用 ORM,如 Drizzle 或 Prisma。

最后,我們需要提供實(shí)際的數(shù)據(jù)。讓我們創(chuàng)建一個(gè)?./data.json?文件,其中包含一些示例恐龍數(shù)據(jù):

[
{
"name": "Aardonyx",
"description": "An early stage in the evolution of sauropods."
},
{
"name": "Abelisaurus",
"description": "\"Abel's lizard\" has been reconstructed from a single skull."
},
{
"name": "Abrictosaurus",
"description": "An early relative of Heterodontosaurus."
},
{
"name": "Abrosaurus",
"description": "A close Asian relative of Camarasaurus."
},
...
]

現(xiàn)在,我們可以創(chuàng)建我們的主服務(wù)器文件,它定義了我們的 tRPC 路由器和過(guò)程。創(chuàng)建一個(gè)?server/index.ts?文件:

import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { z } from "zod";
import { db } from "./db.ts";
import { publicProcedure, router } from "./trpc.ts";

const appRouter = router({
dino: {
list: publicProcedure.query(async () => {
const dinos = await db.dino.findMany();
return dinos;
}),
byName: publicProcedure.input(z.string()).query(async (opts) => {
const { input } = opts;
const dino = await db.dino.findByName(input);
return dino;
}),
create: publicProcedure
.input(z.object({ name: z.string(), description: z.string() }))
.mutation(async (opts) => {
const { input } = opts;
const dino = await db.dino.create(input);
return dino;
}),
},
examples: {
iterable: publicProcedure.query(async function* () {
for (let i = 0; i < 3; i++) {
await new Promise((resolve) => setTimeout(resolve, 500));
yield i;
}
}),
},
});

// Export type router type signature, this is used by the client.
export type AppRouter = typeof appRouter;

const server = createHTTPServer({
router: appRouter,
});

server.listen(3000);

這設(shè)置了三個(gè)主要端點(diǎn):

服務(wù)器配置為監(jiān)聽(tīng) 3000 端口,并將處理所有 tRPC 請(qǐng)求。

雖然你現(xiàn)在可以運(yùn)行服務(wù)器,但你將無(wú)法訪問(wèn)任何路由并返回?cái)?shù)據(jù)。讓我們來(lái)解決這個(gè)問(wèn)題!

設(shè)置 tRPC 客戶端

我們的服務(wù)器準(zhǔn)備好了,我們可以創(chuàng)建一個(gè)客戶端,它以完全類型安全的方式使用我們的 API。創(chuàng)建一個(gè)?client/index.ts?文件:

// client/index.ts
/**
* This is the client-side code that uses the inferred types from the server
*/
import {
createTRPCClient,
splitLink,
unstable_httpBatchStreamLink,
unstable_httpSubscriptionLink,
} from "@trpc/client";
/**
* We only import the AppRouter type from the server - this is not available at runtime */ import type { AppRouter } from "../server/index.ts"; // Initialize the tRPC client const trpc = createTRPCClient<AppRouter>({ links: [ splitLink({ condition: (op) => op.type === "subscription", true: unstable_httpSubscriptionLink({ url: "http://localhost:3000", }), false: unstable_httpBatchStreamLink({ url: "http://localhost:3000", }), }), ], }); const dinos = await trpc.dino.list.query(); console.log("Dinos:", dinos); const createdDino = await trpc.dino.create.mutate({ name: "Denosaur", description: "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast.", }); console.log("Created dino:", createdDino); const dino = await trpc.dino.byName.query("Denosaur"); console.log("Denosaur:", dino); const iterable = await trpc.examples.iterable.query(); for await (const i of iterable) { console.log("Iterable:", i); }

這段客戶端代碼展示了 tRPC 的幾個(gè)關(guān)鍵特性:

  1. 從服務(wù)器路由器類型推斷。客戶端自動(dòng)從服務(wù)器通過(guò)?AppRouter?類型導(dǎo)入繼承所有類型定義。這意味著你將獲得完整的類型支持和編譯時(shí)類型檢查,適用于你所有的 API 調(diào)用。如果你在服務(wù)器上修改了一個(gè)過(guò)程,TypeScript 將立即標(biāo)記任何不兼容的客戶端使用。
  2. 進(jìn)行查詢和修改。示例演示了兩種類型的 API 調(diào)用:用于無(wú)副作用獲取數(shù)據(jù)的查詢(list 和 byName)和用于修改服務(wù)器端狀態(tài)的操作(create)??蛻舳俗詣?dòng)知道每個(gè)過(guò)程的輸入和輸出類型,在整個(gè)請(qǐng)求周期中提供類型安全性。
  3. 使用異步可迭代對(duì)象examples.iterable 演示了 tRPC 對(duì)使用異步可迭代對(duì)象流式傳輸數(shù)據(jù)的支持。這個(gè)特性特別適合實(shí)時(shí)更新或分塊處理大型數(shù)據(jù)集。

現(xiàn)在,讓我們啟動(dòng)服務(wù)器來(lái)看看它在行動(dòng)中的樣子。在我們的?deno.json?配置文件中,讓我們創(chuàng)建一個(gè)新屬性?tasks,包含以下命令:

{
"tasks": {
"start": "deno -A server/index.ts",
"client": "deno -A client/index.ts"
}
}

我們可以使用?deno task?列出可用的任務(wù):

deno task
Available tasks:
- start
deno -A server/index.ts
- client
deno -A client/index.ts

現(xiàn)在,我們可以使用?deno task start?啟動(dòng)服務(wù)器。之后,我們可以使用?deno task client?運(yùn)行客戶端。你應(yīng)該看到像這樣的輸出:

deno task client
Dinos: [
{
name: "Aardonyx",
description: "An early stage in the evolution of sauropods."
},
{
name: "Abelisaurus",
description: "Abel's lizard has been reconstructed from a single skull."
},
{
name: "Abrictosaurus",
description: "An early relative of Heterodontosaurus."
},
...
]
Created dino: {
name: "Denosaur",
description: "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast."
}
Denosaur: {
name: "Denosaur",
description: "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast."
}
Iterable: 0
Iterable: 1
Iterable: 2

成功!運(yùn)行?./client/index.ts?展示了如何創(chuàng)建一個(gè) tRPC 客戶端并使用其 JavaScript API 與數(shù)據(jù)庫(kù)交互。但是我們?nèi)绾螜z查 tRPC 客戶端是否正確地從數(shù)據(jù)庫(kù)推斷類型呢?讓我們?cè)?./client/index.ts?中修改下面的代碼片段,將?description?傳遞一個(gè)?number?而不是?string

// ...
const createdDino = await trpc.dino.create.mutate({
name: "Denosaur",
description:
- "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast.",
+ 100,
});
console.log("Created dino:", createdDino);
// ...

當(dāng)我們重新運(yùn)行客戶端:

deno task client
...
error: Uncaught (in promise) TRPCClientError: [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": [
"description"
],
"message": "Expected string, received number"
}
]
at Function.from (file:///Users/andyjiang/Library/Caches/deno/npm/registry.npmjs.org/@trpc/client/11.0.0-rc.608/dist/TRPCClientError.mjs:35:20)
at file:///Users/andyjiang/Library/Caches/deno/npm/registry.npmjs.org/@trpc/client/11.0.0-rc.608/dist/links/httpBatchStreamLink.mjs:118:56
at eventLoopTick (ext:core/01_core.js:175:7)

tRPC 成功拋出了一個(gè) invalid_type 錯(cuò)誤,因?yàn)樗谕粋€(gè) string 而不是 number。

接下來(lái)是什么?

現(xiàn)在你對(duì)如何使用 Deno 和 tRPC 有了基本的了解,你可以:

  1. 使用 Next.js 或 React 構(gòu)建實(shí)際的前端
  2. 使用 tRPC 中間件為你的 API 添加身份驗(yàn)證
  3. 使用 tRPC 訂閱實(shí)現(xiàn)實(shí)時(shí)功能
  4. 為更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)添加輸入驗(yàn)證
  5. 與 PostgreSQL 等合適的數(shù)據(jù)庫(kù)集成或使用 Drizzle 或 Prisma 等 ORM
  6. 將你的應(yīng)用程序部署到 Deno Deploy 或任何通過(guò) Docker 的公共云

?? 祝你在使用 Deno 和 tRPC 進(jìn)行類型安全編碼時(shí)愉快!參考資料[1]

源碼:?https://github.com/denoland/examples/tree/main/with-trpc

本文章轉(zhuǎn)載微信公眾號(hào)@前端與Deno

上一篇:

LLM之RAG實(shí)戰(zhàn)(二十一)| 使用LlamaIndex的Text2SQL和RAG的功能分析產(chǎn)品評(píng)論

下一篇:

如何使用 Web Scraper API 高效采集 Facebook 用戶帖子信息
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)