考察点分析

该问题主要考察以下核心能力维度:

  1. 工程化构建理解:Webpack资源指纹机制与构建产物管理
  2. 浏览器存储方案:LocalStorage特性边界与存储策略设计
  3. 离线缓存架构:版本控制策略与缓存更新机制设计

具体技术评估点:

  • Webpack文件指纹(chunkhash/contenthash)的运用场景
  • Base64编码与二进制存储的性能取舍
  • 版本清单(manifest)的增量更新策略
  • 缓存淘汰算法在存储限制下的应用
  • 资源加载失败的回退机制设计

技术解析

关键知识点

  1. 资源指纹 > 版本清单设计 > 存储配额管理
  2. 增量更新 > 全量更新 > 缓存失效策略

原理剖析

Webpack通过[contenthash]生成带哈希值的文件名,实现资源内容变更检测。构建时生成manifest文件记录资源映射关系,前端通过对比远程manifest与本地存储的版本号判断缓存状态。LocalStorage存储时需注意:

  1. 文本资源直接存储
  2. 二进制资源转为Base64(需注意33%体积膨胀)
  3. 使用LRU算法管理存储空间

缓存更新采用双轨制:加载时检查版本清单,后台静默更新检测到的新资源,下次访问时生效。更新过程中使用window.requestIdleCallback实现低优先级更新,避免阻塞主线程。

常见误区

  1. 误用同步API导致页面卡顿
  2. 未处理存储配额溢出异常
  3. 忽略Base64编码的性能损耗
  4. 版本对比使用时间戳而非内容哈希

问题解答

实现方案分为四个阶段:

  1. 构建阶段
  // webpack.config.js
output: {
  filename: '[name].[contenthash:8].js',
  chunkFilename: '[name].[contenthash:8].chunk.js'
}

// 生成manifest.json
new WebpackManifestPlugin({
  fileName: 'asset-manifest.json'
})
  
  1. 版本比对
  async function checkUpdate() {
  const remoteManifest = await fetch('/asset-manifest.json')
  const localManifest = localStorage.getItem('ASSET_MANIFEST')
  
  return compareVersions(remoteManifest, localManifest) 
}
  
  1. 缓存策略
  function cacheResource(url) {
  return caches.open('v1').then(cache => {
    return cache.match(url).then(res => {
      return res || fetch(url).then(netRes => {
        // 存储时处理二进制资源
        if (/\.(png|jpg)$/.test(url)) {
          return netRes.blob().then(blob => {
            const reader = new FileReader()
            reader.readAsDataURL(blob)
            reader.onload = () => {
              localStorage.setItem(url, reader.result)
            }
          })
        }
        return netRes.text().then(text => localStorage.setItem(url, text))
      })
    })
  })
}
  
  1. 更新机制
  function silentUpdate() {
  requestIdleCallback(async () => {
    const { changedFiles } = await checkUpdate()
    changedFiles.forEach(file => {
      cacheResource(file.url).then(() => 
        postMessage({ type: 'ASSET_UPDATED' }))
    })
  })
}
  

解决方案

编码示例

  class OfflineCache {
  constructor() {
    this.MAX_SIZE = 4 * 1024 * 1024 // 4MB
  }

  async initialize() {
    const manifest = await this._fetchLatestManifest()
    if (!this._compareWithLocal(manifest)) {
      await this._updateCache(manifest)
    }
    this._registerAutoUpdate()
  }

  _updateCache(manifest) {
    const storageSize = this._calculateStorageSize()
    return Promise.all(
      Object.entries(manifest.files).map(([url, hash]) => {
        if (storageSize > this.MAX_SIZE) {
          this._applyLRU() // 执行缓存淘汰
        }
        return this._cacheFile(url, hash)
      })
    )
  }

  _cacheFile(url, hash) {
    return new Promise((resolve) => {
      const content = localStorage.getItem(url)
      if (content && content.hash === hash) return resolve()
      
      fetch(url).then(response => {
        // ...存储逻辑
      })
    })
  }
}
  

可扩展性建议

  1. 大流量场景:采用分片存储策略,结合IndexedDB存储大文件
  2. 低端设备:增加资源加载超时检测,降级为常规网络请求
  3. 监控体系:集成Performance API监控缓存命中率

深度追问

  1. 如何防止缓存污染?

    • 内容签名校验机制
  2. Service Worker与LocalStorage方案差异?

    • 存储机制/缓存控制粒度/生命周期
  3. LocalStorage存满后的优雅降级?

    • 分级存储策略 + 容量预警

Last updated 06 Mar 2025, 13:07 +0800 . history