TRPC
快来分享你的内容吧~
- 2023-06-04·学如逆水行舟,不进则退。
tRPC | 你可能不再需要传统的 API!
<html> <head></head> <body> <div class="content ql-editor"> <p>本文是 b 站视频的简化文档,并非视频脚本,最好配合视频使用,视频地址在:</p> <p><br></p> <p><a href="https://www.bilibili.com/video/BV17z4y1z7xa/" target="_blank">tRPC 是什么?与 REST 和 GraphQL 有啥关系 | 你可能不再需要传统的 API!</a></p> <p><br></p> <p>掌握 tRPC,也能让纯前端迈向全栈之路!</p> <p><br></p> <p><br></p> <h2>什么是 tRPC</h2> <p><br></p> <p><br></p> <p>tRPC 是一个基于 TypeScript 的 RPC 框架,它的目标是让前后端的开发者可以像调用本地函数一样调用远程函数,而不用关心底层的网络通信细节。</p> <p><br></p> <p>tRPC 的目标是让前后端的开发者可以像调用本地函数一样调用远程函数,而不用关心底层的网络通信细节。</p> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1803709410353942529/nv34lpt7.jpeg"></p> <p><br></p> <p>相关特点从 <a href="https://trpc.io/" target="_blank">tRPC 官网</a>可以直接看到,这里就不搬运官网内容了。</p> <p><br></p> <p><br></p> <h2>本文内容</h2> <p><br></p> <p><br></p> <p>本文会带你实现一个极简的 tRPC 项目,实现 examples-minimal 。</p> <p><br></p> <p>相关源码如下:</p> <p><br></p> <p><a href="https://github.com/chonext/trpc-practice/tree/main/examples-minimal" target="_blank">https://github.com/chonext/trpc-practice/tree/main/examples-minimal</a></p> <p><br></p> <p>tRPC 分为几个包,所以你可以只安装你需要的,确保在代码库的适当部分安装所需的包。</p> <p><br></p> <p>首先安装 @trpc/server 和 @trpc/client 包,这两个包分别使用于服务端和客户端。</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install @trpc/server @trpc/client </div> </div> <p><br></p> <p><br></p> <p><br></p> <h2>编写后端逻辑</h2> <p><br></p> <p><br></p> <p>我们不打算把项目弄复杂了,因此对于后端数据库而言,我们使用虚拟的,用一个数组即可,如下代码所示:</p> <p><br></p> <p>server/db.ts</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">type</span> <span class="ql-token hljs-title">User</span> = { <span class="ql-token hljs-attr">id</span>: <span class="ql-token hljs-built_in">string</span>; <span class="ql-token hljs-attr">name</span>: <span class="ql-token hljs-built_in">string</span>; <span class="ql-token hljs-attr">email</span>: <span class="ql-token hljs-built_in">string</span>; <span class="ql-token hljs-attr">password</span>: <span class="ql-token hljs-built_in">string</span> }; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-comment">// Imaginary database</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> <span class="ql-token hljs-attr">users</span>: <span class="ql-token hljs-title">User</span>[] = []; </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">export</span> <span class="ql-token hljs-keyword">const</span> db = { </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">user</span>: { </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">findMany</span>: <span class="ql-token hljs-keyword">async</span> () => users, </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">findById</span>: <span class="ql-token hljs-keyword">async</span> (<span class="ql-token hljs-attr">id</span>: <span class="ql-token hljs-built_in">string</span>) => users.<span class="ql-token hljs-title">find</span>(<span class="ql-token hljs-function">(user) =></span> user.<span class="ql-token hljs-property">id</span> === id), </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">create</span>: <span class="ql-token hljs-keyword">async</span> (<span class="ql-token hljs-attr">data</span>: { <span class="ql-token hljs-attr">name</span>: <span class="ql-token hljs-built_in">string</span>; <span class="ql-token hljs-attr">email</span>: <span class="ql-token hljs-built_in">string</span>; <span class="ql-token hljs-attr">password</span>: <span class="ql-token hljs-built_in">string</span> }) => { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> user = { <span class="ql-token hljs-attr">id</span>: <span class="ql-token hljs-title">String</span>(users.<span class="ql-token hljs-property">length</span> + <span class="ql-token hljs-number">1</span>), ...data }; </div> <div class="ql-code-block"> users.<span class="ql-token hljs-title">push</span>(user); </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> user; </div> <div class="ql-code-block"> }, </div> <div class="ql-code-block"> }, </div> <div class="ql-code-block"> }; </div> </div> <p><br></p> <p>在模拟的 db 对象中有一些数据的操作方法,如 findById 和 create,这些方法会在后面的代码中用到,也如字面意思一样,findById 用于根据 id 查找用户,create 用于创建用户。</p> <p><br></p> <p>接下来创建 server/trpc.ts 文件,如下代码所示:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { initTRPC } <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"@trpc/server"</span>; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-comment">/*Initialization of tRPC backend</span> </div> <div class="ql-code-block"><span class="ql-token hljs-comment"> *Should be done only once per backend!</span> </div> <div class="ql-code-block"><span class="ql-token hljs-comment"> */</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> t = initTRPC.<span class="ql-token hljs-title">create</span>(); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-comment">/*Export reusable router and procedure helpers</span> </div> <div class="ql-code-block"><span class="ql-token hljs-comment"> *that can be used throughout the router</span> </div> <div class="ql-code-block"><span class="ql-token hljs-comment"> */</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">export</span> <span class="ql-token hljs-keyword">const</span> router = t.<span class="ql-token hljs-property">router</span>; </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">export</span> <span class="ql-token hljs-keyword">const</span> publicProcedure = t.<span class="ql-token hljs-property">procedure</span>; </div> </div> <p><br></p> <p><br></p> <p>在 server/trpc.ts 文件中,我们初始化了 tRPC 后端,然后导出了 router 和 publicProcedure 两个变量,这两个变量会在后面的代码中用到。</p> <p><br></p> <p>接着,有了前面铺垫,就是编写后端逻辑了,我们在 server/index.ts 文件中编写后端逻辑,如下代码所示:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { db } <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"./db"</span>; </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { publicProcedure, router } <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"./trpc"</span>; </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { createHTTPServer } <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"@trpc/server/adapters/standalone"</span>; </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> z <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"zod"</span>; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> appRouter = <span class="ql-token hljs-title">router</span>({ </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">userList</span>: publicProcedure.<span class="ql-token hljs-title">query</span>(<span class="ql-token hljs-keyword">async</span> () => { </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">// Retrieve users from a datasource, this is an imaginary database</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> users = <span class="ql-token hljs-keyword">await</span> db.<span class="ql-token hljs-property">user</span>.<span class="ql-token hljs-title">findMany</span>(); </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> users; </div> <div class="ql-code-block"> }), </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">userById</span>: publicProcedure </div> <div class="ql-code-block"> .<span class="ql-token hljs-title">input</span>(z.<span class="ql-token hljs-title">object</span>({ <span class="ql-token hljs-attr">id</span>: z.<span class="ql-token hljs-title">string</span>() })) </div> <div class="ql-code-block"> .<span class="ql-token hljs-title">query</span>(<span class="ql-token hljs-keyword">async</span> ({ input }) => { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> { id } = input; </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> user = <span class="ql-token hljs-keyword">await</span> db.<span class="ql-token hljs-property">user</span>.<span class="ql-token hljs-title">findById</span>(id); </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> user; </div> <div class="ql-code-block"> }), </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">userCreate</span>: publicProcedure </div> <div class="ql-code-block"> .<span class="ql-token hljs-title">input</span>( </div> <div class="ql-code-block"> z.<span class="ql-token hljs-title">object</span>({ <span class="ql-token hljs-attr">name</span>: z.<span class="ql-token hljs-title">string</span>(), <span class="ql-token hljs-attr">email</span>: z.<span class="ql-token hljs-title">string</span>(), <span class="ql-token hljs-attr">password</span>: z.<span class="ql-token hljs-title">string</span>() }) </div> <div class="ql-code-block"> ) </div> <div class="ql-code-block"> .<span class="ql-token hljs-title">mutation</span>(<span class="ql-token hljs-keyword">async</span> ({ input }) => { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> user = <span class="ql-token hljs-keyword">await</span> db.<span class="ql-token hljs-property">user</span>.<span class="ql-token hljs-title">create</span>(input); </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> user; </div> <div class="ql-code-block"> }), </div> <div class="ql-code-block"> }); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-comment">// Export type router type signature,</span> </div> <div class="ql-code-block"><span class="ql-token hljs-comment">// NOT the router itself.</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">export</span> <span class="ql-token hljs-keyword">type</span> <span class="ql-token hljs-title">AppRouter</span> = <span class="ql-token hljs-keyword">typeof</span> appRouter; </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> server = <span class="ql-token hljs-title">createHTTPServer</span>({ </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">router</span>: appRouter, </div> <div class="ql-code-block"> }); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> server.<span class="ql-token hljs-title">listen</span>(<span class="ql-token hljs-number">3000</span>); </div> </div> <p><br></p> <p><br></p> <p>这里我们导入了 db、publicProcedure 和 router,然后创建了 appRouter,appRouter 是一个对象,它包含了 userList、userById 和 userCreate 三个属性,这三个属性分别对应了 publicProcedure.query、publicProcedure.query 和 publicProcedure.mutation ,这三个属性的值都是一个函数,这个函数的返回值就是我们需要的数据。</p> <p><br></p> <p>值得注意的是,代码的第 31 行我标注了一下,这里导出的是 AppRouter 类型,而不是 appRouter 对象,这是因为 appRouter 对象中的 userList、userById 和 userCreate 三个属性的类型都是 Procedure,而 Procedure 是一个泛型,它的第一个参数是输入类型,第二个参数是输出类型,因此我们需要导出 AppRouter 类型,这样在后面的代码中才能使用 AppRouter 类型。</p> <p><br></p> <p>也就是我们在前端调用后端 api 的时候可以直接通过 . 就可以获得对应的属性和方法了。</p> <p><br></p> <p>到此,我们后端的逻辑差不多写完了,将对应的 appRouter 传入 createHTTPServer 函数中,然后监听 3000 端口即可,下面我们即将编写前端逻辑,来调用后端的接口试一试。</p> <p><br></p> <p><br></p> <h2>编写前端逻辑</h2> <p><br></p> <p><br></p> <p>由于前端只用负责调用 api,所以代码其实也很简单,如 client/index.ts 所示:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> { createTRPCProxyClient, httpBatchLink } <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"@trpc/client"</span>; </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">import</span> <span class="ql-token hljs-keyword">type</span> { <span class="ql-token hljs-title">AppRouter</span> } <span class="ql-token hljs-keyword">from</span> <span class="ql-token hljs-string">"../server"</span>; </div> <div class="ql-code-block"><span class="ql-token hljs-comment">// 👆 type-only import</span> </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-comment">// Pass AppRouter as generic here. 👇 This lets the trpc object know</span> </div> <div class="ql-code-block"><span class="ql-token hljs-comment">// what procedures are available on the server and their input/output types.</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">const</span> trpc = createTRPCProxyClient<<span class="ql-token hljs-title">AppRouter</span>>({ </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">links</span>: [ </div> <div class="ql-code-block"> <span class="ql-token hljs-title">httpBatchLink</span>({ </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">url</span>: <span class="ql-token hljs-string">"http://localhost:3000"</span>, </div> <div class="ql-code-block"> }), </div> <div class="ql-code-block"> ], </div> <div class="ql-code-block"> }); </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">async</span> <span class="ql-token hljs-keyword">function</span> <span class="ql-token hljs-title">main</span>() { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> createdUser = <span class="ql-token hljs-keyword">await</span> trpc.<span class="ql-token hljs-property">userCreate</span>.<span class="ql-token hljs-title">mutate</span>({ </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">name</span>: <span class="ql-token hljs-string">"一百个Chocolate"</span>, </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">email</span>: <span class="ql-token hljs-string">"chocolate@qq.com"</span>, </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">password</span>: <span class="ql-token hljs-string">"Chocolate"</span>, </div> <div class="ql-code-block"> }); </div> <div class="ql-code-block"> <span class="ql-token hljs-variable">console</span>.<span class="ql-token hljs-title">log</span>(<span class="ql-token hljs-string">"createdUser: "</span>, createdUser); </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> userCreated = <span class="ql-token hljs-keyword">await</span> trpc.<span class="ql-token hljs-property">userById</span>.<span class="ql-token hljs-title">query</span>({ <span class="ql-token hljs-attr">id</span>: <span class="ql-token hljs-string">"1"</span> }); </div> <div class="ql-code-block"> <span class="ql-token hljs-variable">console</span>.<span class="ql-token hljs-title">log</span>(<span class="ql-token hljs-string">"userCreated: "</span>, userCreated); </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">const</span> users = <span class="ql-token hljs-keyword">await</span> trpc.<span class="ql-token hljs-property">userList</span>.<span class="ql-token hljs-title">query</span>(); </div> <div class="ql-code-block"> <span class="ql-token hljs-variable">console</span>.<span class="ql-token hljs-title">log</span>(<span class="ql-token hljs-string">"Users:"</span>, users); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"><span class="ql-token hljs-title">main</span>(); </div> </div> <p><br></p> <p><br></p> <p>这里我们导入了 createTRPCProxyClient 和 httpBatchLink ,然后创建了 trpc 对象,trpc 对象就是我们用来调用后端 api 的对象,它的属性和方法就是后端 appRouter 对象中的 userList、userById 和 userCreate 三个属性,这三个属性的类型都是 Procedure,所以我们可以直接通过 . 来调用这三个属性。</p> <p><br></p> <p><br></p> <h3>编写 script 脚本</h3> <p><br></p> <p><br></p> <p>为了让我们前后端代码跑起来,还需要修改一下项目的 script 脚本,如下:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">// xxx</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"scripts":</span> <span class="ql-token hljs-punctuation">{</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"client:dev":</span> <span class="ql-token hljs-string">"esno client",</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-attr">"server:dev":</span> <span class="ql-token hljs-string">"esno server"</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-punctuation">}</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">// ...</span> </div> <div class="ql-code-block"><span class="ql-token hljs-punctuation">}</span> </div> </div> <p><br></p> <p><br></p> <p>这里我是使用的 esno 来执行我们的 ts 文件。也需要安装一下:</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> npm install esno </div> </div> <p><br></p> <p><br></p> <p>esno 是一个命令行工具,它可以直接执行 ts 文件,而不需要我们先编译成 js 文件,然后再执行 js 文件,这样就可以省去编译的过程了。</p> <p><br></p> <p>那么我们就可以在终端中执行 npm run client:dev 和 npm run server:dev 来分别启动前端和后端了,这里需要注意一下,我们需要先执行 npm run server:dev,然后再执行 npm run client:dev ,因为前端需要调用后端的接口,所以后端必须先启动起来,否则前端就会报错。</p> <p><br></p> <p>当然,这里其实也可以使用 wait-port 以及 npm-run-all 来实现前后端一起启动,我们会在进阶篇来介绍,这里就先不展示了。</p> <p><br></p> <p>我们来看看打印结果:</p> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1803709410353942529/ey24jhfv.jpeg"></p> <p><br></p> <p>可以看到,我们成功的调用了后端的接口,获取到了数据。</p> <p><br></p> <p><br></p> <h2>总结</h2> <p><br></p> <p><br></p> <p>到此,我们就完成了一个简单的 tRPC 项目,我们使用 tRPC 来创建了一个后端 api,然后在前端使用 tRPC 来调用后端的接口,获取到了数据。</p> <p><br></p> <p>这里我们使用的是 httpBatchLink,也就是 http 协议,当然 tRPC 还支持其他的协议,比如 wsLink、grpcLink、reactQueryLink 等等,可以从官网文档中进一步学习。</p> <p><br></p> <p><br></p> <h2>文章参考</h2> <p><br></p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><a href="https://trpc.io/docs/" target="_blank">tRPC 官方文档</a></li> <li data-list="bullet"><span class="ql-ui"></span><a href="https://www.youtube.com/watch?v=UfUbBWIFdJs" target="_blank">Learn tRPC In 45 Minutes</a></li> </ol> <p><br></p> </div> </body> </html>
