paint-brush
Tính đơn giản của tRPC với sức mạnh của GraphQLtừ tác giả@wunderstef
6,554 lượt đọc
6,554 lượt đọc

Tính đơn giản của tRPC với sức mạnh của GraphQL

từ tác giả Stefan Avram 13m2022/12/14
Read on Terminal Reader
Read this story w/o Javascript

dài quá đọc không nổi

featured image - Tính đơn giản của tRPC với sức mạnh của GraphQL
Stefan Avram  HackerNoon profile picture
0-item


Tôi là một fan hâm mộ lớn của tRPC. Ý tưởng xuất các loại từ máy chủ và nhập chúng vào máy khách để có một hợp đồng an toàn về loại giữa cả hai, ngay cả khi không có bước thời gian biên dịch, đơn giản là tuyệt vời. Đối với tất cả những người tham gia vào tRPC, bạn đang làm một công việc tuyệt vời.


Điều đó nói rằng, khi tôi xem xét so sánh giữa tRPC và GraphQL, có vẻ như chúng ta đang so sánh táo và cam.


Điều này trở nên đặc biệt rõ ràng khi bạn nhìn vào diễn ngôn công khai xung quanh GraphQL và tRPC. Nhìn vào sơ đồ này theo ví dụ:

Theo đã giải thích cặn kẽ sơ đồ này và thoạt nhìn, nó rất có ý nghĩa. tRPC không yêu cầu bước thời gian biên dịch, trải nghiệm của nhà phát triển thật đáng kinh ngạc và nó đơn giản hơn GraphQL rất nhiều.


Nhưng đó có thực sự là bức tranh đầy đủ hay sự đơn giản này đạt được bằng cái giá của một thứ khác? Hãy cùng tìm hiểu bằng cách xây dựng một ứng dụng đơn giản với cả tRPC và GraphQL.

Hãy xây dựng một bản sao Facebook với tRPC

Hãy tưởng tượng một cây tệp với một trang dành cho nguồn cấp tin tức, một thành phần dành cho danh sách nguồn cấp dữ liệu và một thành phần dành cho mục nguồn cấp dữ liệu.


 src/pages/news-feed ├── NewsFeed.tsx ├── NewsFeedList.tsx └── NewsFeedItem.tsx


Ở đầu trang nguồn cấp dữ liệu, chúng tôi cần một số thông tin về người dùng, thông báo, tin nhắn chưa đọc, v.v.


Khi hiển thị danh sách nguồn cấp dữ liệu, chúng tôi cần biết số lượng mục nguồn cấp dữ liệu, nếu có một trang khác và cách tìm nạp trang đó.


Đối với mục nguồn cấp dữ liệu, chúng ta cần biết tác giả, nội dung, số lượt thích và người dùng có thích nó hay không.


Nếu chúng tôi sử dụng tRPC, chúng tôi sẽ tạo một "quy trình" để tải tất cả dữ liệu này trong một lần. Chúng tôi sẽ gọi quy trình này ở đầu trang và sau đó truyền dữ liệu xuống các thành phần.


Thành phần nguồn cấp dữ liệu của chúng tôi sẽ trông giống như thế này:

 import { trpc } from '../utils/trpc' export function NewsFeed() { const feed = trpc.newsFeed.useQuery() return ( <div> <Avatar>{feed.user}</Avatar> <UnreadMessages> {feed.unreadMessages} unread messages </UnreadMessages> <Notifications> {feed.notifications} notifications </Notifications> <NewsFeedList feed={feed} /> </div> ) }


Tiếp theo, hãy xem thành phần danh sách nguồn cấp dữ liệu:

 export function NewsFeedList({ feed }) { return ( <div> <h1>News Feed</h1> <p>There are {feed.items.length} items</p> {feed.items.map((item) => ( <NewsFeedItem item={item} /> ))} {feed.hasNextPage && ( <button onClick={feed.fetchNextPage}>Load more</button> )} </div> ) }


Và cuối cùng, thành phần mục nguồn cấp dữ liệu:

 export function NewsFeedItem({ item }) { return ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> </div> ) }


Xin lưu ý rằng chúng tôi vẫn là một nhóm duy nhất, tất cả đều là TypeScript, một cơ sở mã duy nhất và chúng tôi vẫn đang sử dụng tRPC.


