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

面试题手册

如何优化 Lottie 动画的性能,有哪些具体的优化策略?

优化 Lottie 动画性能需要从多个方面入手,以下是详细的优化策略:1. 文件大小优化简化动画路径:减少不必要的控制点,使用更简单的形状减少图层数量:合并相似的图层,减少嵌套层级压缩 JSON:使用工具如 LottieFiles 的优化器压缩 JSON 文件移除未使用的资源:删除不使用的图像和预合成降低帧率:从 60fps 降到 30fps 或更低,在保持流畅度的同时减少数据量减少动画时长:缩短不必要的动画帧数2. 渲染性能优化使用硬件加速:启用 useNativeDriver={true}(React Native)缓存机制:使用 Lottie Cache 缓存已加载的动画预加载动画:在需要之前提前加载动画数据避免重复渲染:使用 React.memo 或 shouldComponentUpdate 优化组件使用 Composition:对于复杂动画,使用 Lottie Composition 预编译3. 内存管理优化及时释放资源:在组件卸载时清理动画引用限制同时播放的动画数量:避免在同一屏幕播放多个复杂动画使用轻量级动画:优先选择简单的动画效果动态加载:只在需要时加载动画,避免一次性加载所有动画4. 平台特定优化iOS 优化:// 使用原生驱动<LottieView useNativeDriver={true} hardwareAccelerationAndroid={true}/>// 启用缓存<LottieView cacheComposition={true} renderMode="AUTOMATIC"/>Android 优化:// 启用硬件加速<LottieView hardwareAccelerationAndroid={true} imageAssetsFolder="lottie/"/>// 使用合适的渲染模式<LottieView renderMode="HARDWARE"/>Web 优化:// 使用 Canvas 渲染(性能更好)<lottie-player renderer="canvas" rendererSettings={{ preserveAspectRatio: 'xMidYMid slice', clearCanvas: false, progressiveLoad: true, hideOnTransparent: true }}/>// 启用懒加载<lottie-player lazyLoad={true}/>5. 代码层面优化// 使用 useRef 避免重复创建const animationRef = useRef(null);// 使用 useCallback 优化事件处理const handleAnimationFinish = useCallback(() => { console.log('Animation finished');}, []);// 使用 useMemo 缓存动画数据const animationData = useMemo(() => ({ // 动画数据}), []);// 条件渲染{showAnimation && ( <LottieView source={animationData} autoPlay={showAnimation} />)}6. 列表中的优化// 使用 FlatList 的优化属性<FlatList data={items} renderItem={({ item }) => ( <LottieView source={item.animation} autoPlay={false} loop={false} /> )} removeClippedSubviews={true} maxToRenderPerBatch={10} windowSize={10} initialNumToRender={5}/>7. 动态属性优化// 避免频繁更新动态属性const [color, setColor] = useState('#FF0000');// 使用防抖const debouncedSetColor = useMemo( () => debounce(setColor, 100), []);// 批量更新属性const updateProperties = useCallback(() => { animationRef.current?.update({ colorFilters: [{ keypath: 'layer1', color: color }], progress: 0.5 });}, [color]);8. 网络加载优化// 使用 CDN 加速const animationUrl = 'https://cdn.example.com/animation.json';// 启用压缩const response = await fetch(animationUrl, { headers: { 'Accept-Encoding': 'gzip' }});// 使用 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); }) ); }});9. 监控和调试// 监控动画性能<LottieView onAnimationLoad={() => { const startTime = performance.now(); return () => { const loadTime = performance.now() - startTime; console.log(`Animation loaded in ${loadTime}ms`); }; }} onAnimationFinish={() => { console.log('Animation finished'); }}/>// 使用 React DevTools Profiler// 使用 Chrome DevTools Performance 面板10. 最佳实践在低端设备上禁用复杂动画根据网络状况调整动画质量使用占位符在动画加载期间显示提供降级方案(如静态图片)定期审查和优化现有动画建立动画性能基准和监控机制
阅读 0·2月19日 19:13

在前端框架(React、Vue、Angular)中如何实现 CSRF 防护?

