iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

一些讓你看來很強的全端- trcp 伴讀系列 第 5 篇

Day-05. 一些讓你看來很強的全端 TRPC 伴讀 -TRPC CONTEXT

  • 分享至 

  • xImage
  •  

相信昨天簡單 demo trpc的使用後,多多少少應該知道他的精髓吧哈哈,今天內容會是 trpc 的 context 運作流程,與其他相關的 helper function 介紹,讀者可以參考看看~

額外補充

trpc context 創建流程

從昨天的例子可以知道 trpc context 裡面包含所有 Procedure 會用到的 function,例如 :

const t = initTRPC.context().create();
const publicProcedure = t.procedure
const createTRPCRouter = t.router;

在 trpc 中可以透過 createContext() 共享 context ,如果讀者不懂 context 可以看Day4 的內容,createContext 通常用於 handle db 的 connections,或是一些 authentication 資訊如以下的 demo

import { initTRPC, type inferAsyncReturnType } from '@trpc/server';
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getSession } from 'next-auth/react';
 
export const createContext = async (opts: CreateNextContextOptions) => {
  const session = await getSession({ req: opts.req });
 
  return {
    session,
  };
};
 
const t1 = initTRPC.context<typeof createContext>().create();

這邊我們拿取 next-auth 的 session 放到我們 context 中,值得注意的是因為每次呼叫 procedure,trpc 同時也會 call createContext function,所以如果讀者有用到以下的功能的話記得要把 createContext 也放進去喔

  1. HTTP request
// 1. HTTP request
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
import { createContext } from './context';
import { appRouter } from './router';
const handler = createHTTPHandler({
  router: appRouter,
  createContext,
});
  1. Server Side Calls
// 2. Server-side call
import { createContext } from './context';
import { appRouter } from './router';
const caller = appRouter.createCaller(await createContext());
  1. servers-side helpers
// 3. servers-side helpers
import { createServerSideHelpers } from '@trpc/react-query/server';
import { createContext } from './context';
import { appRouter } from './router';
const helpers = createServerSideHelpers({
  router: appRouter,
  ctx: await createContext(),
});

但如果不需要 context 的話 return {} 就 ok~,下面一一介紹這三個功能。

HTTP request

透過 createHTTPHandler 創建 handler 搭配 createServer 成功 create http 連線 。

import { createServer } from 'http';
import { inferAsyncReturnType, initTRPC } from '@trpc/server';
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
const handler = createHTTPHandler({
  router: appRouter,
  createContext() {
    return {};
  },
});
createServer((req, res) => {
  /**
   * Handle the request however you like,
   * just call the tRPC handler when you're ready
   */
  handler(req, res);
});
server.listen(3333);

但在 next 中你只需要使用 createNextApiHandler 包在你的 api route 就可以~

// /pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';

import { appRouter } from '@/server/api/root';

// @see https://nextjs.org/docs/api-routes/introduction
export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}),
});

Server Side Calls

一個可以讓你在其他 server 呼叫的功能,確保你定義的 trpc route 執行是沒問題的。

const router = t.router({
  // Create procedure at path 'greeting'
  greeting: t.procedure
    .input(z.object({ name: z.string() }))
    .query((opts) => `Hello ${opts.input.name}`),
});

const caller = router.createCaller({});
const result = await caller.greeting({ name: 'trpc' }) // Hello trpc

甚至如果是 mutate 的話

const posts = ['One', 'Two', 'Three'];
const t = initTRPC.create();

const router = t.router({
  post: t.router({
    add:
      t.procedure
        .input(z.string())
        .mutation(({ input }) => {
          posts.push(input)
          return posts
        })
  })
})
const postCaller = router.createCaller({})
const result = await postCaller.post.add('Four') // [ 'One', 'Two', 'Three', 'Four' ]

看到這邊是不是覺得有那種 RPC 的 feeling 了呢!!,相信各個讀者都是很聰明的如果我們可以透過 call 去呼叫 trpc route ,那時不是我們可以透過 next api router 去呼叫呢?沒錯當然是沒問題拉~~來 demo 走起!

首先先定義 caller

這邊寫一個 router 是關於 find post 的 api,透過 input 的 title 去找到對應的 post,如果沒有找到 post 就 throw not found error