Hãy tìm hiểu dữ liệu nào chúng ta thực sự cần để hiển thị trang. Chúng tôi cần người dùng, tin nhắn chưa đọc, thông báo, mục nguồn cấp dữ liệu, số lượng mục nguồn cấp dữ liệu, trang tiếp theo, tác giả, nội dung, số lượt thích và nếu người dùng thích nó.


Chúng ta có thể tìm thông tin chi tiết về tất cả những điều này ở đâu? Để hiểu các yêu cầu dữ liệu cho hình đại diện, chúng ta cần xem thành phần Hình Avatar . Có các thành phần dành cho tin nhắn và thông báo chưa đọc, vì vậy chúng ta cũng cần xem xét chúng. Thành phần danh sách nguồn cấp dữ liệu cần số lượng mục, trang tiếp theo và các mục nguồn cấp dữ liệu. Thành phần mục nguồn cấp dữ liệu chứa các yêu cầu cho từng mục danh sách.


Tổng cộng, nếu muốn hiểu các yêu cầu dữ liệu cho trang này, chúng ta cần xem xét 6 thành phần khác nhau. Đồng thời, chúng tôi không thực sự biết dữ liệu nào thực sự cần thiết cho mỗi thành phần. Không có cách nào để mỗi thành phần khai báo dữ liệu nào nó cần vì tRPC không có khái niệm như vậy.


Hãy nhớ rằng đây chỉ là một trang duy nhất. Điều gì xảy ra nếu chúng tôi thêm các trang tương tự nhưng hơi khác một chút?


Giả sử chúng ta đang xây dựng một biến thể của nguồn cấp tin tức, nhưng thay vì hiển thị các bài đăng mới nhất, chúng tôi đang hiển thị các bài đăng phổ biến nhất.


Chúng tôi ít nhiều có thể sử dụng các thành phần giống nhau, chỉ với một vài thay đổi. Giả sử rằng các bài đăng phổ biến có huy hiệu đặc biệt yêu cầu thêm dữ liệu.


Chúng ta có nên tạo một thủ tục mới cho việc này không? Hoặc có lẽ chúng ta chỉ cần thêm một vài trường nữa vào quy trình hiện có?


Phương pháp này có mở rộng quy mô tốt không nếu chúng tôi thêm ngày càng nhiều trang? Điều này có giống như sự cố mà chúng tôi gặp phải với API REST không? Chúng tôi thậm chí còn có những cái tên nổi tiếng cho những vấn đề này, như Tìm nạp quá mức và Tìm nạp thiếu, và chúng tôi thậm chí còn chưa nói đến vấn đề N+1.


Tại một số điểm, chúng tôi có thể quyết định chia thủ tục thành một thủ tục gốc và nhiều thủ tục con. Điều gì sẽ xảy ra nếu chúng ta đang tìm nạp một mảng ở cấp cơ sở và sau đó đối với mỗi mục trong mảng, chúng ta phải gọi một thủ tục khác để tìm nạp thêm dữ liệu?


Một cách mở khác có thể là đưa các đối số vào phiên bản ban đầu của thủ tục của chúng ta, ví dụ: trpc.newsFeed.useQuery({withPopularBadges: true}) .


Điều này sẽ hiệu quả, nhưng có vẻ như chúng tôi đang bắt đầu phát minh lại các tính năng của GraphQL.

Hãy xây dựng một bản sao Facebook với GraphQL

Bây giờ, hãy so sánh điều này với GraphQL. GraphQL có khái niệm Fragments, cho phép chúng ta khai báo các yêu cầu dữ liệu cho từng thành phần. Các ứng dụng khách như Relay cho phép bạn khai báo một truy vấn GraphQL duy nhất ở đầu trang và đưa các đoạn từ các thành phần con vào truy vấn.


Bằng cách này, chúng tôi vẫn thực hiện một lần tìm nạp duy nhất ở đầu trang, nhưng khung thực sự hỗ trợ chúng tôi trong việc khai báo và thu thập các yêu cầu dữ liệu cho từng thành phần.

Hãy xem cùng một ví dụ sử dụng GraphQL, Fragment và Relay. Vì lý do lười biếng, mã không chính xác 100% vì tôi đang sử dụng Copilot để viết mã, nhưng mã này phải rất gần với giao diện của ứng dụng thực.


 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 ( <div> <Avatar user={feed.user} /> <UnreadMessages unreadMessages={feed.unreadMessages} /> <Notifications notifications={feed.notifications} /> <NewsFeedList feed={feed} /> </div> ) }


