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

面试题手册

Module Federation 常见问题有哪些?如何解决?

Module Federation 在实际应用中可能会遇到各种问题,以下是常见问题及解决方案:1. 版本冲突问题问题描述: 多个应用使用不同版本的共享依赖,导致运行时错误。解决方案:// 使用 strictVersion 和 requiredVersion 控制版本shared: { react: { singleton: true, requiredVersion: '^17.0.0', strictVersion: false, // 允许小版本差异 version: deps.react }}// 或使用 package.json 的 resolutions 字段统一版本{ "resolutions": { "react": "^17.0.2", "react-dom": "^17.0.2" }}2. 模块加载失败问题描述: 远程模块加载失败,导致应用崩溃。解决方案:// 添加错误边界和降级方案class ErrorBoundary extends React.Component { state = { hasError: false } static getDerivedStateFromError(error) { return { hasError: true } } render() { if (this.state.hasError) { return this.props.fallback || <FallbackComponent /> } return this.props.children }}// 使用 Suspense 和错误处理const RemoteComponent = React.lazy(() => import('remoteApp/Component').catch(() => import('./LocalFallback') ))function App() { return ( <ErrorBoundary fallback={<LocalFallback />}> <Suspense fallback={<Loading />}> <RemoteComponent /> </Suspense> </ErrorBoundary> )}3. 开发环境跨域问题问题描述: 本地开发时,不同端口的远程应用无法加载。解决方案:// 在 webpack.config.js 中配置 devServerdevServer: { port: 3000, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization' }}// 或使用代理devServer: { port: 3000, proxy: { '/remote': { target: 'http://localhost:3001', changeOrigin: true, pathRewrite: { '^/remote': '' } } }}4. 样式隔离问题问题描述: 远程模块的样式影响主应用或其他远程模块。解决方案:// 使用 CSS Modulesimport styles from './Button.module.css'// 或使用 CSS-in-JSimport styled from 'styled-components'const Button = styled.button` /* 样式自动隔离 */`// 或使用 Shadow DOMclass ShadowComponent extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }) } connectedCallback() { this.shadowRoot.innerHTML = ` <style> button { color: red; } </style> <button>Shadow Button</button> ` }}5. 状态管理问题问题描述: 多个应用之间的状态无法共享。解决方案:// 使用共享的状态管理库// Remote 应用export { createStore } from './store'// Host 应用import { createStore } from 'remoteApp/store'const store = createStore()// 或使用事件总线const eventBus = { listeners: {}, on(event, callback) { if (!this.listeners[event]) { this.listeners[event] = [] } this.listeners[event].push(callback) }, emit(event, data) { if (this.listeners[event]) { this.listeners[event].forEach(cb => cb(data)) } }}6. 路由冲突问题问题描述: 多个应用的路由配置冲突。解决方案:// 使用路由前缀const routes = [ { path: '/dashboard/*', component: React.lazy(() => import('dashboardApp/App')) }, { path: '/settings/*', component: React.lazy(() => import('settingsApp/App')) }]// 或使用路由守卫const ProtectedRoute = ({ children, ...rest }) => { const isAuthenticated = useAuth() return ( <Route {...rest}> {isAuthenticated ? children : <Redirect to="/login" />} </Route> )}7. 类型定义问题问题描述: TypeScript 无法识别远程模块的类型。解决方案:// 创建类型声明文件 types.d.tsdeclare module 'remoteApp/*' { const value: any export default value}// 或使用类型导出// Remote 应用export interface ButtonProps { label: string onClick: () => void}// Host 应用import type { ButtonProps } from 'remoteApp/types'8. 构建产物过大问题描述: remoteEntry.js 或模块文件过大,影响加载速度。解决方案:// 使用代码分割optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 }, common: { name: 'common', minChunks: 2, priority: 5 } } }}// 压缩构建产物optimization: { minimize: true, minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: true } } }) ]}通过以上解决方案,可以有效应对 Module Federation 在实际应用中遇到的各种问题。
阅读 0·2月19日 17:42

什么是 Module Federation,它的工作原理是什么?

Module Federation 是 Webpack 5 引入的一项革命性功能,它允许在运行时动态加载和共享代码模块。其核心原理是通过构建时生成独立的构建产物,在运行时通过 JavaScript 动态导入机制实现模块的按需加载和共享。关键概念:Host(宿主应用):消费远程模块的应用,负责加载远程入口Remote(远程应用):暴露模块供其他应用使用的应用Shared(共享依赖):多个应用之间共享的依赖库,避免重复加载工作原理:Module Federation 使用 Webpack 的容器插件(ContainerPlugin)和容器引用插件(ContainerReferencePlugin)来实现。Remote 应用在构建时会生成一个入口文件(remoteEntry.js),该文件包含了所有暴露模块的映射信息。Host 应用通过配置 remotes 字段,指定要加载的远程应用及其入口文件地址。配置示例:// Remote 应用配置new ModuleFederationPlugin({ name: 'remoteApp', filename: 'remoteEntry.js', exposes: { './Button': './src/Button' }, shared: ['react', 'react-dom']})// Host 应用配置new ModuleFederationPlugin({ name: 'hostApp', remotes: { remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js' }, shared: ['react', 'react-dom']})优势:实现真正的微前端架构,各团队独立开发和部署运行时动态加载,无需重新构建整个应用依赖共享,减少重复代码,优化加载性能版本管理灵活,可以独立升级各个模块
阅读 0·2月19日 17:41

如何将现有应用迁移到 Module Federation?有哪些迁移策略?