前端框架(如 React、Vue、Angular)中的 CSRF 防护需要考虑框架特性和最佳实践,以确保在单页应用(SPA)中提供有效的安全保护。React 中的 CSRF 防护1. 使用 CSRF Token// 获取 CSRF Tokenimport { useEffect, useState } from 'react';function CSRFProtectedForm() { const [csrfToken, setCsrfToken] = useState(''); const [formData, setFormData] = useState({ name: '', email: '' }); useEffect(() => { // 从服务器获取 CSRF Token fetch('/api/csrf-token') .then(res => res.json()) .then(data => setCsrfToken(data.csrfToken)); }, []); const handleSubmit = async (e) => { e.preventDefault(); try { const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken // 在请求头中发送 Token }, body: JSON.stringify(formData) }); if (response.ok) { alert('提交成功!'); } else { alert('提交失败'); } } catch (error) { console.error('Error:', error); } }; return ( <form onSubmit={handleSubmit}> <input type="text" value={formData.name} onChange={(e) => setFormData({...formData, name: e.target.value})} placeholder="姓名" /> <input type="email" value={formData.email} onChange={(e) => setFormData({...formData, email: e.target.value})} placeholder="邮箱" /> <button type="submit">提交</button> </form> );}2. 使用 Axios 拦截器import axios from 'axios';// 创建 Axios 实例const api = axios.create({ baseURL: '/api', withCredentials: true // 允许发送 Cookie});// 请求拦截器:自动添加 CSRF Tokenapi.interceptors.request.use(async (config) => { // 对于需要 CSRF 保护的请求方法 if (['post', 'put', 'patch', 'delete'].includes(config.method)) { try { const response = await axios.get('/api/csrf-token'); config.headers['X-CSRF-Token'] = response.data.csrfToken; } catch (error) { console.error('Failed to get CSRF token:', error); } } return config;});// 使用示例function SubmitForm() { const handleSubmit = async () => { try { await api.post('/submit', { data: 'example' }); alert('提交成功!'); } catch (error) { alert('提交失败'); } }; return <button onClick={handleSubmit}>提交</button>;}Vue 中的 CSRF 防护1. 使用 Vue Router 和 Axios<template> <form @submit.prevent="handleSubmit"> <input v-model="formData.name" placeholder="姓名" /> <input v-model="formData.email" placeholder="邮箱" /> <button type="submit">提交</button> </form></template><script>import axios from 'axios';export default { data() { return { csrfToken: '', formData: { name: '', email: '' } }; }, async created() { // 获取 CSRF Token const response = await axios.get('/api/csrf-token'); this.csrfToken = response.data.csrfToken; }, methods: { async handleSubmit() { try { const response = await axios.post('/api/submit', this.formData, { headers: { 'X-CSRF-Token': this.csrfToken } }); if (response.data.success) { alert('提交成功!'); } } catch (error) { alert('提交失败'); } } }};</script>2. 使用 Vuex 管理 CSRF Token// store/csrf.jsimport axios from 'axios';export default { namespaced: true, state: { token: null }, mutations: { SET_TOKEN(state, token) { state.token = token; } }, actions: { async fetchToken({ commit }) { try { const response = await axios.get('/api/csrf-token'); commit('SET_TOKEN', response.data.csrfToken); } catch (error) { console.error('Failed to fetch CSRF token:', error); } } }};Angular 中的 CSRF 防护1. 使用 HttpClient 拦截器// csrf.interceptor.tsimport { Injectable } from '@angular/core';import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';import { Observable, from } from 'rxjs';import { switchMap } from 'rxjs/operators';@Injectable()export class CsrfInterceptor implements HttpInterceptor { constructor(private http: HttpClient) {} intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { // 对于需要 CSRF 保护的请求方法 if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { return from(this.getCsrfToken()).pipe( switchMap(token => { const csrfReq = req.clone({ setHeaders: { 'X-CSRF-Token': token } }); return next.handle(csrfReq); }) ); } return next.handle(req); } private async getCsrfToken(): Promise<string> { const response = await this.http.get<{ csrfToken: string }>('/api/csrf-token').toPromise(); return response.csrfToken; }}// app.module.tsimport { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';import { CsrfInterceptor } from './csrf.interceptor';@NgModule({ imports: [HttpClientModule], providers: [ { provide: HTTP_INTERCEPTORS, useClass: CsrfInterceptor, multi: true } ]})export class AppModule {}通用最佳实践1. Token 刷新策略// 通用 Token 刷新逻辑class CSRFTokenManager { constructor() { this.token = null; this.tokenExpiry = null; } async getToken() { // 检查 Token 是否过期 if (!this.token || Date.now() > this.tokenExpiry) { await this.refreshToken(); } return this.token; } async refreshToken() { const response = await fetch('/api/csrf-token'); const data = await response.json(); this.token = data.csrfToken; this.tokenExpiry = Date.now() + 3600000; // 1 小时后过期 } clearToken() { this.token = null; this.tokenExpiry = null; }}2. 错误处理和重试// 带有重试机制的请求async function makeRequestWithRetry(url, data, maxRetries = 3) { let retries = 0; while (retries < maxRetries) { try { const token = await csrfTokenManager.getToken(); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token }, body: JSON.stringify(data) }); if (response.status === 403) { // Token 可能已过期,刷新并重试 csrfTokenManager.clearToken(); retries++; continue; } return await response.json(); } catch (error) { retries++; if (retries >= maxRetries) { throw error; } } }}3. 安全配置// Cookie 配置(服务器端)res.cookie('sessionId', sessionId, { httpOnly: true, // 防止 XSS 窃取 secure: true, // 仅 HTTPS sameSite: 'strict', // 最严格的 CSRF 防护 maxAge: 3600000 // 1 小时过期});框架特定的注意事项React使用 Context API 共享 CSRF Token考虑使用 React Query 或 SWR 管理请求在组件卸载时清理 TokenVue使用 Vuex 或 Pinia 管理 Token 状态利用 Vue 的生命周期钩子获取 Token考虑使用 VueUse 的 useFetchAngular使用 HTTP 拦截器自动处理 Token利用依赖注入管理 Token 服务使用 RxJS 处理异步操作前端框架中的 CSRF 防护需要结合框架特性和最佳实践,确保在提供良好用户体验的同时,不牺牲安全性。
阅读 0·2月19日 19:13

如何优化 Tauri 应用的性能

