Lenis与Service Worker:离线环境的滚动处理全攻略

Lenis与Service Worker:离线环境的滚动处理全攻略

【免费下载链接】lenis How smooth scroll should be 【免费下载链接】lenis 项目地址: https://gitcode.com/GitHub_Trending/le/lenis

引言:离线时代的滚动体验痛点

你是否遇到过这样的情况:在弱网或断网环境下访问网页,页面加载完成但滚动时出现卡顿、跳帧甚至完全无响应?随着PWA(Progressive Web App,渐进式Web应用)技术的普及,用户对离线体验的期望越来越高。根据Google 2024年Web性能报告,离线可访问性已成为用户留存率的关键指标之一,而滚动体验作为最基础的交互形式,直接影响用户对应用质量的判断。

Lenis作为一款轻量级平滑滚动库,以其60fps的流畅表现和丰富的API深受开发者喜爱。但在离线环境中,如何确保Lenis的核心功能不受网络状况影响?Service Worker(服务工作线程)作为PWA的核心技术,能否与Lenis协同工作,构建真正无缝的离线滚动体验?

本文将深入探讨这一技术交叉点,通过3大核心模块7个实战案例12段关键代码,带你掌握Lenis与Service Worker的集成方案,解决离线环境下的滚动处理难题。

一、技术背景与核心概念

1.1 Lenis:现代滚动体验的重构者

Lenis是一个专注于平滑滚动的JavaScript库,采用"虚拟滚动"技术,通过拦截原生滚动事件、模拟物理运动曲线实现超流畅的滚动效果。其核心特性包括:

  • 高性能动画系统:基于requestAnimationFrame实现60fps滚动动画
  • 丰富的配置选项:支持自定义缓动函数、惯性参数、滚动方向等
  • 完善的事件机制:提供scroll、scrollend等事件,便于状态监听
  • 轻量级设计:核心包体积仅5KB(gzip压缩后)
// Lenis基本初始化示例
import { Lenis } from '@studio-freight/lenis'

const lenis = new Lenis({
  duration: 1.2, // 动画持续时间(秒)
  easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // 缓动函数
  orientation: 'vertical', // 滚动方向
  smoothWheel: true, // 平滑鼠标滚轮
  syncTouch: false // 同步触摸事件
})

// 绑定动画帧更新
function raf(time) {
  lenis.raf(time)
  requestAnimationFrame(raf)
}

requestAnimationFrame(raf)

1.2 Service Worker:离线Web的基石

Service Worker是运行在浏览器后台的脚本,独立于网页线程,具备以下能力:

  • 离线资源缓存:拦截网络请求,从缓存提供资源
  • 后台同步:延迟同步用户操作,待网络恢复后执行
  • 推送通知:即使网页关闭也能接收服务器推送
  • 资源预加载:智能预缓存关键资源,提升加载速度

其生命周期包括安装(Install)、激活(Activate)和运行(Fetch)三个阶段,通过事件驱动方式处理各种场景。

1.3 离线滚动的技术挑战

将两者结合时,需解决以下关键问题:

挑战类型具体表现影响程度
资源可用性Lenis库文件未缓存导致加载失败⭐⭐⭐⭐⭐
事件延迟Service Worker线程与主线程通信延迟⭐⭐⭐⭐
状态同步离线时滚动位置无法持久化⭐⭐⭐
性能损耗缓存逻辑增加主线程负担⭐⭐
错误处理离线状态下的异常捕获与恢复⭐⭐⭐⭐

二、Lenis与Service Worker集成架构

2.1 系统架构设计

以下是Lenis与Service Worker协同工作的整体架构:

mermaid

2.2 关键集成点

  1. 资源缓存策略:确保Lenis核心库及依赖在离线时可用
  2. 状态持久化:使用IndexedDB存储关键滚动状态
  3. 事件桥接:建立主线程与Service Worker的通信通道
  4. 降级机制:网络不可用时自动切换到基础滚动模式

三、实战:离线滚动实现步骤

3.1 Service Worker注册与Lenis资源缓存

首先,在主应用中注册Service Worker,并缓存Lenis相关资源:

// main.js - 应用入口文件
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('ServiceWorker注册成功:', registration.scope)
        initLenis() // 注册成功后初始化Lenis
      })
      .catch(err => {
        console.log('ServiceWorker注册失败:', err)
        initLenisFallback() // 注册失败时使用降级方案
      })
  })
} else {
  // 不支持ServiceWorker的浏览器直接初始化Lenis
  initLenis()
}

// 初始化Lenis核心功能
function initLenis() {
  import('https://cdn.jsdelivr.net/npm/@studio-freight/lenis@1.0.34/dist/lenis.min.js')
    .then(({ Lenis }) => {
      const lenis = new Lenis({
        // 配置参数
        duration: 1.2,
        easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t))
      })
      
      // 存储实例到全局,便于调试
      window.lenis = lenis
      
      // 启动动画循环
      function raf(time) {
        lenis.raf(time)
        requestAnimationFrame(raf)
      }
      requestAnimationFrame(raf)
      
      // 监听滚动事件,用于状态保存
      lenis.on('scroll', (e) => {
        if (navigator.onLine) {
          // 在线时实时同步
          saveScrollState(e.scroll)
        } else {
          // 离线时本地暂存
          saveScrollStateLocally(e.scroll)
        }
      })
    })
    .catch(err => {
      console.error('Lenis加载失败,使用原生滚动:', err)
      initLenisFallback()
    })
}