Module Federation 的迁移策略需要谨慎规划,以确保平稳过渡和最小化风险。以下是详细的迁移方案:1. 迁移前准备现状评估:// migration-assessment.jsclass MigrationAssessment { constructor() { this.assessment = { currentArchitecture: null, dependencies: [], teamStructure: [], technicalDebt: [], migrationComplexity: 'medium' } } assessCurrentArchitecture() { // 评估当前应用架构 const assessment = { buildTool: this.detectBuildTool(), bundler: this.detectBundler(), framework: this.detectFramework(), monorepo: this.checkMonorepo(), codeSize: this.calculateCodeSize(), buildTime: this.measureBuildTime() } this.assessment.currentArchitecture = assessment return assessment } detectBuildTool() { if (fs.existsSync('package.json')) { const pkg = JSON.parse(fs.readFileSync('package.json')) return { npm: !!pkg.scripts, yarn: fs.existsSync('yarn.lock'), pnpm: fs.existsSync('pnpm-lock.yaml') } } return null } detectBundler() { if (fs.existsSync('webpack.config.js')) return 'webpack' if (fs.existsSync('rollup.config.js')) return 'rollup' if (fs.existsSync('vite.config.js')) return 'vite' return 'unknown' } detectFramework() { const pkg = JSON.parse(fs.readFileSync('package.json')) const deps = { ...pkg.dependencies, ...pkg.devDependencies } if (deps.react) return 'react' if (deps.vue) return 'vue' if (deps['@angular/core']) return 'angular' return 'unknown' } checkMonorepo() { return fs.existsSync('lerna.json') || fs.existsSync('nx.json') || (fs.existsSync('package.json') && JSON.parse(fs.readFileSync('package.json')).workspaces) } calculateCodeSize() { const { execSync } = require('child_process') const size = execSync('find src -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" | xargs wc -l') return parseInt(size.toString().split('\n').pop()) } measureBuildTime() { const { execSync } = require('child_process') const start = Date.now() execSync('npm run build', { stdio: 'ignore' }) return Date.now() - start } generateMigrationPlan() { const assessment = this.assessCurrentArchitecture() return { phases: this.determinePhases(assessment), timeline: this.estimateTimeline(assessment), risks: this.identifyRisks(assessment), resources: this.calculateResources(assessment) } }}export const migrationAssessment = new MigrationAssessment()2. 渐进式迁移策略阶段一:基础设施准备// phase1-infrastructure.jsconst Phase1Infrastructure = { setupWebpack5() { // 升级到 Webpack 5 const upgradeWebpack = () => { const pkg = JSON.parse(fs.readFileSync('package.json')) // 更新依赖 pkg.devDependencies = { ...pkg.devDependencies, 'webpack': '^5.0.0', 'webpack-cli': '^4.0.0', 'webpack-dev-server': '^4.0.0' } fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)) console.log('✅ Webpack 5 dependencies updated') } // 配置 Module Federation 插件 const configureModuleFederation = () => { const { ModuleFederationPlugin } = require('webpack').container return new ModuleFederationPlugin({ name: 'mainApp', filename: 'remoteEntry.js', exposes: {}, shared: { react: { singleton: true, eager: true }, 'react-dom': { singleton: true, eager: true } } }) } return { upgradeWebpack, configureModuleFederation } }, setupMonorepo() { // 初始化 Monorepo 结构 const initMonorepo = () => { const packagesDir = 'packages' if (!fs.existsSync(packagesDir)) { fs.mkdirSync(packagesDir, { recursive: true }) } // 创建 package.json const rootPkg = { name: 'monorepo', version: '1.0.0', private: true, workspaces: ['packages/*'] } fs.writeFileSync('package.json', JSON.stringify(rootPkg, null, 2)) console.log('✅ Monorepo structure initialized') } return { initMonorepo } }}阶段二:模块拆分// phase2-module-splitting.jsconst Phase2ModuleSplitting = { extractModule(moduleName, sourcePath, targetPath) { // 提取模块到独立包 const extract = () => { // 创建目标目录 if (!fs.existsSync(targetPath)) { fs.mkdirSync(targetPath, { recursive: true }) } // 复制源代码 this.copyDirectory(sourcePath, targetPath) // 创建 package.json const pkg = { name: moduleName, version: '1.0.0', main: 'index.js', dependencies: { react: '^17.0.0', 'react-dom': '^17.0.0' } } fs.writeFileSync( path.join(targetPath, 'package.json'), JSON.stringify(pkg, null, 2) ) // 配置 Module Federation this.configureModuleFederation(moduleName, targetPath) console.log(`✅ Module extracted: ${moduleName}`) } return { extract } }, copyDirectory(source, target) { const files = fs.readdirSync(source) files.forEach(file => { const sourcePath = path.join(source, file) const targetPath = path.join(target, file) if (fs.statSync(sourcePath).isDirectory()) { this.copyDirectory(sourcePath, targetPath) } else { fs.copyFileSync(sourcePath, targetPath) } }) }, configureModuleFederation(moduleName, modulePath) { const config = `const { ModuleFederationPlugin } = require('webpack').containermodule.exports = { plugins: [ new ModuleFederationPlugin({ name: '${moduleName}', filename: 'remoteEntry.js', exposes: { './index': './src/index' }, shared: { react: { singleton: true, eager: true }, 'react-dom': { singleton: true, eager: true } } }) ]}` fs.writeFileSync( path.join(modulePath, 'webpack.config.js'), config ) }}阶段三:集成和测试// phase3-integration.jsconst Phase3Integration = { integrateRemoteModule(hostConfig, remoteConfig) { // 集成远程模块到主应用 const integrate = () => { // 更新主应用的 webpack 配置 const updatedConfig = { ...hostConfig, plugins: hostConfig.plugins.map(plugin => { if (plugin.constructor.name === 'ModuleFederationPlugin') { return new ModuleFederationPlugin({ ...plugin.options, remotes: { ...plugin.options.remotes, [remoteConfig.name]: `${remoteConfig.name}@${remoteConfig.url}` } }) } return plugin }) } return updatedConfig } // 测试远程模块加载 const testRemoteModule = async (moduleName) => { try { const module = await import(`${moduleName}/index`) console.log(`✅ Remote module loaded successfully: ${moduleName}`) return module } catch (error) { console.error(`❌ Failed to load remote module: ${moduleName}`, error) throw error } } return { integrate, testRemoteModule } }, runIntegrationTests() { // 运行集成测试 const testModuleCommunication = async () => { const tests = [ this.testRemoteModuleLoading(), this.testSharedDependencies(), this.testModuleInteractions() ] const results = await Promise.allSettled(tests) const passed = results.filter(r => r.status === 'fulfilled').length const failed = results.filter(r => r.status === 'rejected').length console.log(`Integration tests: ${passed} passed, ${failed} failed`) return { passed, failed, results } } return { testModuleCommunication } }}3. 回滚策略回滚机制:// rollback-strategy.jsclass RollbackStrategy { constructor() { this.snapshots = new Map() this.currentVersion = null } createSnapshot(version) { // 创建快照 const snapshot = { version, timestamp: Date.now(), files: this.captureFiles(), dependencies: this.captureDependencies(), configuration: this.captureConfiguration() } this.snapshots.set(version, snapshot) this.currentVersion = version console.log(`✅ Snapshot created: ${version}`) return snapshot } captureFiles() { const files = {} const captureDir = (dir, prefix = '') => { const items = fs.readdirSync(dir) items.forEach(item => { const fullPath = path.join(dir, item) const relativePath = path.join(prefix, item) if (fs.statSync(fullPath).isDirectory()) { captureDir(fullPath, relativePath) } else { files[relativePath] = fs.readFileSync(fullPath, 'utf8') } }) } captureDir('src') return files } captureDependencies() { const pkg = JSON.parse(fs.readFileSync('package.json')) return { dependencies: pkg.dependencies, devDependencies: pkg.devDependencies } } captureConfiguration() { const configs = {} const configFiles = [ 'webpack.config.js', 'package.json', 'tsconfig.json', '.babelrc' ] configFiles.forEach(file => { if (fs.existsSync(file)) { configs[file] = fs.readFileSync(file, 'utf8') } }) return configs } rollback(version) { // 回滚到指定版本 const snapshot = this.snapshots.get(version) if (!snapshot) { throw new Error(`Snapshot not found: ${version}`) } try { // 恢复文件 this.restoreFiles(snapshot.files) // 恢复依赖 this.restoreDependencies(snapshot.dependencies) // 恢复配置 this.restoreConfiguration(snapshot.configuration) // 重新安装依赖 this.installDependencies() this.currentVersion = version console.log(`✅ Rolled back to version: ${version}`) return true } catch (error) { console.error(`❌ Rollback failed: ${version}`, error) throw error } } restoreFiles(files) { Object.entries(files).forEach(([filePath, content]) => { const fullPath = path.join('src', filePath) const dir = path.dirname(fullPath) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } fs.writeFileSync(fullPath, content) }) } restoreDependencies(dependencies) { const pkg = JSON.parse(fs.readFileSync('package.json')) pkg.dependencies = dependencies.dependencies pkg.devDependencies = dependencies.devDependencies fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)) } restoreConfiguration(configuration) { Object.entries(configuration).forEach(([file, content]) => { fs.writeFileSync(file, content) }) } installDependencies() { const { execSync } = require('child_process') execSync('npm install', { stdio: 'inherit' }) } getAvailableSnapshots() { return Array.from(this.snapshots.keys()) }}export const rollbackStrategy = new RollbackStrategy()4. 监控和验证迁移监控:// migration-monitor.jsclass MigrationMonitor { constructor() { this.metrics = new Map() this.alerts = [] } trackMigrationPhase(phase, status, duration) { const metric = { phase, status, duration, timestamp: Date.now() } this.metrics.set(phase, metric) if (status === 'error') { this.alerts.push({ type: 'migration_error', phase, message: `Migration phase ${phase} failed`, timestamp: Date.now() }) } console.log(`📊 Migration phase: ${phase} - ${status} (${duration}ms)`) } validateMigration() { // 验证迁移结果 const validations = [ this.validateBuild(), this.validateRuntime(), this.validatePerformance(), this.validateDependencies() ] const results = validations.map(validation => validation()) const allPassed = results.every(result => result.passed) return { allPassed, results, summary: this.generateSummary(results) } } validateBuild() { // 验证构建 try { const { execSync } = require('child_process') execSync('npm run build', { stdio: 'ignore' }) return { name: 'Build Validation', passed: true, message: 'Build successful' } } catch (error) { return { name: 'Build Validation', passed: false, message: error.message } } } validateRuntime() { // 验证运行时 return { name: 'Runtime Validation', passed: true, message: 'Runtime validation passed' } } validatePerformance() { // 验证性能 const metrics = this.getPerformanceMetrics() return { name: 'Performance Validation', passed: metrics.loadTime < 3000, message: `Load time: ${metrics.loadTime}ms`, metrics } } validateDependencies() { // 验证依赖 const pkg = JSON.parse(fs.readFileSync('package.json')) const deps = { ...pkg.dependencies, ...pkg.devDependencies } return { name: 'Dependency Validation', passed: Object.keys(deps).length > 0, message: `${Object.keys(deps).length} dependencies found` } } getPerformanceMetrics() { return { loadTime: Math.random() * 2000 + 1000, bundleSize: Math.random() * 500 + 200 } } generateSummary(results) { const passed = results.filter(r => r.passed).length const failed = results.filter(r => !r.passed).length return { total: results.length, passed, failed, successRate: (passed / results.length * 100).toFixed(2) + '%' } }}export const migrationMonitor = new MigrationMonitor()通过以上迁移策略,可以安全、平稳地将现有应用迁移到 Module Federation 架构。
阅读 0·2月19日 17:40

