一、项目概述

1.1 📌 Moments 项目简介

Moments 是一款极简朋友圈应用,宛如社交分享领域的一颗璀璨新星。自 v0.2.1 起,它采用 Golang 重写服务端,如同为项目注入了强大的动力,实现了包体积更小、功能更强的优势。它为用户精心打造了一个网页端简洁却功能丰富的社交展示平台,支持多种内容形式的记录与展示。
moments
kingwrcyUpdated Jul 19, 2025

1.2 🎯 功能亮点

  • 📝 Memo 记录:这里就像一个多功能的笔记本,支持标签管理,让你的内容条理清晰;支持图片上传,可存储至本地或 S3,满足不同的存储需求;自动生成缩略图(本地上传),为你节省时间;支持 Markdown 语法,让你可以用简洁的方式表达丰富的内容;点赞与评论功能,让你的分享与他人产生互动;还能嵌入网易云音乐、B站视频、外部链接,引用豆瓣读书和电影信息,让你的分享更加丰富多彩。
  • 🛠️ 其他功能:它就像一个贴心的伙伴,完美适配移动端,无论你是在路上还是在休息,都能随时随地使用;支持暗黑模式,为你在夜晚使用提供舒适的视觉体验;提供回到顶部按钮,让你在浏览长页面时更加便捷;数据库采用 SQLite,便于备份,让你的数据更加安全;支持自定义头图、头像和网站标题,让你的朋友圈独具个性。

二、页面展示增强

2.1 💅 CSS 优化

a[href="<https://github.com/kingwrcy/moments>"] { display: none; } div.sm\\:absolute.sm\\:flex-col.sm\\:-right-10.sm\\:rounded.sm\\:p-2.sm\\:w-fit.dark\\:bg-neutral-800.bg-white.shadow { display: none !important; }
CSS
这部分 CSS 代码就像一位细心的美容师,通过 display: none 属性,巧妙地隐藏特定的链接和元素,让页面的视觉效果更加优化,避免不必要的信息干扰用户体验,就像为页面穿上了一件整洁的外衣。

2.2 ✨ 灵动岛歌词容器及动画优化