优化 Tauri 应用性能可以从多个方面入手:1. 减少包体积前端优化使用 Tree Shaking 移除未使用的代码压缩和混淆 JavaScript 代码优化图片资源(使用 WebP 格式、压缩)按需加载第三方库Rust 优化# Cargo.toml[profile.release]opt-level = "z" # 优化大小lto = true # 链接时优化codegen-units = 1 # 减少代码生成单元strip = true # 移除符号表2. 优化启动速度延迟加载// 延迟加载大型模块const heavyModule = await import('./heavy-module');减少初始渲染使用虚拟列表处理长列表懒加载组件和路由预渲染关键内容3. IPC 通信优化批量处理请求// Rust 端#[tauri::command]async fn batch_process(items: Vec<String>) -> Vec<Result> { // 批量处理而不是单个处理}使用二进制数据// 对于大量数据,使用 Uint8Array 而非 JSONconst data = new Uint8Array(await invoke('get_binary_data'));减少通信频率使用防抖和节流本地缓存常用数据使用事件推送替代轮询4. WebView 优化禁用不必要的功能{ "tauri": { "webview": { "transparent": false, "dragDropEnabled": false } }}优化 CSS 和 JavaScript使用 CSS containment减少 DOM 操作使用 Web Workers 处理计算密集型任务5. 内存管理前端内存优化// 及时清理监听器const unlisten = await listen('event', handler);// 使用后取消监听unlisten();Rust 内存优化// 使用 Arc 共享数据use std::sync::Arc;let shared_data = Arc::new(data);// 避免不必要的克隆6. 构建优化# 使用并行构建cargo build --release -j $(nproc)# 使用增量编译export CARGO_INCREMENTAL=17. 监控和分析使用性能分析工具import { invoke } from '@tauri-apps/api/tauri';// 监控 IPC 调用时间const start = performance.now();await invoke('command');console.log(`IPC took ${performance.now() - start}ms`);Rust 性能分析# 使用 flamegraphcargo install flamegraphcargo flamegraph最佳实践定期使用 Chrome DevTools 分析性能监控内存使用情况,避免内存泄漏使用 Web Workers 处理 CPU 密集型任务合理使用缓存策略保持依赖项更新到最新稳定版本
阅读 0·2月19日 19:12

如何在 Jest 中测试 TypeScript 项目?如何配置 ts-jest?

在 Jest 中测试 TypeScript 项目需要正确的配置和类型支持:1. 安装必要的依赖:npm install --save-dev jest @types/jest ts-jest @types/node2. 配置 Jest:// jest.config.jsmodule.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], transform: { '^.+\\.ts$': 'ts-jest', }, collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', ], moduleFileExtensions: ['ts', 'js', 'json'],};3. tsconfig.json 配置:{ "compilerOptions": { "types": ["jest", "node"], "esModuleInterop": true, "allowSyntheticDefaultImports": true }}4. 编写 TypeScript 测试:// calculator.tsexport function add(a: number, b: number): number { return a + b;}// calculator.test.tsimport { add } from './calculator';describe('Calculator', () => { test('adds two numbers', () => { expect(add(2, 3)).toBe(5); });});5. 测试类型:interface User { id: number; name: string;}function getUser(id: number): User { return { id, name: 'John' };}test('returns user with correct type', () => { const user: User = getUser(1); expect(user).toEqual({ id: 1, name: 'John' });});6. Mock TypeScript 模块:// api.tsexport interface ApiResponse<T> { data: T; error: string | null;}export async function fetchData<T>(url: string): Promise<ApiResponse<T>> { const response = await fetch(url); const data = await response.json(); return { data, error: null };}// api.test.tsimport { fetchData } from './api';jest.mock('./api', () => ({ fetchData: jest.fn(),}));test('fetches data', async () => { const mockData = { name: 'John' }; (fetchData as jest.Mock).mockResolvedValue({ data: mockData, error: null, }); const result = await fetchData('/api/users'); expect(result.data).toEqual(mockData);});最佳实践:使用 ts-jest 预设简化配置确保 @types/jest 已安装使用 TypeScript 类型增强测试可读性在测试中显式声明类型使用类型安全的 Mock配置正确的路径别名
阅读 0·2月19日 19:11

如何在 Jest 中测试文件系统和 I/O 操作?如何 Mock fs 模块?