Module Federation 的 shared 配置如何工作?如何解决版本冲突?

Module Federation 的 shared 配置用于管理多个应用之间共享的依赖,避免重复加载相同版本的库。以下是详细说明:基本配置语法:new ModuleFederationPlugin({ shared: { react: { singleton: true }, 'react-dom': { singleton: true }, lodash: { singleton: false } }})关键配置选项:singleton(单例模式)true:确保整个应用只有一个该依赖的实例false:允许多个版本共存适用场景:React、Vue 等需要全局单例的库必须设为 truerequiredVersion(版本要求) shared: { react: { singleton: true, requiredVersion: '^17.0.0' } }指定所需的版本范围如果加载的版本不符合要求,会加载本地版本strictVersion(严格版本)true:严格匹配版本,不满足则报错false:允许版本不匹配(默认)eager(急切加载)true:在初始加载时就加载该依赖,不使用异步加载false:按需异步加载(默认)适用场景:某些库需要在应用启动时就初始化版本冲突解决策略:Module Federation 使用以下策略解决版本冲突:优先使用已加载的版本:如果某个依赖已经被加载,其他应用会复用该版本版本协商:多个应用需要不同版本时,会选择满足所有应用要求的版本降级处理:如果无法满足所有要求,会降级到本地版本实际应用示例:// Remote 应用new ModuleFederationPlugin({ name: 'remoteApp', filename: 'remoteEntry.js', exposes: { './Button': './src/Button' }, shared: { react: { singleton: true, requiredVersion: deps.react, eager: true }, 'react-dom': { singleton: true, requiredVersion: deps['react-dom'] } }})// Host 应用new ModuleFederationPlugin({ name: 'hostApp', remotes: { remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js' }, shared: { react: { singleton: true, requiredVersion: deps.react }, 'react-dom': { singleton: true, requiredVersion: deps['react-dom'] } }})注意事项:共享依赖时,确保所有应用使用相同的版本范围对于有副作用的库,考虑设置 eager: true使用 package.json 中的版本信息作为 requiredVersion
阅读 0·2月19日 16:57

在实际项目中使用 Garfish 时,有哪些最佳实践和开发规范需要遵循?

在实际项目中使用 Garfish 时,需要遵循一定的最佳实践和开发规范,以确保项目的可维护性和可扩展性。项目架构设计1. 主应用设计职责明确:主应用负责路由管理、子应用加载、全局状态管理轻量化:避免在主应用中包含过多业务逻辑统一规范:制定统一的开发规范和代码风格示例:// 主应用入口import Garfish from 'garfish';Garfish.run({ apps: [ { name: 'app1', entry: '//localhost:3001', activeWhen: '/app1', props: { shared: { userInfo: getUserInfo(), theme: getTheme() } } } ], beforeLoad: (app) => { console.log('Loading app:', app.name); }});2. 子应用设计独立部署:每个子应用独立开发和部署生命周期管理:正确实现生命周期钩子资源清理:确保卸载时完全清理资源示例:// 子应用生命周期export async function bootstrap() { console.log('App bootstrap');}export async function mount(props) { const { container, shared } = props; ReactDOM.render(<App shared={shared} />, container);}export async function unmount(props) { const { container } = props; ReactDOM.unmountComponentAtNode(container);}开发规范1. 命名规范应用名称:使用小写字母和连字符,如 user-center路由前缀:与应用名称保持一致组件命名:遵循框架的命名规范样式类名:使用 BEM 或 CSS Modules2. 代码规范统一代码风格:使用 ESLint 和 Prettier类型检查:使用 TypeScript 或 Flow代码审查:建立代码审查机制文档完善:编写清晰的文档和注释3. 版本管理语义化版本:遵循 SemVer 规范版本兼容:确保子应用版本兼容性更新策略:制定版本更新策略回滚机制:准备版本回滚方案团队协作1. 团队分工主应用团队:负责主应用开发和维护子应用团队:负责各自子应用的开发架构团队:负责整体架构设计和技术选型测试团队:负责集成测试和质量保证2. 协作流程需求评审:统一评审跨应用需求接口定义:提前定义应用间接口联调测试:定期进行联调测试发布协调:协调各应用的发布时间3. 沟通机制定期会议:定期召开技术会议文档共享:共享技术文档和设计文档问题反馈:建立问题反馈机制知识分享:组织技术分享会测试策略1. 单元测试测试覆盖率:确保核心逻辑的测试覆盖测试隔离:确保测试之间相互独立Mock 依赖:使用 Mock 隔离外部依赖示例:// 测试生命周期函数describe('SubApp lifecycle', () => { it('should mount correctly', async () => { const container = document.createElement('div'); await mount({ container }); expect(container.innerHTML).not.toBe(''); });});2. 集成测试应用集成:测试主应用和子应用的集成路由测试:测试路由切换和应用加载通信测试:测试应用间通信机制示例:// 测试应用加载describe('App loading', () => { it('should load sub-app when route changes', async () => { await Garfish.router.push('/app1'); expect(document.querySelector('#app1')).toBeTruthy(); });});3. E2E 测试用户流程:测试完整的用户操作流程跨应用场景:测试跨应用的用户场景性能测试:测试应用性能指标示例:// E2E 测试describe('User flow', () => { it('should complete user journey across apps', async () => { await page.goto('/'); await page.click('#app1-link'); await page.waitForSelector('#app1-content'); await page.click('#navigate-to-app2'); await page.waitForSelector('#app2-content'); });});部署策略1. 独立部署CI/CD 流程:为每个应用建立独立的 CI/CD 流程环境管理:管理开发、测试、生产环境发布策略:采用灰度发布或蓝绿部署回滚机制:快速回滚到稳定版本2. 资源管理CDN 加速:使用 CDN 加速静态资源缓存策略:合理设置资源缓存版本控制:使用版本号管理资源资源压缩:压缩 JavaScript、CSS、图片3. 监控告警性能监控:监控应用性能指标错误监控:监控应用错误和异常用户行为:监控用户行为和使用情况告警机制:建立告警机制及时发现问题常见问题解决1. 样式冲突问题:不同子应用样式相互影响解决:使用样式隔离方案,如 Shadow DOM 或 CSS 作用域2. 状态共享困难问题:跨应用状态共享复杂解决:使用 Garfish 的共享状态机制或事件总线3. 性能问题问题:应用加载慢或运行卡顿解决:优化加载策略,使用代码分割和懒加载4. 调试困难问题:微前端调试复杂解决:使用 Garfish 提供的调试工具和浏览器 DevTools通过遵循这些最佳实践,可以构建高质量、可维护的微前端应用。
阅读 0·2月19日 16:57

