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

前端面试题手册

MobX 的依赖追踪系统是如何工作的?

MobX 的依赖追踪系统是其核心机制,它通过细粒度的追踪实现了高效的响应式更新。以下是 MobX 依赖追踪的详细工作原理:依赖追踪的基本原理MobX 使用观察者模式和依赖图来实现依赖追踪。当 observable 被访问时,MobX 会建立依赖关系;当 observable 被修改时,MobX 会通知所有依赖它的观察者。核心组件1. Reaction(反应)Reaction 是依赖追踪的执行单元,包括:autorun:立即执行,并在依赖变化时自动重新执行reaction:提供更细粒度的控制,可以指定追踪函数和效果函数observer(React 组件):包装 React 组件,使其能够响应状态变化computed:计算属性,也是一种特殊的 reaction2. Derivation(派生)Derivation 表示依赖于 observable 的计算或副作用。每个 derivation 维护一个依赖列表。3. Atom(原子)Atom 是最小的可观察单元,每个 observable 对象、数组、Map 等都由多个 atom 组成。依赖追踪的执行流程1. 追踪阶段(Tracing)当 reaction 执行时:autorun(() => { console.log(store.count); // 访问 observable});执行步骤:MobX 将当前 reaction 标记为"正在追踪"当访问 store.count 时,MobX 记录下这个 reaction 依赖于 count 这个 atom继续执行,记录所有访问的 observable执行完成后,reaction 进入"稳定"状态2. 通知阶段(Notification)当 observable 被修改时:runInAction(() => { store.count++; // 修改 observable});执行步骤:MobX 检测到 count atom 被修改查找所有依赖于 count 的 reaction将这些 reaction 标记为"过时"(stale)在下一个事件循环中,重新执行这些 reaction依赖图的结构MobX 维护一个双向的依赖图:Atom → Derivation:每个 atom 知道哪些 derivation 依赖于它Derivation → Atom:每个 derivation 知道自己依赖于哪些 atom这种双向关系使得 MobX 能够高效地进行依赖更新和清理。细粒度更新MobX 的依赖追踪是细粒度的,这意味着:只更新真正需要更新的部分避免不必要的重新计算和重新渲染自动处理嵌套的依赖关系示例:class Store { @observable firstName = 'John'; @observable lastName = 'Doe'; @observable age = 30; @computed get fullName() { return `${this.firstName} ${this.lastName}`; }}const observerComponent = observer(() => { // 只依赖 fullName,不依赖 age return <div>{store.fullName}</div>;});当 age 变化时,组件不会重新渲染;只有当 firstName 或 lastName 变化时才会重新渲染。批量更新MobX 会自动批量更新,避免多次触发 reaction:runInAction(() => { store.firstName = 'Jane'; store.lastName = 'Smith'; store.age = 25;});即使修改了多个 observable,相关的 reaction 只会执行一次。依赖清理当 reaction 不再需要时,MobX 会自动清理依赖关系:组件卸载时,observer 会自动清理使用 dispose() 方法手动清理 reaction避免内存泄漏性能优化MobX 的依赖追踪系统提供了多种性能优化:懒计算:computed 只在需要时才计算缓存机制:computed 的结果会被缓存批量更新:多个状态变化合并为一次更新细粒度追踪:只追踪真正需要的依赖调试依赖追踪MobX 提供了调试工具来查看依赖关系:import { trace } from 'mobx';// 追踪 computed 的依赖trace(store.fullName);// 追踪 reaction 的依赖autorun(() => { console.log(store.count);}, { name: 'myReaction' });常见问题1. 循环依赖MobX 能够检测和避免循环依赖,但设计时应尽量避免。2. 过度追踪避免在循环或条件中访问 observable,这可能导致不必要的依赖。3. 内存泄漏确保在组件卸载时清理 reaction,避免内存泄漏。总结MobX 的依赖追踪系统通过观察者模式和依赖图实现了高效的响应式更新。理解这个系统的工作原理有助于编写更高效的 MobX 代码,并避免常见的性能问题。
阅读 0·2月21日 15:45

如何测试 MobX 应用?

MobX 的测试策略和工具对于构建可靠的应用至关重要。以下是 MobX 测试的完整指南:1. 测试 Store基本测试import { UserStore } from './UserStore';describe('UserStore', () => { let store; beforeEach(() => { store = new UserStore(); }); it('should initialize with default values', () => { expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); }); it('should login user', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).not.toBeNull(); expect(store.isAuthenticated).toBe(true); }); it('should logout user', () => { store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; store.logout(); expect(store.user).toBeNull(); expect(store.isAuthenticated).toBe(false); });});测试 computed 属性describe('ProductStore', () => { let store; beforeEach(() => { store = new ProductStore(); }); it('should compute featured products', () => { store.products = [ { id: 1, name: 'Product 1', featured: true }, { id: 2, name: 'Product 2', featured: false }, { id: 3, name: 'Product 3', featured: true } ]; expect(store.featuredProducts).toHaveLength(2); expect(store.featuredProducts[0].name).toBe('Product 1'); expect(store.featuredProducts[1].name).toBe('Product 3'); }); it('should update when products change', () => { store.products = [{ id: 1, name: 'Product 1', featured: true }]; expect(store.featuredProducts).toHaveLength(1); store.products.push({ id: 2, name: 'Product 2', featured: true }); expect(store.featuredProducts).toHaveLength(2); });});测试异步 actiondescribe('AsyncStore', () => { let store; let mockApi; beforeEach(() => { mockApi = { fetchData: jest.fn().mockResolvedValue({ data: 'test' }) }; store = new AsyncStore(mockApi); }); it('should fetch data successfully', async () => { await store.fetchData(); expect(store.data).toEqual({ data: 'test' }); expect(store.loading).toBe(false); expect(mockApi.fetchData).toHaveBeenCalled(); }); it('should handle errors', async () => { mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false); });});2. 测试 React 组件测试 observer 组件import { render, screen, fireEvent } from '@testing-library/react';import { observer } from 'mobx-react-lite';import { UserStore } from './UserStore';const TestComponent = observer(({ store }) => ( <div> {store.isAuthenticated ? ( <div>Welcome, {store.user?.name}</div> ) : ( <div>Please login</div> )} <button onClick={store.login}>Login</button> </div>));describe('TestComponent', () => { it('should show login message when not authenticated', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); }); it('should show welcome message when authenticated', () => { const store = new UserStore(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; render(<TestComponent store={store} />); expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); }); it('should update when state changes', () => { const store = new UserStore(); render(<TestComponent store={store} />); expect(screen.getByText('Please login')).toBeInTheDocument(); store.user = { id: 1, name: 'Test' }; store.isAuthenticated = true; expect(screen.getByText('Welcome, Test')).toBeInTheDocument(); });});测试表单组件describe('FormComponent', () => { it('should update form data', () => { const store = new FormStore(); render(<FormComponent store={store} />); const input = screen.getByLabelText('Name'); fireEvent.change(input, { target: { value: 'John' } }); expect(store.formData.name).toBe('John'); }); it('should submit form', async () => { const store = new FormStore(); store.submit = jest.fn(); render(<FormComponent store={store} />); const button = screen.getByText('Submit'); fireEvent.click(button); expect(store.submit).toHaveBeenCalled(); });});3. 使用 MobX 测试工具使用 spyimport { spy } from 'mobx';describe('Spy Usage', () => { it('should spy on observable changes', () => { const store = observable({ count: 0 }); const countSpy = jest.fn(); spy(store, 'count', (change) => { countSpy(change); }); store.count = 1; store.count = 2; expect(countSpy).toHaveBeenCalledTimes(2); });});使用 traceimport { trace } from 'mobx';describe('Trace Usage', () => { it('should trace computed dependencies', () => { const store = observable({ firstName: 'John', lastName: 'Doe' }); const fullName = computed(() => `${store.firstName} ${store.lastName}`); // 追踪依赖 trace(fullName); expect(fullName.get()).toBe('John Doe'); });});使用 isObservableimport { isObservable } from 'mobx';describe('IsObservable Usage', () => { it('should check if object is observable', () => { const observableObj = observable({ count: 0 }); const plainObj = { count: 0 }; expect(isObservable(observableObj)).toBe(true); expect(isObservable(plainObj)).toBe(false); });});4. Mock API 调用使用 Jest mockimport { UserStore } from './UserStore';describe('UserStore with API', () => { let store; let mockApi; beforeEach(() => { mockApi = { login: jest.fn(), logout: jest.fn(), fetchUser: jest.fn() }; store = new UserStore(mockApi); }); it('should call API on login', async () => { mockApi.login.mockResolvedValue({ id: 1, name: 'Test' }); await store.login({ username: 'test', password: 'test' }); expect(mockApi.login).toHaveBeenCalledWith({ username: 'test', password: 'test' }); }); it('should handle API errors', async () => { mockApi.login.mockRejectedValue(new Error('Invalid credentials')); await expect(store.login({ username: 'test', password: 'test' })) .rejects.toThrow('Invalid credentials'); expect(store.error).toBe('Invalid credentials'); });});使用 MSW (Mock Service Worker)import { setupServer, rest } from 'msw';import { UserStore } from './UserStore';const server = setupServer( rest.post('/api/login', (req, res, ctx) => { return res( ctx.status(200), ctx.json({ id: 1, name: 'Test' }) ); }));describe('UserStore with MSW', () => { let store; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); beforeEach(() => { store = new UserStore(); }); it('should login successfully', async () => { await store.login({ username: 'test', password: 'test' }); expect(store.user).toEqual({ id: 1, name: 'Test' }); expect(store.isAuthenticated).toBe(true); });});5. 测试 reactiondescribe('Reaction Testing', () => { it('should trigger reaction when observable changes', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 1; expect(reactionSpy).toHaveBeenCalledWith(1); store.count = 2; expect(reactionSpy).toHaveBeenCalledWith(2); }); it('should not trigger when value is same', () => { const store = observable({ count: 0 }); const reactionSpy = jest.fn(); reaction( () => store.count, (count) => { reactionSpy(count); } ); store.count = 0; expect(reactionSpy).not.toHaveBeenCalled(); });});6. 测试中间件describe('Middleware Testing', () => { it('should call middleware before action', () => { const middlewareSpy = jest.fn(); const actionSpy = jest.fn(); const store = { @observable count: 0, @action increment() { this.count++; } }; const originalIncrement = store.increment; store.increment = function(...args) { middlewareSpy(...args); return originalIncrement.apply(this, args); }; store.increment(); expect(middlewareSpy).toHaveBeenCalled(); expect(actionSpy).toHaveBeenCalled(); });});7. 集成测试describe('Integration Tests', () => { it('should handle complete user flow', async () => { const store = new RootStore(); // 登录 await store.userStore.login({ username: 'test', password: 'test' }); expect(store.userStore.isAuthenticated).toBe(true); // 加载数据 await store.dataStore.loadData(); expect(store.dataStore.data).not.toBeNull(); // 添加到购物车 store.cartStore.addItem(store.dataStore.data[0]); expect(store.cartStore.items).toHaveLength(1); // 结账 await store.cartStore.checkout(); expect(store.cartStore.items).toHaveLength(0); });});8. 测试最佳实践1. 隔离测试// 每个测试都应该独立beforeEach(() => { store = new Store();});// 清理副作用afterEach(() => { if (store.dispose) { store.dispose(); }});2. 使用快照测试it('should match snapshot', () => { const store = new Store(); store.data = { id: 1, name: 'Test' }; expect(toJS(store.data)).toMatchSnapshot();});3. 测试边界情况it('should handle empty array', () => { const store = new Store(); store.items = []; expect(store.itemCount).toBe(0);});it('should handle null values', () => { const store = new Store(); store.user = null; expect(store.userName).toBe('Guest');});4. 测试错误处理it('should handle network errors gracefully', async () => { const store = new Store(); mockApi.fetchData.mockRejectedValue(new Error('Network error')); await expect(store.fetchData()).rejects.toThrow('Network error'); expect(store.error).toBe('Network error'); expect(store.loading).toBe(false);});总结MobX 测试的关键点:测试 Store:验证 observable、computed 和 action 的行为测试组件:验证 observer 组件的响应性使用测试工具:spy、trace、isObservableMock API:使用 Jest mock 或 MSW测试 reaction:验证副作用是否正确触发测试中间件:验证中间件是否正确执行集成测试:验证多个 store 之间的交互最佳实践:隔离测试、快照测试、边界情况、错误处理遵循这些测试策略,可以构建可靠、可维护的 MobX 应用。
阅读 0·2月21日 15:45

OffscreenCanvas 如何在 Web Worker 中进行渲染?

OffscreenCanvas 是 HTML5 提供的一个功能,允许在 Web Worker 中进行 Canvas 渲染,从而将复杂的图形计算从主线程移到后台线程。OffscreenCanvas 的核心概念特点可以在 Worker 中进行 Canvas 绘图操作支持大部分 Canvas 2D API 和 WebGL API通过 transferControlToOffscreen() 方法将 Canvas 控制权转移适用于复杂的图形渲染和动画基本使用主线程设置// 获取 Canvas 元素const canvas = document.getElementById('myCanvas');// 将 Canvas 控制权转移到 OffscreenCanvasconst offscreen = canvas.transferControlToOffscreen();// 创建 Workerconst worker = new Worker('canvas-worker.js');// 将 OffscreenCanvas 发送给 Workerworker.postMessage({ canvas: offscreen }, [offscreen]);Worker 中渲染// canvas-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); // 在 Worker 中进行绘图 function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制图形 ctx.fillStyle = 'blue'; ctx.fillRect(50, 50, 100, 100); // 继续动画 requestAnimationFrame(render); } render();};实际应用场景1. 复杂动画渲染// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('animation-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// animation-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); let particles = []; function initParticles() { for (let i = 0; i < 1000; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, size: Math.random() * 3 + 1 }); } } function updateParticles() { particles.forEach(p => { p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > canvas.width) p.vx *= -1; if (p.y < 0 || p.y > canvas.height) p.vy *= -1; }); } function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(100, 150, 255, 0.7)`; ctx.fill(); }); updateParticles(); requestAnimationFrame(render); } initParticles(); render();};2. 图像处理// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('image-worker.js');// 加载图像const img = new Image();img.onload = function() { worker.postMessage({ canvas: offscreen, image: img }, [offscreen]);};img.src = 'image.jpg';// image-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); const img = e.data.image; // 设置 Canvas 大小 canvas.width = img.width; canvas.height = img.height; // 绘制原始图像 ctx.drawImage(img, 0, 0); // 获取图像数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // 图像处理:灰度化 for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = avg; // R data[i + 1] = avg; // G data[i + 2] = avg; // B } // 放回处理后的图像 ctx.putImageData(imageData, 0, 0);};3. WebGL 渲染// 主线程const canvas = document.getElementById('glCanvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('webgl-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// webgl-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const gl = canvas.getContext('webgl'); // WebGL 初始化代码 const vertexShaderSource = ` attribute vec4 aVertexPosition; void main() { gl_Position = aVertexPosition; } `; const fragmentShaderSource = ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } `; // 编译着色器 const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建程序 const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // 渲染 function render() { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(shaderProgram); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(render); } render();};function compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); return shader;}与主线程交互动态调整 Canvas 大小// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 监听窗口大小变化window.addEventListener('resize', function() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; worker.postMessage({ type: 'resize', width: canvas.width, height: canvas.height });});// canvas-worker.jsself.onmessage = function(e) { if (e.data.type === 'resize') { canvas.width = e.data.width; canvas.height = e.data.height; }};接收用户输入// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 发送鼠标位置canvas.addEventListener('mousemove', function(e) { const rect = canvas.getBoundingClientRect(); worker.postMessage({ type: 'mousemove', x: e.clientX - rect.left, y: e.clientY - rect.top });});// canvas-worker.jslet mouseX = 0, mouseY = 0;self.onmessage = function(e) { if (e.data.type === 'mousemove') { mouseX = e.data.x; mouseY = e.data.y; }};注意事项1. Canvas 控制权只能转移一次// ❌ 错误:多次转移const offscreen1 = canvas.transferControlToOffscreen();const offscreen2 = canvas.transferControlToOffscreen(); // 报错// ✅ 正确:只转移一次const offscreen = canvas.transferControlToOffscreen();2. OffscreenCanvas 不支持所有 Canvas API// ❌ 不支持canvas.toDataURL(); // 在 Worker 中不可用canvas.toBlob(); // 在 Worker 中不可用// ✅ 使用 ImageBitmap 代替const bitmap = await createImageBitmap(canvas);3. 浏览器兼容性// 检查浏览器支持if ('transferControlToOffscreen' in HTMLCanvasElement.prototype) { // 支持 OffscreenCanvas} else { // 不支持,使用回退方案}性能优化1. 批量绘制// ❌ 频繁调用绘制方法for (let i = 0; i < 1000; i++) { ctx.beginPath(); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2); ctx.fill();}// ✅ 批量绘制ctx.beginPath();for (let i = 0; i < 1000; i++) { ctx.moveTo(particles[i].x, particles[i].y); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2);}ctx.fill();2. 使用 ImageBitmap// 加载图像为 ImageBitmapconst bitmap = await createImageBitmap(image);// 在 Worker 中绘制ctx.drawImage(bitmap, 0, 0);3. 降低渲染频率let lastRenderTime = 0;const targetFPS = 30;const frameInterval = 1000 / targetFPS;function render(timestamp) { if (timestamp - lastRenderTime >= frameInterval) { // 执行渲染 ctx.clearRect(0, 0, canvas.width, canvas.height); // ... 绘制代码 lastRenderTime = timestamp; } requestAnimationFrame(render);}最佳实践复杂渲染使用 OffscreenCanvas:将计算密集型图形渲染移到 Worker合理控制渲染频率:避免不必要的重绘批量处理:减少绘制调用次数使用 ImageBitmap:提高图像加载和渲染性能检查浏览器兼容性:提供回退方案及时释放资源:使用完毕后清理资源
阅读 0·2月21日 15:44

如何优化Expo应用的性能?有哪些常见的性能问题?

Expo应用的性能优化是确保良好用户体验的关键。Expo基于React Native,因此许多React Native的性能优化技巧同样适用,同时Expo也提供了一些特定的优化工具和策略。性能优化策略:组件渲染优化使用React.memo、useMemo和useCallback减少不必要的渲染:// 使用React.memo避免不必要的重新渲染const MyComponent = React.memo(({ data }) => { return <Text>{data.value}</Text>;});// 使用useMemo缓存计算结果function ExpensiveComponent({ items }) { const sortedItems = useMemo(() => { return items.sort((a, b) => a.id - b.id); }, [items]); return <FlatList data={sortedItems} />;}// 使用useCallback缓存函数function ParentComponent() { const handleClick = useCallback(() => { console.log('Clicked'); }, []); return <ChildComponent onClick={handleClick} />;}列表优化使用FlatList而不是ScrollView处理长列表:<FlatList data={items} renderItem={({ item }) => <Item item={item} />} keyExtractor={(item) => item.id} removeClippedSubviews={true} maxToRenderPerBatch={10} windowSize={10} initialNumToRender={10} getItemLayout={(data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index, })}/>关键属性说明:removeClippedSubviews:移除屏幕外的视图maxToRenderPerBatch:每批渲染的项目数windowSize:渲染窗口大小initialNumToRender:初始渲染项目数getItemLayout:提供项目布局信息图片优化使用expo-image和图片缓存策略:import { Image } from 'expo-image';<Image source={{ uri: 'https://example.com/image.jpg' }} style={{ width: 200, height: 200 }} cachePolicy="memory-disk" contentFit="cover" transition={200}/>优化技巧:使用适当的图片尺寸启用缓存策略使用WebP格式懒加载图片网络请求优化使用缓存和请求去重:import { useQuery } from '@tanstack/react-query';function UserProfile({ userId }) { const { data, isLoading } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 5 * 60 * 1000, // 5分钟 cacheTime: 10 * 60 * 1000, // 10分钟 }); if (isLoading) return <Loading />; return <Text>{data.name}</Text>;}动画优化使用react-native-reanimated进行高性能动画:import Animated, { useSharedValue, useAnimatedStyle, withTiming,} from 'react-native-reanimated';function AnimatedBox() { const opacity = useSharedValue(0); const animatedStyle = useAnimatedStyle(() => { return { opacity: withTiming(opacity.value, { duration: 500 }), }; }); useEffect(() => { opacity.value = 1; }, []); return <Animated.View style={animatedStyle} />;}内存管理及时释放资源:// 取消网络请求useEffect(() => { const controller = new AbortController(); fetchData(controller.signal); return () => controller.abort();}, []);// 清理定时器useEffect(() => { const timer = setInterval(() => { console.log('Tick'); }, 1000); return () => clearInterval(timer);}, []);// 清理订阅useEffect(() => { const subscription = someEvent.subscribe(); return () => subscription.unsubscribe();}, []);Bundle优化减少应用包大小:// 代码分割const LazyComponent = React.lazy(() => import('./LazyComponent'));// 动态导入const loadModule = async () => { const module = await import('./heavyModule'); module.doSomething();};// 移除未使用的依赖npm prune使用Expo的性能工具Expo提供了一些性能监控工具:import { Performance } from 'react-native';// 记录性能标记Performance.mark('component-start');// 组件渲染完成后Performance.mark('component-end');// 测量性能Performance.measure('component-render', 'component-start', 'component-end');性能分析工具:React DevTools Profiler分析组件渲染性能识别性能瓶颈优化渲染次数Flipper网络请求监控布局检查内存分析Expo DevTools实时性能监控Bundle大小分析加载时间追踪常见性能问题及解决方案:列表滚动卡顿使用FlatList而不是ScrollView启用removeClippedSubviews提供正确的getItemLayout图片加载慢使用expo-image启用缓存使用适当的图片尺寸应用启动慢优化初始化代码延迟加载非关键模块使用启动画面内存泄漏及时清理订阅和定时器使用WeakMap和WeakSet定期检查内存使用最佳实践:性能监控:定期使用性能工具分析应用渐进优化:先优化关键路径,再优化次要功能测试覆盖:在不同设备上测试性能持续优化:将性能优化作为持续过程用户体验:平衡性能和功能,优先保证核心体验通过系统性的性能优化,可以显著提升Expo应用的用户体验和竞争力。
阅读 0·2月21日 15:43

Expo应用中如何管理权限?有哪些最佳实践?

Expo应用的权限管理是开发过程中的重要环节,特别是在处理敏感功能如相机、位置、麦克风等时。Expo提供了统一的权限管理API,简化了跨平台权限请求流程。权限管理基础:Expo使用expo-permissions和各个模块的权限API来管理应用权限。安装权限模块:npx expo install expo-permissions基本权限请求流程:import * as Permissions from 'expo-permissions';import { Camera } from 'expo-camera';async function requestCameraPermission() { // 请求相机权限 const { status } = await Camera.requestCameraPermissionsAsync(); if (status === 'granted') { console.log('Camera permission granted'); } else { console.log('Camera permission denied'); }}常用权限类型:相机权限import { Camera } from 'expo-camera';// 请求相机权限const { status } = await Camera.requestCameraPermissionsAsync();// 检查权限状态const { status: currentStatus } = await Camera.getCameraPermissionsAsync();// 请求麦克风权限(用于视频录制)const { status: audioStatus } = await Camera.requestMicrophonePermissionsAsync();位置权限import * as Location from 'expo-location';// 请求前台位置权限const { status } = await Location.requestForegroundPermissionsAsync();// 请求后台位置权限const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync();// 获取当前位置const location = await Location.getCurrentPositionAsync({});通知权限import * as Notifications from 'expo-notifications';// 请求通知权限const { status } = await Notifications.requestPermissionsAsync();// 配置通知处理程序Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: false, shouldSetBadge: false, }),});媒体库权限import * as MediaLibrary from 'expo-media-library';// 请求媒体库权限const { status } = await MediaLibrary.requestPermissionsAsync();// 保存图片到媒体库const asset = await MediaLibrary.createAssetAsync(uri);联系人权限import * as Contacts from 'expo-contacts';// 请求联系人权限const { status } = await Contacts.requestPermissionsAsync();// 获取联系人const { data } = await Contacts.getContactsAsync();日历权限import * as Calendar from 'expo-calendar';// 请求日历权限const { status } = await Calendar.requestCalendarPermissionsAsync();// 创建日历事件const eventId = await Calendar.createEventAsync(calendarId, eventDetails);权限状态:权限请求返回的状态包括:granted:权限已授予denied:权限被拒绝undetermined:用户尚未做出选择limited:部分权限已授予(iOS特有)权限配置:在app.json中声明权限:{ "expo": { "ios": { "infoPlist": { "NSCameraUsageDescription": "需要相机权限来拍照", "NSLocationWhenInUseUsageDescription": "需要位置权限来显示附近信息", "NSMicrophoneUsageDescription": "需要麦克风权限来录制音频" } }, "android": { "permissions": [ "CAMERA", "ACCESS_FINE_LOCATION", "RECORD_AUDIO", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE" ] } }}最佳实践:适时请求权限// 在用户需要使用功能时才请求权限function CameraButton() { const [hasPermission, setHasPermission] = useState(null); useEffect(() => { (async () => { const { status } = await Camera.requestCameraPermissionsAsync(); setHasPermission(status === 'granted'); })(); }, []); if (hasPermission === null) { return <Text>请求权限中...</Text>; } if (hasPermission === false) { return <Text>没有相机权限</Text>; } return <Button title="打开相机" onPress={openCamera} />;}提供清晰的权限说明async function requestPermissionWithExplanation() { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { Alert.alert( '需要位置权限', '应用需要位置权限来显示附近的信息,请在设置中授予权限。', [ { text: '取消', style: 'cancel' }, { text: '打开设置', onPress: () => Linking.openSettings() } ] ); }}处理权限被拒绝的情况async function handlePermissionDenied() { const { status } = await Camera.requestCameraPermissionsAsync(); if (status !== 'granted') { // 检查是否可以再次请求 const { canAskAgain } = await Camera.getCameraPermissionsAsync(); if (canAskAgain) { Alert.alert( '需要相机权限', '应用需要相机权限来拍照功能', [ { text: '取消' }, { text: '授予权限', onPress: () => requestCameraPermission() } ] ); } else { Alert.alert( '权限被永久拒绝', '请在系统设置中手动授予权限', [ { text: '取消' }, { text: '打开设置', onPress: () => Linking.openSettings() } ] ); } }}权限状态缓存import { useState, useEffect } from 'react';function usePermission(permissionGetter) { const [status, setStatus] = useState(null); useEffect(() => { (async () => { const { status } = await permissionGetter(); setStatus(status); })(); }, [permissionGetter]); return status;}// 使用const cameraStatus = usePermission(() => Camera.getCameraPermissionsAsync());平台差异:iOS权限需要在Info.plist中声明使用目的某些权限只能请求一次用户可以在设置中随时更改权限Android权限需要在AndroidManifest.xml中声明可以多次请求权限运行时权限从Android 6.0开始常见问题:权限请求失败检查权限是否在配置文件中声明确保使用正确的权限API处理用户拒绝权限的情况权限状态不一致缓存权限状态在需要时重新检查权限处理权限状态变化后台权限后台位置权限需要特殊处理通知后台权限需要额外配置遵循平台特定的后台权限规则安全考虑:最小权限原则:只请求必要的权限透明度:清晰解释为什么需要权限用户控制:允许用户撤销权限数据保护:妥善处理敏感数据良好的权限管理不仅能提升用户体验,还能确保应用符合各个平台的隐私政策和法律法规要求。
阅读 0·2月21日 15:43

如何将 Prometheus 与 Grafana 集成,有哪些最佳实践?

Prometheus 与 Grafana 的集成和最佳实践:集成配置:添加 Prometheus 数据源:{ "name": "Prometheus", "type": "prometheus", "url": "http://prometheus:9090", "access": "proxy", "isDefault": true}创建仪表盘:使用变量实现动态查询使用模板变量实现多环境切换配置告警面板常用查询示例:CPU 使用率:100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)内存使用率:(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100磁盘使用率:(1 - (node_filesystem_avail_bytes{fstype!="tmpfs"} / node_filesystem_size_bytes)) * 100网络流量:rate(container_network_receive_bytes_total[5m])变量配置示例:# 实例变量instance: label_values(up, instance)# 命名空间变量namespace: label_values(kube_pod_info, namespace)# 时间范围变量interval: 30s, 1m, 5m, 15m, 1h告警配置:在 Grafana 中配置告警规则支持多种通知渠道(邮件、Slack、Webhook)可与 Prometheus Alertmanager 集成最佳实践:仪表盘组织:按业务或系统分类使用文件夹管理添加描述和标签查询优化:使用 Recording Rules 预计算避免复杂查询合理设置刷新间隔可视化技巧:选择合适的图表类型使用阈值标注添加图例和注释权限管理:配置基于角色的访问控制限制敏感数据访问使用 API Key 自动化导入社区仪表盘:使用 Grafana 官方仪表盘库搜索关键词:Prometheus、Kubernetes、Node Exporter根据需求自定义修改
阅读 0·2月21日 15:40

Qwik City 的核心功能有哪些?

Qwik City 是 Qwik 的全栈框架,提供了完整的路由、数据获取和服务端功能。以下是 Qwik City 的核心概念和使用方法:1. Qwik City 简介Qwik City 构建在 Qwik 之上,提供了:基于文件系统的路由服务端数据加载表单处理和验证中间件支持SEO 优化国际化支持2. 路由系统文件系统路由Qwik City 使用基于文件系统的路由,文件结构直接映射到 URL:src/├── routes/│ ├── index.tsx -> /│ ├── about/│ │ └── index.tsx -> /about│ ├── products/│ │ ├── index.tsx -> /products│ │ └── [id]/│ │ └── index.tsx -> /products/:id│ └── layout.tsx -> 全局布局动态路由// routes/products/[id]/index.tsximport { component$ } from '@builder.io/qwik';import { routeLoader$ } from '@builder.io/qwik-city';import { useLocation } from '@builder.io/qwik-city';export const useProductData = routeLoader$(async ({ params, url, env }) => { const response = await fetch(`https://api.example.com/products/${params.id}`); return response.json();});export default component$(() => { const product = useProductData(); const location = useLocation(); return ( <div> <h1>{product.value.name}</h1> <p>{product.value.description}</p> <p>Price: ${product.value.price}</p> </div> );});嵌套路由// routes/layout.tsximport { component$, Slot } from '@builder.io/qwik';export default component$(() => { return ( <div> <header>Header</header> <main> <Slot /> </main> <footer>Footer</footer> </div> );});3. 数据加载routeLoader$ - 服务端数据加载import { routeLoader$ } from '@builder.io/qwik-city';export const useUserData = routeLoader$(async ({ params, url, env, requestEvent }) => { // 访问请求参数 const userId = params.id; // 访问 URL 查询参数 const searchParams = url.searchParams; const page = searchParams.get('page'); // 访问环境变量 const apiKey = env.get('API_KEY'); // 访问请求事件 const cookie = requestEvent.cookie.get('session'); const response = await fetch(`https://api.example.com/users/${userId}`); return response.json();});clientLoader$ - 客户端数据加载import { clientLoader$ } from '@builder.io/qwik-city';export const useClientData = clientLoader$(async ({ params, navigate }) => { // 客户端数据获取 const response = await fetch(`/api/data/${params.id}`); return response.json();});useResource$ - 组件级数据加载import { component$, useResource$ } from '@builder.io/qwik';export const UserList = component$(() => { const users = useResource$(({ track, cleanup }) => { // 追踪依赖 track(() => /* 依赖项 */); // 清理函数 cleanup(() => { // 清理逻辑 }); return fetch('https://api.example.com/users'); }); return ( <div> {users.value ? ( <ul> {users.value.map(user => <li key={user.id}>{user.name}</li>)} </ul> ) : ( <p>Loading...</p> )} </div> );});4. 表单处理action$ - 服务端表单处理import { action$, zod$, z } from '@builder.io/qwik-city';import { component$, Form } from '@builder.io/qwik-city';// 定义表单验证export const useContactForm = action$(async (data, { requestEvent }) => { // 服务端处理逻辑 const { name, email, message } = data; // 发送邮件 await sendEmail({ name, email, message }); return { success: true };}, zod$({ name: z.string().min(2), email: z.string().email(), message: z.string().min(10)}));export default component$(() => { const action = useContactForm(); return ( <Form action={action}> <input name="name" placeholder="Name" /> <input name="email" type="email" placeholder="Email" /> <textarea name="message" placeholder="Message"></textarea> <button type="submit">Submit</button> {action.value?.success && <p>Message sent!</p>} </Form> );});clientAction$ - 客户端表单处理import { clientAction$ } from '@builder.io/qwik-city';export const useClientAction = clientAction$(async (data) => { // 客户端处理逻辑 console.log('Client action:', data); return { success: true };});5. 中间件请求中间件// routes/middleware.tsimport { middleware$ } from '@builder.io/qwik-city';export const onRequest = middleware$(async ({ requestEvent, next }) => { // 请求前处理 const url = requestEvent.url; const cookie = requestEvent.cookie.get('session'); if (!cookie && url.pathname !== '/login') { throw requestEvent.redirect(302, '/login'); } return next();});export const onResponse = middleware$(async ({ requestEvent, next }) => { // 响应后处理 const response = await next(); // 添加响应头 response.headers.set('X-Custom-Header', 'value'); return response;});6. SEO 优化元数据设置import { component$ } from '@builder.io/qwik';import { routeLoader$, useDocumentHead, useLocation } from '@builder.io/qwik-city';export const useProductData = routeLoader$(async ({ params }) => { const response = await fetch(`https://api.example.com/products/${params.id}`); return response.json();});export default component$(() => { const product = useProductData(); return <div>{product.value.name}</div>;});export const head = useDocumentHead$(({ resolveValue }) => { const product = resolveValue(useProductData); return { title: product.name, meta: [ { name: 'description', content: product.description }, { property: 'og:title', content: product.name }, { property: 'og:description', content: product.description }, { property: 'og:image', content: product.image } ] };});7. 国际化i18n 配置// src/entry.ssr.tsximport { renderToStream } from '@builder.io/qwik/server';import { Root } from './root';import { I18nProvider } from 'qwik-speak';export default function (opts) { return renderToStream(<Root />, { ...opts, containerAttributes: { lang: opts.lang } });}使用翻译import { component$ } from '@builder.io/qwik';import { useSpeak } from 'qwik-speak';export const MyComponent = component$(() => { const { t } = useSpeak(); return ( <div> <h1>{t('welcome.title')}</h1> <p>{t('welcome.description')}</p> </div> );});8. 最佳实践1. 合理使用 routeLoader$ 和 useResource$routeLoader$:用于页面级数据,在服务器执行useResource$:用于组件级数据,可以动态重新获取2. 错误处理export const useData = routeLoader$(async ({ params }) => { try { const response = await fetch(`https://api.example.com/data/${params.id}`); if (!response.ok) { throw new Error('Failed to fetch data'); } return response.json(); } catch (error) { throw requestEvent.redirect(302, '/error'); }});3. 缓存策略export const useCachedData = routeLoader$(async ({ requestEvent }) => { const cacheKey = 'data'; const cached = requestEvent.sharedMap.get(cacheKey); if (cached) { return cached; } const data = await fetchData(); requestEvent.sharedMap.set(cacheKey, data); return data;});总结:Qwik City 提供了完整的全栈开发体验,通过路由、数据加载、表单处理等功能,开发者可以快速构建高性能的 Web 应用程序。
阅读 0·2月21日 15:37

Qwik 和 React 有什么区别?

Qwik 和 React 在架构上有几个关键区别,主要体现在加载策略、状态管理和性能优化方面:1. 加载策略Qwik:采用"按需加载"策略,所有 JavaScript 代码默认都是延迟加载的只有当用户与页面交互时,才会加载和执行相关的代码不需要下载整个应用程序包,而是按需加载单个函数或组件React:通常需要下载整个应用程序包(或多个 chunk)使用代码分割(Code Splitting)来实现懒加载,但需要手动配置即使使用 SSR,仍需要下载 hydration 代码2. 水合(Hydration)Qwik:不需要传统的水合过程通过恢复性(Resumability)直接从 HTML 中恢复状态和功能事件监听器通过 HTML 属性直接附加,无需 JavaScript 执行React:必须进行水合过程,重新执行 JavaScript 来附加事件监听器水合过程需要执行大量 JavaScript,影响首屏性能使用 React 18 的 Selective Hydration 可以部分优化,但仍不如 Qwik3. 状态管理Qwik:使用 useSignal 和 useStore 进行状态管理状态变化会自动触发细粒度的更新状态序列化到 HTML 中,可以在客户端直接恢复React:使用 useState、useReducer、Context API 等进行状态管理状态变化会触发组件重新渲染需要额外的状态管理库(如 Redux、Zustand)来管理复杂状态4. 性能优化Qwik:编译器自动优化代码分割和加载零 JavaScript 启动成本自动实现细粒度的更新,避免不必要的重新渲染React:需要手动优化性能(如 useMemo、useCallback)使用 React.memo 来避免不必要的重新渲染需要开发者有深入的性能优化知识5. 开发体验Qwik:语法与 React 相似,学习曲线较平缓编译器处理大部分优化工作不需要关心代码分割和加载策略React:生态系统成熟,有丰富的第三方库社区支持强大,文档完善需要开发者手动处理性能优化6. 适用场景Qwik:适合需要极致性能的应用内容密集型网站需要良好 SEO 的应用大型企业级应用React:适合各种规模的应用团队已经熟悉 React 生态需要丰富的第三方库支持快速原型开发总结:Qwik 通过其独特的恢复性架构,在性能方面优于 React,特别是在首屏加载和交互响应方面。但 React 的生态系统和社区支持更加成熟,适合更广泛的应用场景。
阅读 0·2月21日 15:37

什么是 Qwik 的恢复性(Resumability)概念?

Qwik 的核心概念是"恢复性"(Resumability),这意味着应用程序可以在服务器端渲染后,在客户端无缝恢复执行状态,而不需要重新执行 JavaScript。Qwik 通过以下方式实现恢复性:延迟加载(Lazy Loading):Qwik 默认将所有 JavaScript 代码分割成小块,只有当用户交互时才加载必要的代码。这与传统框架不同,传统框架通常需要下载整个应用程序包。序列化状态:Qwik 将应用程序的状态序列化为 HTML,当页面加载时,浏览器可以直接从 HTML 中恢复状态,而不需要重新执行初始化代码。无水合(No Hydration):传统框架(如 React、Vue)需要进行水合过程,即重新执行 JavaScript 来附加事件监听器。Qwik 通过其独特的架构避免了这一步骤,直接从 HTML 中恢复功能。细粒度加载:Qwik 可以加载单个函数或组件,而不是整个模块。这意味着点击一个按钮只会加载该按钮的事件处理程序,而不是整个应用程序。可恢复的执行上下文:Qwik 维护了一个执行上下文,可以在服务器和客户端之间传递,确保代码可以在不同环境中无缝恢复。恢复性的优势包括:更快的首屏加载时间:由于不需要下载和执行大量 JavaScript更好的性能:按需加载减少了不必要的代码执行更好的 SEO:服务器端渲染提供了完整的 HTML 内容更低的带宽使用:只加载用户实际需要的代码Qwik 的恢复性是通过其编译器实现的,编译器会自动处理代码分割和序列化,开发者不需要手动管理这些细节。
阅读 0·2月21日 15:36

Qwik 的组件系统是如何工作的?

Qwik 的组件系统设计遵循几个核心原则,使其能够实现高性能和细粒度的代码分割:1. 组件定义Qwik 组件使用 $ 前缀来标识,这告诉编译器该组件需要特殊处理:import { component$ } from '@builder.io/qwik';export const MyComponent = component$(() => { return <div>Hello Qwik</div>;});component$ 是一个编译时宏,它将组件转换为可恢复的格式。2. 组件特性懒加载所有 Qwik 组件默认都是懒加载的。编译器会自动将每个组件分割成独立的 chunk,只有当组件被渲染时才会加载。可恢复性组件的状态和执行上下文被序列化到 HTML 中,可以在客户端无缝恢复执行。细粒度更新Qwik 只更新发生变化的 DOM 节点,而不是重新渲染整个组件树。3. Props 和状态PropsProps 通过编译时类型检查,并在序列化时自动处理:export const ChildComponent = component$((props: { name: string; count: number }) => { return <div>{props.name}: {props.count}</div>;});状态管理Qwik 提供了两种主要的状态管理方式:useSignal:用于简单值的状态管理import { useSignal } from '@builder.io/qwik';export const Counter = component$(() => { const count = useSignal(0); return ( <button onClick$={() => count.value++}> Count: {count.value} </button> );});useStore:用于复杂对象的状态管理import { useStore } from '@builder.io/qwik';export const Form = component$(() => { const form = useStore({ name: '', email: '' }); return ( <form> <input value={form.name} onInput$={(e) => form.name = (e.target as HTMLInputElement).value} /> </form> );});4. 事件处理Qwik 事件处理函数使用 $ 后缀,表示它们是可恢复的:export const Button = component$(() => { const handleClick$ = () => { console.log('Button clicked'); }; return <button onClick$={handleClick$}>Click me</button>;});事件处理函数会被编译器自动分割和懒加载。5. 生命周期Qwik 的生命周期钩子也使用 $ 后缀:useTask$:在组件挂载和更新时执行useVisibleTask$:在组件可见时执行(用于客户端特定逻辑)useResource$:用于异步数据获取export const DataComponent = component$(() => { useTask$(() => { console.log('Component mounted or updated'); }); return <div>Data Component</div>;});6. 组件通信父子通信通过 props 传递数据:export const Parent = component$(() => { return <Child message="Hello from parent" />;});export const Child = component$((props: { message: string }) => { return <div>{props.message}</div>;});上下文使用 useContext 和 Context 进行跨组件通信:import { createContext, useContext } from '@builder.io/qwik';const ThemeContext = createContext('light');export const ThemeProvider = component$(() => { return ( <ThemeContext.Provider value="dark"> <Child /> </ThemeContext.Provider> );});export const Child = component$(() => { const theme = useContext(ThemeContext); return <div>Current theme: {theme}</div>;});7. 组件优化Qwik 编译器自动处理组件优化,开发者不需要手动使用 React.memo 或类似的优化技术。编译器会:自动分割组件代码只加载必要的代码避免不必要的重新渲染优化事件处理函数的加载总结:Qwik 的组件系统通过编译时优化和独特的 $ 语法,实现了自动的代码分割和懒加载,使开发者能够编写高性能的应用程序而无需关心底层的性能优化细节。
阅读 0·2月21日 15:36