iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
Modern Web

從 Next.js 開始的 Functional Programming系列 第 19

D19 - 實作異步流程 (五)

  • 分享至 

  • xImage
  •  

今天的目標是把前端的步驟 1 、2、6、7 串聯起來。

步驟 1

分析來自 UI 的未知事件,在我們的應用上具有什麼商業意義

const parseEvent = (event: unknown): 'Nothing' | 'AddUserEvent' =>
  pipe(
    Match.value(event),
    Match.when({ key: Match.string }, ({ key }) =>
      pipe(
        Match.value(key),
        Match.when('Enter', () => 'AddUserEvent' as const),
        Match.orElse(() => 'Nothing' as const)
      )
    ),
    Match.orElse(() => 'AddUserEvent' as const)
  )

步驟 2

送出請求,把可能發生的未知的錯誤轉換成 RequestError

const getUser = (name: string): Effect.Effect<never, RequestError, any> =>
  Effect.tryPromise({
    try: () => axios.get(`/api/v1/users/${name}`).then((res) => res.data),
    catch: (error) => requestErrorOf(error),
  })

axios 提供了一個 isAxiosError 函式,可以用來幫助我們確認型別。例如以下範例,我們可以根據與後端資料提供服務約定好的格式,來把不同的錯誤代碼轉換成具有意義的錯誤型別。

export type RequestError =
  | UnexpectedRequestError
  | NotFoundError
  | UnexpectedAxiosError

export const requestErrorOf = (error: unknown): RequestError =>
  isAxiosError(error)
    ? pipe(
        Match.value(error.response?.status),
        Match.when(404, () => notFoundErrorOf(error)),
        // Match.when(400, 401, 403, 500
        Match.orElse(() => unexpectedRequestErrorOf(error))
      )
    : unexpectedRequestErrorOf(error)

步驟 6

在上一步驟, getUser 的回傳值是 any,這非常危險,所以我們要用講了好幾天的 schema 來解析它。

class Administrator extends S.Class<Administrator>()({
  _tag: S.literal('Administrator'),
  name: S.Date,
}) {}

class Participant extends S.Class<Participant>()({
  _tag: S.literal('Participant'),
  name: S.string,
}) {}

const userSchema = S.union(Administrator, Participant)

export const validateUser: (
  props: unknown
) => Either.Either<ValidateError, User> = flow(
  S.parseEither(userSchema),
  // 這邊 ValidateError.of 只是把原生的 ParseError 再包裝一層,有興趣可看原始碼
  Either.mapLeft(ValidateError.of) 
)

不過 validateUserEithergetUser 的輸出是 Effect,會不會有不能合在一起的問題呢? 其實不用擔心,我們可以把 Either 當作一種特殊的 Effect,直接 flatMap 串起來就好囉 !

type GetUserError = ValidateError | RequestError

const getUser = (name: string): Effect.Effect<never, GetUserError, User> =>
  pipe(
    Effect.tryPromise({
      try: () => axios.get(`/api/v1/users/${name}`).then((res) => res.data),
      catch: (error) => requestErrorOf(error),
    }),
    Effect.flatMap(validateUser)
  )

藉由以上一連串操作,新版本的 getUser 函式加上驗證,就從 any 變成了 User。此外錯誤型別也擴展了,變成 GetUserError

步驟 7

接下來是今天的重頭戲,回歸新增使用者的異步流程,我們要思考我們要放回 Atom狀態 是甚麼 ? 使用者按下 Enter 或是 Add 後畫面會如何發生改變 ? 我們如何測試 ? 如何確保變更如我們預期 ?

以下概念很適合用來概括前端框架的畫面變更模式

type update = (event:Event) => (stateBefore:Sate) => (stateAfter:State)

把以上概念轉換成新增使用者 這個需求,實作出來就會變成這樣

const onAddUserEvent = (event: 'AddUserEvent') => (field: UsersField) =>
  pipe(
    getUser(field.input),
    Effect.match({
      onFailure: (error) => UsersField.updateError(error)(field),
      onSuccess: (user) => UsersField.addUser(user)(field),
    })
  )

// UsersField.on
const on = (event: 'AddUserEvent' | 'Nothing') => (field: UsersField) =>
  pipe(
    Match.value(event),
    Match.when('Nothing', () => Effect.succeed(field)),
    Match.when('AddUserEvent', (event) => onAddUserEvent(event)(field)),
    Match.exhaustive
  )

對我們來說 UsersField 變成了我們可以掌控的 狀態機。現在它可以根據收到的 事件 來決定該如何做 狀態 的轉換,讓複雜的流程在這樣的設計下,變得簡單而透明。


上一篇
D18 - 實作異步流程 (四)
下一篇
D20 - 實作異步流程 (六)
系列文
從 Next.js 開始的 Functional Programming30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言