Yew 和 React 有哪些主要区别,如何选择适合的框架?

Yew 与 React 的对比分析Yew 和 React 都是现代前端框架,但它们在语言、运行时和性能方面有显著差异。核心差异对比| 特性 | Yew | React ||------|-----|-------|| 编程语言 | Rust | JavaScript/TypeScript || 运行时 | WebAssembly (Wasm) | JavaScript || 类型系统 | 静态强类型 | 动态类型(可选 TypeScript) || 编译时 | 编译到 Wasm | JIT 编译 || 性能 | 接近原生 | 依赖 JS 引擎优化 || 包大小 | 较大(包含 Wasm 运行时) | 较小 || 学习曲线 | 较陡(需要学习 Rust) | 较平缓 || 生态系统 | 较新,较小 | 成熟,庞大 || 开发工具 | 有限 | 丰富(DevTools, ESLint 等) |代码示例对比组件定义Yew:use yew::prelude::*;#[function_component(HelloWorld)]fn hello_world() -> Html { html! { <div class="greeting"> <h1>{ "Hello, World!" }</h1> </div> }}React:function HelloWorld() { return ( <div className="greeting"> <h1>Hello, World!</h1> </div> );}状态管理Yew:#[function_component(Counter)]fn counter() -> Html { let counter = use_state(|| 0); let onclick = { let counter = counter.clone(); Callback::from(move |_| counter.set(*counter + 1)) }; html! { <div> <button onclick={onclick}>{ "Increment" }</button> <p>{ *counter }</p> </div> }}React:function Counter() { const [counter, setCounter] = useState(0); return ( <div> <button onClick={() => setCounter(counter + 1)}>Increment</button> <p>{counter}</p> </div> );}Props 传递Yew:#[derive(Properties, PartialEq)]pub struct ButtonProps { pub label: String, #[prop_or_default] pub disabled: bool, pub onclick: Callback<MouseEvent>,}#[function_component(Button)]fn button(props: &ButtonProps) -> Html { html! { <button disabled={props.disabled} onclick={props.onclick.clone()} > { &props.label } </button> }}React:function Button({ label, disabled = false, onClick }) { return ( <button disabled={disabled} onClick={onClick}> {label} </button> );}性能对比执行性能Yew 优势:编译为 WebAssembly,执行速度接近原生代码Rust 的零成本抽象和优化更好的内存管理和安全性适合计算密集型任务React 优势:JavaScript 引擎持续优化更小的初始加载时间(无需 Wasm 运行时)更快的热重载和开发体验基准测试场景| 场景 | Yew | React ||------|-----|-------|| 简单列表渲染 | 相当 | 相当 || 复杂计算 | 更快 | 较慢 || 大型应用启动 | 较慢 | 较快 || DOM 操作 | 相当 | 相当 |开发体验对比Yew 开发体验优点:类型安全,编译时错误检测更好的代码可维护性Rust 的工具链(cargo, clippy)强大的并发支持缺点:编译时间较长Wasm 调试相对困难生态系统较小学习曲线陡峭React 开发体验优点:快速开发迭代丰富的生态系统和库成熟的开发工具庞大的社区支持缺点:运行时错误类型安全需要 TypeScript性能优化需要更多手动工作适用场景选择 Yew 的场景高性能要求:需要处理大量计算或数据的应用安全敏感:金融、医疗等对安全性要求高的领域Rust 团队:团队熟悉 Rust 语言长期维护:大型项目需要长期维护和稳定性WebAssembly 优势:需要充分利用 Wasm 的能力选择 React 的场景快速原型:需要快速开发和迭代团队熟悉 JS:团队主要使用 JavaScript/TypeScript生态系统需求:需要使用丰富的第三方库开发效率:优先考虑开发速度而非运行时性能传统 Web 应用:典型的 CRUD 应用迁移考虑从 React 迁移到 Yew挑战:需要学习 Rust 语言重写所有组件和逻辑适配不同的 API 和模式生态系统差异收益:更好的性能类型安全更少的运行时错误从 Yew 迁移到 React挑战:失去类型安全(除非使用 TypeScript)性能可能下降需要适应不同的开发模式收益:更快的开发速度更丰富的生态系统更好的开发工具未来展望Yew:WebAssembly 生态系统持续发展Rust 前端社区增长工具链和调试工具改进React:Server Components 和 RSC更好的性能优化继续主导前端市场结论选择 Yew 还是 React 取决于项目需求、团队技能和长期目标。Yew 提供了更好的性能和类型安全,但需要更高的学习成本;React 提供了更好的开发体验和生态系统,但可能在性能和类型安全方面有所妥协。
阅读 0·2月19日 16:25

Yew 如何与 WebAssembly 集成,有哪些性能优化技巧?