Tiếp theo, hãy xem thành phần danh sách nguồn cấp dữ liệu. Thành phần danh sách nguồn cấp dữ liệu khai báo một đoạn cho chính nó và bao gồm đoạn đó cho thành phần mục nguồn cấp dữ liệu.


 import { graphql } from 'react-relay' export function NewsFeedList({ feed }) { const list = useFragment( graphql` fragment NewsFeedList_feed on NewsFeed { items { ...NewsFeedItem_item } hasNextPage } `, feed ) return ( <div> <h1>News Feed</h1> <p>There are {feed.items.length} items</p> {feed.items.map((item) => ( <NewsFeedItem item={item} /> ))} {feed.hasNextPage && ( <button onClick={feed.fetchNextPage}>Load more</button> )} </div> ) }


Và cuối cùng, thành phần mục nguồn cấp dữ liệu:

 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 ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> </div> ) }


Tiếp theo, hãy tạo một biến thể của nguồn cấp tin tức với các huy hiệu phổ biến trên các mục nguồn cấp dữ liệu. Chúng tôi có thể sử dụng lại các thành phần giống nhau, vì chúng tôi có thể sử dụng lệnh @include để bao gồm đoạn huy hiệu phổ biến một cách có điều kiện.

 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 ( <div> <Avatar user={feed.user} /> <UnreadMessages unreadMessages={feed.unreadMessages} /> <Notifications notifications={feed.notifications} /> <NewsFeedList feed={feed} /> </div> ) }


Tiếp theo, hãy xem mục danh sách nguồn cấp dữ liệu được cập nhật trông như thế nào:

 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 ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> {item.popularBadge && <PopularBadge badge={item.popularBadge} />} </div> ) }


Như bạn có thể thấy, GraphQL khá linh hoạt và cho phép chúng ta xây dựng các ứng dụng web phức tạp, bao gồm các biến thể của cùng một trang mà không phải sao chép quá nhiều mã.

GraphQL Fragments cho phép chúng ta khai báo các yêu cầu dữ liệu ở cấp độ thành phần

Ngoài ra, GraphQL Fragment cho phép chúng ta khai báo rõ ràng các yêu cầu dữ liệu cho từng thành phần, sau đó được đưa lên đầu trang và sau đó được tìm nạp trong một yêu cầu.


GraphQL tách việc triển khai API khỏi việc tìm nạp dữ liệu

Trải nghiệm tRPC tuyệt vời dành cho nhà phát triển đạt được bằng cách hợp nhất hai mối quan tâm rất khác nhau thành một khái niệm, triển khai API và mức tiêu thụ dữ liệu.


Điều quan trọng là phải hiểu rằng đây là một sự đánh đổi. Không có bữa ăn trưa miễn phí. Sự đơn giản của tRPC đi kèm với chi phí linh hoạt.


Với GraphQL, bạn phải đầu tư nhiều hơn vào thiết kế lược đồ, nhưng khoản đầu tư này sẽ mang lại kết quả khi bạn phải mở rộng ứng dụng của mình thành nhiều trang nhưng có liên quan.


Bằng cách tách việc triển khai API khỏi quá trình tìm nạp dữ liệu, việc sử dụng lại cùng một triển khai API cho các trường hợp sử dụng khác nhau sẽ trở nên dễ dàng hơn nhiều.

Mục đích của API là tách phần triển khai nội bộ khỏi giao diện bên ngoài

Có một khía cạnh quan trọng khác cần xem xét khi xây dựng API. Bạn có thể đang bắt đầu với một API nội bộ được sử dụng độc quyền bởi giao diện người dùng của riêng bạn và tRPC có thể rất phù hợp cho trường hợp sử dụng này.


Nhưng những gì về tương lai của nỗ lực của bạn? Khả năng bạn sẽ phát triển nhóm của mình là gì? Có thể các nhóm khác hoặc thậm chí bên thứ 3 muốn sử dụng API của bạn không?

Cả REST và GraphQL đều được xây dựng với mục đích cộng tác. Không phải tất cả các nhóm sẽ sử dụng TypeScript và nếu bạn đang vượt qua ranh giới của công ty, bạn sẽ muốn hiển thị API theo cách dễ hiểu và sử dụng.


