iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0

昨天的依賴包含三個部分,副作用函式、設定檔以及文本資料,今天會展示如何把它們實做出來。

依賴注入的「依賴」依照不同慣例可能有不同的命名,接下來在程式碼中我們會稱它為 service (服務)

範例程式碼請參考 D12/dependency-injection

建立服務

https://ithelp.ithome.com.tw/upload/images/20230928/20158615M1uGd6AWmE.png

  1. 首先把表單的標題存起來,放在 en.ts

    export const texts = {
      addCourseForm: {
        title: 'Add Course', 
      },
    }
    
  2. 然後再把以下程式碼放到上圖中的 index.ts

    import { texts } from './en'
    
    //根據英文版本推演型別,這樣就可以確保以後新增其他語言的時候,型別相符
    type _Texts = typeof texts 
    
    //特別弄成 interface 是為了方便確認型別"名稱",差別可以查看下面比較圖
    export interface Texts extends _Texts {}
    
    //定義各語言的載入函式,不僅限於這種載入方式,也可以考慮透過 fetch 等方式取得
    const map = {
    en: (): Promise<Texts> => import('./en').then((module) => module.texts),
    }
    
    //把物件轉換成具有輸入型別限制的 import function
    export const importTexts = (locale: keyof typeof map): Promise<Texts> =>
    map[locale]()
    

    滑鼠移到變數上
    interface 只顯示名稱
    https://ithelp.ithome.com.tw/upload/images/20230928/20158615dI7FMJ4xMl.png
    type 會顯示內容
    https://ithelp.ithome.com.tw/upload/images/20230928/20158615pWmj7osLEE.png

  3. 以此類推建立 settings

  4. 建立時間服務

    export interface Clock {
      now: () => Date
    }
    
    export const clock = () => new Date()
    
  5. 把各種服務整理成一個 Service

    import { Clock } from './clock'
    import { Settings } from './settings'
    import { Texts } from './texts'
    
    export interface Constants {
      settings: Settings
      texts: Texts
    }
    
    export interface Functions {
      clock: Clock
    }
    
    // 注意我們這邊看到的都是 "interface" ,這表示我們隨時可以抽換實作
    export interface Service extends Constants, Functions {}
    

建立儲存服務的 Atom

https://ithelp.ithome.com.tw/upload/images/20230928/20158615alxx1LWPmk.png

import { atom } from 'jotai'
import { Constants, Functions, Service } from '.'
import { Clock, clock } from './clock'
import { Settings } from './settings'
import { settings } from './settings/default'
import { Texts } from './texts'
import { texts } from './texts/en'

//三個基礎的 Atom

export const settingsAtom = atom<Settings>(settings)

export const textsAtom = atom<Texts>(texts)

export const clockAtom = atom<Clock>(clock)

//基於基礎型別堆疊出兩個大一點的 atom

export const functionsAtom = atom(
  (get): Functions => ({   // get function 決定如何讀取 atom
    clock: get(clockAtom),
  }),
  (_, set, { clock }: Functions) => {
    set(clockAtom, clock)  // set function 決定如何寫入 atom
  }
)

export const constantsAtom = atom(
  (get): Constants => ({
    settings: get(settingsAtom),
    texts: get(textsAtom),
  }),
  (_, set, { settings, texts }: Constants) => {
    set(settingsAtom, settings)
    set(textsAtom, texts)
  }
)

//最後堆疊出整個 service atom

export const serviceAtom = atom(
  (get): Service => ({
    ...get(constantsAtom),
    ...get(functionsAtom),
  }),
  (_, set, { settings, texts, clock }: Service) => {
    set(constantsAtom, { settings, texts })
    set(functionsAtom, { clock })
  }
)

新增負責提供服務的客戶端元件

hydrate atom 的用途是可以替換掉 atom 的初始值

回顧昨天的這張圖,我們需要透過 server component 在 runtime 做 atom 的初始化
要達成這個目標我們就需要 hydrate atom 的協助

'use client' // 一定要用 use client,才可以使用 react context 相關的 hook

import { useHydrateAtoms } from 'jotai/utils'
import { FC } from 'react'
import { Constants } from '../data/service'
import { constantsAtom } from '../data/service/atoms'

export const ServiceProvider: FC<Constants> = (constants) => {
  useHydrateAtoms([
      [constantsAtom, constants] //[ 替換初始值的 atom, 要替換掉的初始值]
  ])
  return <></>
}

這邊要注意的是一個 atom 只應該做一次 useHydrate()
如果需要在不同的地方對同一個 atom 載入不同的初始值,可以依照不同 store 提供
這邊有點離題,建議有興趣可以參考 Jotai SSR

透過伺服器元件注入常數依賴

https://ithelp.ithome.com.tw/upload/images/20230928/20158615MerVDdWnVP.png

// 注意 這是一個執行在伺服器端的非同步函式,所以我們可以用非同步的方式載入依賴
const RootLayout = async ({ children }: { children: React.ReactNode }) => {

  // 我們可以透過所處的環境動態決定要讀取哪種設置、哪一國語言的文本
  // 例如從 user 的 profile、從 router 的路徑等等
 
  const settings = await importSettings('default')
  const texts = await importTexts('en')
  const constants = { settings, texts }

  return (
    <html lang="en" className="light">
      <body className={inter.className}>
        <ServiceProvider {...constants} /> //然後把讀回來的依賴交給客戶端元件設定 Atom
        <div className="h-screen w-screen schema flex flex-col">
          <div className="border-b dark:border-gray-600 h-[65px]">
            <NavBar />
          </div>
          <div className="container mx-auto min-w-screen h-[calc(100vh-65px)]">
            {children}
          </div>
        </div>
      </body>
    </html>
  )
}

不過這邊只注入了常數依賴,那副作用函式的依賴該如何注入呢?
這邊比較可惜就是,伺服器元件是要經過序列化,化成資料傳遞給客戶端元件的,因此我們沒有辦法把函式用像上面這樣用 <ServiceProvider {...constants} /> 的方式傳遞給客戶端元件。

透過伺服器元件注入常數依賴

所以我們就只能在 ServiceProvider 內部做初始化設定了,例如這樣

'use client' // 一定要用 use client,才可以使用 react context 相關的 hook

const myNewClock:Clock = { 
    now: ()=>new Date('2020-02-02T02:02:02Z') 
}

export const ServiceProvider: FC<Constants> = (constants) => {
  useHydrateAtoms([
      [constantsAtom, constants],
      [clockAtom, myNewClock]
  ])
  return <></>
}

引用外部依賴

https://ithelp.ithome.com.tw/upload/images/20230928/20158615B9172AjLxM.png

const AddCourseForm: FC = () => {
  const texts = useAtomValue(textsAtom)
  return (
    <SideMenu
      title={texts.addCourseForm.title}
      className="w-[500px]"
      Footer={Footer}
    >
      <CourseName />
      <DateRange />
      <Description />
      <Users />
    </SideMenu>
  )
}

透過這些方式,我們就可以在整個 Next.js 的最外圍 Layout.tsx 做依賴的注入
讓所有客戶端元件隨時能透過 Atom 取用依賴資源,達到我們昨天的設計目標。


上一篇
D12 - 設計依賴注入
下一篇
D14 - 設計異步流程
系列文
從 Next.js 開始的 Functional Programming30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言