Yew 与 WebAssembly 的集成Yew 框架的核心优势在于它与 WebAssembly (Wasm) 的深度集成,这使得用 Rust 编写的前端应用能够在浏览器中高效运行。WebAssembly 基础什么是 WebAssembly?WebAssembly (Wasm) 是一种低级类汇编语言,可以在现代 Web 浏览器中运行。它设计为与 JavaScript 并存,允许开发者使用 Rust、C++、Go 等语言编写高性能的 Web 应用。主要特性:二进制格式,体积小、加载快接近原生代码的执行性能与 JavaScript 互操作安全的内存模型跨平台兼容性Yew 的 Wasm 架构编译流程Rust 源代码 ↓Cargo 编译 ↓wasm-pack 打包 ↓WebAssembly 二进制文件 ↓浏览器加载执行项目配置Cargo.toml:[package]name = "yew-app"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "rlib"][dependencies]yew = { version = "0.21", features = ["csr"] }wasm-bindgen = "0.2"wasm-bindgen-futures = "0.4"web-sys = { version = "0.3", features = [ "Window", "Document", "Element", "HtmlElement", "console",] }[dev-dependencies]wasm-bindgen-test = "0.3"index.html:<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Yew Wasm App</title> <link data-trunk rel="css" href="index.css"></head><body> <div id="app"></div> <script type="module"> import init from './wasm/yew_app.js'; init().then(() => { console.log('Wasm module loaded'); }); </script></body></html>Wasm 与 JavaScript 互操作1. 调用 JavaScript 函数use wasm_bindgen::prelude::*;#[wasm_bindgen]extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); #[wasm_bindgen(js_namespace = window, js_name = alert)] fn alert(s: &str);}#[function_component(JSInterop)]fn js_interop() -> Html { let onclick = Callback::from(|_| { log("Button clicked from Rust!"); alert("Hello from WebAssembly!"); }); html! { <button onclick={onclick}> { "Call JavaScript" } </button> }}2. 从 JavaScript 调用 Rust 函数use wasm_bindgen::prelude::*;#[wasm_bindgen]pub fn greet(name: &str) -> String { format!("Hello, {}!", name)}#[wasm_bindgen]pub fn add(a: i32, b: i32) -> i32 { a + b}#[wasm_bindgen]pub struct Calculator { value: i32,}#[wasm_bindgen]impl Calculator { #[wasm_bindgen(constructor)] pub fn new(initial: i32) -> Calculator { Calculator { value: initial } } #[wasm_bindgen] pub fn add(&mut self, amount: i32) { self.value += amount; } #[wasm_bindgen] pub fn get_value(&self) -> i32 { self.value }}在 JavaScript 中使用:// 调用简单函数const greeting = wasm.greet("World");console.log(greeting); // "Hello, World!"// 调用计算函数const result = wasm.add(5, 3);console.log(result); // 8// 使用 Rust 结构体const calc = new wasm.Calculator(10);calc.add(5);console.log(calc.get_value()); // 153. 复杂数据类型传递use serde::{Deserialize, Serialize};use wasm_bindgen::prelude::*;#[derive(Serialize, Deserialize)]pub struct UserData { pub name: String, pub age: u32, pub email: String,}#[wasm_bindgen]pub fn process_user_data(json: &str) -> Result<String, JsValue> { let user: UserData = serde_json::from_str(json) .map_err(|e| JsValue::from_str(&e.to_string()))?; let processed = format!( "User: {} ({} years old) - {}", user.name, user.age, user.email ); Ok(processed)}#[wasm_bindgen]pub fn create_user_data(name: String, age: u32, email: String) -> JsValue { let user = UserData { name, age, email }; serde_wasm_bindgen::to_value(&user).unwrap()}性能优化1. 减少序列化开销// 不好的做法:频繁序列化#[wasm_bindgen]pub fn process_large_data(data: JsValue) -> JsValue { let parsed: Vec<i32> = serde_wasm_bindgen::from_value(data).unwrap(); let result: Vec<i32> = parsed.iter().map(|x| x * 2).collect(); serde_wasm_bindgen::to_value(&result).unwrap()}// 好的做法:使用内存共享use wasm_bindgen::JsCast;#[wasm_bindgen]pub fn process_array_in_place(array: &js_sys::Uint32Array) { for i in 0..array.length() { let value = array.get(i); array.set(i, value * 2); }}2. 使用 Web Workersuse wasm_bindgen::prelude::*;use web_sys::{DedicatedWorkerGlobalScope, WorkerOptions};#[wasm_bindgen]pub fn start_worker() { let worker = web_sys::Worker::new("worker.js").unwrap(); worker.set_onmessage(Some(Closure::wrap(Box::new(|event: MessageEvent| { let data = event.data(); web_sys::console::log_1(&data); }) as Box<dyn FnMut(_)>).into_js_value().unchecked_ref()));}// worker.jsself.importScripts('wasm/yew_app.js');wasm.run_worker();3. 优化 Wasm 包大小# Cargo.toml[profile.release]opt-level = "z" # 优化大小lto = true # 链接时优化codegen-units = 1panic = "abort" # 减少恐慌处理代码[dependencies]wee_alloc = { version = "0.4", optional = true }[features]default = ["wee_alloc"]// lib.rs#[cfg(feature = "wee_alloc")]#[global_allocator]static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;调试 Wasm 代码1. 使用浏览器开发工具use web_sys::console;#[function_component(DebugExample)]fn debug_example() -> Html { let onclick = Callback::from(|_| { console::log_1(&"Debug message".into()); console::error_1(&"Error message".into()); console::warn_1(&"Warning message".into()); // 格式化日志 console::log_2( &"Value:".into(), &42.into() ); }); html! { <button onclick={onclick}> { "Log to Console" } </button> }}2. 使用 wasm-pack 测试# 运行测试wasm-pack test --firefox --headless# 运行特定测试wasm-pack test --firefox --headless test_name实际应用场景1. 图像处理use wasm_bindgen::prelude::*;#[wasm_bindgen]pub fn process_image(data: &[u8], width: u32, height: u32) -> Vec<u8> { let mut result = data.to_vec(); for i in (0..result.len()).step_by(4) { // 简单的灰度转换 let r = result[i] as f32; let g = result[i + 1] as f32; let b = result[i + 2] as f32; let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8; result[i] = gray; result[i + 1] = gray; result[i + 2] = gray; } result}2. 数据加密use sha2::{Sha256, Digest};use wasm_bindgen::prelude::*;#[wasm_bindgen]pub fn hash_data(data: &str) -> String { let mut hasher = Sha256::new(); hasher.update(data.as_bytes()); let result = hasher.finalize(); format!("{:x}", result)}3. 复杂计算use wasm_bindgen::prelude::*;#[wasm_bindgen]pub fn fibonacci(n: u64) -> u64 { match n { 0 => 0, 1 => 1, _ => { let mut a = 0u64; let mut b = 1u64; for _ in 2..=n { let temp = a + b; a = b; b = temp; } b } }}#[wasm_bindgen]pub fn calculate_primes(limit: u32) -> Vec<u32> { let mut primes = Vec::new(); let mut is_prime = vec![true; (limit + 1) as usize]; for i in 2..=limit { if is_prime[i as usize] { primes.push(i); let mut j = i * i; while j <= limit { is_prime[j as usize] = false; j += i; } } } primes}最佳实践最小化 Wasm-JS 边界:减少跨边界调用,批量处理数据使用类型安全的绑定:利用 wasm-bindgen 的类型系统优化包大小:使用 wee_alloc、LTO 和适当的优化级别错误处理:使用 Result 类型正确处理错误内存管理:注意 Wasm 的线性内存限制测试:使用 wasm-bindgen-test 进行单元测试性能对比| 操作 | JavaScript | WebAssembly (Yew) | 提升 ||------|-----------|-------------------|------|| 简单计算 | 100ms | 20ms | 5x || 图像处理 | 500ms | 50ms | 10x || 数据加密 | 200ms | 30ms | 6.7x || 复杂算法 | 1000ms | 150ms | 6.7x |总结Yew 与 WebAssembly 的集成提供了强大的性能优势,特别适合计算密集型任务。通过合理使用 Wasm-JS 互操作、优化包大小和利用 Rust 的类型系统,可以构建高性能、安全的前端应用。
阅读 0·2月19日 16:24

Yew 中如何使用 Hooks,有哪些常用的 Hook 函数?

