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 渲染到原生组件的方案。

最后一起看看它现在的样子!

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。