IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    主题系统、Local-First

    Hyoban发表于 2024-07-28 17:47:02
    love 0

    第二周我简单定义了一下 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>) {
        console.info('subscribing to', key)
    
        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)
    
          listener = addDatabaseChangeListener(({ tableName }) => {
            if (config.name === tableName) {
              query.then((data) => { next(undefined, data) })
                .catch((error) => { next(error) })
            }
          })
        }
    
        return () => {
          listener?.remove()
        }
      }
    
      return useSWRSubscription<Awaited<T>, any, SWRSubKey>(
        key,
        subscribe as any,
      )
    }
    

    OK,这样我们只要在请求数据的时候正确地设置 key,就能高效地获取最新的数据了。配合下拉刷新和定时同步数据,我们的 APP 就能够实现基本的 Local First 了。

    What's Next?

    这周我还使用 webview 展示了 feed entry 的详情,但总觉得首次加载的时候有点慢?不知道是需要特殊的优化策略还是 webview 局限就是如此。下周看看把 html 渲染到原生组件的方案。

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



沪ICP备19023445号-2号
友情链接