Yew 中的 Hooks 使用详解Yew 从 0.19 版本开始支持 Hooks API,提供了类似 React Hooks 的功能,使组件编写更加简洁和函数式。基础 Hooks1. use_stateuse_state 用于在函数组件中管理状态,类似于 React 的 useState。#[function_component(Counter)]fn counter() -> Html { let counter = use_state(|| 0); let increment = { let counter = counter.clone(); Callback::from(move |_| counter.set(*counter + 1)) }; let decrement = { let counter = counter.clone(); Callback::from(move |_| counter.set(*counter - 1)) }; html! { <div> <button onclick={decrement}>{ "-" }</button> <span>{ *counter }</span> <button onclick={increment}>{ "+" }</button> </div> }}使用技巧:初始值通过闭包提供,只在首次渲染时计算返回的 UseStateHandle 可以克隆以在闭包中使用使用 set() 方法更新状态使用 * 解引用获取当前值2. use_reduceruse_reducer 用于复杂的状态逻辑,类似于 React 的 useReducer。#[derive(Clone, PartialEq)]pub enum CounterAction { Increment, Decrement, IncrementBy(i32), Reset,}fn counter_reducer(state: i32, action: CounterAction) -> i32 { match action { CounterAction::Increment => state + 1, CounterAction::Decrement => state - 1, CounterAction::IncrementBy(amount) => state + amount, CounterAction::Reset => 0, }}#[function_component(ReducerCounter)]fn reducer_counter() -> Html { let (counter, dispatch) = use_reducer(counter_reducer, 0); let increment = dispatch.reform(|_| CounterAction::Increment); let decrement = dispatch.reform(|_| CounterAction::Decrement); let increment_by_5 = dispatch.reform(|_| CounterAction::IncrementBy(5)); let reset = dispatch.reform(|_| CounterAction::Reset); html! { <div> <button onclick={decrement}>{ "-" }</button> <span>{ *counter }</span> <button onclick={increment}>{ "+" }</button> <button onclick={increment_by_5}>{ "+5" }</button> <button onclick={reset}>{ "Reset" }</button> </div> }}3. use_effectuse_effect 用于处理副作用,类似于 React 的 useEffect。#[function_component(EffectExample)]fn effect_example() -> Html { let count = use_state(|| 0); // 基本效果 use_effect(move || { web_sys::console::log_1(&"Component mounted".into()); // 清理函数 || { web_sys::console::log_1(&"Component unmounted".into()); } }); // 带依赖的效果 let count_effect = { let count = count.clone(); use_effect(move || { web_sys::console::log_2( &"Count changed:".into(), &(*count).into() ); || {} }) }; let onclick = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; html! { <div> <button onclick={onclick}>{ "Increment" }</button> <p>{ *count }</p> </div> }}4. use_refuse_ref 用于存储可变值,类似于 React 的 useRef。#[function_component(RefExample)]fn ref_example() -> Html { let input_ref = use_node_ref(); let count_ref = use_ref(|| 0); let onfocus = Callback::from(|_| { web_sys::console::log_1(&"Input focused".into()); }); let onclick = Callback::from({ let input_ref = input_ref.clone(); let count_ref = count_ref.clone(); move |_| { if let Some(input) = input_ref.cast::<HtmlInputElement>() { web_sys::console::log_2( &"Input value:".into(), &input.value().into() ); } *count_ref.borrow_mut() += 1; web_sys::console::log_2( &"Click count:".into(), &(*count_ref.borrow()).into() ); } }); html! { <div> <input ref={input_ref} type="text" placeholder="Focus me" onfocus={onfocus} /> <button onclick={onclick}>{ "Log Value" }</button> </div> }}高级 Hooks5. use_contextuse_context 用于消费 Context,类似于 React 的 useContext。#[derive(Clone, PartialEq, Default)]pub struct ThemeContext { pub is_dark: bool,}#[function_component(ThemeProvider)]fn theme_provider() -> Html { let is_dark = use_state(|| false); let context = ThemeContext { is_dark: *is_dark, }; html! { <ContextProvider<ThemeContext> context={context}> <ThemedComponent /> </ContextProvider<ThemeContext>> }}#[function_component(ThemedComponent)]fn themed_component() -> Html { let theme = use_context::<ThemeContext>().unwrap(); let class = if theme.is_dark { "dark-theme" } else { "light-theme" }; html! { <div class={class}> { "Themed content" } </div> }}6. use_memouse_memo 用于记忆化计算结果,类似于 React 的 useMemo。#[function_component(MemoExample)]fn memo_example() -> Html { let count = use_state(|| 0); let other = use_state(|| 0); // 记忆化计算 let expensive_value = use_memo( (*count, *other), |(count, other)| { web_sys::console::log_1(&"Computing expensive value".into()); count * 2 + other * 3 } ); let increment_count = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1)) }; let increment_other = { let other = other.clone(); Callback::from(move |_| other.set(*other + 1)) }; html! { <div> <button onclick={increment_count}>{ "Count: " }{ *count }</button> <button onclick={increment_other}>{ "Other: " }{ *other }</button> <p>{ "Expensive value: " }{ **expensive_value }</p> </div> }}7. use_callbackuse_callback 用于记忆化回调函数,类似于 React 的 useCallback。#[function_component(CallbackExample)]fn callback_example() -> Html { let count = use_state(|| 0); // 记忆化回调 let increment = use_callback(count.clone(), |count, _| { count.set(*count + 1); }); // 带参数的记忆化回调 let add_value = use_callback(count.clone(), |count, value: i32| { count.set(*count + value); }); html! { <div> <button onclick={increment}>{ "Increment" }</button> <button onclick={Callback::from(move |_| add_value.emit(5))}> { "Add 5" } </button> <p>{ *count }</p> </div> }}自定义 Hooks自定义 Hooks 是复用逻辑的强大方式。// 自定义 Hook:使用本地存储pub fn use_local_storage<T>(key: &str, initial_value: T) -> UseStateHandle<T>where T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned + 'static,{ let state = use_state(|| { if let Ok(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { if let Ok(Some(value)) = storage.get_item(key) { if let Ok(deserialized) = serde_json::from_str::<T>(&value) { return deserialized; } } } } initial_value }); let key = key.to_string(); let state_effect = state.clone(); use_effect(move || { if let Ok(window) = web_sys::window() { if let Ok(Some(storage)) = window.local_storage() { if let Ok(value) = serde_json::to_string(&*state_effect) { let _ = storage.set_item(&key, &value); } } } || {} }); state}// 使用自定义 Hook#[function_component(StorageExample)]fn storage_example() -> Html { let name = use_local_storage("username", String::new()); let oninput = { let name = name.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); name.set(input.value()); }) }; html! { <div> <input type="text" value={(*name).clone()} oninput={oninput} placeholder="Enter your name" /> <p>{ "Your name: " }{ &*name }</p> </div> }}Hooks 规则使用 Hooks 时必须遵循以下规则:只在函数组件或自定义 Hook 中调用 Hooks // ✅ 正确 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); html! { <div>{ *count }</div> } } // ❌ 错误 fn regular_function() { let count = use_state(|| 0); // 不能在普通函数中调用 }只在顶层调用 Hooks // ✅ 正确 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); let name = use_state(|| String::new()); html! { <div>{ *count }</div> } } // ❌ 错误 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); if *count > 0 { let name = use_state(|| String::new()); // 不能在条件中调用 } html! { <div>{ *count }</div> } }保持 Hooks 调用顺序一致 // ✅ 正确:每次渲染都按相同顺序调用 #[function_component(MyComponent)] fn my_component() -> Html { let count = use_state(|| 0); let name = use_state(|| String::new()); html! { <div>{ *count }</div> } }性能优化技巧1. 使用 use_memo 避免重复计算let expensive_result = use_memo( (*input1, *input2), |(a, b)| { // 只在 input1 或 input2 变化时重新计算 complex_calculation(a, b) });2. 使用 use_callback 稳定回调引用let handle_click = use_callback( dependency.clone(), |dep, _| { // 只在 dependency 变化时创建新的回调 do_something(dep) });3. 合理使用 use_ref 避免不必要的状态更新let ref_count = use_ref(|| 0);// 不会触发重新渲染*ref_count.borrow_mut() += 1;
阅读 0·2月19日 16:24

Yew 中如何处理事件,支持哪些类型的事件?

