من یکی از طرفداران پر و پا قرص tRPC هستم. ایده export کردن type ها از سرور و وارد کردن آنها به client برای داشتن قرارداد type-safe بین هر دو، حتی بدون مرحله compile-time ، به طور سادهای درخشان است. شما با این کار برای تمام افرادی که در tRPC درگیر هستند، کار بزرگی انجام میدهید. با این حال وقتی tRPC و GraphQL را با هم مقایسه میکنم، به نظر میرسد که سیب و پرتقال را با هم مقایسه کردهام.
وقتی به گفتمان عمومی پیرامون GraphQL و tRPC نگاه می کنید، این امر به ویژه آشکار می شود. به عنوان مثال به این نمودار نگاه کنید:

این نمودار در نگاه اول بسیار منطقی است. tRPC به مرحله compile-time نیازی ندارد، چرا که تجربه توسعهدهنده آن باورنکردنی بوده و بسیار سادهتر از GraphQL است.
اما آیا این واقعاً یک تصویر کامل است؟ یا این سادگی به قیمت چیز دیگری حاصل میشود؟ پس بیایید با ساختن یک برنامه ساده با tRPC و GraphQL این موضوع را کشف کنیم.
بیایید با tRPC یک clone فیسبوک بسازیم
بیایید یک file tree را با یک صفحه برای فید خبری، یک جزء برای feed list و یک جزء برای feed item تصویر کنیم.
src/pages/news-feed
├── NewsFeed.tsx
├── NewsFeedList.tsx
└── NewsFeedItem.tsx
در بالای feed page ، به اطلاعاتی در مورد کاربر، ناتیفیکیشنها، پیامهای خوانده نشده و غیره نیاز داریم.
هنگام رندر کردن feed list ، باید تعداد feed item را بدانیم. و اگر صفحه دیگری وجود داشته باشد، باید نحوه fetch کردن آن را هم بدانیم.
اگر بخواهیم از tRPC استفاده کنیم، در یک حرکت، یک روش (procedure) برای بارگیری همه این دادهها ایجاد میکنیم. ما این روش را در بالای صفحه فراخوانی میکنیم و سپس دادهها را به اجزای سازنده منتقل میکنیم.
اجزای feed ما چیزی شبیه به این خواهد بود:
import { trpc } from '../utils/trpc'
export function NewsFeed() {
const feed = trpc.newsFeed.useQuery()
return (
{feed.user}
{feed.unreadMessages} unread messages
{feed.notifications} notifications
)
}
در مرحله بعد، بیایید به اجزای feed list نگاه کنیم:
export function NewsFeedList({ feed }) {
return (
News Feed
There are {feed.items.length} items
{feed.items.map((item) => (
))}
{feed.hasNextPage && (
)}
)
}
و در نهایت اجزای feed item:
export function NewsFeedItem({ item }) {
return (
{item.author.name}
{item.content}
)
}
به خاطر داشته باشد، ما هنوز یک تیم واحد هستیم. همه آنها TypeScript هستند، یک codebase واحد، و ما هنوز از tRPC استفاده میکنیم.
برای رندر کردن صفحه با tRPC به چه دادههایی نیاز داریم؟
بیایید بفهمیم که واقعاً برای رندر کردن صفحه به چه دادههایی نیاز داریم. ما به کاربر، پیامهای خوانده نشده، ناتیفیکیشنها، feed item ها، تعداد feed item ها، صفحه بعدی، نویسنده، محتوا، و در صورت پسند کاربر تعداد لایکها نیاز داریم.
از کجا میتوانیم اطلاعات دقیقی در مورد همه اینها پیدا کنیم؟ برای درک نیازهای داده برای avatar ، باید به اجزای avatar نگاه کنیم. اجزایی برای پیامها و ناتیفیکیشنهای خوانده نشده وجود دارد، بنابراین آنها را نیز باید بررسی کنیم. مؤلفه feed list به تعداد item ها، صفحه بعدی و feed item ها نیاز دارد. مؤلفه feed item شامل الزامات هر item لیست است.
در مجموع، اگر بخواهیم اطلاعات مورد نیاز این صفحه را درک کنیم، باید به 6 مؤلفه مختلف نگاه کنیم. در عین حال، ما واقعاً نمیدانیم که چه دادههایی برای هر مؤلفه مورد نیاز است. هیچ راهی برای هر جزء وجود ندارد که بتواند اطلاعات مورد نیاز خود را اعلام کند. زیرا tRPC چنین مفهومی ندارد.
به خاطر داشته باشید که این تنها یک صفحه است. اگر صفحات مشابه اما کمی متفاوت به آن اضافه کنیم، چه اتفاقی میافتد؟
فرض کنید که ما در حال ساخت یک نوع از news feed هستیم، اما به جای نمایش آخرین پستها، محبوبترین پستها را نشان میدهیم.
ما میتوانیم کم و بیش از اجزای یکسان استفاده کنیم، فقط با چند تغییر. بیایید بگوییم پستهای محبوب دارای نشانهای خاصی هستند که به دادههای اضافی نیاز دارند.
آیا باید procedure جدیدی برای این کار ایجاد کنیم؟ یا شاید بتوانیم چند فیلد دیگر را به procedure موجود اضافه کنیم؟
سؤالات متداول درباره tRPC
اگر صفحات بیشتر و بیشتری اضافه کنیم، آیا این رویکرد مقیاس خوبی دارد؟ آیا این به نظر مشکلی نیست که ما با REST API ها داشتیم؟ ما حتی نامهای معروفی برای این مشکلات داریم. مانند Overfetching و Underfetching. و حتی به نقطهای نرسیدیم که در مورد مشکل N+1 صحبت کنیم.
در برخی موارد ممکن است تصمیم بگیریم که procedure را به یک root procedure و چندین procedure فرعی تقسیم کنیم. اگر یک array را در سطح root ، fetch کنیم، و سپس برای هر item در array ، باید procedure دیگری را برای fetch کردن دادههای بیشتر فراخوانی کنیم، چه؟
یکی دیگر از موارد باز می تواند معرفی آرگومانها به نسخه اولیه procedure ما باشد، به عنوان مثال. trpc.newsFeed.useQuery({withPopularBadges: true}).
این کار میکند، اما به نظر میرسد که ما شروع به اختراع مجدد ویژگیهای GraphQL کردهایم.
بیایید یک clone فیس بوک با GraphQL بسازیم
حالا بیایید این را با GraphQL مقایسه کنیم. GraphQL دارای مفهوم Fragments است که به ما امکان میدهد دادههای مورد نیاز برای هر جزء را اعلام کنیم. کلاینتهایی مانند Relay به شما این امکان را میدهند که یک Query GraphQL را در بالای صفحه اعلام کنید و قطعاتی از اجزای فرزند را در Query قرار دهید.
به این ترتیب، ما هنوز یک fetch واحد در بالای صفحه میسازیم، اما در واقع framework از ما در اعلام و جمعآوری دادههای مورد نیاز برای هر جزء پشتیبانی میکند.
بیایید با استفاده از GraphQL، Fragments و Relay به همین مثال نگاه کنیم. به دلیل تنبلی، کد 100٪ درست نیست زیرا من از Copilot برای نوشتن آن استفاده میکنم، اما باید بسیار نزدیک به چیزی باشد که در یک برنامه واقعی استفاده میشود:
import { graphql } from 'react-relay'
export function NewsFeed() {
const feed = useQuery(graphql`
query NewsFeedQuery {
user {
...Avatar_user
}
unreadMessages {
...UnreadMessages_unreadMessages
}
notifications {
...Notifications_notifications
}
...NewsFeedList_feed
}
`)
return (
)
}
در مرحله بعد، بیایید به مؤلفه feed list نگاه کنیم. مؤلفه feed list ، یک قطعه را برای خود اعلام میکند، و قطعه را برای مؤلفه feed item شامل میشود.
import { graphql } from 'react-relay'
export function NewsFeedList({ feed }) {
const list = useFragment(
graphql`
fragment NewsFeedList_feed on NewsFeed {
items {
...NewsFeedItem_item
}
hasNextPage
}
`,
feed
)
return (
News Feed
There are {feed.items.length} items
{feed.items.map((item) => (
))}
{feed.hasNextPage && (
)}
)
}
و در نهایت، مؤلفه feed item:
import { graphql } from 'react-relay'
export function NewsFeedItem({ item }) {
const item = useFragment(
graphql`
fragment NewsFeedItem_item on NewsFeedItem {
author {
name
}
content
likes
hasLiked
}
`,
item
)
return (
{item.author.name}
{item.content}
)
}
در مرحله بعد، بیایید یک تنوع از news feed با نشان هایمحبوب بر روی feed item ها ایجاد کنیم. میتوانیم از همان مؤلفهها دوباره استفاده کنیم، زیرا میتوانیم از دستورالعمل @include برای گنجاندن قطعه نشان محبوب به صورت مشروط استفاده کنیم.
import { graphql } from 'react-relay'
export function PopularNewsFeed() {
const feed = useQuery(graphql`
query PopularNewsFeedQuery($withPopularBadges: Boolean!) {
user {
...Avatar_user
}
unreadMessages {
...UnreadMessages_unreadMessages
}
notifications {
...Notifications_notifications
}
...NewsFeedList_feed
}
`)
return (
)
}
در مرحله بعد، بیایید ببینیم که feed list item بهروزرسانی شده چگونه میتواند به نظر برسد:
import { graphql } from 'react-relay'
export function NewsFeedItem({ item }) {
const item = useFragment(
graphql`
fragment NewsFeedItem_item on NewsFeedItem {
author {
name
}
content
likes
hasLiked
...PopularBadge_item @include(if: $withPopularBadges)
}
`,
item
)
return (
{item.author.name}
{item.content}
{item.popularBadge && }
)
}
همانطور که می بینید، GraphQL کاملاً منعطف است و به ما اجازه میدهد تا برنامههای وب پیچیده، از جمله تغییرات در همان صفحه را، بدون نیاز به کپی کدهای زیاد بسازیم.
GraphQL Fragments به ما این امکان را میدهد که الزامات داده را در سطح مؤلفه اعلام کنیم.
علاوه بر این، GraphQL Fragments به ما این امکان را میدهد که به صراحت الزامات داده را برای هر مؤلفه اعلام کنیم، که سپس تا بالای صفحه بالا میرود و سپس در یک درخواست fetch میشود.
GraphQL پیاده سازی API را از fetch کردن داده جدا میکند
تجربه توسعه دهندگان tRPC با ادغام دو نگرانی بسیار متفاوت در یک کانسپت، یعنی پیادهسازی API و data consumption ، به دست میآید.
درک این نکته مهم است که این یک معامله است. جایزهای وجود ندارد. سادگی tRPC به قیمت انعطاف پذیری آن است.
با GraphQL، شما باید خیلی بیشتر روی طراحی schema سرمایهگذاری کنید، اما این سرمایهگذاری در لحظهای که باید برنامه خود را به صفحات زیاد اما مرتبط مقیاس دهید، نتیجه میدهد.
با جدا کردن اجرای API از fetch کردن داده، استفاده مجدد از همان پیادهسازی API برای موارد استفاده مختلف بسیار آسانتر میشود.
هدف API ها جداسازی پیاده سازی داخلی از interface خارجی است
یک جنبه مهم دیگر برای ساختن API وجود دارد. ممکن است با یک API داخلی شروع کنید که منحصراً توسط frontend خودتان استفاده میشود، و tRPC ممکن است برای این مورد مناسب باشد.
اما در مورد آینده؟ احتمال اینکه تیم خود را رشد دهید چقدر است؟ آیا ممکن است تیمهای دیگر یا حتی اشخاص ثالث بخواهند APIهای شما را مصرف کنند؟
REST و GraphQL هر دو با collaboration ذهنی ساخته شدهاند. همه تیمها از TypeScript استفاده نمیکنند، و اگر از مرزهای شرکتتان عبور میکنید، میخواهید APIها را بهگونهای آشکار کنید که درک و مصرف آنها آسان باشد.
ابزارهای زیادی برای افشا و مستندسازی APIهای REST و GraphQL وجود دارد، در حالی که واضح است tRPC برای این مورد طراحی نشده است.
شروع با tRPC عالی است، اما به احتمال زیاد در نقطهای از آن فراتر خواهید رفت.
مطمئناً امکان تولید مشخصات OpenAPI از یک API tRPC وجود دارد، یعنی یک نوع ابزارسازی وجود دارد، اما اگر کسبوکاری ایجاد میکنید که در نهایت به افشای APIها در معرض اشخاص ثالث متکی است، RPCهای شما نمیتوانند با REST و API های GraphQL رقابت کنند.
نتیجهگیری
همانطور که در ابتدا گفته شد، من از طرفداران بزرگ tRPC هستم. این گامی عالی به سمت مسیر درست است که fetch کردن دادهها را سادهتر و با توسعهدهندگان سازگارتر میکند.
از طرف دیگر GraphQL، Fragments و Relay ابزارهای قدرتمندی هستند که به شما در ساخت برنامه های پیچیده وب کمک میکنند. در عین حال، راه اندازی آنها بسیار پیچیده است و مفاهیم زیادی وجود دارد که باید یاد بگیرید.
در حالی که tRPC شما را به سرعت راه میاندازد، به احتمال بسیار زیاد در برخی مواقع از معماری آن فراتر خواهید رفت. اگر امروز تصمیم دارید روی GraphQL یا tRPC شرط بندی کنید، باید در نظر داشته باشید که آینده پروژه خود را در چه جایگاهی میبینید. الزامات fetch کردن دادهها چقدر پیچیده خواهد بود؟ آیا چندین تیم وجود خواهند داشت که API های شما را مصرف میکنند؟ آیا API های خود را در معرض اشخاص ثالث قرار خواهید داد؟
چشم انداز
با تمام آنچه گفته شد، چه میشود اگر بتوانیم با یک تیر دو نشان بزنیم؟یک API client چگونه به نظر میرسد که سادگی tRPC را با قدرت GraphQL ترکیب میکند؟ آیا میتوانیم یک TypeScript API client خالص بسازیم که به ما قدرت Fragments و Relay را همراه با سادگی tRPC ارائه کند؟
تصور کنید ایدههای tRPC را گرفته و با آنچه از GraphQL و Relay آموختهایم ترکیب میکنیم.
در اینجا یک پیش نمایش کوچک از آن را میبینید:
// src/pages/index.tsx
import { useQuery } from '../../.wundergraph/generated/client'
import { Avatar_user } from '../components/Avatar'
import { UnreadMessages_unreadMessages } from '../components/UnreadMessages'
import { Notifications_notifications } from '../components/Notifications'
import { NewsFeedList_feed } from '../components/NewsFeedList'
export function NewsFeed() {
const feed = useQuery({
operationName: 'NewsFeed',
query: (q) => ({
user: q.user({
...Avatar_user.fragment,
}),
unreadMessages: q.unreadMessages({
...UnreadMessages_unreadMessages.fragment,
}),
notifications: q.notifications({
...Notifications_notifications.fragment,
}),
...NewsFeedList_feed.fragment,
}),
})
return (
)
}
// src/components/Avatar.tsx
import { useFragment, Fragment } from '../../.wundergraph/generated/client'
export const Avatar_user = Fragment({
on: 'User',
fragment: ({ name, avatar }) => ({
name,
avatar,
}),
})
export function Avatar() {
const data = useFragment(Avatar_user)
return (
{data.name}
)
}
// src/components/NewsFeedList.tsx
import { useFragment, Fragment } from '../../.wundergraph/generated/client'
import { NewsFeedItem_item } from './NewsFeedItem'
export const NewsFeedList_feed = Fragment({
on: 'NewsFeed',
fragment: ({ items }) => ({
items: items({
...NewsFeedItem_item.fragment,
}),
}),
})
export function NewsFeedList() {
const data = useFragment(NewsFeedList_feed)
return (
{data.items.map((item) => (
))}
)
}
// src/components/NewsFeedItem.tsx
import { useFragment, Fragment } from '../../.wundergraph/generated/client'
export const NewsFeedItem_item = Fragment({
on: 'NewsFeedItem',
fragment: ({ id, author, content }) => ({
id,
author,
content,
}),
})
export function NewsFeedItem() {
const data = useFragment(NewsFeedItem_item)
return (
{data.title}
{data.content}
)
}
Leave feedback about this