// Lenis加载失败时的降级方案
function initLenisFallback() {
  document.documentElement.classList.add('lenis-fallback')
  // 实现基础的平滑滚动
  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
    anchor.addEventListener('click', function(e) {
      e.preventDefault()
      const target = document.querySelector(this.getAttribute('href'))
      if (target) {
        target.scrollIntoView({ behavior: 'smooth' })
      }
    })
  })
}

3.2 Service Worker实现(sw.js)

创建Service Worker文件,实现Lenis资源缓存:

// sw.js - Service Worker文件
const CACHE_NAME = 'lenis-offline-v1'
const LENIS_CDN_URL = 'https://cdn.jsdelivr.net/npm/@studio-freight/lenis@1.0.34/dist/lenis.min.js'
const CRITICAL_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  LENIS_CDN_URL,
  '/offline-fallback.html'
]

// 安装阶段:缓存关键资源
self.addEventListener('install', (event) => {
  // 强制激活新的Service Worker
  self.skipWaiting()
  
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('缓存初始化成功')
        return cache.addAll(CRITICAL_ASSETS)
      })
      .catch(err => {
        console.error('缓存初始化失败:', err)
      })
  )
})

// 激活阶段:清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(name => {
          if (name !== CACHE_NAME) {
            console.log('删除旧缓存:', name)
            return caches.delete(name)
          }
        })
      )
    }).then(() => {
      // 获得控制权
      return self.clients.claim()
    })
  )
})

//  fetch事件:拦截请求并提供缓存响应
self.addEventListener('fetch', (event) => {
  // 对Lenis资源采用网络优先、缓存后备策略
  if (event.request.url.includes('lenis')) {
    event.respondWith(
      fetch(event.request)
        .then(networkResponse => {
          // 更新缓存
          caches.open(CACHE_NAME).then(cache => {
            cache.put(event.request, networkResponse.clone())
          })
          return networkResponse
        })
        .catch(() => {
          // 网络失败时从缓存获取
          return caches.match(event.request)
            .then(cacheResponse => {
              if (cacheResponse) {
                return cacheResponse
              }
              // 最后的后备方案
              return caches.match('/offline-fallback.html')
            })
        })
    )
    return
  }
  
  // 对其他资源采用缓存优先策略
  event.respondWith(
    caches.match(event.request)
      .then(cacheResponse => {
        // 缓存命中则返回
        if (cacheResponse) {
          // 后台更新缓存
          fetch(event.request).then(networkResponse => {
            caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, networkResponse)
            })
          })
          return cacheResponse
        }
        
        // 缓存未命中则请求网络
        return fetch(event.request)
          .then(networkResponse => {
            // 更新缓存
            caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, networkResponse.clone())
            })
            return networkResponse
          })
          .catch(() => {
            // 所有失败时的后备页面
            if (event.request.mode === 'navigate') {
              return caches.match('/offline-fallback.html')
            }
          })
      })
  )
})

// 监听来自主线程的消息
self.addEventListener('message', (event) => {
  if (event.data.type === 'SCROLL_STATE') {
    console.log('收到滚动状态:', event.data.payload)
    // 可以在这里实现后台同步逻辑
  }
})

3.3 离线滚动状态持久化

使用IndexedDB存储滚动位置,确保页面刷新后恢复状态:

// scroll-storage.js - 滚动状态存储模块
export class ScrollStorage {
  constructor() {
    this.dbName = 'LenisOfflineStorage'
    this.storeName = 'scrollStates'
    this.version = 1
  }
  
  // 打开数据库连接
  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.version)
      
      request.onupgradeneeded = (event) => {
        const db = event.target.result
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName, { keyPath: 'pageUrl' })
        }
      }
      
      request.onsuccess = (event) => {
        resolve(event.target.result)
      }
      
      request.onerror = (event) => {
        console.error('IndexedDB打开失败:', event.target.error)
        reject(event.target.error)
      }
    })
  }
  
  // 保存滚动状态
  async saveScrollState(pageUrl, scrollPosition) {
    try {
      const db = await this.openDB()
      const transaction = db.transaction(this.storeName, 'readwrite')
      const store = transaction.objectStore(this.storeName)
      
      await store.put({
        pageUrl,
        scrollPosition,
        timestamp: Date.now()
      })
      
      console.log('滚动状态已保存:', pageUrl, scrollPosition)
      
      // 通知Service Worker
      if (navigator.serviceWorker.controller) {
        navigator.serviceWorker.controller.postMessage({
          type: 'SCROLL_STATE',
          payload: { pageUrl, scrollPosition }
        })
      }
    } catch (err) {
      console.error('保存滚动状态失败:', err)
    }
  }
  
  // 获取滚动状态
  async getScrollState(pageUrl) {
    try {
      const db = await this.openDB()
      const transaction = db.transaction(this.storeName, 'readonly')
      const store = transaction.objectStore(this.storeName)
      
      const result = await store.get(pageUrl)
      return result ? result.scrollPosition : 0
    } catch (err) {
      console.error('获取滚动状态失败:', err)
      return 0
    }
  }
  
  // 清除过期状态
  async cleanupOldStates(maxAge = 86400000) { // 默认24小时
    try {
      const db = await this.openDB()
      const transaction = db.transaction(this.storeName, 'readwrite')
      const store = transaction.objectStore(this.storeName)
      const now = Date.now()
      
      const cursor = store.openCursor()
      const promises = []
      
      cursor.onsuccess = (event) => {
        const cursor = event.target.result
        if (cursor) {
          if (now - cursor.value.timestamp > maxAge) {
            promises.push(cursor.delete())
          }
          cursor.continue()
        }
      }
      
      await Promise.all(promises)
      console.log('已清理过期滚动状态')
    } catch (err) {
      console.error('清理滚动状态失败:', err)
    }
  }
}