Yew 中的事件处理机制Yew 提供了强大的事件处理机制,类似于 React,但使用 Rust 的类型系统来确保类型安全。基本事件处理1. 点击事件#[function_component(ClickHandler)]fn click_handler() -> Html { let onclick = Callback::from(|_| { web_sys::console::log_1(&"Button clicked!".into()); }); html! { <button onclick={onclick}> { "Click Me" } </button> }}2. 输入事件#[function_component(InputHandler)]fn input_handler() -> Html { let value = use_state(|| String::new()); let oninput = { let value = value.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); value.set(input.value()); }) }; html! { <div> <input type="text" value={(*value).clone()} oninput={oninput} placeholder="Type something..." /> <p>{ "You typed: " }{ &*value }</p> </div> }}3. 表单提交事件#[function_component(FormHandler)]fn form_handler() -> Html { let username = use_state(|| String::new()); let password = use_state(|| String::new()); let onsubmit = { let username = username.clone(); let password = password.clone(); Callback::from(move |e: SubmitEvent| { e.prevent_default(); web_sys::console::log_2( &"Username:".into(), &(*username).clone().into() ); }) }; let onusername_input = { let username = username.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); username.set(input.value()); }) }; let onpassword_input = { let password = password.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); password.set(input.value()); }) }; html! { <form onsubmit={onsubmit}> <div> <label>{ "Username:" }</label> <input type="text" value={(*username).clone()} oninput={onusername_input} /> </div> <div> <label>{ "Password:" }</label> <input type="password" value={(*password).clone()} oninput={onpassword_input} /> </div> <button type="submit">{ "Submit" }</button> </form> }}事件类型Yew 支持多种事件类型,每种类型都有对应的 Rust 类型:| 事件类型 | Rust 类型 | 常见用途 ||---------|-----------|---------|| onclick | MouseEvent | 鼠标点击 || ondblclick | MouseEvent | 鼠标双击 || onmouseover | MouseEvent | 鼠标悬停 || onmouseout | MouseEvent | 鼠标移出 || oninput | InputEvent | 输入变化 || onchange | Event | 值改变 || onsubmit | SubmitEvent | 表单提交 || onkeydown | KeyboardEvent | 键盘按下 || onkeyup | KeyboardEvent | 键盘抬起 || onfocus | FocusEvent | 获得焦点 || onblur | FocusEvent | 失去焦点 |高级事件处理1. 事件冒泡和捕获#[function_component(EventBubbling)]fn event_bubbling() -> Html { let parent_click = Callback::from(|e: MouseEvent| { web_sys::console::log_1(&"Parent clicked".into()); e.stop_propagation(); }); let child_click = Callback::from(|_| { web_sys::console::log_1(&"Child clicked".into()); }); html! { <div onclick={parent_click} style="padding: 20px; background: lightblue;"> { "Parent" } <div onclick={child_click} style="padding: 10px; background: lightgreen;"> { "Child" } </div> </div> }}2. 自定义事件#[function_component(CustomEvent)]fn custom_event() -> Html { let on_custom = Callback::from(|data: String| { web_sys::console::log_1(&format!("Custom event received: {}", data).into()); }); html! { <ChildComponent on_custom={on_custom} /> }}#[derive(Properties, PartialEq)]pub struct ChildProps { pub on_custom: Callback<String>,}#[function_component(ChildComponent)]fn child_component(props: &ChildProps) -> Html { let trigger_custom = { let on_custom = props.on_custom.clone(); Callback::from(move |_| { on_custom.emit("Custom data from child".to_string()); }) }; html! { <button onclick={trigger_custom}> { "Trigger Custom Event" } </button> }}3. 防抖和节流use std::rc::Rc;use std::cell::RefCell;use std::time::Duration;#[function_component(DebouncedInput)]fn debounced_input() -> Html { let value = use_state(|| String::new()); let debounced_value = use_state(|| String::new()); let timeout_handle = use_mut_ref(|| None::<gloo_timers::callback::Timeout>); let oninput = { let value = value.clone(); let debounced_value = debounced_value.clone(); let timeout_handle = timeout_handle.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); let new_value = input.value(); value.set(new_value.clone()); // 清除之前的定时器 if let Some(handle) = timeout_handle.borrow_mut().take() { handle.cancel(); } // 设置新的防抖定时器 let debounced_value = debounced_value.clone(); let timeout = gloo_timers::callback::Timeout::new(300, move || { debounced_value.set(new_value); }); *timeout_handle.borrow_mut() = Some(timeout); }) }; html! { <div> <input type="text" value={(*value).clone()} oninput={oninput} placeholder="Type with debounce..." /> <p>{ "Debounced value: " }{ &*debounced_value }</p> </div> }}事件处理最佳实践1. 使用 Callback::from 简化代码// 简单事件处理let onclick = Callback::from(|_| { web_sys::console::log_1(&"Clicked".into());});// 带状态的事件处理let onclick = { let count = count.clone(); Callback::from(move |_| count.set(*count + 1))};2. 避免不必要的克隆// 不好的做法:每次渲染都克隆let onclick = Callback::from({ let value = value.clone(); move |_| { // 使用 value }});// 好的做法:使用 Rc 或引用let onclick = Callback::from({ let value = Rc::clone(&value); move |_| { // 使用 value }});3. 类型安全的事件处理// 使用具体的事件类型let oninput = Callback::from(|e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); let value = input.value(); // 处理输入值});// 而不是使用通用的事件类型let oninput = Callback::from(|e: Event| { let target = e.target().unwrap(); let input: HtmlInputElement = target.unchecked_into(); // 处理输入值});常见问题1. 如何阻止默认行为?let onsubmit = Callback::from(|e: SubmitEvent| { e.prevent_default(); // 处理表单提交});2. 如何阻止事件冒泡?let onclick = Callback::from(|e: MouseEvent| { e.stop_propagation(); // 处理点击});3. 如何获取事件目标?let onclick = Callback::from(|e: MouseEvent| { let target = e.target().unwrap(); let element: HtmlElement = target.unchecked_into(); // 使用元素});性能优化1. 使用 use_callback 避免重复创建#[function_component(OptimizedHandler)]fn optimized_handler() -> Html { let count = use_state(|| 0); let onclick = use_callback(count.clone(), |count, _| { count.set(*count + 1); }); html! { <button onclick={onclick}> { "Click me: " }{ *count } </button> }}2. 事件委托#[function_component(EventDelegation)]fn event_delegation() -> Html { let items = use_state(|| vec!["Item 1", "Item 2", "Item 3"]); let onclick = Callback::from(|e: MouseEvent| { let target = e.target().unwrap(); if let Some(element) = target.dyn_ref::<HtmlElement>() { if let Some(text) = element.text_content() { web_sys::console::log_1(&format!("Clicked: {}", text).into()); } } }); html! { <div onclick={onclick}> { items.iter().map(|item| { html! { <div key={item.to_string()}> { item } </div> } }).collect::<Html>() } </div> }}
阅读 0·2月19日 16:24

Yew 中如何处理异步数据,有哪些常用的异步处理模式?

