信息流页面接入了 RCP 的 HTTP 缓存,第一版跑得很舒服:内存缓存加磁盘缓存,重复请求直接命中,流量和加载时间都降下来了。然后测试提了一个问题:A 用户退出登录,B 用户登录,B 在首页看到的第一条内容是 A 刚才浏览过的。

去看缓存键,只有 URL。A 和 B 请求同一个接口地址,缓存键完全一样,系统按服务端的 Cache-Control 判定缓存有效,直接把 A 的响应体返回给了 B。

RCP 的 HTTP Cache 是双层结构:内存缓存速度最快,应用退出即失效;磁盘缓存持久化在应用沙箱里,重启后仍在。缓存命中与否依据 HTTP 响应头里的 Cache-Control、ETag 等信息判断,不是所有请求都该缓存。但“该不该缓存”和“缓存归谁”是两件事,后者不在 HTTP 协议的管辖范围内。

自定义拦截器改写缓存键

RCP 提供了拦截器机制,允许在请求进入缓存管线之前改写 Request。带鉴权信息的请求,在拦截器里把用户 ID 拼进 URL 或者用一个独立的缓存分区标记,让不同用户的请求落在不同的缓存空间上。

typescript

import { rcp } from '@kit.RemoteCommunicationKit'

class UserScopedCacheInterceptor implements rcp.Interceptor {
  async intercept(
    context: rcp.RequestContext,
    next: rcp.RequestHandler
  ): Promise<rcp.Response> {
    const userId = AuthStore.currentUserId()
    if (userId) {
      const url = new URL(context.request.url)
      url.searchParams.set('_cache_scope', userId)
      context.request.url = url.toString()
    }
    return next.handle(context)
  }
}

const session = rcp.createSession({
  interceptors: [new UserScopedCacheInterceptor()]
})

_cache_scope 参数不会发给服务端,它只参与缓存键的计算。服务端看到的仍然是原始 URL,缓存层看到的 URL 已经带上了用户维度。登录和退出时切换 AuthStore.currentUserId() 的返回值,下一次请求的缓存键自然切换到新用户。

如果不想污染 URL 结构,也可以在拦截器里维护一个独立的 Map<string, string> 把“业务 URL + userId”映射成一个内部缓存 key,但 RCP 的缓存层不直接暴露缓存键的读写接口,URL 参数是改动面最小的做法。

动态接口和静态资源要分开的缓存策略

所有接口一律缓存是缓存体系里最容易踩的坑:动态接口更新之后用户看到的还是旧数据。静态资源可以长缓存,用户信息、列表这类动态接口应该短缓存或者不缓存,敏感数据(token 刷新接口、支付结果查询)完全不应该进入 HTTP 缓存层。

typescript

class CachePolicyInterceptor implements rcp.Interceptor {
  private static readonly NO_CACHE_PATHS = ['/auth/refresh', '/payment/status']
  private static readonly SHORT_CACHE_PATHS = ['/feed', '/notifications']

  async intercept(context: rcp.RequestContext, next: rcp.RequestHandler): Promise<rcp.Response> {
    const path = new URL(context.request.url).pathname

    if (CachePolicyInterceptor.NO_CACHE_PATHS.some(p => path.startsWith(p))) {
      context.request.headers = {
        ...context.request.headers,
        'Cache-Control': 'no-store'
      }
    }

    return next.handle(context)
  }
}

no-store 让请求完全跳过缓存读写。短缓存路径可以保持服务端 Cache-Control 的默认行为,配合用户维度的缓存键,数据更新后用户切一下 tab 或者下拉刷新就能拿到新值。

多个 Session 共享同一份缓存实例

RCP 的缓存挂在 Session 上。如果应用里创建了多个 Session,每个 Session 各自建一套内存和磁盘缓存,同一个资源在不同 Session 之间会被重复缓存,磁盘占用翻倍。

正确的做法是整个应用共用一个 rcp.Session,或者至少让所有 Session 引用同一个缓存配置。createSession 是重调用,它背后持有连接池和拦截器管线,不是“每次请求 new 一个”的轻量对象。

typescript

class ApiSession {
  private static instance: rcp.Session | null = null

  static get(): rcp.Session {
    if (!ApiSession.instance) {
      ApiSession.instance = rcp.createSession({
        baseAddress: 'https://api.example.com',
        interceptors: [
          new UserScopedCacheInterceptor(),
          new CachePolicyInterceptor()
        ]
      })
    }
    return ApiSession.instance
  }
}

缓存键带上用户维度、动态接口显式排除、Session 单例共享缓存实例,三件事做完,HTTP 缓存的收益才会落在“该省的地方省、该新的地方新”上。只按 URL 做缓存键,本质上是在赌这个 URL 永远不区分用户,线上环境不会给你这个面子。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