Có rất nhiều công cụ để hiển thị và ghi lại API REST và GraphQL, trong khi tRPC rõ ràng không được thiết kế cho trường hợp sử dụng này.


Vì vậy, mặc dù thật tuyệt khi bắt đầu với tRPC, nhưng bạn rất có thể sẽ phát triển nhanh hơn nó vào một thời điểm nào đó, điều mà tôi nghĩ rằng theo cũng đã đề cập trong một trong các video của anh ấy.


Chắc chắn có thể tạo đặc tả OpenAPI từ API tRPC, công cụ này tồn tại, nhưng nếu bạn đang xây dựng một doanh nghiệp mà cuối cùng sẽ dựa vào việc hiển thị API cho bên thứ 3, RPC của bạn sẽ không thể cạnh tranh với REST được thiết kế tốt và API GraphQL.

Sự kết luận

Như đã nói ở phần đầu, tôi rất hâm mộ những ý tưởng đằng sau tRPC. Đó là một bước tuyệt vời để đi đúng hướng, giúp việc tìm nạp dữ liệu trở nên đơn giản hơn và thân thiện hơn với nhà phát triển.


Mặt khác, GraphQL, Fragment và Relay là những công cụ mạnh mẽ giúp bạn xây dựng các ứng dụng web phức tạp. Đồng thời, việc thiết lập khá phức tạp và có nhiều khái niệm cần tìm hiểu cho đến khi bạn hiểu rõ về nó.


Mặc dù tRPC giúp bạn bắt đầu nhanh chóng, nhưng rất có khả năng bạn sẽ phát triển nhanh hơn kiến trúc của nó vào một thời điểm nào đó. Nếu hôm nay bạn đưa ra quyết định đặt cược vào GraphQL hoặc tRPC, thì bạn nên tính đến việc bạn thấy dự án của mình sẽ đi đến đâu trong tương lai. Tương lai. Các yêu cầu tìm nạp dữ liệu sẽ phức tạp đến mức nào? Sẽ có nhiều nhóm sử dụng API của bạn chứ? Bạn sẽ hiển thị API của mình cho bên thứ 3 chứ?


Quan điểm

Với tất cả những gì đã nói, điều gì sẽ xảy ra nếu chúng ta có thể kết hợp những điều tốt nhất của cả hai thế giới? Ứng dụng khách API sẽ trông như thế nào khi kết hợp tính đơn giản của tRPC với sức mạnh của GraphQL? Chúng tôi có thể xây dựng ứng dụng khách API TypeScript thuần túy cung cấp cho chúng tôi sức mạnh của Phân đoạn và Chuyển tiếp, kết hợp với sự đơn giản của tRPC không?


Hãy tưởng tượng chúng tôi lấy ý tưởng về tRPC và kết hợp chúng với những gì chúng tôi đã học được từ GraphQL và Relay.


Đây là một bản xem trước nhỏ:


 // 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 ( <div> <Avatar /> <UnreadMessages /> <Notifications /> <NewsFeedList /> </div> ) } // 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 ( <div> <h1>{data.name}</h1> <img src={data.avatar} /> </div> ) } // 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 ( <div> {data.items.map((item) => ( <NewsFeedItem item={item} /> ))} </div> ) } // 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 ( <div> <h1>{data.title}</h1> <p>{data.content}</p> </div> ) }


Bạn nghĩ sao? Bạn sẽ sử dụng một cái gì đó như thế này? Bạn có thấy giá trị trong việc xác định các phụ thuộc dữ liệu ở cấp độ thành phần hay bạn muốn gắn bó với việc xác định các thủ tục từ xa ở cấp độ trang? Tôi rất muốn nghe suy nghĩ của bạn ...


Chúng tôi hiện đang trong giai đoạn thiết kế để xây dựng trải nghiệm tìm nạp dữ liệu tốt nhất cho React, NextJS và tất cả các khuôn khổ khác. Nếu bạn quan tâm đến chủ đề này, hãy theo dõi tôi trên Twitter để cập nhật.


Nếu bạn muốn tham gia thảo luận và thảo luận về RFC với chúng tôi, vui lòng tham gia máy chủ Discord của chúng tôi.

Tiếp theo