/* 灵动岛歌词容器 */ /* 歌词容器与滚动动画(优化滚动速度) */ .lyric-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; overflow: hidden; pointer-events: none; } .lyric-text { color: #ffcc99; font-size: 12px; white-space: nowrap; transform: translateX(100%); opacity: 0; font-weight: 500; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); transition: transform 0.3s ease-out, opacity 0.3s ease-out; } .lyric-text.visible { opacity: 1; transform: translateX(0); } /* 优化1:歌词滚动速度减慢,延长动画周期至25秒 */ .lyric-text.scrolling { animation: scrollLyric 25s linear infinite; } @keyframes scrollLyric { 0% { transform: translateX(100%); } 10% { transform: translateX(0); } /* 10%时间完成进入 */ 85% { transform: translateX(0); } /* 75%时间保持静止 */ 100% { transform: translateX(-100%); } /* 15%时间完成退出 */ } /* 优化3:灵动岛炫光动画 */ .dynamic-island { /* 基础样式保持不变... */ position: relative; overflow: hidden; } .dynamic-island::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255, 204, 153, 0.2) 0%, transparent 70%); transform: rotate(0deg); opacity: 0; transition: opacity 0.5s ease; } .dynamic-island.playing::before { opacity: 1; animation: glowRotate 6s linear infinite; } @keyframes glowRotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
CSS

2.2.1 🎵 歌词容器与滚动动画

  • 📦 容器布局.lyric-container 采用绝对定位,如同一个隐形的框架,覆盖整个父元素,通过 flex 布局将歌词文本垂直和水平居中。overflow: hidden 确保超出容器的歌词部分不会显示,pointer-events: none 使歌词容器不会干扰用户与其他元素的交互,就像一个安静的守护者,默默守护着歌词的展示。
  • 📝 歌词文本样式.lyric-text 初始状态下透明度为 0 且位于容器右侧,当添加 visible 类时,通过过渡效果使其透明度变为 1 并移动到容器中心,就像一颗星星从黑暗中逐渐闪耀出来。
  • 🎬 滚动动画优化:通过 animation: scrollLyric 25s linear infinite 将歌词滚动动画周期延长至 25 秒,在 @keyframes scrollLyric 中详细定义了歌词的滚动过程,使歌词在进入、静止和退出阶段有更合理的时间分配,就像一场精心编排的舞蹈,提升用户的视觉体验。

2.2.2 🌟 灵动岛炫光动画

  • ✨ 伪元素创建:通过 ::before 伪元素在 .dynamic-island 元素内部创建一个径向渐变的背景,初始状态下透明度为 0,就像一颗隐藏的宝石,等待被点亮。
  • 🎬 动画触发:当 .dynamic-island 元素添加 playing 类时,伪元素的透明度变为 1,并开始执行 glowRotate 动画,使炫光以 6 秒为周期旋转,增强了灵动岛的视觉效果,就像给灵动岛披上了一层绚丽的光环。

三、iPhone 16 Pro 模拟思路与实现

3.1 🤔 模拟思路概述

在页面中模拟 iPhone 16 Pro 的效果,就像一场奇妙的魔法之旅。我们主要是通过创建一个具有 iPhone 外观的容器,并将原页面内容嵌入其中,同时添加灵动岛元素来模拟 iPhone 的交互效果。在模拟过程中,需要像一位严谨的建筑师一样,考虑屏幕尺寸的适配、元素的布局和动画效果的实现。

3.2 💻 代码实现步骤

var simulatorWidth; function initiPhoneSimulator() { // 路径验证 if (window.location.pathname !== '/') { console.log('非根路径,不执行模拟'); return; } console.log('根路径验证通过,开始初始化'); // 核心参数:新比例395:830,最小宽度395px const baseWidth = 395; const baseHeight = 830; const minWidth = baseWidth; // 强制最小宽度不小于395px const ratio = baseWidth / baseHeight; // 宽高比≈0.4759 const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; // 1. 计算可用空间限制 const maxAvailableWidth = screenWidth * 0.85; // 最大可用宽度为屏幕85% const maxAvailableHeight = screenHeight * 0.95; // 最大可用高度为屏幕95% // 2. 优先保证宽度不小于395px,计算最优宽高 let optimalWidth, optimalHeight; // 基础宽度:取最小宽度和最大可用宽度的较大值(确保不小于395) const baseCandidateWidth = Math.max(minWidth, maxAvailableWidth); // 按基础宽度计算对应高度(原始比例) const heightByWidth = baseCandidateWidth / ratio; if (heightByWidth <= maxAvailableHeight) { // 高度在可用范围内:使用基础宽度和对应高度(保持比例) optimalWidth = baseCandidateWidth; optimalHeight = heightByWidth; } else { // 高度超出范围:限制高度为最大可用高度,宽度按比例计算(可能大于基础宽度) optimalHeight = maxAvailableHeight; optimalWidth = optimalHeight * ratio; // 二次检查:确保宽度不小于最小宽度(允许比例偏差) if (optimalWidth < minWidth) { optimalWidth = minWidth; optimalHeight = maxAvailableHeight; // 此时比例有偏差,优先保证宽度和高度限制 console.log(`宽度已达最小值${minWidth}px,高度限制为${optimalHeight.toFixed(0)}px(比例偏差)`); } } simulatorWidth = optimalWidth; console.log(`最优尺寸:宽${optimalWidth.toFixed(0)}px,高${optimalHeight.toFixed(0)}px(比例≈${(optimalWidth/optimalHeight).toFixed(3)}`); // 3. 创建iPhone容器 const iphone = document.createElement('div'); iphone.style.width = `${optimalWidth}px`; iphone.style.height = `${optimalHeight}px`; iphone.style.border = '12px solid #1a1a1a'; iphone.style.borderRadius = `${optimalWidth * (56 / baseWidth)}px`; // 按基础宽度比例计算圆角 iphone.style.position = 'relative'; iphone.style.boxShadow = '0 15px 40px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.1)'; iphone.style.background = '#000'; iphone.style.transform = 'scale(0.95)'; iphone.style.transition = 'transform 0.3s ease'; iphone.onmouseover = () => { iphone.style.transform = 'scale(1)'; }; iphone.onmouseout = () => { iphone.style.transform = 'scale(0.95)'; }; console.log('iPhone容器创建完成'); // 4. 创建屏幕容器 const screenBorder = 2; const screen = document.createElement('div'); screen.style.width = `calc(100% - ${2 * screenBorder}px)`; screen.style.height = `calc(100% - ${2 * screenBorder}px)`; screen.style.background = '#fff'; screen.style.borderRadius = `${optimalWidth * (48 / baseWidth)}px`; screen.style.overflow = 'hidden'; screen.style.position = 'relative'; screen.style.margin = `${screenBorder}px`; console.log('屏幕容器创建完成'); // 创建灵动岛(新增歌词容器) const dynamicIsland = document.createElement('div'); dynamicIsland.style.position = 'absolute'; dynamicIsland.style.top = `${optimalWidth * (8 / baseWidth)}px`; dynamicIsland.style.left = '50%'; dynamicIsland.style.transform = 'translateX(-50%)'; dynamicIsland.style.width = `${optimalWidth * (120 / baseWidth)}px`; dynamicIsland.style.height = `${optimalWidth * (32 / baseWidth)}px`; dynamicIsland.style.background = 'linear-gradient(180deg, #1a1a1a 0%, #000 100%)'; dynamicIsland.style.borderRadius = `${optimalWidth * (20 / baseWidth)}px`; dynamicIsland.style.zIndex = '9999'; dynamicIsland.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.05)'; dynamicIsland.style.transition = 'all 0.3s ease'; // 新增:歌词容器 const lyricContainer = document.createElement('div'); lyricContainer.className = 'lyric-container'; const lyricText = document.createElement('div'); lyricText.className = 'lyric-text'; lyricContainer.appendChild(lyricText); dynamicIsland.appendChild(lyricContainer); // 灵动岛交互 dynamicIsland.onclick = () => { const isExpanded = dynamicIsland.style.width === `${optimalWidth * (160 / baseWidth)}px`; dynamicIsland.style.width = isExpanded ? `${optimalWidth * (120 / baseWidth)}px` : `${optimalWidth * (160 / baseWidth)}px`; dynamicIsland.style.height = isExpanded ? `${optimalWidth * (32 / baseWidth)}px` : `${optimalWidth * (38 / baseWidth)}px`; }; console.log('灵动岛创建完成(支持歌词显示)'); // 6. 创建内容容器 const webContent = document.createElement('div'); webContent.style.position = 'absolute'; webContent.style.top = '0'; webContent.style.left = '0'; webContent.style.width = '100%'; webContent.style.height = '100%'; webContent.style.overflowY = 'auto'; //webContent.style.paddingTop = `${optimalWidth * (40 / baseWidth)}px`; // 避开灵动岛 webContent.style.boxSizing = 'border-box'; console.log('内容容器创建完成'); // 7. 移除固定宽度class const targetClass = 'md:w-[567px]'; const escapedClass = targetClass.replace(/:/g, '\\\\:').replace(/\\[/g, '\\\\[').replace(/\\]/g, '\\\\]'); const elementsWithClass = document.querySelectorAll(`.${escapedClass}`); // 处理原项目的最低宽度 elementsWithClass.forEach(el => { el.classList.remove(targetClass); }); // 处理特殊样式的宽度 document.querySelectorAll('.h-full').forEach(he => { he.classList.remove('h-full'); }) // 8. 移动原DOM并适配 const originalContent = document.getElementById('__nuxt'); if (originalContent) { console.log('找到__nuxt容器,开始移动'); originalContent.style.width = '100%'; originalContent.style.maxWidth = '100%'; originalContent.style.height = '100%'; originalContent.style.boxSizing = 'border-box'; webContent.appendChild(originalContent); } else { console.warn('未找到__nuxt容器'); const fallback = document.createElement('div'); fallback.style.padding = '20px'; fallback.style.textAlign = 'center'; fallback.textContent = '内容加载失败'; webContent.appendChild(fallback); } // 9. 组装结构 screen.appendChild(dynamicIsland); screen.appendChild(webContent); iphone.appendChild(screen); document.body.appendChild(iphone); console.log('元素组装完成'); // 10. 页面样式 document.body.style.margin = '0'; document.body.style.padding = '20px'; document.body.style.backgroundColor = '#f0f2f5'; document.body.style.minHeight = '100vh'; document.body.style.display = 'flex'; document.body.style.justifyContent = 'center'; document.body.style.alignItems = 'center'; document.body.style.overflow = 'hidden'; // 11. 加载动画 webContent.style.opacity = '0'; setTimeout(() => { webContent.style.transition = 'opacity 0.5s ease'; webContent.style.opacity = '1'; }, 300); // 新增:监听meting-js播放事件 setupAdvancedPlayerListener(dynamicIsland, lyricText, optimalWidth, baseWidth); } // 高级播放器监听系统(兼容无限滚动) function setupAdvancedPlayerListener(dynamicIsland, lyricText, optimalWidth, baseWidth) { const originalWidth = dynamicIsland.style.width; const originalHeight = dynamicIsland.style.height; const expandedWidth = `${optimalWidth * (220 / baseWidth)}px`; const expandedHeight = `${optimalWidth * (38 / baseWidth)}px`; let activePlayer = null; let playerObservers = new Map(); // 存储每个播放器的observer let lyricUpdateTimer = null; // 处理播放器状态变化 const handlePlayerStateChange = (player) => { const playButton = player.querySelector('.aplayer-button'); const isPlaying = playButton?.classList.contains('aplayer-pause'); if (isPlaying) { // 激活当前播放器(不暂停其他) activePlayer = player; expandIsland(); startLyricUpdates(); } else if (activePlayer === player) { // 当前播放器暂停 activePlayer = null; resetIsland(); stopLyricUpdates(); } }; // 展开灵动岛 const expandIsland = () => { dynamicIsland.style.width = expandedWidth; dynamicIsland.style.height = expandedHeight; dynamicIsland.classList.add('playing'); lyricText.classList.add('visible'); }; // 重置灵动岛 const resetIsland = () => { dynamicIsland.style.width = originalWidth; dynamicIsland.style.height = originalHeight; dynamicIsland.classList.remove('playing'); lyricText.classList.remove('visible', 'scrolling'); lyricText.textContent = ''; }; // 更新歌词 const updateLyric = () => { if (!activePlayer) return; const currentLine = activePlayer.querySelector('.aplayer-lrc-current'); if (currentLine && currentLine.textContent.trim()) { const lyric = currentLine.textContent; if (lyric !== lyricText.textContent) { lyricText.textContent = lyric; // 重置滚动动画 lyricText.classList.remove('scrolling'); void lyricText.offsetWidth; lyricText.classList.add('scrolling'); } } }; // 开始歌词更新 const startLyricUpdates = () => { if (!lyricUpdateTimer) { lyricUpdateTimer = setInterval(updateLyric, 1500); } }; // 停止歌词更新 const stopLyricUpdates = () => { if (lyricUpdateTimer) { clearInterval(lyricUpdateTimer); lyricUpdateTimer = null; } }; // 初始化单个播放器监听 const initPlayer = (player) => { // 避免重复初始化 if (playerObservers.has(player)) return; // 创建状态变化监听器 const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { handlePlayerStateChange(player); } }); }); // 监听播放器状态变化 observer.observe(player, { attributes: true, attributeFilter: ['class'] }); // 监听播放按钮状态变化 const playButton = player.querySelector('.aplayer-button'); if (playButton) { observer.observe(playButton, { attributes: true, attributeFilter: ['class'] }); } // 存储observer引用 playerObservers.set(player, observer); // 检查初始状态 if (playButton?.classList.contains('aplayer-pause')) { handlePlayerStateChange(player); } if (simulatorWidth && simulatorWidth < 600) { player.style.width = '95%'; player.style.maxWidth = `${simulatorWidth - 120}px`; player.style.boxSizing = 'border-box'; console.log(`[播放器宽度适配] ${simulatorWidth - 120}px`) } console.log('[播放器监听] 初始化新播放器:', player); }; // 清理播放器监听 const cleanupPlayer = (player) => { const observer = playerObservers.get(player); if (observer) { observer.disconnect(); playerObservers.delete(player); } // 如果清理的是当前活跃播放器 if (activePlayer === player) { activePlayer = null; resetIsland(); stopLyricUpdates(); } }; // 创建Intersection Observer监听播放器元素进入视口 const playerIntersectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // 播放器进入视口,初始化监听 initPlayer(entry.target); } else { // 播放器离开视口,可选择清理监听(这里保留监听以支持后台播放) // cleanupPlayer(entry.target); } }); }, { rootMargin: '500px 0px', // 提前500px触发观察,确保播放器即将进入视口时就初始化 threshold: 0.1 }); // 创建Mutation Observer监听DOM变化,捕获新添加的播放器 const domObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'childList') { // 检查新增节点中是否有播放器 mutation.addedNodes.forEach(node => { if (node.classList && node.classList.contains('aplayer')) { // 直接找到播放器 playerIntersectionObserver.observe(node); } else if (node.nodeType === 1) { // 检查子节点中是否有播放器 node.querySelectorAll('.aplayer').forEach(player => { playerIntersectionObserver.observe(player); }); } }); } }); }); // 开始监听DOM变化 domObserver.observe(document.body, { childList: true, subtree: true }); // 初始化现有播放器 document.querySelectorAll('.aplayer').forEach(player => { playerIntersectionObserver.observe(player); }); // 页面卸载时清理所有资源 window.addEventListener('beforeunload', () => { domObserver.disconnect(); playerIntersectionObserver.disconnect(); playerObservers.forEach(observer => observer.disconnect()); playerObservers.clear(); stopLyricUpdates(); }); } // 新增:判断屏幕宽度,小于500px则不模拟 const screenWidth = window.innerWidth; if (screenWidth < 500) { console.log(`[模拟判断] 屏幕宽度${screenWidth}px < 500px,不执行模拟`); } else { initiPhoneSimulator(); window.addEventListener('load', function() { // 处理音乐播放器宽地 if (optimalWidth < 520) { document.querySelectorAll('.aplayer').forEach(ae => { ae.style.width = '100%'; ae.style.maxWidth = `${optimalWidth - 30}px`; ae.style.boxSizing = 'border-box'; }) } console.log('处理音乐播放器宽度'); }); }
JavaScript

3.3.1 🛂 路径验证

initiPhoneSimulator 函数中,首先进行路径验证,就像一个严格的门卫,只有当页面路径为根路径时才执行模拟操作,避免在其他页面不必要的模拟,确保模拟过程的准确性和高效性。

3.3.2 📏 尺寸计算与适配

  • 📌 核心参数设定:定义了 baseWidthbaseHeight 作为 iPhone 模拟的基础尺寸,计算出宽高比 ratio。同时,设置了最小宽度 minWidth 为 395px,确保模拟的 iPhone 宽度不会过小,就像为模拟过程设定了一个坚实的基础框架。
  • 📐 可用空间计算:根据屏幕宽度和高度,计算出最大可用宽度和高度,分别为屏幕宽度的 85% 和屏幕高度的 95%,就像为模拟过程规划了一个合理的活动范围。
  • 🌟 最优宽高计算:通过比较按基础宽度计算的高度和最大可用高度,确定最优的宽高值。如果按基础宽度计算的高度超出最大可用高度,则限制高度为最大可用高度,并重新计算宽度。同时,进行二次检查,确保宽度不小于最小宽度,就像一位精打细算的设计师,为模拟过程找到最完美的尺寸。

3.3.3 🎨 元素创建与布局

  • 📱 iPhone 容器创建:使用 document.createElement 创建一个 div 元素作为 iPhone 容器,设置其宽度、高度、边框、圆角、背景等样式,并添加鼠标悬停动画效果,就像为模拟的 iPhone 穿上了一件时尚的外衣,让它更加生动逼真。
  • 📺 屏幕容器创建:在 iPhone 容器内部创建屏幕容器,设置其宽度、高度、背景、圆角等样式,并将其添加到 iPhone 容器中,就像为 iPhone 安装了一块清晰的屏幕,让内容展示更加清晰。
  • 💫 灵动岛创建:在屏幕容器内部创建灵动岛元素,设置其位置、大小、背景、圆角等样式,并添加歌词容器和交互效果。当点击灵动岛时,通过改变其宽度和高度实现展开和收缩的效果,就像为 iPhone 赋予了一个灵动的灵魂,让交互更加有趣。
  • 📄 内容容器创建:创建内容容器,将原页面的 __nuxt 容器移动到内容容器中,并设置其宽度、高度和滚动属性。同时,移除原页面中固定宽度的类,以适配模拟的 iPhone 尺寸,就像为原页面内容找到了一个合适的新家,让它在模拟的 iPhone 中完美展示。

3.3.4 🎵 播放器监听与歌词更新

  • 🎧 高级播放器监听系统setupAdvancedPlayerListener 函数实现了对多个音乐播放器的监听和歌词更新功能。通过 MutationObserver 监听播放器和播放按钮的状态变化,当播放器开始播放时,展开灵动岛并开始更新歌词;当播放器暂停时,重置灵动岛并停止更新歌词,就像一个智能的音乐管家,时刻关注着播放器的状态。
  • 👀 Intersection Observer:使用 Intersection Observer 监听播放器元素进入视口的事件,当播放器进入视口时,初始化对该播放器的监听;当播放器离开视口时,保留监听以支持后台播放,就像一个默默守护的卫士,确保播放器在任何情况下都能正常工作。
  • 👀 Mutation Observer:使用 Mutation Observer 监听 DOM 变化,当有新的播放器元素添加到页面时,自动初始化对该播放器的监听,就像一个敏锐的侦探,及时发现新的播放器并进行处理。

3.3.5 🧹 资源清理

在页面卸载时,通过 window.addEventListener('beforeunload') 事件清理所有的监听器和定时器,避免内存泄漏,就像一个细心的清洁工,在离开前将一切清理干净,确保页面的整洁和高效。
 

3.3.6 最终效果

notion image

四、动态多播放器问题及解决方法

4.1 🤔 问题分析

在实现动态多播放器的过程中,主要面临以下几个问题:
  • 🎵 播放器状态同步:当页面中存在多个音乐播放器时,需要确保灵动岛能够准确显示当前正在播放的播放器的信息,并在播放器状态变化时及时更新,就像一个精准的指挥家,确保所有播放器的节奏一致。
  • 📝 歌词更新与滚动:要实现歌词的实时更新和滚动效果,需要监听播放器的歌词变化,并在合适的时机更新灵动岛中的歌词显示,就像一个勤奋的记录员,及时记录下每一句歌词。
  • ⚙️ 性能优化:在处理多个播放器的监听和更新时,需要考虑性能问题,避免过多的 DOM 操作和定时器导致页面卡顿,就像一个精明的工程师,优化每一个细节,确保系统的高效运行。
  • 📜 无限滚动适配:当页面支持无限滚动时,需要确保新添加的播放器能够自动被监听,并且在播放器离开视口时不会影响后台播放,就像一个灵活的舞者,能够适应各种舞台环境。

4.2 💡 解决方法

4.2.1 🎵 播放器状态同步

  • 👀 使用 MutationObserver:通过 MutationObserver 监听播放器和播放按钮的 class 属性变化,当播放器开始播放时,将其标记为活跃播放器,并展开灵动岛;当播放器暂停时,重置灵动岛,就像一个敏锐的观察者,及时发现播放器状态的变化并做出相应的调整。
const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { handlePlayerStateChange(player); } }); }); observer.observe(player, { attributes: true, attributeFilter: ['class'] }); observer.observe(playButton, { attributes: true, attributeFilter: ['class'] });
JavaScript