// "~/server/api/routers/example"
const postTitles = ['title1', 'title2', 'title3'];
const t = initTRPC.create();

export const helperRouter = t.router({
  post: t.router({
    getByTitle:
      t.procedure
        .input(z.object({
          title: z.string({
            required_error: 'postTitle is required',
            invalid_type_error: 'postTitle type is invalidate'
          })
        }))
        .query(({ input, ctx }) => {
          const { title } = input
          const post = postTitles.find(postTitle => postTitle === title)
          if (!post?.length) {
            throw new TRPCError({ code: 'NOT_FOUND', message: 'post not  found' })
          }
          return { post, createAt: new Date() }
        })
  })
})
export const postCaller = helperRouter.createCaller({})

// "~/server/api/root"
export const appRouter = createTRPCRouter({
  example: exampleRouter,
  helper: helperRouter
});

https://trpc.io/docs/server/error-handling

call caller in next api route

之後把 caller 放到 next api route 中,這邊我們定義一個 api. endpoint http://localhost:3000/api/trpccaller?postTitle=title1。

end point : /api/trpccaller
query : postTitle

整個邏輯很簡單,透過 caller 去呼叫 getByTitle ,同時把 query 帶進去,如果有找到就 return value,如果有 error 就去判斷是不是 trpc error,再根據 TRPCError error format 去 return result,這也是為什麼推薦在 trpc 用 TRPCError 做 error handler,因為你不管是 server 端還是 caller 端甚至是日後在 client 端呼叫時有一個通用的 error format 可以參照,不用怕其他 api error 不同,最後要記得加 500 error 的情況喔~

getHTTPStatusCodeFromError : TRPC 的 caller 只會回傳 response data ,可以透過 getHTTPStatusCodeFromError 來獲取 status 訊息 。

import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
import { NextApiRequest, NextApiResponse } from "next"
import { postCaller } from "~/server/api/routers/example"
import { RouterOutputs } from "~/utils/api";

type ResponseData = {
  data?: {
    //  這邊遇到一個 bug ,預期 createAt 是 Date type,但結果是 string,所以先用 Omit自行註解
    result: Omit<RouterOutputs['helper']['post']['getByTitle'], 'createAt'> & { createAt: Date };
  };
  error?: {
    message: string;
  };
};
// title1
export default async (
  req: NextApiRequest,
  res: NextApiResponse<ResponseData>,
) => {
  const postTitle = req.query.postTitle as string
  try {
    const result = await postCaller.post.getByTitle({ title: postTitle })
    res.status(200).json({ data: { result } });
  } catch (e) {
    if (e instanceof TRPCError) {
      const errorMessage = formatTrpcError(e)
      const httpStatusCode = getHTTPStatusCodeFromError(e);
      res.status(httpStatusCode).json({ error: { message: errorMessage } });
    }
    res.status(500).json({
      error: { message: `Error while accessing post with title: ${postTitle}` },
    })
  }
}
interface ErrorMessage {
  code: string;
  expected: string;
  received: string;
  path: string[];
  message: string;
}
const formatTrpcError = (e: TRPCError) => {
  const { code, message } = e
  if (code === 'BAD_REQUEST') {

    const errorResults: ErrorMessage[] = JSON.parse(e.cause?.message as string)
    const { code, expected, received, message } = errorResults[0] as ErrorMessage
    return `${message} ,expected ${expected} received ${received} `
  }
  return message
}

RouterOutputs 是一個幫助你查詢 api result 的 interface,可以定義 input 跟 output 結果。

/**
 * Inference helper for inputs.
 *
 * @example type HelloInput = RouterInputs['example']['hello']
 */
export type RouterInputs = inferRouterInputs<AppRouter>;

/**
 * Inference helper for outputs.
 *
 * @example type HelloOutput = RouterOutputs['example']['hello']
 */
export type RouterOutputs = inferRouterOutputs<AppRouter>;

同時 trpc 很貼心的是有先定義好 error format TRPCError , 返回格式如下:

{
  "id": null,
  "error": {
    "message": "\"password\" must be at least 4 characters",
    "code": -32600,
    "data": {
      "code": "BAD_REQUEST",
      "httpStatus": 400,
      "stack": "...",
      "path": "user.changepassword"
    }
  }
}

