昨天的依賴包含三個部分,副作用函式、設定檔以及文本資料,今天會展示如何把它們實做出來。
依賴注入的「依賴」依照不同慣例可能有不同的命名,接下來在程式碼中我們會稱它為 service (服務)
範例程式碼請參考 D12/dependency-injection
首先把表單的標題存起來,放在 en.ts
export const texts = {
addCourseForm: {
title: 'Add Course',
},
}
然後再把以下程式碼放到上圖中的 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 只顯示名稱
type 會顯示內容
以此類推建立 settings
建立時間服務
export interface Clock {
now: () => Date
}
export const clock = () => new Date()
把各種服務整理成一個 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 {}
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
// 注意 這是一個執行在伺服器端的非同步函式,所以我們可以用非同步的方式載入依賴
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 <></>
}
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 取用依賴資源,達到我們昨天的設計目標。