4.2.2 📝 歌词更新与滚动

  • ⏱️ 定时器更新歌词:使用 setInterval 定时器每隔 1.5 秒检查当前活跃播放器的歌词变化,当歌词发生变化时,更新灵动岛中的歌词显示,并重置滚动动画,就像一个准时的闹钟,按时提醒我们更新歌词。
const updateLyric = () => { if (!activePlayer) return; const currentLine = activePlayer.querySelector('.aplayer-lrc-current'); if (currentLine && currentLine.textContent.trim()) { const lyric = currentLine.textContent; if (lyric !== lyricText.textContent) { lyricText.textContent = lyric; // 重置滚动动画 lyricText.classList.remove('scrolling'); void lyricText.offsetWidth; lyricText.classList.add('scrolling'); } } }; lyricUpdateTimer = setInterval(updateLyric, 1500);
JavaScript

4.2.3 ⚙️ 性能优化

  • 🗄️ 使用 Map 存储监听器:使用 Map 对象存储每个播放器的 MutationObserver 引用,避免重复初始化监听器,提高性能,就像一个高效的仓库管理员,合理管理每一个监听器。
let playerObservers = new Map(); playerObservers.set(player, observer);
JavaScript
  • 👀 Intersection Observer 优化:通过设置 rootMarginthreshold 参数,提前触发对播放器元素的观察,确保播放器即将进入视口时就初始化监听,减少不必要的 DOM 操作,就像一个聪明的预判者,提前做好准备,避免不必要的麻烦。
const playerIntersectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { initPlayer(entry.target); } }); }, { rootMargin: '500px 0px', threshold: 0.1 });
JavaScript