// 使用示例
const scrollStorage = new ScrollStorage()

// 页面加载时恢复滚动位置
window.addEventListener('DOMContentLoaded', async () => {
  const savedPosition = await scrollStorage.getScrollState(window.location.href)
  if (savedPosition > 0) {
    // 恢复滚动位置
    if (window.lenis) {
      window.lenis.scrollTo(savedPosition, { immediate: true })
    } else {
      window.scrollTo(0, savedPosition)
    }
  }
  
  // 清理过期数据
  scrollStorage.cleanupOldStates()
})

// 页面卸载前保存滚动位置
window.addEventListener('beforeunload', async () => {
  const currentPosition = window.lenis ? window.lenis.scroll : window.scrollY
  await scrollStorage.saveScrollState(window.location.href, currentPosition)
})

3.4 离线事件处理与通信

建立主线程与Service Worker的双向通信机制:

// sw-communication.js - 通信模块
export class SWCommunicator {
  constructor() {
    this.channel = new BroadcastChannel('lenis-offline-channel')
    this.listeners = new Map()
    
    // 监听来自Service Worker的消息
    this.channel.addEventListener('message', (event) => {
      this.handleMessage(event.data)
    })
    
    // 监听Service Worker状态变化
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.addEventListener('controllerchange', () => {
        this.notifyStatusChange('controllerchange')
      })
    }
  }
  
  // 发送消息到Service Worker
  sendMessage(type, payload) {
    if (!navigator.serviceWorker.controller) {
      console.warn('Service Worker未激活,无法发送消息')
      return Promise.reject(new Error('Service Worker未激活'))
    }
    
    return new Promise((resolve, reject) => {
      const messageId = Date.now().toString()
      
      // 监听响应
      const listener = (event) => {
        if (event.data.id === messageId) {
          this.channel.removeEventListener('message', listener)
          if (event.data.error) {
            reject(event.data.error)
          } else {
            resolve(event.data.payload)
          }
        }
      }
      
      this.channel.addEventListener('message', listener)
      
      // 发送消息
      this.channel.postMessage({
        id: messageId,
        type,
        payload
      })
    })
  }
  
  // 处理接收到的消息
  handleMessage(data) {
    if (this.listeners.has(data.type)) {
      this.listeners.get(data.type).forEach(callback => callback(data.payload))
    }
  }
  
  // 注册消息监听器
  on(type, callback) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, new Set())
    }
    this.listeners.get(type).add(callback)
  }
  
  // 移除消息监听器
  off(type, callback) {
    if (this.listeners.has(type)) {
      this.listeners.get(type).delete(callback)
      if (this.listeners.get(type).size === 0) {
        this.listeners.delete(type)
      }
    }
  }
  
  // 通知状态变化
  notifyStatusChange(status) {
    this.sendMessage('status-change', {
      status,
      timestamp: Date.now(),
      online: navigator.onLine
    })
  }
  
  // 销毁通信通道
  destroy() {
    this.channel.close()
    this.listeners.clear()
  }
}

// 在Lenis中集成通信功能
export function integrateSWCommunication(lenis, communicator) {
  // 监听滚动事件并发送到Service Worker
  lenis.on('scroll', (e) => {
    communicator.sendMessage('scroll-update', {
      position: e.scroll,
      velocity: e.velocity,
      direction: e.direction,
      timestamp: Date.now()
    }).catch(err => {
      console.error('发送滚动更新失败:', err)
    })
  })
  
  // 监听滚动结束事件
  lenis.on('scrollend', () => {
    communicator.sendMessage('scroll-end', {
      position: lenis.scroll,
      timestamp: Date.now()
    })
  })
  
  // 监听来自Service Worker的控制命令
  communicator.on('control-command', (command) => {
    switch (command.type) {
      case 'scroll-to':
        lenis.scrollTo(command.position, command.options)
        break
      case 'stop':
        lenis.stop()

【免费下载链接】lenis How smooth scroll should be 【免费下载链接】lenis 项目地址: https://gitcode.com/GitHub_Trending/le/lenis

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值