在 Jest 中测试文件系统和 I/O 操作需要使用 Mock 来隔离文件系统依赖:1. Mock fs 模块:import fs from 'fs';jest.mock('fs');test('reads file content', () => { const mockContent = 'Hello, World!'; fs.readFileSync.mockReturnValue(mockContent); const content = readFile('test.txt'); expect(content).toBe(mockContent); expect(fs.readFileSync).toHaveBeenCalledWith('test.txt', 'utf8');});2. 测试文件写入:test('writes file content', () => { fs.writeFileSync.mockReturnValue(undefined); writeFile('test.txt', 'content'); expect(fs.writeFileSync).toHaveBeenCalledWith('test.txt', 'content', 'utf8');});3. 测试异步文件操作:test('reads file asynchronously', async () => { const mockContent = 'Async content'; fs.promises.readFile.mockResolvedValue(mockContent); const content = await readFileAsync('test.txt'); expect(content).toBe(mockContent); expect(fs.promises.readFile).toHaveBeenCalledWith('test.txt', 'utf8');});4. 测试文件存在性检查:test('checks if file exists', () => { fs.existsSync.mockReturnValue(true); const exists = fileExists('test.txt'); expect(exists).toBe(true); expect(fs.existsSync).toHaveBeenCalledWith('test.txt');});5. 测试目录操作:test('creates directory', () => { fs.mkdirSync.mockReturnValue(undefined); createDirectory('new-dir'); expect(fs.mkdirSync).toHaveBeenCalledWith('new-dir', { recursive: true });});6. 测试文件遍历:test('lists directory contents', () => { const mockFiles = ['file1.txt', 'file2.txt']; fs.readdirSync.mockReturnValue(mockFiles); const files = listDirectory('test-dir'); expect(files).toEqual(mockFiles); expect(fs.readdirSync).toHaveBeenCalledWith('test-dir');});7. 使用临时文件进行测试:import { tmpdir } from 'os';import { join } from 'path';test('writes and reads temporary file', () => { const tempFile = join(tmpdir(), 'test.txt'); const content = 'Test content'; fs.writeFileSync(tempFile, content); const readContent = fs.readFileSync(tempFile, 'utf8'); expect(readContent).toBe(content); fs.unlinkSync(tempFile); // 清理});最佳实践:使用 Mock 隔离文件系统操作测试成功和失败场景验证文件路径和内容参数清理临时文件和资源使用 jest.clearAllMocks() 清除调用记录考虑使用内存文件系统进行集成测试
阅读 0·2月19日 18:14

如何在 Jest 中测试错误处理和异常?如何使用 toThrow 和 rejects?

在 Jest 中测试错误处理和异常情况是确保代码健壮性的重要部分:1. 测试同步函数抛出错误:test('throws error when invalid input', () => { expect(() => divide(10, 0)).toThrow(); expect(() => divide(10, 0)).toThrow('Division by zero'); expect(() => divide(10, 0)).toThrow(/zero/);});2. 测试 Promise 拒绝:test('rejects when API fails', async () => { await expect(fetchData()).rejects.toThrow('API Error');});// 或使用 .catchtest('rejects when API fails', async () => { await expect(fetchData()).rejects.toEqual({ message: 'API Error' });});3. 测试异步回调中的错误:test('callback with error', (done) => { fetchData((err, data) => { expect(err).toBeInstanceOf(Error); expect(err.message).toBe('Network error'); done(); });});4. 测试自定义错误类型:test('throws custom error', () => { expect(() => validate(null)).toThrow(ValidationError);});5. 使用 try-catch 测试:test('handles error gracefully', async () => { try { await riskyOperation(); fail('Should have thrown an error'); } catch (error) { expect(error).toBeInstanceOf(Error); expect(error.message).toContain('failed'); }});6. 测试错误边界(React):test('ErrorBoundary catches errors', () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); render( <ErrorBoundary> <ThrowingComponent /> </ErrorBoundary> ); expect(screen.getByText('Something went wrong')).toBeInTheDocument(); consoleSpy.mockRestore();});最佳实践:明确测试错误消息和类型使用 toThrow() 匹配错误消息或正则表达式测试错误处理逻辑,而不仅仅是错误本身确保清理 Mock 和 Spy测试边界情况和无效输入
阅读 0·2月19日 18:09

如何在 React 中使用 MobX 的 observer?

在 MobX 中,observer 是一个高阶组件(HOC),用于将 React 组件转换为响应式组件。当组件使用的数据发生变化时,组件会自动重新渲染。observer 的基本用法1. 类组件中使用 observerimport React from 'react';import { observer } from 'mobx-react';import { observable } from 'mobx';class Store { @observable count = 0;}const store = new Store();@observerclass Counter extends React.Component { render() { return ( <div> <p>Count: {store.count}</p> <button onClick={() => store.count++}>Increment</button> </div> ); }}2. 函数组件中使用 observerimport React from 'react';import { observer } from 'mobx-react-lite';import { observable } from 'mobx';class Store { @observable count = 0;}const store = new Store();const Counter = observer(() => { return ( <div> <p>Count: {store.count}</p> <button onClick={() => store.count++}>Increment</button> </div> );});observer 的工作原理1. 组件挂载时MobX 创建一个 reaction 来追踪组件 render 函数中访问的所有 observable建立组件与 observable 之间的依赖关系2. 状态变化时当 observable 被修改时,MobX 检测到依赖变化将组件标记为需要重新渲染在下一个事件循环中,触发组件的重新渲染3. 组件卸载时自动清理 reaction 和依赖关系避免内存泄漏observer 的优化特性1. 细粒度更新observer 只会重新渲染真正需要更新的组件:@observerclass Parent extends React.Component { render() { return ( <div> <ChildA /> <ChildB /> </div> ); }}@observerclass ChildA extends React.Component { render() { // 只依赖 store.count return <div>Count: {store.count}</div>; }}@observerclass ChildB extends React.Component { render() { // 只依赖 store.name return <div>Name: {store.name}</div>; }}当 store.count 变化时,只有 ChildA 会重新渲染,ChildB 不会。2. shouldComponentUpdate 优化observer 会自动实现 shouldComponentUpdate,避免不必要的渲染:只有当组件依赖的 observable 真正变化时才重新渲染即使父组件重新渲染,子组件也可能不会重新渲染3. 批量更新多个状态变化会被批量处理,只触发一次重新渲染:runInAction(() => { store.count++; store.name = 'New Name';});observer 的最佳实践1. 只在需要的地方使用 observer不是所有组件都需要 observer,只在需要响应状态变化的组件上使用:// 不需要 observerconst Header = () => <h1>My App</h1>;// 需要 observerconst Counter = observer(() => { return <div>Count: {store.count}</div>;});2. 避免在 render 中创建新对象在 render 中创建新对象会导致不必要的重新渲染:// 不好的做法const BadComponent = observer(() => { const style = { color: 'red' }; // 每次渲染都创建新对象 return <div style={style}>{store.count}</div>;});// 好的做法const style = { color: 'red' }; // 在组件外部定义const GoodComponent = observer(() => { return <div style={style}>{store.count}</div>;});3. 使用 computed 优化计算在组件外部使用 computed 来优化计算逻辑:// 不好的做法const BadComponent = observer(() => { const fullName = `${store.firstName} ${store.lastName}`; return <div>{fullName}</div>;});// 好的做法class Store { @observable firstName = 'John'; @observable lastName = 'Doe'; @computed get fullName() { return `${this.firstName} ${this.lastName}`; }}const GoodComponent = observer(() => { return <div>{store.fullName}</div>;});4. 使用 React.memo 配合 observer对于纯展示组件,可以结合 React.memo 使用:const PureComponent = React.memo(observer(() => { return <div>{store.count}</div>;}));常见问题1. 组件不更新确保:组件被 observer 包装访问的是 observable 而不是普通对象状态修改在 action 中进行2. 过度渲染如果组件过度渲染,检查:是否在 render 中创建了新对象是否使用了 computed 来优化计算是否可以拆分组件以减少依赖3. 内存泄漏确保:组件卸载时 observer 会自动清理手动创建的 reaction 需要手动清理总结observer 是 MobX 与 React 集成的核心,它通过细粒度的依赖追踪实现了高效的响应式更新。正确使用 observer 和遵循最佳实践,可以构建高性能的 React 应用。
阅读 0·2月19日 17:58

移动应用中如何实施 CSRF 防护,有哪些特殊考虑?

移动应用中的 CSRF 防护与 Web 应用有所不同,因为移动应用通常不使用浏览器自动发送 Cookie 的机制,但仍然需要考虑各种安全风险。移动应用 CSRF 的特殊性1. 认证方式差异Web 应用:使用 Cookie 存储 Session ID浏览器自动发送 Cookie容易受到 CSRF 攻击移动应用:使用 Token(JWT、OAuth)手动管理认证信息相对不易受到传统 CSRF 攻击但存在其他安全风险2. 网络环境差异// 移动应用面临的网络挑战const mobileChallenges = { networkUnreliable: '网络不稳定可能导致重放攻击', deviceCompromise: '设备被 Root 或越狱', appTampering: '应用被篡改或重新打包', insecureStorage: '不安全的本地存储'};移动应用 CSRF 防护策略1. 使用 Token 认证(推荐)iOS 实现// Swift - Token 认证管理class TokenManager { static let shared = TokenManager() private let keychain = Keychain() func saveToken(_ token: String) { // 使用 Keychain 安全存储 Token keychain["authToken"] = token } func getToken() -> String? { return keychain["authToken"] } func clearToken() { keychain["authToken"] = nil }}// 网络请求管理器class NetworkManager { static let shared = NetworkManager() private let session = URLSession.shared func request<T: Decodable>( _ endpoint: String, method: String = "GET", body: Data? = nil, completion: @escaping (Result<T, Error>) -> Void ) { guard let url = URL(string: endpoint) else { completion(.failure(NetworkError.invalidURL)) return } var request = URLRequest(url: url) request.httpMethod = method request.httpBody = body // 添加认证 Token if let token = TokenManager.shared.getToken() { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } session.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NetworkError.noData)) return } do { let result = try JSONDecoder().decode(T.self, from: data) completion(.success(result)) } catch { completion(.failure(error)) } }.resume() }}Android 实现// Java - Token 认证管理public class TokenManager { private static TokenManager instance; private SharedPreferences preferences; private TokenManager(Context context) { preferences = context.getSharedPreferences("auth", Context.MODE_PRIVATE); } public static synchronized TokenManager getInstance(Context context) { if (instance == null) { instance = new TokenManager(context); } return instance; } public void saveToken(String token) { preferences.edit().putString("authToken", token).apply(); } public String getToken() { return preferences.getString("authToken", null); } public void clearToken() { preferences.edit().remove("authToken").apply(); }}// 网络请求拦截器public class AuthInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); // 添加认证 Token String token = TokenManager.getInstance(context).getToken(); if (token != null) { Request authenticatedRequest = originalRequest.newBuilder() .header("Authorization", "Bearer " + token) .build(); return chain.proceed(authenticatedRequest); } return chain.proceed(originalRequest); }}2. 设备指纹识别// 服务器端 - 设备指纹验证class DeviceFingerprintService { async generateFingerprint(deviceInfo) { const fingerprintData = { deviceId: deviceInfo.deviceId, os: deviceInfo.os, osVersion: deviceInfo.osVersion, appVersion: deviceInfo.appVersion, screenResolution: deviceInfo.screenResolution, timestamp: Date.now() }; // 生成设备指纹 const fingerprint = crypto.createHash('sha256') .update(JSON.stringify(fingerprintData)) .digest('hex'); return fingerprint; } async validateFingerprint(userId, fingerprint) { const storedFingerprint = await this.getStoredFingerprint(userId); if (!storedFingerprint) { // 首次使用,存储指纹 await this.storeFingerprint(userId, fingerprint); return true; } // 验证指纹是否匹配 return storedFingerprint === fingerprint; }}3. 请求签名// Swift - 请求签名class RequestSigner { static func signRequest(_ request: URLRequest, secretKey: String) -> URLRequest { var signedRequest = request // 生成时间戳和随机数 let timestamp = String(Int(Date().timeIntervalSince1970)) let nonce = UUID().uuidString // 构造签名字符串 let method = request.httpMethod ?? "GET" let url = request.url?.absoluteString ?? "" let body = request.httpBody?.base64EncodedString() ?? "" let signString = "\(method)\n\(url)\n\(timestamp)\n\(nonce)\n\(body)" // 生成 HMAC-SHA256 签名 let signature = signString.hmacSHA256(key: secretKey) // 添加签名头 signedRequest.setValue(timestamp, forHTTPHeaderField: "X-Timestamp") signedRequest.setValue(nonce, forHTTPHeaderField: "X-Nonce") signedRequest.setValue(signature, forHTTPHeaderField: "X-Signature") return signedRequest }}// HMAC-SHA256 扩展extension String { func hmacSHA256(key: String) -> String { let keyData = key.data(using: .utf8)! let messageData = self.data(using: .utf8)! var digestData = Data(count: Int(CC_SHA256_DIGEST_LENGTH)) keyData.withUnsafeBytes { keyBytes in messageData.withUnsafeBytes { messageBytes in CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes.baseAddress, keyData.count, messageBytes.baseAddress, messageData.count, &digestData) } } return digestData.base64EncodedString() }}// Java - 请求签名public class RequestSigner { public static Request signRequest(Request request, String secretKey) { // 生成时间戳和随机数 String timestamp = String.valueOf(System.currentTimeMillis() / 1000); String nonce = UUID.randomUUID().toString(); // 构造签名字符串 String method = request.method(); String url = request.url().toString(); String body = bodyToString(request.body()); String signString = method + "\n" + url + "\n" + timestamp + "\n" + nonce + "\n" + body; // 生成 HMAC-SHA256 签名 String signature = hmacSHA256(signString, secretKey); // 添加签名头 return request.newBuilder() .addHeader("X-Timestamp", timestamp) .addHeader("X-Nonce", nonce) .addHeader("X-Signature", signature) .build(); } private static String hmacSHA256(String data, String key) { try { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "HmacSHA256"); mac.init(secretKeySpec); byte[] hash = mac.doFinal(data.getBytes()); return Base64.encodeToString(hash, Base64.NO_WRAP); } catch (Exception e) { throw new RuntimeException("Failed to generate signature", e); } }}4. 双因素认证// Swift - 双因素认证class TwoFactorAuthManager { static func verifyOTP(userId: String, otp: String, completion: @escaping (Bool) -> Void) { // 发送 OTP 到服务器验证 let endpoint = "https://api.example.com/verify-otp" var request = URLRequest(url: URL(string: endpoint)!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body = [ "userId": userId, "otp": otp ] request.httpBody = try? JSONSerialization.data(withJSONObject: body) URLSession.shared.dataTask(with: request) { data, response, error in if let httpResponse = response as? HTTPURLResponse { completion(httpResponse.statusCode == 200) } else { completion(false) } }.resume() }}服务器端验证1. Token 验证中间件// Express.js - Token 验证中间件const jwt = require('jsonwebtoken');function authenticateMobile(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.substring(7); try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (error) { return res.status(401).json({ error: 'Invalid token' }); }}// 请求签名验证中间件function verifySignature(req, res, next) { const timestamp = req.headers['x-timestamp']; const nonce = req.headers['x-nonce']; const signature = req.headers['x-signature']; if (!timestamp || !nonce || !signature) { return res.status(401).json({ error: 'Missing signature headers' }); } // 验证时间戳(防止重放攻击) const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) { // 5 分钟 return res.status(401).json({ error: 'Request expired' }); } // 验证 nonce(防止重放攻击) if (isNonceUsed(nonce)) { return res.status(401).json({ error: 'Nonce already used' }); } // 验证签名 const expectedSignature = generateSignature(req, timestamp, nonce); if (signature !== expectedSignature) { return res.status(401).json({ error: 'Invalid signature' }); } // 标记 nonce 为已使用 markNonceAsUsed(nonce); next();}2. 设备指纹验证// 设备指纹验证中间件function verifyDeviceFingerprint(req, res, next) { const userId = req.user.id; const fingerprint = req.headers['x-device-fingerprint']; if (!fingerprint) { return res.status(401).json({ error: 'Device fingerprint required' }); } deviceFingerprintService.validateFingerprint(userId, fingerprint) .then(isValid => { if (!isValid) { return res.status(401).json({ error: 'Invalid device fingerprint' }); } next(); }) .catch(error => { res.status(500).json({ error: 'Fingerprint validation failed' }); });}最佳实践1. 安全存储// iOS - 使用 Keychainimport Securityclass KeychainHelper { static func save(key: String, data: Data) -> Bool { let query = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data ] as [String: Any] SecItemDelete(query as CFDictionary) return SecItemAdd(query as CFDictionary, nil) == errSecSuccess } static func load(key: String) -> Data? { let query = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] as [String: Any] var dataTypeRef: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) if status == errSecSuccess { return dataTypeRef as? Data } return nil }}// Android - 使用 EncryptedSharedPreferencespublic class SecureStorage { private static EncryptedSharedPreferences preferences; public static void init(Context context) { try { MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); preferences = (EncryptedSharedPreferences) EncryptedSharedPreferences.create( context, "secure_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } catch (Exception e) { throw new RuntimeException("Failed to initialize secure storage", e); } } public static void saveString(String key, String value) { preferences.edit().putString(key, value).apply(); } public static String getString(String key) { return preferences.getString(key, null); }}2. 证书绑定// iOS - SSL 证书绑定class CertificatePinning { static func validateCertificate(for serverTrust: SecTrust) -> Bool { // 获取服务器证书 guard let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else { return false } // 获取证书数据 let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data // 加载本地证书 guard let localCertificatePath = Bundle.main.path(forResource: "certificate", ofType: "cer"), let localCertificateData = try? Data(contentsOf: URL(fileURLWithPath: localCertificatePath)) else { return false } // 比较证书 return serverCertificateData == localCertificateData }}移动应用的 CSRF 防护需要结合平台特性和安全最佳实践,确保在各种网络环境和设备状态下都能提供有效的安全保护。
阅读 0·2月19日 17:57

MobX 中的 observable、computed 和 action 有什么区别?

在 MobX 中,observable、computed 和 action 是三个核心概念,它们各自有不同的用途和特点:Observable(可观察对象)用途:创建可以被追踪的状态当状态发生变化时,通知所有依赖它的观察者使用方式:import { observable } from 'mobx';class Store { @observable count = 0; @observable user = { name: 'John', age: 30 }; @observable items = [];}// 或者使用函数式 APIconst store = observable({ count: 0, user: { name: 'John', age: 30 }, items: []});特点:可以装饰类属性或创建可观察对象支持对象、数组、Map、Set 等数据结构深度可观察:嵌套的对象也会自动变为可观察的在 MobX 6 中,默认使用装饰器 API 或 makeObservableComputed(计算属性)用途:创建派生值,这些值基于其他 observable 状态自动计算具有缓存机制,只在依赖项变化时重新计算避免重复计算,提高性能使用方式:import { observable, computed } from 'mobx';class Store { @observable firstName = 'John'; @observable lastName = 'Doe'; @computed get fullName() { return `${this.firstName} ${this.lastName}`; }}// 或者使用函数式 APIconst store = observable({ firstName: 'John', lastName: 'Doe'});const fullName = computed(() => `${store.firstName} ${store.lastName}`);特点:只读属性(默认情况下)自动缓存计算结果只有当依赖的 observable 变化时才重新计算可以被其他 computed 或 observer 使用懒计算:只有在被访问时才会计算Action(动作)用途:封装状态修改逻辑确保状态修改是可追踪和可预测的在 MobX 6 中,所有状态修改都必须在 action 中进行使用方式:import { observable, action } from 'mobx';class Store { @observable count = 0; @action increment() { this.count++; } @action.bound decrement = () => { this.count--; }; @action async fetchData() { this.loading = true; const data = await api.getData(); this.data = data; this.loading = false; }}// 或者使用函数式 APIconst store = observable({ count: 0});store.increment = action(() => { store.count++;});特点:在 MobX 6 中是强制性的可以是同步或异步的action.bound 会自动绑定 this可以嵌套使用提供更好的调试体验和可预测性三者的关系Observable 是基础,提供可观察的状态Computed 依赖于 observable,自动计算派生值Action 用于修改 observable,触发依赖更新最佳实践使用 observable:只对需要追踪的状态使用 observable避免对整个应用状态都使用 observable使用 makeAutoObservable 自动推断使用 computed:用于计算派生值,而不是在组件中计算避免在 computed 中产生副作用复杂的计算逻辑使用 computed使用 action:所有状态修改都应该在 action 中进行异步操作也应该包装在 action 中使用 action.bound 避免绑定问题性能考虑Observable 本身不会带来性能开销Computed 通过缓存机制提高性能Action 帮助批量更新,减少不必要的重新渲染理解这三个概念的区别和正确使用它们,是掌握 MobX 的关键。
阅读 0·2月19日 17:57

在 MobX 中如何处理异步操作?

在 MobX 中,处理异步操作需要特别注意,因为异步操作可能会在多个时刻修改状态。以下是处理异步操作的最佳实践:1. 使用 action 包装异步操作基本用法import { observable, action, runInAction } from 'mobx';class Store { @observable data = null; @observable loading = false; @observable error = null; @action async fetchData() { this.loading = true; this.error = null; try { const response = await fetch('/api/data'); const data = await response.json(); // 使用 runInAction 包装状态更新 runInAction(() => { this.data = data; }); } catch (error) { runInAction(() => { this.error = error.message; }); } finally { runInAction(() => { this.loading = false; }); } }}为什么需要 runInAction在异步操作中,状态更新可能不在 action 的执行上下文中。使用 runInAction 确保所有状态更新都在 action 中进行。2. 使用 flow 处理异步操作MobX 提供了 flow 工具函数,可以更优雅地处理异步操作:import { observable, flow } from 'mobx';class Store { @observable data = null; @observable loading = false; @observable error = null; fetchData = flow(function* () { this.loading = true; this.error = null; try { const response = yield fetch('/api/data'); const data = yield response.json(); this.data = data; } catch (error) { this.error = error.message; } finally { this.loading = false; } });}flow 的优势自动处理异步操作代码更简洁,类似于同步代码自动包装状态更新更好的错误处理3. 处理多个异步操作并行执行@actionasync fetchMultipleData() { this.loading = true; try { const [users, posts] = await Promise.all([ fetch('/api/users').then(r => r.json()), fetch('/api/posts').then(r => r.json()) ]); runInAction(() => { this.users = users; this.posts = posts; }); } finally { runInAction(() => { this.loading = false; }); }}串行执行fetchDataSequential = flow(function* () { this.loading = true; try { const userResponse = yield fetch('/api/user'); const user = yield userResponse.json(); this.user = user; const postsResponse = yield fetch(`/api/users/${user.id}/posts`); const posts = yield postsResponse.json(); this.posts = posts; } finally { this.loading = false; }});4. 取消异步操作使用 AbortControllerclass Store { @observable data = null; @observable loading = false; abortController = null; @action async fetchData() { // 取消之前的请求 if (this.abortController) { this.abortController.abort(); } this.abortController = new AbortController(); this.loading = true; try { const response = await fetch('/api/data', { signal: this.abortController.signal }); const data = await response.json(); runInAction(() => { this.data = data; }); } catch (error) { if (error.name !== 'AbortError') { runInAction(() => { this.error = error.message; }); } } finally { runInAction(() => { this.loading = false; }); } } @action cancelRequest() { if (this.abortController) { this.abortController.abort(); } }}5. 错误处理和重试基本错误处理@actionasync fetchDataWithRetry(maxRetries = 3) { this.loading = true; let retries = 0; while (retries < maxRetries) { try { const response = await fetch('/api/data'); const data = await response.json(); runInAction(() => { this.data = data; this.error = null; }); return; } catch (error) { retries++; if (retries >= maxRetries) { runInAction(() => { this.error = error.message; }); throw error; } // 等待一段时间后重试 await new Promise(resolve => setTimeout(resolve, 1000 * retries)); } finally { if (retries >= maxRetries) { runInAction(() => { this.loading = false; }); } } }}6. 在 React 组件中使用异步操作import React, { useEffect } from 'react';import { observer } from 'mobx-react-lite';import { useLocalObservable } from 'mobx-react-lite';const DataComponent = observer(() => { const store = useLocalObservable(() => ({ data: null, loading: false, error: null, fetchData: flow(function* () { this.loading = true; this.error = null; try { const response = yield fetch('/api/data'); const data = yield response.json(); this.data = data; } catch (error) { this.error = error.message; } finally { this.loading = false; } }) })); useEffect(() => { store.fetchData(); }, []); if (store.loading) return <div>Loading...</div>; if (store.error) return <div>Error: {store.error}</div>; return <div>{JSON.stringify(store.data)}</div>;});7. 最佳实践总结1. 始终在 action 中修改状态使用 @action 装饰器使用 runInAction 包装异步状态更新使用 flow 处理复杂的异步流程2. 正确处理加载状态在开始异步操作前设置 loading在操作完成后清除 loading在 finally 块中确保 loading 被清除3. 处理错误捕获所有可能的错误将错误信息存储在 observable 中在 UI 中显示错误信息4. 取消不需要的请求使用 AbortController 取消请求在组件卸载时取消请求避免内存泄漏5. 使用 flow 简化代码对于复杂的异步流程,优先使用 flowflow 使代码更易读和维护自动处理 action 包装8. 常见陷阱1. 忘记使用 runInAction// 错误@actionasync fetchData() { const data = await fetch('/api/data').then(r => r.json()); this.data = data; // 不在 action 中}// 正确@actionasync fetchData() { const data = await fetch('/api/data').then(r => r.json()); runInAction(() => { this.data = data; });}2. 在循环中创建多个 action// 错误@actionasync fetchMultiple() { const items = await fetch('/api/items').then(r => r.json()); items.forEach(item => { runInAction(() => { // 多次调用 runInAction this.items.push(item); }); });}// 正确@actionasync fetchMultiple() { const items = await fetch('/api/items').then(r => r.json()); runInAction(() => { this.items.push(...items); });}3. 忘记清理副作用// 错误useEffect(() => { store.fetchData(); // 组件卸载时请求可能仍在进行}, []);// 正确useEffect(() => { const task = store.fetchData(); return () => { task.cancel(); // 取消请求 };}, []);总结在 MobX 中处理异步操作需要遵循以下原则:始终在 action 中修改状态使用 runInAction 或 flow 包装异步状态更新正确处理加载状态和错误取消不需要的请求遵循最佳实践避免常见陷阱正确处理异步操作是构建可靠 MobX 应用的关键。
阅读 0·2月19日 17:56