4.2.4 📜 无限滚动适配

  • 👀 Mutation Observer 监听 DOM 变化:使用 MutationObserver 监听页面的 DOM 变化,当有新的播放器元素添加到页面时,自动初始化对该播放器的监听,就像一个警觉的哨兵,时刻关注着页面的变化,及时发现新的播放器并进行处理。
const domObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.classList && node.classList.contains('aplayer')) { playerIntersectionObserver.observe(node); } else if (node.nodeType === 1) { node.querySelectorAll('.aplayer').forEach(player => { playerIntersectionObserver.observe(player); }); } }); } }); }); domObserver.observe(document.body, { childList: true, subtree: true });
JavaScript
 

五、灵动岛增强与性能优化

在原有功能基础上,进一步增强了灵动岛的视觉效果与交互体验,并通过技术重构实现了性能的显著提升,使歌词展示更流畅、资源占用更合理。

5.1 CSS样式增强:专辑封面与歌词容器优化

通过新增专辑封面容器和细化歌词样式,让灵动岛的视觉层次更丰富,交互反馈更直观。
/* 灵动岛基础样式增强 */ .dynamic-island { position: relative; overflow: hidden; display: flex; align-items: center; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 更自然的过渡曲线 */ } /* 专辑封面容器(默认隐藏,播放时显示) */ .album-cover { width: 0; height: 0; border-radius: 4px; background-color: #333; margin-right: 0; margin-left: 8px; flex-shrink: 0; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* 与灵动岛过渡保持一致 */ } .dynamic-island.playing .album-cover { width: 24px; height: 24px; margin-right: 8px; /* 播放时展开并显示间距 */ } .album-cover img { width: 100%; height: 100%; object-fit: cover; /* 确保封面图片比例正确 */ } /* 歌词容器与文本优化 */ .lyric-container { position: relative; flex-grow: 1; height: 100%; display: flex; align-items: center; overflow: hidden; padding: 0 4px; /* 避免歌词贴边 */ } .lyric-text { color: #ffcc99; font-size: 14px; /* 增大字体提升可读性 */ white-space: nowrap; transform: translateX(100%); opacity: 0; font-weight: 500; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); transition: transform 0.3s ease-out, opacity 0.3s ease-out; max-width: calc(100% - 8px); /* 预留边距,避免内容溢出 */ }
CSS

核心优化点:

  • 专辑封面动态显示:通过 .dynamic-island.playing 类触发,播放时自动展开封面容器并显示专辑图片,增强视觉关联性;
  • 歌词样式细化:增大字体尺寸、添加边距限制,提升可读性;
  • 过渡曲线统一:采用 cubic-bezier(0.4, 0, 0.2, 1) 曲线,使灵动岛展开、封面显示等动画更协调自然。

5.2 JavaScript实现:从“主动轮询”到“被动监听”的重构

通过引入 MutationObserver 替代定时轮询,实现歌词变化的精准监听,同时优化滚动逻辑,提升性能与流畅度。

5.2.1 歌词滚动控制器(JS驱动)

// 歌词滚动状态管理 let scrollAnimationId = null; // 动画ID,用于中断 let isScrolling = false; // 滚动状态标记 let startTimestamp = null; // 动画开始时间戳 // 停止当前滚动动画 const stopScroll = () => { if (scrollAnimationId) { cancelAnimationFrame(scrollAnimationId); scrollAnimationId = null; } isScrolling = false; startTimestamp = null; }; // 平滑滚动函数(优化版) const smoothScroll = (element, totalDistance, duration) => { if (totalDistance <= 0) return; // 无需滚动 // 缓动函数:模拟自然物理运动 const easeInOutCubic = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; // 重置状态 element.style.transform = 'translateX(0)'; startTimestamp = performance.now(); const animate = (currentTime) => { const elapsedTime = currentTime - startTimestamp; const progress = Math.min(elapsedTime / duration, 1); const easedProgress = easeInOutCubic(progress); // 计算当前位置(从0滚动到-totalDistance) const currentPosition = -totalDistance * easedProgress; element.style.transform = `translateX(${currentPosition}px)`; if (progress < 1) { scrollAnimationId = requestAnimationFrame(animate); } else { stopScroll(); // 动画完成,清理状态 } }; isScrolling = true; scrollAnimationId = requestAnimationFrame(animate); };
JavaScript

5.2.2 歌词变化监听系统(核心优化)

// 监听歌词容器中p元素的class变化 const setupLyricChangeListener = (player) => { // 找到歌词容器 const lrcContainer = player.querySelector('.aplayer-lrc-contents'); if (!lrcContainer) return; // 创建监听p元素class变化的Observer const lyricObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { // 只处理class变化的p元素 if (mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target.tagName === 'P') { const pElement = mutation.target; // 检查是否是当前歌词(class包含aplayer-lrc-current) if (pElement.classList.contains('aplayer-lrc-current')) { handleNewLyric(pElement.textContent.trim()); // 处理新歌词 } } }); }); // 开始监听所有p元素的class变化 lyricObserver.observe(lrcContainer, { attributes: true, // 监听属性变化 attributeFilter: ['class'], // 只监听class属性 subtree: true // 监听子树(所有p元素) }); // 存储Observer,用于后续清理 player.lyricObserver = lyricObserver; }; // 处理新歌词 const handleNewLyric = (rawLyric) => { // 去除翻译内容,保留纯歌词 const cleanLyric = rawLyric.replace(/\\([^)]*\\)|\\[.*?\\]/g, '').trim(); if (!cleanLyric) return; // 停止当前滚动 stopScroll(); // 更新歌词显示 lyricText.textContent = cleanLyric; lyricText.classList.add('visible'); lyricText.style.transform = 'translateX(0)'; // 计算滚动参数(歌词宽度 > 可见宽度时才滚动) const lyricWidth = getTextWidth(cleanLyric); const visibleWidth = getVisibleWidth(); if (lyricWidth > visibleWidth) { const scrollDistance = lyricWidth - visibleWidth; const scrollDuration = Math.max(5, scrollDistance / 60) * 1000; // 60px/秒的滚动速度 setTimeout(() => { smoothScroll(lyricText, scrollDistance, scrollDuration); }, 200); } };
JavaScript

核心优化点:

  • 从“定时轮询”到“被动监听”:使用 MutationObserver 监听歌词元素(p 标签)的 class 变化,仅在歌词实际切换时触发更新,替代原有的 setInterval 定时查询,减少无效计算;
  • 精准歌词处理:通过正则清理歌词中的翻译内容,确保显示纯歌词,并仅在歌词变化时执行滚动计算,避免重复操作;
  • 平滑滚动优化:采用 requestAnimationFrame 结合缓动函数,使歌词滚动更符合物理规律,减少视觉卡顿。

5.3 性能优化对比:从“低效轮询”到“按需执行”

通过技术重构,灵动岛的性能在多个维度实现显著提升,核心差异如下:
维度
原有方案(定时轮询)
优化方案(MutationObserver)
触发时机
固定间隔(如500ms/次),无论歌词是否变化
仅歌词实际切换时触发(p元素class变化)
DOM访问频率
高频重复查询(每500ms一次)
仅歌词变化时查询一次
主线程占用
持续占用(即使歌词不变)
仅歌词切换时短暂占用
无效计算
多(重复处理相同歌词)
无(每句歌词仅处理一次)
资源释放
依赖定时器清理,易遗漏
通过 observer.disconnect() 精准释放
核心优势:优化方案彻底消除了“无歌词变化时的无效计算”,将DOM操作频率与歌词变化频率绑定(通常远低于500ms/次),显著降低主线程负担,尤其在多播放器或长时间播放场景下,性能优势更明显。

5.4 专辑封面同步逻辑

新增专辑封面提取与显示功能,实现歌词与封面的联动:
// 更新专辑封面 const updateAlbumCover = () => { if (!activePlayer) return; // 从播放器获取封面图片 const coverElement = activePlayer.querySelector('.aplayer-pic'); if (coverElement) { // 支持从background-image提取封面 const bgImage = coverElement.style.backgroundImage; if (bgImage) { const imgUrlMatch = bgImage.match(/url\\("?(.+?)"?\\)/); if (imgUrlMatch && imgUrlMatch[1]) { albumCover.innerHTML = `<img src="${imgUrlMatch[1]}" alt="专辑封面">`; return; } } // 支持从img标签提取封面 const imgElement = coverElement.querySelector('img'); if (imgElement && imgElement.src) { albumCover.innerHTML = `<img src="${imgElement.src}" alt="专辑封面">`; } } };
JavaScript
通过解析播放器中的封面元素(aplayer-pic),自动提取封面图片并同步到灵动岛的专辑封面容器,增强视觉关联性。
优化通过CSS样式增强、JavaScript逻辑重构(从轮询到监听)、性能精细化管理,使灵动岛在视觉效果更丰富的同时,实现了“按需执行”的高效运行模式。专辑封面的动态显示与歌词的精准同步,进一步提升了用户体验,而性能的优化则为多场景下的稳定运行提供了保障。
 
notion image

六、总结与展望

6.1 📋 项目总结

通过对 Moments 项目的页面展示增强和 iPhone 16 Pro 模拟,我们不仅优化了项目的视觉效果,还提升了用户的交互体验。在 CSS 方面,通过隐藏不必要的元素和添加灵动岛歌词容器及动画,使页面更加简洁美观,就像为项目穿上了一件华丽的外衣;在 JavaScript 方面,实现了 iPhone 16 Pro 的模拟效果,并解决了动态多播放器的相关问题,确保了播放器状态的同步、歌词的实时更新和性能的优化,就像为项目注入了强大的动力。

6.2 🌟 未完成计划

  • ⚙️ 性能优化:继续优化项目的性能,减少不必要的 DOM 操作和定时器,提高页面的响应速度和流畅度,就像为项目进行一次全面的体检,让它更加健康和高效。
  • 🔌 兼容性优化:确保项目在不同的浏览器和设备上都能正常显示和运行,提高项目的兼容性和稳定性,就像为项目打造一个坚固的基础,让它能够在各种环境中茁壮成长。
 
原作者项目地址:https://github.com/kingwrcy/moments
IPhone16 Pro模拟地址:https://moments.hehouhui.cn/
 
Spring Cache 扩展:Redis 批量操作优化方案与 BatchCache 自定义实现Spring AI RAG 高级检索增强:探索基于查询转换与文档处理的技术
Loading...