Hyoban

Hyoban

Don’t do what you should do, do you want.
x
github
email
telegram
follow

主題系統、Local-First

第二周我簡單定義了一下 APP 的主題(其實主要就是顏色),搭建 APP 的資料庫並利用數據構建展示介面。

主題系統#

初始化項目的時候我選的 Tamagui,但是當我要開始自定義主題系統的時候,看文檔看得我頭暈😵‍💫。加上它大包大攬的風格,讓我切換到了 Unistyles

它的主題系統就是普通的對象,我只需要將我十分喜歡的 Radix Color 傳遞給它就好。和 Tailwind 的配色不同的是,它為每個顏色都設計了對應的深色,支持深色主題變得十分簡單。

export const lightTheme = {
  colors: {
    ...accent,
    ...accentA,
  },
} as const

export const darkTheme = {
  colors: {
    ...accentDark,
    ...accentDarkA,
  },
} as const

因為要傳遞的顏色還比較多,容易忘記在對應的深色主題也添加上對應的主題,可以通過類型檢查來進行約束。參考 How to test your types 一文。

type Expect<T extends true> = T
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
type _ExpectLightAndDarkThemesHaveSameKeys = Expect<Equal<
  keyof typeof lightTheme.colors,
  keyof typeof darkTheme.colors
>>

此外,你可以利用它的運行時來輕鬆修改主題,那麼寫像下面這樣的動態主題切換就十分簡單了。

UnistylesRuntime.updateTheme(
  UnistylesRuntime.themeName,
  oldTheme => ({
    ...oldTheme,
    colors: {
      ...oldTheme.colors,
      ...accent,
      ...accentA,
      ...accentDark,
      ...accentDarkA,
    },
  }),
)

Local First#

如果說是寫網頁的話,不做 Local First 還情有所原。APP 作為可以跑 SQLite 的環境,沒什麼理由不能在無網的環境中打開。目前我的想法是 APP 主要和本地的資料庫進行交互,利用網絡請求來進行數據的同步。

關於技術棧的選型,毫不猶豫的就選擇了 drizzle,原因有如下幾點:

  1. 目前 Follow 的 server 端也在用,我甚至能 copy 很多表的定義。
  2. 比起 Prisma 這種利用代碼生成來做類型的庫,我還是更喜歡用 ts 來寫表定義,讓類型即時刷新。
  3. Expo 官方文檔 推薦的和 Expo SQLite 的整合就是 drizzle,Prisma 的集成 還處在 Early Access 階段。

Expo SQLite 提供了 addDatabaseChangeListener 的介面,使得我們可以實時獲得資料庫中最新的數據,drizzle 就提供了 useLiveQuery 的封裝。不過目前它的 hook 存在沒有正確處理 useEffect 依賴數組的問題:

此外,我們還需要對結果進行快取,否則來會切換頁面時會有很多不必要的資料庫查詢。所以,我們自己利用 swr 來包裝一個 hook。

import { is, SQL, Subquery } from 'drizzle-orm'
import type { AnySQLiteSelect } from 'drizzle-orm/sqlite-core'
import { getTableConfig, getViewConfig, SQLiteTable, SQLiteView } from 'drizzle-orm/sqlite-core'
import { SQLiteRelationalQuery } from 'drizzle-orm/sqlite-core/query-builders/query'
import { addDatabaseChangeListener } from 'expo-sqlite/next'
import type { Key } from 'swr'
import type { SWRSubscriptionOptions } from 'swr/subscription'
import useSWRSubscription from 'swr/subscription'

export function useQuerySubscription<
  T extends
  | Pick<AnySQLiteSelect, '_' | 'then'>
  | SQLiteRelationalQuery<'sync', unknown>,
  SWRSubKey extends Key,
>(
  query: T,
  key: SWRSubKey,
) {
  function subscribe(_key: SWRSubKey, { next }: SWRSubscriptionOptions<Awaited<T>, any>) {
    const entity = is(query, SQLiteRelationalQuery)
    // @ts-expect-error
      ? query.table
      // @ts-expect-error
      : (query as AnySQLiteSelect).config.table

    if (is(entity, Subquery) || is(entity, SQL)) {
      next(new Error('Selecting from subqueries and SQL are not supported in useQuerySubscription'))
      return
    }

    query.then((data) => { next(undefined, data) })
      .catch((error) => { next(error) })

    let listener: ReturnType<typeof addDatabaseChangeListener> | undefined

    if (is(entity, SQLiteTable) || is(entity, SQLiteView)) {
      const config = is(entity, SQLiteTable) ? getTableConfig(entity) : getViewConfig(entity)

      let queryTimeout: NodeJS.Timeout | undefined
      listener = addDatabaseChangeListener(({ tableName }) => {
        if (config.name === tableName) {
          if (queryTimeout) {
            clearTimeout(queryTimeout)
          }
          queryTimeout = setTimeout(() => {
            query.then((data) => { next(undefined, data) })
              .catch((error) => { next(error) })
          }, 0)
        }
      })
    }

    return () => {
      listener?.remove()
    }
  }

  return useSWRSubscription<Awaited<T>, any, SWRSubKey>(
    key,
    subscribe as any,
  )
}

注意這裡的 queryTimeout !!!。由於表變化可能十分頻繁,我們需要取消掉之前的查詢,否則會影響查詢的效率。Drizzle 還不支持用 AbortSignal 來取消查詢,所以用 setTimeout 來處理。

https://github.com/drizzle-team/drizzle-orm/issues/1602

OK,這樣我們只要在請求數據的時候正確地設置 key,就能高效地獲取最新的數據了。配合下拉刷新和定時同步數據,我們的 APP 就能夠實現基本的 Local First 了。

What's Next?#

這周我還使用 webview 展示了 feed entry 的詳情,但總覺得首次加載的時候有點慢?不知道是需要特殊的優化策略還是 webview 局限就是如此。下周看看把 html 渲染到原生組件的方案。

最後一起看看它現在的樣子!

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。