乐闻世界logo
搜索文章和话题

前端面试题手册

Garfish 的生命周期管理包括哪些钩子函数,它们的作用和执行顺序是什么?

Garfish 提供了完整的生命周期管理机制,确保子应用的加载、挂载、更新和卸载过程可控且可预测。生命周期钩子1. bootstrap(初始化)触发时机:子应用首次加载时作用:执行子应用的初始化逻辑,如配置加载、依赖注入只执行一次:在子应用生命周期中只调用一次示例:export function bootstrap() { console.log('子应用初始化'); return Promise.resolve();}2. mount(挂载)触发时机:子应用需要渲染到页面时作用:将子应用渲染到指定的容器中可多次调用:每次路由切换到该子应用时都会触发示例:export function mount(container) { ReactDOM.render(<App />, container); return Promise.resolve();}3. unmount(卸载)触发时机:子应用需要从页面移除时作用:清理子应用的 DOM、事件监听、定时器等资源必须执行:确保完全清理,避免内存泄漏示例:export function unmount(container) { ReactDOM.unmountComponentAtNode(container); return Promise.resolve();}4. update(更新)触发时机:子应用需要更新时(可选)作用:处理子应用的更新逻辑非必需:不是所有子应用都需要实现示例:export function update(props) { // 处理属性更新 return Promise.resolve();}生命周期流程首次加载:bootstrap → mount路由切换:unmount(旧应用)→ mount(新应用)重新激活:直接调用 mount(不重复 bootstrap)完全卸载:unmount + 清理所有资源最佳实践异步处理:所有生命周期函数都应返回 Promise错误处理:在生命周期中添加错误捕获逻辑资源清理:在 unmount 中彻底清理所有副作用性能优化:避免在 mount 中执行耗时操作状态管理:合理管理子应用的状态,避免重复初始化通过合理使用生命周期钩子,可以确保子应用的稳定运行和资源的有效管理。
阅读 0·2月21日 16:01

JavaScript 如何操作和操作 SVG

JavaScript 与 SVG 的结合可以实现强大的动态交互功能。以下是 JavaScript 操作 SVG 的主要方法:1. 选择 SVG 元素使用标准 DOM 方法选择 SVG 元素。// 通过 ID 选择const circle = document.getElementById('myCircle');// 通过类名选择const circles = document.querySelectorAll('.circle');// 通过标签名选择const allRects = document.querySelectorAll('rect');// 通过属性选择const filledElements = document.querySelectorAll('[fill="red"]');2. 创建 SVG 元素使用 createElementNS 创建 SVG 元素(注意命名空间)。const svgNS = 'http://www.w3.org/2000/svg';// 创建 SVG 元素const circle = document.createElementNS(svgNS, 'circle');circle.setAttribute('cx', '100');circle.setAttribute('cy', '100');circle.setAttribute('r', '50');circle.setAttribute('fill', 'blue');// 添加到 SVGconst svg = document.querySelector('svg');svg.appendChild(circle);3. 修改 SVG 属性使用 setAttribute 和 getAttribute 方法。const circle = document.querySelector('circle');// 修改属性circle.setAttribute('fill', 'red');circle.setAttribute('r', '60');circle.setAttribute('stroke', 'black');circle.setAttribute('stroke-width', '3');// 获取属性const fill = circle.getAttribute('fill');const radius = circle.getAttribute('r');4. 修改 SVG 样式使用 style 属性或 classList。const circle = document.querySelector('circle');// 直接设置样式circle.style.fill = 'green';circle.style.opacity = '0.5';circle.style.transform = 'scale(1.2)';// 使用 classListcircle.classList.add('highlight');circle.classList.remove('normal');circle.classList.toggle('active');5. 事件监听为 SVG 元素添加事件监听器。const circle = document.querySelector('circle');// 鼠标事件circle.addEventListener('click', function() { console.log('Circle clicked!'); this.setAttribute('fill', 'red');});circle.addEventListener('mouseover', function() { this.style.cursor = 'pointer';});circle.addEventListener('mouseout', function() { this.style.cursor = 'default';});// 键盘事件(需要 tabindex)circle.setAttribute('tabindex', '0');circle.addEventListener('keydown', function(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); console.log('Circle activated!'); }});6. 动画实现使用 JavaScript 实现 SVG 动画。const circle = document.querySelector('circle');// 使用 requestAnimationFramelet progress = 0;function animate() { progress += 0.01; const x = 100 + Math.sin(progress * 2 * Math.PI) * 50; circle.setAttribute('cx', x); if (progress < 1) { requestAnimationFrame(animate); }}animate();// 使用 CSS 过渡circle.style.transition = 'all 0.5s ease';circle.setAttribute('fill', 'red');circle.setAttribute('r', '60');7. 获取鼠标位置获取鼠标在 SVG 中的相对位置。const svg = document.querySelector('svg');svg.addEventListener('click', function(event) { const point = svg.createSVGPoint(); point.x = event.clientX; point.y = event.clientY; const svgPoint = point.matrixTransform(svg.getScreenCTM().inverse()); console.log(`SVG coordinates: x=${svgPoint.x}, y=${svgPoint.y}`);});8. 拖拽功能实现 SVG 元素的拖拽。let selectedElement = null;let offset = { x: 0, y: 0 };function getMousePosition(evt) { const CTM = svg.getScreenCTM(); return { x: (evt.clientX - CTM.e) / CTM.a, y: (evt.clientY - CTM.f) / CTM.d };}function startDrag(evt) { selectedElement = evt.target; offset = getMousePosition(evt); offset.x -= parseFloat(selectedElement.getAttribute('cx')); offset.y -= parseFloat(selectedElement.getAttribute('cy'));}function drag(evt) { if (selectedElement) { evt.preventDefault(); const coord = getMousePosition(evt); selectedElement.setAttribute('cx', coord.x - offset.x); selectedElement.setAttribute('cy', coord.y - offset.y); }}function endDrag(evt) { selectedElement = null;}const svg = document.querySelector('svg');svg.addEventListener('mousedown', startDrag);svg.addEventListener('mousemove', drag);svg.addEventListener('mouseup', endDrag);svg.addEventListener('mouseleave', endDrag);9. 动态创建复杂图形使用 JavaScript 动态创建复杂的 SVG 图形。function createStar(cx, cy, spikes, outerRadius, innerRadius) { const svgNS = 'http://www.w3.org/2000/svg'; const polygon = document.createElementNS(svgNS, 'polygon'); let points = []; for (let i = 0; i < spikes * 2; i++) { const radius = i % 2 === 0 ? outerRadius : innerRadius; const angle = (Math.PI / spikes) * i; const x = cx + Math.cos(angle) * radius; const y = cy + Math.sin(angle) * radius; points.push(`${x},${y}`); } polygon.setAttribute('points', points.join(' ')); polygon.setAttribute('fill', 'gold'); polygon.setAttribute('stroke', 'orange'); polygon.setAttribute('stroke-width', '2'); return polygon;}const svg = document.querySelector('svg');const star = createStar(100, 100, 5, 50, 25);svg.appendChild(star);10. 数据可视化使用 SVG 和 JavaScript 创建数据可视化。const data = [10, 25, 40, 30, 50];const svg = document.querySelector('svg');const barWidth = 40;const gap = 20;data.forEach((value, index) => { const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); const x = 20 + index * (barWidth + gap); const y = 200 - value * 3; rect.setAttribute('x', x); rect.setAttribute('y', y); rect.setAttribute('width', barWidth); rect.setAttribute('height', value * 3); rect.setAttribute('fill', `hsl(${index * 60}, 70%, 50%)`); svg.appendChild(rect);});最佳实践:使用 createElementNS 创建 SVG 元素合理使用事件委托优化动画性能,使用 requestAnimationFrame注意命名空间考虑使用 SVG 库(如 D3.js)处理复杂场景测试跨浏览器兼容性
阅读 0·2月21日 15:58

Expo应用如何实现国际化(i18n)?有哪些推荐的库?

Expo应用的国际化(i18n)是面向全球用户的重要功能。Expo支持多种国际化解决方案,使开发者能够轻松实现多语言支持。国际化库选择:i18next最流行的国际化库,功能强大且易于使用。安装:npm install i18next react-i18next expo-localization配置i18next:// i18n.tsimport i18n from 'i18next';import { initReactI18next } from 'react-i18next';import { getLocales } from 'expo-localization';const resources = { en: { translation: { welcome: 'Welcome', login: 'Login', logout: 'Logout', 'hello.name': 'Hello, {{name}}!', }, }, zh: { translation: { welcome: '欢迎', login: '登录', logout: '退出', 'hello.name': '你好,{{name}}!', }, },};i18n .use(initReactI18next) .init({ resources, lng: getLocales()[0]?.languageCode || 'en', fallbackLng: 'en', interpolation: { escapeValue: false, }, });export default i18n;使用i18next:import { useTranslation } from 'react-i18next';function WelcomeScreen() { const { t, i18n } = useTranslation(); const changeLanguage = (lang: string) => { i18n.changeLanguage(lang); }; return ( <View> <Text>{t('welcome')}</Text> <Text>{t('hello.name', { name: 'John' })}</Text> <Button title="English" onPress={() => changeLanguage('en')} /> <Button title="中文" onPress={() => changeLanguage('zh')} /> </View> );}expo-localizationExpo官方的本地化库,用于获取设备语言设置。使用expo-localization:import * as Localization from 'expo-localization';function getDeviceLanguage() { const locale = Localization.locale; const languageCode = Localization.locale.split('-')[0]; console.log('Locale:', locale); console.log('Language:', languageCode); return languageCode;}获取本地化信息:function getLocalizationInfo() { const locale = Localization.locale; const timezone = Localization.timezone; const isoCurrencyCodes = Localization.isoCurrencyCodes; return { locale, timezone, currency: isoCurrencyCodes[0], };}React Native Localization轻量级的国际化解决方案。安装:npm install react-native-localize配置:import * as RNLocalize from 'react-native-localize';const translations = { en: require('./en.json'), zh: require('./zh.json'),};const fallback = { languageTag: 'en', isRTL: false };const { languageTag } = RNLocalize.findBestAvailableLanguage(Object.keys(translations)) || fallback;i18n.init({ resources: { [languageTag]: translations[languageTag], }, lng: languageTag, fallbackLng: 'en',});多语言资源管理:JSON文件结构// locales/en.json{ "common": { "ok": "OK", "cancel": "Cancel", "save": "Save" }, "auth": { "login": "Login", "logout": "Logout", "register": "Register", "forgotPassword": "Forgot Password?" }, "home": { "title": "Home", "welcome": "Welcome back!", "recentActivity": "Recent Activity" }}// locales/zh.json{ "common": { "ok": "确定", "cancel": "取消", "save": "保存" }, "auth": { "login": "登录", "logout": "退出", "register": "注册", "forgotPassword": "忘记密码?" }, "home": { "title": "首页", "welcome": "欢迎回来!", "recentActivity": "最近活动" }}命名空间组织// 使用命名空间i18n.init({ resources: { en: { common: require('./locales/en/common.json'), auth: require('./locales/en/auth.json'), home: require('./locales/en/home.json'), }, zh: { common: require('./locales/zh/common.json'), auth: require('./locales/zh/auth.json'), home: require('./locales/zh/home.json'), }, },});// 使用命名空间const { t } = useTranslation('common');<Text>{t('ok')}</Text>// 跨命名空间<Text>{t('auth:login')}</Text>日期和时间本地化:import { format } from 'date-fns';import { zhCN, enUS } from 'date-fns/locale';function formatDate(date: Date, locale: string) { const localeMap = { en: enUS, zh: zhCN, }; return format(date, 'PPP', { locale: localeMap[locale] || enUS, });}function formatDateTime(date: Date, locale: string) { return format(date, 'PPPppp', { locale: locale === 'zh' ? zhCN : enUS, });}数字和货币本地化:function formatCurrency(amount: number, locale: string) { return new Intl.NumberFormat(locale, { style: 'currency', currency: locale === 'zh' ? 'CNY' : 'USD', }).format(amount);}function formatNumber(number: number, locale: string) { return new Intl.NumberFormat(locale).format(number);}function formatPercent(value: number, locale: string) { return new Intl.NumberFormat(locale, { style: 'percent', minimumFractionDigits: 2, }).format(value);}RTL(从右到左)支持:import { I18nManager } from 'react-native';function setupRTL(locale: string) { const isRTL = locale === 'ar' || locale === 'he'; if (I18nManager.isRTL !== isRTL) { I18nManager.allowRTL(isRTL); I18nManager.forceRTL(isRTL); // 需要重启应用 Updates.reloadAsync(); }}动态语言切换:function LanguageSwitcher() { const { i18n } = useTranslation(); const [currentLang, setCurrentLang] = useState(i18n.language); const changeLanguage = async (lang: string) => { await i18n.changeLanguage(lang); setCurrentLang(lang); // 保存用户偏好 await AsyncStorage.setItem('userLanguage', lang); // 如果需要RTL支持 setupRTL(lang); }; const languages = [ { code: 'en', name: 'English' }, { code: 'zh', name: '中文' }, { code: 'es', name: 'Español' }, ]; return ( <View> {languages.map((lang) => ( <Button key={lang.code} title={lang.name} onPress={() => changeLanguage(lang.code)} disabled={currentLang === lang.code} /> ))} </View> );}最佳实践:资源文件组织按功能模块组织翻译文件使用命名空间避免冲突保持翻译文件结构一致翻译质量使用专业的翻译工具考虑文化差异和习惯定期审查和更新翻译性能优化按需加载语言包缓存翻译结果避免频繁的语言切换用户体验自动检测设备语言提供语言切换选项保存用户语言偏好测试覆盖测试所有语言的显示检查文本溢出问题验证RTL布局常见问题:翻译缺失设置fallback语言使用翻译管理工具定期检查翻译完整性文本溢出使用flex布局提供文本截断选项为不同语言调整布局动态加载使用代码分割懒加载语言包预加载常用语言通过完善的国际化实现,Expo应用可以更好地服务全球用户,提升用户体验和市场竞争力。
阅读 0·2月21日 15:58

Lottie 支持哪些动画类型和效果?

Lottie 动画支持多种动画类型和效果,以下是详细的分类和说明:1. 基础动画类型位置动画{ "p": { "a": 1, "k": [ { "i": {x: 0.833, y: 0.833}, "o": {x: 0.167, y: 0.167}, "t": 0, "s": [100, 100, 0] }, { "t": 60, "s": [400, 300, 0] } ] }}缩放动画{ "s": { "a": 1, "k": [ { "t": 0, "s": [100, 100, 100] }, { "t": 60, "s": [150, 150, 100] } ] }}旋转动画{ "r": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 360 } ] }}不透明度动画{ "o": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 30, "s": 100 }, { "t": 60, "s": 0 } ] }}2. 形状动画路径变形动画{ "ks": { "k": [ { "t": 0, "i": [[0, 0], [0, 0], [0, 0]], "o": [[0, 0], [0, 0], [0, 0]], "v": [[0, 0], [100, 0], [100, 100]] }, { "t": 60, "i": [[0, 0], [0, 0], [0, 0]], "o": [[0, 0], [0, 0], [0, 0]], "v": [[50, 50], [150, 50], [150, 150]] } ] }}圆角矩形动画{ "ty": "rc", "r": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 20 } ] }}椭圆动画{ "ty": "el", "s": { "a": 1, "k": [ { "t": 0, "s": [50, 50] }, { "t": 60, "s": [100, 80] } ] }}星形和多边形动画{ "ty": "sr", "pt": { "a": 1, "k": [ { "t": 0, "s": 5 }, { "t": 60, "s": 8 } ] }, "or": { "a": 1, "k": [ { "t": 0, "s": 50 }, { "t": 60, "s": 80 } ] }}3. 颜色和渐变动画填充颜色动画{ "ty": "fl", "c": { "a": 1, "k": [ { "t": 0, "s": [1, 0, 0, 1] // 红色 }, { "t": 60, "s": [0, 0, 1, 1] // 蓝色 } ] }}描边颜色动画{ "ty": "st", "c": { "a": 1, "k": [ { "t": 0, "s": [0, 0, 0, 1] }, { "t": 60, "s": [1, 1, 1, 1] } ] }}渐变动画{ "ty": "gf", "g": { "p": 1, // 线性渐变 "k": { "a": 1, "k": [ { "t": 0, "g": [ { "p": 0, "c": [1, 0, 0, 1] }, { "p": 1, "c": [0, 0, 1, 1] } ] }, { "t": 60, "g": [ { "p": 0, "c": [0, 1, 0, 1] }, { "p": 1, "c": [1, 1, 0, 1] } ] } ] } }}4. 文本动画文本内容动画{ "ty": 1, "t": { "d": { "k": [ { "t": 0, "s": { "t": "Hello", "f": "Arial", "s": 50, "j": 1, "tr": 0, "lh": 60, "ls": 0, "fc": [0, 0, 0, 1] } }, { "t": 60, "s": { "t": "World", "f": "Arial", "s": 50, "j": 1, "tr": 0, "lh": 60, "ls": 0, "fc": [0, 0, 0, 1] } } ] } }}文本追踪动画{ "t": { "d": { "k": [ { "t": 0, "s": { "t": "Hello", "ls": 0 } }, { "t": 60, "s": { "t": "Hello", "ls": 50 } } ] } }}5. 3D 变换动画3D 旋转动画{ "rx": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 180 } ] }, "ry": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 90 } ] }, "rz": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 45 } ] }}6. 遮罩和蒙版动画遮罩路径动画{ "ty": "mask", "pt": { "a": 1, "k": [ { "t": 0, "i": [[0, 0], [0, 0]], "o": [[0, 0], [0, 0]], "v": [[0, 0], [100, 0], [100, 100], [0, 100]] }, { "t": 60, "i": [[0, 0], [0, 0]], "o": [[0, 0], [0, 0]], "v": [[50, 50], [150, 50], [150, 150], [50, 150]] } ] }}蒙版不透明度动画{ "ty": "mask", "o": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 100 } ] }}7. 效果动画阴影效果动画{ "ty": 25, "nm": "Drop Shadow", "ef": [ { "ty": 1, "nm": "Shadow Color", "v": { "a": 1, "k": [ { "t": 0, "s": [0, 0, 0, 0.5] }, { "t": 60, "s": [1, 0, 0, 0.8] } ] } }, { "ty": 0, "nm": "Shadow Distance", "v": { "a": 1, "k": [ { "t": 0, "s": 5 }, { "t": 60, "s": 20 } ] } } ]}模糊效果动画{ "ty": 0, "nm": "Gaussian Blur", "ef": [ { "ty": 0, "nm": "Blurriness", "v": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 10 } ] } } ]}8. 复合动画父子层级动画{ "ty": 0, "parent": 1, "ks": { "p": { "a": 1, "k": [ { "t": 0, "s": [0, 0, 0] }, { "t": 60, "s": [100, 100, 0] } ] } }}表达式动画{ "ks": { "p": { "a": 0, "k": [ { "s": true, "ix": 2, "x": "wiggle(5, 20)" // 随机摆动表达式 } ] } }}9. 时间重映射{ "tm": { "a": 1, "k": [ { "t": 0, "s": 0 }, { "t": 60, "s": 30 } ] }}10. 缓动和缓出{ "ks": { "p": { "a": 1, "k": [ { "i": {x: 0.25, y: 1}, // 缓入 "o": {x: 0.75, y: 0}, // 缓出 "t": 0, "s": [0, 0, 0] }, { "t": 60, "s": [100, 100, 0] } ] } }}支持的动画特性总结:基础变换:位置、缩放、旋转、不透明度形状变形:路径变形、圆角、椭圆、多边形颜色动画:填充、描边、渐变文本动画:内容、大小、间距、颜色3D 变换:X/Y/Z 轴旋转、3D 位置遮罩和蒙版:路径、不透明度、混合模式效果:阴影、模糊、发光等复合动画:父子层级、表达式时间控制:时间重映射、缓动函数
阅读 0·2月21日 15:52

什么是 Lottie 动画库,它的工作原理是什么?

Lottie 是一个由 Airbnb 开发的开源库,用于在移动应用和 Web 平台上渲染高质量的动画。它允许设计师在 Adobe After Effects 中创建动画,然后通过 Bodymovin 插件导出为 JSON 格式,开发者可以直接使用这些 JSON 文件在应用中播放动画,而无需编写复杂的动画代码。Lottie 的核心优势在于:跨平台支持:支持 iOS、Android、React Native、Web 等多个平台高性能:使用原生渲染,动画流畅且性能优异小文件体积:JSON 文件通常比 GIF 或视频文件小得多可编程控制:可以通过代码控制动画的播放、暂停、速度等矢量图形:支持缩放而不失真,适合各种屏幕尺寸技术实现方面,Lottie 通过解析 JSON 文件中的动画数据,使用各平台的绘图 API(如 iOS 的 Core Animation、Android 的 Canvas、Web 的 Canvas 或 SVG)来实时渲染动画。JSON 文件包含了图层、形状、路径、关键帧等信息,Lottie 库负责解析这些数据并创建相应的动画对象。Lottie 的工作流程:设计师在 After Effects 中创建动画使用 Bodymovin 插件导出为 JSON 文件开发者将 JSON 文件集成到项目中使用 Lottie 库加载和播放动画Lottie 支持的动画特性包括:形状动画、遮罩、蒙版、渐变、3D 变换、文本动画等。它还支持动态属性修改,允许在运行时更改动画的颜色、文本内容等。在性能优化方面,Lottie 提供了缓存机制、硬件加速、帧率控制等功能,确保动画在各种设备上都能流畅运行。
阅读 0·2月21日 15:52

Lottie 动画相比 GIF 和视频有哪些性能优势?

Lottie 动画相比传统的 GIF、PNG 序列帧和视频格式有显著的性能优势:1. 文件体积Lottie:JSON 文件通常只有几 KB 到几百 KB,压缩率极高GIF:文件体积较大,通常在几百 KB 到几 MBPNG 序列帧:文件体积最大,需要存储每一帧的完整图像视频:体积中等,但编码后仍有较大文件2. 渲染性能Lottie:使用原生绘图 API(Core Animation、Canvas、SVG),支持硬件加速,渲染流畅GIF:解码开销大,不支持硬件加速,容易造成卡顿PNG 序列帧:内存占用高,加载时间长,影响性能视频:解码开销中等,但播放控制不灵活3. 内存占用Lottie:内存占用低,只存储动画数据,不存储位图GIF:需要解码并缓存所有帧,内存占用高PNG 序列帧:需要加载所有图片到内存,内存占用最高视频:需要解码缓存,内存占用中等4. 交互控制Lottie:支持播放、暂停、进度控制、速度调节、反向播放等GIF:无法控制,只能循环播放PNG 序列帧:控制困难,需要手动管理帧视频:基本控制,但交互性有限5. 响应式支持Lottie:矢量图形,任意缩放不失真,适合各种屏幕尺寸GIF:位图格式,缩放会失真PNG 序列帧:位图格式,缩放会失真视频:位图格式,缩放会失真6. 动态修改Lottie:支持运行时修改颜色、文本、路径等属性GIF:无法修改PNG 序列帧:无法修改视频:无法修改7. 加载速度Lottie:文件小,加载快,支持渐进式加载GIF:加载慢,需要完整下载才能播放PNG 序列帧:加载最慢,需要下载所有帧视频:支持流媒体加载,但初始加载仍需时间性能优化建议:使用 Lottie Cache 缓存已加载的动画对于复杂动画,考虑使用 Lottie Composition合理设置动画的帧率和持续时间避免在列表中同时播放多个 Lottie 动画使用 Lottie 的 autoPlay 和 loop 属性控制播放行为
阅读 0·2月21日 15:52

Lottie 动画开发中常见的问题和解决方案有哪些?

Lottie 动画开发中常见的问题和解决方案如下:1. 动画不显示问题原因:JSON 文件路径错误JSON 文件格式不正确容器元素没有设置宽高动画数据加载失败解决方案:// 检查 JSON 文件路径import animationData from './animation.json';// 设置容器宽高const container = document.getElementById('lottie-container');container.style.width = '300px';container.style.height = '300px';// 添加错误处理const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json', rendererSettings: { preserveAspectRatio: 'xMidYMid slice' }});animation.addEventListener('data_failed', (error) => { console.error('Animation data failed to load:', error); // 显示降级内容 container.innerHTML = '<img src="fallback.png" alt="Animation fallback">';});2. 动画卡顿或性能差问题原因:动画文件过大同时播放多个动画设备性能不足渲染器选择不当解决方案:// 使用 Canvas 渲染器(性能更好)const animation = lottie.loadAnimation({ container: container, renderer: 'canvas', // 使用 canvas 而不是 svg loop: true, autoplay: true, path: 'animation.json', rendererSettings: { preserveAspectRatio: 'xMidYMid slice', clearCanvas: false, progressiveLoad: true, hideOnTransparent: true }});// 降低帧率animation.setSpeed(0.5); // 降低播放速度// 懒加载动画const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const animation = lottie.loadAnimation({ container: entry.target, renderer: 'canvas', loop: true, autoplay: true, path: 'animation.json' }); observer.unobserve(entry.target); } });});// 在低端设备上禁用复杂动画if (navigator.hardwareConcurrency < 4) { // 使用简化版本或静态图片}3. 动画在不同平台表现不一致问题原因:不同平台的渲染引擎差异字体支持不一致缓动函数实现差异解决方案:// 使用标准的缓动函数const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json', rendererSettings: { preserveAspectRatio: 'xMidYMid meet' }});// 检测平台并调整const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);const isAndroid = /Android/.test(navigator.userAgent);if (isIOS) { // iOS 特定调整 animation.setSpeed(1.0);} else if (isAndroid) { // Android 特定调整 animation.setSpeed(0.9);}4. 内存泄漏问题原因:动画实例未正确销毁事件监听器未移除组件卸载时未清理资源解决方案:// React 中正确清理import { useEffect, useRef } from 'react';function MyComponent() { const animationRef = useRef(null); const containerRef = useRef(null); useEffect(() => { if (containerRef.current) { animationRef.current = lottie.loadAnimation({ container: containerRef.current, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json' }); } return () => { // 清理动画实例 if (animationRef.current) { animationRef.current.destroy(); animationRef.current = null; } }; }, []); return <div ref={containerRef}></div>;}// 移除所有事件监听器function cleanupAnimation(animation) { const events = ['complete', 'loopComplete', 'enterFrame', 'config_ready', 'data_ready', 'DOMLoaded', 'destroy']; events.forEach(event => { animation.removeEventListener(event); }); animation.destroy();}5. 动画加载慢问题原因:JSON 文件过大网络延迟未使用缓存解决方案:// 压缩 JSON 文件// 使用 LottieFiles 优化器:https://lottiefiles.com/tools/optimize// 使用 CDNconst animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'https://cdn.example.com/animation.json'});// 启用 Service Worker 缓存// 在 service-worker.js 中self.addEventListener('fetch', (event) => { if (event.request.url.includes('.json')) { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request).then((response) => { return caches.open('lottie-cache').then((cache) => { cache.put(event.request, response.clone()); return response; }); }); }) ); }});// 显示加载状态let isLoading = true;const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, autoplay: true, path: 'animation.json'});animation.addEventListener('DOMLoaded', () => { isLoading = false; container.classList.remove('loading');});6. 动画颜色不正确问题原因:颜色格式不匹配动态颜色修改失败平台颜色渲染差异解决方案:// 使用正确的颜色格式(RGBA)animation.setColorFilter([ { keypath: 'layer1', color: 'rgba(255, 0, 0, 1)' }]);// 或者直接修改 JSON 数据function modifyAnimationColor(animationData, keypath, newColor) { const colorArray = hexToRgba(newColor); animationData.layers.forEach(layer => { if (layer.nm === keypath) { layer.shapes.forEach(shape => { if (shape.ty === 'fl') { shape.c.k = colorArray; } }); } }); return animationData;}function hexToRgba(hex) { const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; return [r, g, b, 1];}7. 动画在列表中性能问题问题原因:同时渲染多个动画实例未使用虚拟化列表动画未正确卸载解决方案:// 使用虚拟化列表(React)import { FixedSizeList as List } from 'react-window';function Row({ index, style }) { const containerRef = useRef(null); const animationRef = useRef(null); useEffect(() => { if (containerRef.current) { animationRef.current = lottie.loadAnimation({ container: containerRef.current, renderer: 'canvas', loop: false, autoplay: false, path: animations[index].url }); } return () => { if (animationRef.current) { animationRef.current.destroy(); } }; }, [index]); return ( <div style={style}> <div ref={containerRef} style={{ width: 100, height: 100 }}></div> </div> );}<List height={600} itemCount={animations.length} itemSize={120} width={400}> {Row}</List>// 或者使用 Intersection Observerconst observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const animation = lottie.loadAnimation({ container: entry.target, renderer: 'canvas', loop: true, autoplay: true, path: entry.target.dataset.url }); } else { // 暂停不可见的动画 const animation = entry.target.lottieAnimation; if (animation) { animation.pause(); } } });}, { threshold: 0.1 });8. iOS 上动画不工作问题原因:未安装 CocoaPods 依赖iOS 版本过低内存限制解决方案:# 安装 CocoaPods 依赖cd ios && pod install# 检查 iOS 版本兼容性# Lottie iOS 支持的最低版本:iOS 9.0+# 在 Info.plist 中添加内存警告处理9. Android 上动画不工作问题原因:Gradle 配置问题权限问题内存限制解决方案:// 在 app/build.gradle 中添加android { defaultConfig { vectorDrawables.useSupportLibrary = true }}dependencies { implementation 'com.airbnb.android:lottie:6.0.0'}10. 动画循环问题问题原因:循环设置不正确动画结束事件未正确处理多个动画实例冲突解决方案:// 正确设置循环const animation = lottie.loadAnimation({ container: container, renderer: 'svg', loop: true, // 启用循环 autoplay: true, path: 'animation.json'});// 或者手动控制循环animation.addEventListener('complete', () => { animation.goToAndPlay(0, true);});// 限制循环次数let loopCount = 0;const maxLoops = 3;animation.addEventListener('loopComplete', () => { loopCount++; if (loopCount >= maxLoops) { animation.loop = false; }});最佳实践总结:始终添加错误处理和降级方案在组件卸载时清理动画实例使用适当的渲染器(Canvas 用于性能,SVG 用于质量)实现懒加载和虚拟化列表压缩和优化动画文件使用 CDN 和缓存加速加载测试不同设备和平台的兼容性监控动画性能和内存使用
阅读 0·2月21日 15:52

Lottie 动画与其他动画技术(GIF、视频、CSS 动画等)相比有哪些区别和优势?

Lottie 动画与其他动画技术相比有明显的区别和优势,以下是详细的对比分析:1. Lottie vs GIF文件大小Lottie:JSON 文件通常只有几 KB 到几百 KB,压缩率极高GIF:文件体积较大,通常在几百 KB 到几 MB,且压缩率低渲染质量Lottie:矢量图形,任意缩放不失真,支持透明背景GIF:位图格式,缩放会失真,不支持真正的透明背景(只有 1 位透明)性能表现Lottie:使用原生渲染,支持硬件加速,流畅度高GIF:解码开销大,不支持硬件加速,容易造成卡顿交互控制Lottie:支持播放、暂停、进度控制、速度调节、反向播放等GIF:无法控制,只能循环播放动态修改Lottie:支持运行时修改颜色、文本、路径等属性GIF:无法修改适用场景Lottie:适合需要高质量、可交互、可缩放的动画场景GIF:适合简单的、不需要交互的动画场景2. Lottie vs PNG 序列帧文件大小Lottie:JSON 文件小,只存储动画数据PNG 序列帧:需要存储每一帧的完整图像,文件体积最大内存占用Lottie:内存占用低,只存储动画数据PNG 序列帧:需要加载所有图片到内存,内存占用最高加载速度Lottie:加载快,支持渐进式加载PNG 序列帧:加载最慢,需要下载所有帧性能表现Lottie:渲染流畅,性能优异PNG 序列帧:切换帧时有性能开销,容易卡顿响应式支持Lottie:矢量图形,任意缩放不失真PNG 序列帧:位图格式,缩放会失真适用场景Lottie:适合需要高性能、小体积的动画场景PNG 序列帧:适合需要精确控制每一帧的场景3. Lottie vs 视频(MP4/WebM)文件大小Lottie:JSON 文件小,压缩率高视频:体积中等,编码后仍有较大文件渲染性能Lottie:使用原生渲染,性能优异视频:解码开销中等,播放控制不灵活交互控制Lottie:支持播放、暂停、进度控制、速度调节等视频:基本控制,但交互性有限动态修改Lottie:支持运行时修改属性视频:无法修改加载速度Lottie:加载快,支持渐进式加载视频:支持流媒体加载,但初始加载仍需时间适用场景Lottie:适合需要交互控制、动态修改的动画场景视频:适合复杂的、需要高保真的视频内容4. Lottie vs CSS 动画开发复杂度Lottie:设计师在 After Effects 中创建,开发者直接使用 JSON 文件CSS 动画:需要开发者编写 CSS 代码,复杂动画实现困难动画能力Lottie:支持复杂的形状动画、路径变形、3D 变换等CSS 动画:支持基础变换,复杂动画实现困难跨平台一致性Lottie:跨平台渲染一致CSS 动画:不同浏览器可能有差异性能表现Lottie:使用原生渲染,性能优异CSS 动画:使用浏览器渲染引擎,性能良好动态修改Lottie:支持运行时修改颜色、文本等CSS 动画:可以通过 CSS 变量动态修改适用场景Lottie:适合复杂的、需要设计师参与的动画场景CSS 动画:适合简单的、由开发者实现的动画场景5. Lottie vs Canvas 动画开发效率Lottie:设计师创建,开发者直接使用,开发效率高Canvas 动画:需要开发者编写 JavaScript 代码,开发效率低动画质量Lottie:矢量图形,高质量,任意缩放Canvas 动画:位图渲染,缩放会失真性能表现Lottie:使用原生渲染,性能优异Canvas 动画:使用 Canvas API,性能良好但需要优化可维护性Lottie:JSON 文件易于管理和更新Canvas 动画:代码复杂,维护困难适用场景Lottie:适合需要高质量、易于维护的动画场景Canvas 动画:适合需要高度自定义、复杂交互的动画场景6. Lottie vs SVG 动画开发复杂度Lottie:设计师在 After Effects 中创建,自动导出SVG 动画:需要手动编写 SVG 代码或使用工具生成动画能力Lottie:支持复杂的形状动画、路径变形、3D 变换等SVG 动画:支持基础的形状和路径动画文件大小Lottie:JSON 文件小,压缩率高SVG 动画:SVG 文件相对较大跨平台支持Lottie:支持 iOS、Android、Web 等多个平台SVG 动画:主要支持 Web 平台适用场景Lottie:适合跨平台、复杂的动画场景SVG 动画:适合 Web 平台的简单动画场景7. Lottie vs 原生动画(iOS Core Animation / Android Animator)开发效率Lottie:设计师创建,开发者直接使用,开发效率高原生动画:需要开发者编写原生代码,开发效率低跨平台一致性Lottie:跨平台渲染一致原生动画:不同平台需要分别实现动画能力Lottie:支持复杂的形状动画、路径变形等原生动画:支持基础变换,复杂动画实现困难性能表现Lottie:使用原生渲染,性能优异原生动画:直接使用平台 API,性能最佳适用场景Lottie:适合需要跨平台、快速开发的动画场景原生动画:适合需要最佳性能、高度定制的动画场景8. Lottie vs FLIP 动画开发复杂度Lottie:设计师创建,开发者直接使用FLIP 动画:需要开发者计算元素位置和状态,复杂度高动画类型Lottie:适合预定义的、独立的动画FLIP 动画:适合布局变化、元素移动等过渡动画性能表现Lottie:使用原生渲染,性能优异FLIP 动画:使用 transform 和 opacity,性能良好适用场景Lottie:适合独立的、预定义的动画场景FLIP 动画:适合布局变化、元素移动等过渡场景9. Lottie vs Three.js / WebGL 动画动画类型Lottie:适合 2D 矢量动画Three.js / WebGL:适合 3D 动画和复杂视觉效果性能表现Lottie:使用原生渲染,性能优异Three.js / WebGL:使用 GPU 加速,性能强大但需要优化开发复杂度Lottie:设计师创建,开发者直接使用Three.js / WebGL:需要开发者编写复杂的 3D 代码适用场景Lottie:适合 2D 矢量动画场景Three.js / WebGL:适合 3D 动画和复杂视觉效果场景10. 选择建议选择 Lottie 的场景:需要跨平台一致的动画需要高质量的矢量动画需要交互控制和动态修改需要小文件体积需要快速开发和迭代选择其他技术的场景:GIF:简单的、不需要交互的动画PNG 序列帧:需要精确控制每一帧的场景视频:复杂的、需要高保真的视频内容CSS 动画:简单的、由开发者实现的动画Canvas 动画:需要高度自定义、复杂交互的动画SVG 动画:Web 平台的简单动画原生动画:需要最佳性能、高度定制的动画FLIP 动画:布局变化、元素移动等过渡动画Three.js / WebGL:3D 动画和复杂视觉效果总结:Lottie 动画在文件大小、渲染质量、性能表现、交互控制等方面具有明显优势,特别适合需要跨平台、高质量、可交互的动画场景。但在某些特定场景下,其他动画技术可能更适合。选择时需要根据具体需求、性能要求、开发资源等因素综合考虑。
阅读 0·2月21日 15:52

MobX 6 有哪些主要变化和新特性?

MobX 6 是 MobX 的一个重大版本更新,引入了许多重要的变化和新特性。以下是 MobX 6 的主要变化:1. 强制使用 ActionMobX 5 及之前class Store { @observable count = 0; increment() { this.count++; // 可以直接修改 }}MobX 6class Store { @observable count = 0; @action // 必须使用 action increment() { this.count++; }}在 MobX 6 中,所有状态修改都必须在 action 中进行。这是为了提供更好的可预测性和调试体验。2. 装饰器 API 的变化MobX 5import { observable, computed, action } from 'mobx';class Store { @observable count = 0; @computed get doubled() { return this.count * 2; } @action increment() { this.count++; }}MobX 6import { makeObservable, observable, computed, action } from 'mobx';class Store { count = 0; constructor() { makeObservable(this); } get doubled() { return this.count * 2; } increment() { this.count++; }}MobX 6 推荐使用 makeObservable 而不是装饰器,但装饰器仍然支持。3. makeAutoObservable 的引入MobX 6 引入了 makeAutoObservable,它可以自动推断属性的类型:import { makeAutoObservable } from 'mobx';class Store { count = 0; firstName = 'John'; lastName = 'Doe'; constructor() { makeAutoObservable(this); } get fullName() { return `${this.firstName} ${this.lastName}`; } increment() { this.count++; }}makeAutoObservable 会自动:将 getter 标记为 computed将方法标记为 action将字段标记为 observable4. 配置 API 的简化MobX 5import { configure } from 'mobx';configure({ enforceActions: 'always', useProxies: 'always', computedRequiresReaction: true});MobX 6import { configure } from 'mobx';configure({ enforceActions: 'always', // 默认值 useProxies: 'ifavailable', // 默认值 computedRequiresReaction: false // 默认值});MobX 6 的默认配置更加严格和合理。5. Proxy 的强制使用MobX 6 强制使用 Proxy(在支持的浏览器中),这提供了更好的性能和更简单的 API。Proxy 的优势更好的性能更简单的 API更好的 TypeScript 支持更少的限制不支持 Proxy 的环境在旧浏览器或 Node.js 环境中,MobX 6 会自动降级到兼容模式。6. 移除的 API以下 API 在 MobX 6 中被移除:isObservableObject → 使用 isObservableintercept → 使用 observe 的拦截功能extras API → 大部分功能已集成到主 APItoJS → 使用 toJSON 或手动转换7. TypeScript 支持的改进MobX 6 对 TypeScript 的支持更加完善:import { makeAutoObservable } from 'mobx';class Store { count: number = 0; firstName: string = 'John'; lastName: string = 'Doe'; constructor() { makeAutoObservable(this); } get fullName(): string { return `${this.firstName} ${this.lastName}`; } increment(): void { this.count++; }}类型推断更加准确,类型定义更加简洁。8. 性能优化MobX 6 引入了多项性能优化:更快的依赖追踪更高效的 computed 缓存更好的批量更新机制减少的内存占用9. 调试体验的改进MobX 6 提供了更好的调试工具:更清晰的错误消息更好的堆栈跟踪改进的 DevTools 支持10. 迁移指南从 MobX 5 迁移到 MobX 6更新依赖npm install mobx@6 mobx-react@6添加 action 装饰器// 之前increment() { this.count++;}// 之后@actionincrement() { this.count++;}使用 makeObservable 或 makeAutoObservableconstructor() { makeAutoObservable(this);}更新配置configure({ enforceActions: 'always'});移除已废弃的 API替换 toJS 为 toJSON更新 isObservableObject 为 isObservable总结MobX 6 是一个重要的版本更新,主要改进包括:强制使用 action 提高可预测性简化的 API 和更好的 TypeScript 支持强制使用 Proxy 提高性能更好的调试体验移除已废弃的 API迁移到 MobX 6 需要一些工作,但带来的改进是值得的。建议新项目直接使用 MobX 6,现有项目逐步迁移。
阅读 0·2月21日 15:51

MobX 常见问题和解决方案有哪些?

MobX 是一个功能强大的状态管理库,但在使用过程中可能会遇到一些常见问题。了解这些问题及其解决方案可以帮助开发者更好地使用 MobX。1. 组件不更新问题描述组件使用 observer 包装,但状态变化时组件不更新。常见原因和解决方案原因 1:访问的是普通对象而不是 observable// 错误const store = { count: 0};@observerclass Counter extends React.Component { render() { return <div>{store.count}</div>; // 不会更新 }}// 正确import { observable } from 'mobx';const store = observable({ count: 0});@observerclass Counter extends React.Component { render() { return <div>{store.count}</div>; // 会更新 }}原因 2:在 action 外修改状态(MobX 6)// 错误(MobX 6)class Store { @observable count = 0; increment() { this.count++; // 不在 action 中,会报错 }}// 正确class Store { @observable count = 0; @action increment() { this.count++; // 在 action 中 }}原因 3:在 render 中创建新对象// 错误@observerclass Component extends React.Component { render() { const style = { color: 'red' }; // 每次渲染都创建新对象 return <div style={style}>{store.count}</div>; }}// 正确const style = { color: 'red' }; // 在组件外部定义@observerclass Component extends React.Component { render() { return <div style={style}>{store.count}</div>; }}2. 性能问题问题描述应用性能下降,组件频繁重新渲染。常见原因和解决方案原因 1:过度追踪// 错误:在循环中访问 observable@observerclass List extends React.Component { render() { return ( <div> {store.items.map(item => ( <div key={item.id}> {item.name} - {item.value} - {item.description} </div> ))} </div> ); }}// 正确:使用 computed 预处理数据class Store { @observable items = []; @computed get itemDisplayData() { return this.items.map(item => ({ id: item.id, display: `${item.name} - ${item.value} - ${item.description}` })); }}@observerclass List extends React.Component { render() { return ( <div> {store.itemDisplayData.map(item => ( <div key={item.id}>{item.display}</div> ))} </div> ); }}原因 2:组件依赖太多状态// 错误:组件依赖太多状态@observerclass Dashboard extends React.Component { render() { return ( <div> <UserInfo /> <Settings /> <DataCount /> </div> ); }}// 正确:拆分为多个组件@observerclass UserInfo extends React.Component { render() { return ( <div> <div>{store.user.name}</div> <div>{store.user.email}</div> </div> ); }}3. 内存泄漏问题描述组件卸载后,reaction 仍然在运行,导致内存泄漏。解决方案// 错误:忘记清理 reactionuseEffect(() => { autorun(() => { console.log(store.count); });}, []);// 正确:清理 reactionuseEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => { dispose(); // 清理 reaction };}, []);4. 异步操作问题问题描述异步操作中的状态修改不生效。解决方案// 错误:异步操作中的状态修改@actionasync fetchData() { this.loading = true; const data = await fetch('/api/data').then(r => r.json()); this.data = data; // 不在 action 中 this.loading = false; // 不在 action 中}// 正确:使用 runInAction@actionasync fetchData() { this.loading = true; try { const data = await fetch('/api/data').then(r => r.json()); runInAction(() => { this.data = data; }); } finally { runInAction(() => { this.loading = false; }); }}// 或者使用 flowfetchData = flow(function* () { this.loading = true; try { const response = yield fetch('/api/data'); const data = yield response.json(); this.data = data; } finally { this.loading = false; }});5. computed 不更新问题描述computed 属性没有按预期更新。常见原因和解决方案原因 1:在 computed 中产生副作用// 错误:在 computed 中产生副作用@computed get badComputed() { console.log('Side effect!'); // 不应该在 computed 中 fetch('/api/data'); // 不应该在 computed 中 return this.data;}// 正确:computed 应该是纯函数@computed get goodComputed() { return this.data.filter(item => item.active);}原因 2:依赖项没有正确追踪// 错误:依赖项没有正确追踪@computed get badComputed() { const data = this.data; // 没有在返回值中使用 return this.items.length;}// 正确:依赖项正确追踪@computed get goodComputed() { return this.data.length + this.items.length;}6. 循环依赖问题描述多个 store 之间存在循环依赖,导致无限循环或性能问题。解决方案// 错误:循环依赖class StoreA { @observable value = 0; @computed get doubled() { return storeB.value * 2; }}class StoreB { @observable value = 0; @computed get doubled() { return storeA.value * 2; }}// 正确:避免循环依赖class Store { @observable valueA = 0; @observable valueB = 0; @computed get doubledA() { return this.valueA * 2; } @computed get doubledB() { return this.valueB * 2; }}7. 装饰器不工作问题描述使用装饰器时出现错误或不生效。解决方案确保配置正确// package.json{ "babel": { "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose": true }] ] }}或者使用 makeObservable// 不使用装饰器class Store { count = 0; constructor() { makeObservable(this, { count: observable }); }}8. TypeScript 类型错误问题描述使用 TypeScript 时出现类型错误。解决方案// 错误:没有类型参数class Store { count = 0; constructor() { makeObservable(this, { count: observable }); }}// 正确:使用类型参数class Store { count: number = 0; constructor() { makeObservable<Store>(this, { count: observable }); }}9. 数组操作问题问题描述数组操作不触发更新。解决方案// 错误:重新赋值整个数组@actionbadAddItem(item) { this.items = [...this.items, item];}// 正确:使用数组方法@actiongoodAddItem(item) { this.items.push(item);}// 或者使用 replace@actionreplaceItems(newItems) { this.items.replace(newItems);}10. 调试困难问题描述难以追踪状态变化和依赖关系。解决方案使用 traceimport { trace } from 'mobx';// 追踪 computedtrace(store.fullName);// 追踪组件渲染@observerclass MyComponent extends React.Component { render() { trace(true); // 追踪组件渲染 return <div>{store.count}</div>; }}使用 MobX DevToolsimport { configure } from 'mobx';configure({ // 启用调试模式 useProxies: 'ifavailable', isolateGlobalState: true});最佳实践总结始终在 action 中修改状态(MobX 6)使用 observer 包装需要响应状态变化的组件避免在 render 中创建新对象使用 computed 优化计算逻辑及时清理 reaction 和副作用正确处理异步操作避免循环依赖使用 trace 和 DevTools 调试合理拆分组件以减少依赖遵循 MobX 的最佳实践遵循这些最佳实践,可以避免大多数常见的 MobX 问题,构建稳定、高效的应用。
阅读 0·2月21日 15:50