所以我們就可以在 formatTrpcError 根據 TRPCError 格式處理 error message。
如果想知道其他 error code 可以參考 官網

最後放上結果~

success return

Not found error

忘記加 query

server error

Server Side helpers

Server Side helpers 的目的就是 prefetch queries result 在 server 上,通常會是在 next 的 ssr 或是 ssg 上做使用,prefetch 目的是 populating query cache on the server,這樣的好處是可以優化 client 端在在呼叫 useQuery 時 call api的時間,本來是透過 http request 改成拿 query cahce 資料,這是一種前端優化效能的方式,這樣 client 端可以更快載入頁面內容,prefetch 一直是前端性能很大的輔助工具,可以提高 FCP(First Contentful Paint) 用戶體驗。

demo 如下:

import { GetServerSideProps, InferGetServerSidePropsType } from 'next'
import { createServerSideHelpers } from '@trpc/react-query/server';
import React from 'react'
import { helperRouter } from '~/server/api/routers/example';
import superjson from 'superjson';
import { api } from '~/utils/api';

export const getServerSideProps: GetServerSideProps<{ title: string }> = async (context) => {
  const title = context.params?.title as string
  // 所有 ssg 內容都是透過 createServerSideHelpers 產生
  const helpers = createServerSideHelpers({
    router: helperRouter,
    ctx: {},
    transformer: superjson
  })
  // prefetch 並不會 return result
  await helpers.post.getByTitle.prefetch({ title })
  return {
    props: {
      trpcState: helpers.dehydrate(),
      title
    }
  }
}
function Page({ title }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  const { data, status, ...rest } = api.helper.post.getByTitle.useQuery({ title })
  if (status === 'loading') return 'loading...'
  if (data === undefined) return 'no data'
  return (
    <div>
      {data?.post ? data.post : 'no title'}
      <em>CreateAt : {new Date(data?.createAt).toLocaleDateString()}</em>
    </div>
  )
}

export default Page

createServerSideHelpers : 所有 ssr 的 method 都是透過他使用。
helpers.dehydrate() : 這一行比較特別的是一定要加,原因有點小複雜,簡單來說在 ssr 中呼叫 trpc 得方式跟在 client 不同的點在於,client 端是透過 fetch 的方式 ,ssr 則是 prefetch,差別在於 fetch 會 return 結果,但 prefetch 並不會甚至不會 throw error ,而如果必須把 prefetch 的內容放到 cache 中,那就必須把 prefetch 結果或序列化後傳到 client 端,也就是透過 helpers.dehydrate 方式 ,使用 prefetch 一定要加這段。

額外補充 data transform

在 context 中你可以指定 request input 或是 output 序列化的方式確保 value 不會因為 js 做型別轉換,例如 new Date() 變成 string。

step1 npm install

npm i superjson devalue

step 2 definded transformer

// src/transformer.ts
import { uneval } from 'devalue';
import superjson from 'superjson';
export const transformer = {
  input: superjson,
  output: {
    serialize: (object) => uneval(object),
    // This `eval` only ever happens on the **client**
    deserialize: (object) => eval(`(${object})`),
  },
};

step 3 add to the route

import { initTRPC } from '@trpc/server';
import { transformer } from '../../utils/trpc';
export const t = initTRPC.create({
  transformer,
});
export const appRouter = t.router({
  // [...]
});

step 4 add to the createTRPCNext()

export const api = createTRPCNext<AppRouter>({
  config() {
    return {
      /**
       * Transformer used for data de-serialization from the server.
       *
       * @see https://trpc.io/docs/data-transformers
       */
      transformer: superjson,
        ...
    }
})

今日回顧:

  1. context HTTP request 如何 share。
  2. 自定義 api handle Server Side Calls。
  3. Server Side helpers 幫你 request 做 prefetch。
  4. data transform。

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day-04. 一些讓你看來很強的全端 TRPC 伴讀 - T3 Stack 介紹(下)
下一篇
Day-06. 一些讓你看來很強的全端 TRPC 伴讀 - Define Routers
系列文
一些讓你看來很強的全端- trcp 伴讀 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言