Yew 中的异步数据处理Yew 提供了强大的异步数据处理能力,通过 wasm-bindgen-futures 和 Rust 的 async/await 语法,可以优雅地处理异步操作。基础异步操作1. 使用 send_future 处理异步任务use yew::prelude::*;use wasm_bindgen_futures::spawn_local;#[function_component(AsyncExample)]fn async_example() -> Html { let data = use_state(|| String::new()); let loading = use_state(|| false); let fetch_data = { let data = data.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { // 模拟异步操作 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; data.set("Async data loaded!".to_string()); loading.set(false); }); }) }; html! { <div> <button onclick={fetch_data} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Data" } } </button> <p>{ &*data }</p> </div> }}2. 使用 use_future Hookuse yew::prelude::*;use yew_hooks::prelude::*;#[function_component(UseFutureExample)]fn use_future_example() -> Html { let state = use_future(|| async { // 模拟 API 调用 gloo_timers::future::sleep(std::time::Duration::from_secs(2)).await; "Data from future!".to_string() }); html! { <div> { match &*state { UseFutureState::Pending => html! { <p>{ "Loading..." }</p> }, UseFutureState::Ready(data) => html! { <p>{ data }</p> }, UseFutureState::Failed(_) => html! { <p>{ "Error loading data" }</p> }, }} </div> }}HTTP 请求处理1. 使用 reqwest 进行 HTTP 请求[dependencies]reqwest = { version = "0.11", features = ["json"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"use serde::{Deserialize, Serialize};use yew::prelude::*;#[derive(Debug, Serialize, Deserialize, Clone)]pub struct User { pub id: u32, pub name: String, pub email: String,}#[function_component(UserFetcher)]fn user_fetcher() -> Html { let user = use_state(|| None::<User>); let loading = use_state(|| false); let error = use_state(|| None::<String>); let fetch_user = { let user = user.clone(); let loading = loading.clone(); let error = error.clone(); Callback::from(move |_| { loading.set(true); error.set(None); spawn_local(async move { let client = reqwest::Client::new(); match client .get("https://jsonplaceholder.typicode.com/users/1") .send() .await { Ok(response) => { match response.json::<User>().await { Ok(data) => { user.set(Some(data)); } Err(e) => { error.set(Some(format!("Parse error: {}", e))); } } } Err(e) => { error.set(Some(format!("Request error: {}", e))); } } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_user} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch User" } } </button> { if let Some(ref err) = *error { html! { <p class="error">{ err }</p> } } else if let Some(ref u) = *user { html! { <div class="user-card"> <h2>{ &u.name }</h2> <p>{ "ID: " }{ u.id }</p> <p>{ "Email: " }{ &u.email }</p> </div> } } else { html! { <p>{ "No user data" }</p> }} </div> }}2. 使用 gloo-net 进行 HTTP 请求[dependencies]gloo-net = { version = "0.4", features = ["http", "fetch"] }serde = { version = "1.0", features = ["derive"] }use gloo_net::http::Request;use serde::{Deserialize, Serialize};use yew::prelude::*;#[derive(Debug, Serialize, Deserialize, Clone)]pub struct Post { pub id: u32, pub title: String, pub body: String,}#[function_component(PostFetcher)]fn post_fetcher() -> Html { let posts = use_state(|| Vec::<Post>::new()); let loading = use_state(|| false); let fetch_posts = { let posts = posts.clone(); let loading = loading.clone(); Callback::from(move |_| { loading.set(true); spawn_local(async move { match Request::get("https://jsonplaceholder.typicode.com/posts") .send() .await { Ok(response) => { if response.ok() { match response.json::<Vec<Post>>().await { Ok(data) => { posts.set(data); } Err(e) => { web_sys::console::error_1(&format!("Parse error: {}", e).into()); } } } } Err(e) => { web_sys::console::error_1(&format!("Request error: {}", e).into()); } } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_posts} disabled={*loading}> { if *loading { "Loading..." } else { "Fetch Posts" } } </button> <div class="posts"> { posts.iter().map(|post| { html! { <div key={post.id} class="post-card"> <h3>{ &post.title }</h3> <p>{ &post.body }</p> </div> } }).collect::<Html>() } </div> </div> }}WebSocket 通信1. 使用 gloo-net WebSocket[dependencies]gloo-net = { version = "0.4", features = ["websocket"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"use gloo_net::websocket::{futures::WebSocket, Message, WebSocketError};use serde::{Deserialize, Serialize};use yew::prelude::*;#[derive(Debug, Serialize, Deserialize, Clone)]pub struct ChatMessage { pub id: String, pub text: String, pub timestamp: u64,}#[function_component(ChatApp)]fn chat_app() -> Html { let messages = use_state(|| Vec::<ChatMessage>::new()); let input_value = use_state(|| String::new()); let connected = use_state(|| false); let connect = { let messages = messages.clone(); let connected = connected.clone(); Callback::from(move |_| { let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); let onmessage = { let messages = messages.clone(); Callback::from(move |msg: Message| { if let Message::Text(text) = msg { if let Ok(chat_msg) = serde_json::from_str::<ChatMessage>(&text) { let mut msgs = (*messages).clone(); msgs.push(chat_msg); messages.set(msgs); } } }) }; ws.set_onmessage(onmessage); connected.set(true); }) }; let send_message = { let input_value = input_value.clone(); let messages = messages.clone(); Callback::from(move |_| { if !input_value.is_empty() { let chat_msg = ChatMessage { id: uuid::Uuid::new_v4().to_string(), text: (*input_value).clone(), timestamp: chrono::Utc::now().timestamp() as u64, }; let mut msgs = (*messages).clone(); msgs.push(chat_msg.clone()); messages.set(msgs); input_value.set(String::new()); } }) }; let oninput = { let input_value = input_value.clone(); Callback::from(move |e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); input_value.set(input.value()); }) }; html! { <div class="chat-app"> <div class="chat-header"> <h2>{ "Chat Room" }</h2> <button onclick={connect} disabled={*connected}> { if *connected { "Connected" } else { "Connect" } } </button> </div> <div class="messages"> { messages.iter().map(|msg| { html! { <div key={msg.id.clone()} class="message"> <span class="timestamp">{ msg.timestamp }</span> <span class="text">{ &msg.text }</span> </div> } }).collect::<Html>() } </div> <div class="input-area"> <input type="text" value={(*input_value).clone()} oninput={oninput} placeholder="Type a message..." /> <button onclick={send_message} disabled={input_value.is_empty()}> { "Send" } </button> </div> </div> }}错误处理和重试机制1. 实现重试逻辑use yew::prelude::*;#[function_component(RetryExample)]fn retry_example() -> Html { let data = use_state(|| None::<String>); let loading = use_state(|| false); let error = use_state(|| None::<String>); let retry_count = use_state(|| 0); let fetch_with_retry = { let data = data.clone(); let loading = loading.clone(); let error = error.clone(); let retry_count = retry_count.clone(); Callback::from(move |_| { loading.set(true); error.set(None); retry_count.set(*retry_count + 1); spawn_local(async move { let max_retries = 3; let mut attempt = 0; let mut result: Option<String> = None; while attempt < max_retries { attempt += 1; // 模拟可能失败的请求 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await; // 模拟 50% 失败率 if rand::random::<bool>() { result = Some(format!("Success on attempt {}", attempt)); break; } } if let Some(value) = result { data.set(Some(value)); error.set(None); } else { error.set(Some("Max retries exceeded".to_string())); } loading.set(false); }); }) }; html! { <div> <button onclick={fetch_with_retry} disabled={*loading}> { if *loading { "Retrying..." } else { "Fetch with Retry" } } </button> <p>{ "Attempts: " }{ *retry_count }</p> { if let Some(ref err) = *error { html! { <p class="error">{ err }</p> } } else if let Some(ref value) = *data { html! { <p class="success">{ value }</p> } }} </div> }}2. 使用 use_async Hookuse yew::prelude::*;use yew_hooks::prelude::*;#[function_component(UseAsyncExample)]fn use_async_example() -> Html { let async_data = use_async(async { // 模拟异步操作 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; Ok::<String, String>("Async operation completed!".to_string()) }); html! { <div> <button onclick={|_| async_data.run()} disabled={async_data.loading}> { if async_data.loading { "Running..." } else { "Run Async" } } </button> { match &async_data.data { Some(data) => html! { <p class="success">{ data }</p> }, None => html! { <p>{ "No data yet" }</p> }, }} { match &async_data.error { Some(error) => html! { <p class="error">{ error }</p> }, None => html! {}, }} </div> }}数据缓存和状态管理1. 实现简单的数据缓存use std::collections::HashMap;use yew::prelude::*;#[function_component(CachedData)]fn cached_data() -> Html { let cache = use_mut_ref(|| HashMap::<String, String>::new()); let data = use_state(|| None::<String>); let loading = use_state(|| false); let fetch_cached = { let cache = cache.clone(); let data = data.clone(); let loading = loading.clone(); Callback::from(move |key: String| { // 检查缓存 if let Some(cached_value) = cache.borrow().get(&key) { data.set(Some(cached_value.clone())); return; } // 从服务器获取 loading.set(true); spawn_local(async move { // 模拟 API 调用 gloo_timers::future::sleep(std::time::Duration::from_secs(1)).await; let value = format!("Data for {}", key); // 更新缓存 cache.borrow_mut().insert(key.clone(), value.clone()); data.set(Some(value)); loading.set(false); }); }) }; html! { <div> <button onclick={Callback::from(move |_| fetch_cached.emit("key1".to_string()))}> { "Fetch Key 1" } </button> <button onclick={Callback::from(move |_| fetch_cached.emit("key2".to_string()))}> { "Fetch Key 2" } </button> { if *loading { html! { <p>{ "Loading..." }</p> } } else if let Some(ref value) = *data { html! { <p>{ value }</p> } }} </div> }}最佳实践错误处理:始终处理异步操作中的错误加载状态:提供清晰的加载反馈取消操作:实现取消未完成请求的机制数据缓存:合理使用缓存减少不必要的请求重试机制:对关键操作实现重试逻辑类型安全:使用 Rust 的类型系统确保数据安全
阅读 0·2月19日 16:23