iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

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

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

  • 分享至 

  • xImage
  •  

相信昨天簡單 demo trpc的使用後,多多少少應該知道他的精髓吧哈哈,今天內容會是 trpccontext 運作流程,與其他相關的 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 dbconnections,或是一些 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-authsession 放到我們 context 中,值得注意的是因為每次呼叫 proceduretrpc 同時也會 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' ]

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

首先先定義 caller

這邊寫一個 router 是關於 find postapi,透過 inputtitle 去找到對應的 post,如果沒有找到 postthrow 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 formatreturn result,這也是為什麼推薦在 trpcTRPCErrorerror handler,因為你不管是 server 端還是 caller 端甚至是日後在 client 端呼叫時有一個通用的 error format 可以參照,不用怕其他 api error 不同,最後要記得加 500 error 的情況喔~

getHTTPStatusCodeFromError : TRPCcaller 只會回傳 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 resultinterface,可以定義 inputoutput 結果。

/**
 * 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 resultserver 上,通常會是在 nextssr 或是 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 : 所有 ssrmethod 都是透過他使用。
helpers.dehydrate() : 這一行比較特別的是一定要加,原因有點小複雜,簡單來說在 ssr 中呼叫 trpc 得方式跟在 client 不同的點在於,client 端是透過 fetch 的方式 ,ssr 則是 prefetch,差別在於 fetchreturn 結果,但 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) }}
直播中

尚未有邦友留言

立即登入留言