当 optional SDK 像 core infrastructure 一样行事

Vibe-coded mobile app 里一个很常见的 launch failure 是:analytics、attribution、crash reporting 和真正 critical 的 service 被放在同一条初始化路径上。如果这些 optional SDK 里有一个在某个 device 或某种 network condition 下出问题,整个 app 可能会在第一个 screen 出来之前就直接死掉。

之所以会这样,是因为 AI 生成的 setup code 往往会把每一个 dependency 都当成同等重要。没有明确的 tier system,must-have infrastructure 和 nice-to-have observer 之间就没有架构层面的区别。结果就是所有东西都会意外地变成 must-have。

依赖有三个 tier

你的 app 里的每一个 dependency,实际上都只能落在三个 bucket 里的一个。把这三条线明确画出来,是一个能随着增长继续活下去的 app,和一个被无数 production crash 慢慢拖死的 app 之间的差别。

  • Hard dependencies:这些一旦失败,app 就无法 render。config parsing、storage 创建、core service 构建。这些是地基。
  • Soft dependencies:这些可以 graceful fail。auth、依赖 network 的 feature、依赖 permission 的 flow。app shell 仍然能 render,用户看到的是降级体验,而不是 crash。
  • Optional dependencies:这些可以 silent fail。analytics、crash reporting、A/B testing。它们绝不能阻塞 app。它们是 observer,不是 participant。

大多数 Vibe-coded app 会犯的错误,就是把所有东西都当成 hard dependency。每一个 SDK 都在 startup 阶段初始化。每一次 failure 都变成 startup crash。但 analytics SDK 根本不配和 storage layer 站在同一个等级上。

Hard Dependency Boundary

hard boundary 负责 validate config 和构建 services。如果它失败了,app 应该展示一个带 retry button 的 error screen,而不是一个会让用户去 App Store 打一星的纯白空屏。

// src/context/hard-dependency-boundary.tsx
// This is the gatekeeper. If it fails, nothing else renders.

import * as SplashScreen from 'expo-splash-screen'

void SplashScreen.preventAutoHideAsync()

export function HardDependencyBoundary({
  children,
}: {
  children: (services: Services) => ReactNode
}) {
  const { services, ready, error, retry } =
    useHardDependencies()

  useEffect(() => {
    if (ready || error) void SplashScreen.hideAsync()
  }, [ready, error])

  if (error) {
    return (
      <ErrorScreen
        title='Startup failed'
        message={error.message}
        onRetry={retry}
      />
    )
  }

  if (!ready || !services) return null

  return <>{children(services)}</>
}

注意这里的 render prop pattern。boundary 会把初始化好的 services 传给 children。children 不负责构建 services,只负责接收它们。构建只发生一次,任何 failure 都在 boundary 这一层被接住,而不是一路漏进 production crash report。

Optional Boundary

optional dependencies 就完全不同了。它们绝不能让你的 app crash。即使 Mixpanel 挂了,你的 app 也不应该抖一下。

// src/context/optional-dependency-boundary.tsx
// If analytics fails, we log a warning and keep going.

export function OptionalDependencyBoundary({
  children,
}: PropsWithChildren) {
  const { analytics, crashReporting } = useServices()

  useEffect(() => {
    try {
      analytics.track('app_started')
    } catch (error) {
      if (__DEV__) console.warn('Analytics failed:', error)
    }

    try {
      crashReporting.setContext('app', { started: true })
    } catch (error) {
      if (__DEV__) console.warn('Crash reporting failed:', error)
    }
  }, [analytics, crashReporting])

  return <>{children}</>
}

这两种姿态的差别非常明显。hard boundary 失败了就展示 error screen。optional boundary 失败了就记一条 warning,然后继续往下跑。两者都是明确的,谁也不假装自己是另一种东西。

Vibe-coded app 的大多数 production crash,最后都能追溯到同一个错误:把 soft 或 optional dependency 当成 hard dependency 来处理。Autotomy Expo Starter Pack 从第一天就把这几条线画清楚了,所以你不用在凌晨 2 点才学会这个教训。