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

前端面试题手册

Astro 组件的基本结构是什么?如何定义和使用 Props、插槽?

Astro 组件使用 .astro 文件扩展名,具有独特的语法结构,结合了服务端代码和客户端模板。组件结构:---// 1. 前置脚本(Frontmatter)// 在这里编写服务端代码const title = "我的博客文章";const date = new Date().toLocaleDateString();// 可以导入其他组件import Card from './Card.astro';// 可以执行异步操作const posts = await fetch('/api/posts').then(r => r.json());---<!-- 2. 模板区域 --><!-- 在这里编写 HTML/JSX --><h1>{title}</h1><p>发布于 {date}</p><div class="posts"> {posts.map(post => ( <Card title={post.title} /> ))}</div><style> /* 3. 样式作用域 */ h1 { color: #333; }</style>三个主要部分:前置脚本(Frontmatter):使用 --- 分隔符包裹在构建时执行,不会发送到浏览器可以使用 JavaScript/TypeScript支持导入、异步操作、数据处理模板区域:类似 HTML 的语法支持表达式插值 {variable}支持条件渲染 {condition && <Component />}支持列表渲染 {items.map(item => <Item />)}样式作用域:使用 <style> 标签默认是作用域样式(scoped)不会影响其他组件可以使用 :global() 选择器定义全局样式Props 传递:---// 子组件 Card.astroconst { title, description } = Astro.props;---<div class="card"> <h2>{title}</h2> <p>{description}</p></div><style> .card { border: 1px solid #ddd; padding: 1rem; }</style>使用子组件:---import Card from './Card.astro';---<Card title="文章标题" description="文章描述" />插槽(Slots):---// Layout.astroconst { title } = Astro.props;---<html> <head> <title>{title}</title> </head> <body> <header> <slot name="header" /> </header> <main> <slot /> <!-- 默认插槽 --> </main> <footer> <slot name="footer" /> </footer> </body></html>使用布局:---import Layout from './Layout.astro';---<Layout title="我的页面"> <slot slot="header"> <h1>页面标题</h1> </slot> <p>主要内容</p> <slot slot="footer"> <p>页脚信息</p> </slot></Layout>TypeScript 支持:---interface Props { title: string; count?: number;}const { title, count = 0 } = Astro.props satisfies Props;---<h1>{title}</h1><p>数量: {count}</p>注意事项:前置脚本中的代码不会在浏览器中执行模板中的表达式在构建时求值样式默认是作用域的,不会泄漏组件默认是静态的,需要交互时使用 client:* 指令可以在组件中使用任何前端框架的组件Astro 组件语法简洁而强大,提供了优秀的开发体验和性能表现。
阅读 0·2月21日 16:14

Astro 的视图转换(View Transitions)是如何工作的?如何实现平滑的页面过渡效果?

Astro 的视图转换(View Transitions)是一个强大的功能,可以实现类似单页应用(SPA)的平滑页面切换体验,同时保持静态站点的性能优势。核心概念:视图转换通过浏览器原生的 View Transitions API 实现,在页面导航时提供平滑的视觉过渡效果。基本用法:---// src/layouts/Layout.astroimport { ViewTransitions } from 'astro:transitions';---<html> <head> <title>我的网站</title> <ViewTransitions /> </head> <body> <slot /> </body></html>过渡效果类型:淡入淡出(Fade): <ViewTransitions transition="fade" />滑动(Slide): <ViewTransitions transition="slide" />无过渡(None): <ViewTransitions transition="none" />自定义过渡效果:---// src/layouts/Layout.astroimport { ViewTransitions } from 'astro:transitions';---<html> <head> <title>我的网站</title> <ViewTransitions /> <style is:global> ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.5s; } @keyframes custom-fade { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } ::view-transition-new(root) { animation: custom-fade 0.5s ease-out; } </style> </head> <body> <slot /> </body></html>共享元素过渡:---// src/pages/index.astroimport { transition } from 'astro:transitions';---<h1 transition:name="hero-title">欢迎来到我的网站</h1><img src="/hero.jpg" alt="Hero Image" transition:name="hero-image"/><a href="/about" transition:name="cta-button">了解更多</a>---// src/pages/about.astroimport { transition } from 'astro:transitions';---<h1 transition:name="hero-title">关于我们</h1><img src="/about.jpg" alt="About Image" transition:name="hero-image"/><a href="/" transition:name="cta-button">返回首页</a>编程式导航:---import { transition } from 'astro:transitions';---<button onClick={() => transition.navigate('/about')}> 关于我们</button><a href="/contact" data-astro-transition="fade"> 联系我们</a>高级配置:---// src/layouts/Layout.astroimport { ViewTransitions } from 'astro:transitions';---<html> <head> <title>我的网站</title> <ViewTransitions /> </head> <body> <script> import { navigate } from 'astro:transitions/client'; // 监听导航事件 document.addEventListener('astro:page-load', () => { console.log('页面加载完成'); }); document.addEventListener('astro:after-preparation', () => { console.log('页面准备完成'); }); // 自定义导航 function customNavigate(url) { navigate(url, { history: 'push', state: { customData: 'value' }, }); } </script> <slot /> </body></html>条件过渡:---// src/layouts/Layout.astroimport { ViewTransitions } from 'astro:transitions';---<html> <head> <title>我的网站</title> <ViewTransitions /> <script> // 只对特定链接应用过渡 document.querySelectorAll('a[href^="/blog/"]').forEach(link => { link.setAttribute('data-astro-transition', 'fade'); }); </script> </head> <body> <slot /> </body></html>与客户端组件集成:// src/components/Navigation.jsximport { useNavigate } from 'astro:transitions/client';export function Navigation() { const navigate = useNavigate(); return ( <nav> <button onClick={() => navigate('/')}>首页</button> <button onClick={() => navigate('/about')}>关于</button> <button onClick={() => navigate('/contact')}>联系</button> </nav> );}性能优化:预加载链接: <a href="/about" data-astro-transition-prefetch> 关于我们 </a>禁用特定页面的过渡: --- // src/pages/no-transition.astro --- <script> // 禁用视图转换 document.documentElement.dataset.astroTransition = 'false'; </script> <h1>这个页面没有过渡效果</h1>优化图片加载: --- import { Image } from 'astro:assets'; import heroImage from '../assets/hero.jpg'; --- <Image src={heroImage} alt="Hero" transition:name="hero-image" loading="eager" />事件监听:---// src/layouts/Layout.astroimport { ViewTransitions } from 'astro:transitions';---<html> <head> <title>我的网站</title> <ViewTransitions /> </head> <body> <script> // 导航开始 document.addEventListener('astro:before-preparation', (ev) => { console.log('准备导航到:', ev.to.pathname); }); // 导航准备完成 document.addEventListener('astro:after-preparation', () => { console.log('导航准备完成'); }); // 页面开始加载 document.addEventListener('astro:before-swap', () => { console.log('开始替换页面'); }); // 页面加载完成 document.addEventListener('astro:page-load', () => { console.log('页面加载完成'); // 重新初始化客户端组件 initClientComponents(); }); // 导航错误 document.addEventListener('astro:after-swap', (ev) => { if (ev.detail?.error) { console.error('导航错误:', ev.detail.error); } }); </script> <slot /> </body></html>最佳实践:在布局组件中添加 <ViewTransitions />为关键元素使用 transition:name 实现共享元素过渡使用 data-astro-transition-prefetch 预加载重要链接监听导航事件以处理客户端状态为不同的页面类型使用不同的过渡效果考虑用户体验,不要过度使用动画效果在移动设备上简化过渡效果兼容性:视图转换功能需要浏览器支持 View Transitions API。对于不支持的浏览器,Astro 会自动降级为普通的页面导航。Astro 的视图转换功能为静态站点提供了类似 SPA 的用户体验,同时保持了静态站点的性能和 SEO 优势。
阅读 0·2月21日 16:13

Astro 的 SEO 优化有哪些特性?如何配置 Meta 标签、结构化数据和站点地图?

Astro 的 SEO 优化功能非常强大,帮助开发者构建搜索引擎友好的网站。了解如何利用 Astro 的 SEO 特性对于提高网站可见性至关重要。核心 SEO 优势:静态 HTML 输出:默认输出纯 HTML,易于搜索引擎爬取快速加载速度:零 JavaScript 默认,提升 Core Web Vitals服务器端渲染:支持 SSR,确保动态内容也能被索引语义化 HTML:鼓励使用正确的 HTML 标签Meta 标签配置:---// src/pages/index.astroconst title = "我的网站标题";const description = "这是网站描述";const image = "/og-image.jpg";const url = new URL(Astro.url.pathname, Astro.site);---<html lang="zh-CN"> <head> <!-- 基本 Meta 标签 --> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <meta name="description" content={description} /> <meta name="keywords" content="astro, seo, web development" /> <!-- Open Graph 标签 --> <meta property="og:type" content="website" /> <meta property="og:title" content={title} /> <meta property="og:description" content={description} /> <meta property="og:image" content={new URL(image, Astro.site)} /> <meta property="og:url" content={url} /> <!-- Twitter Card 标签 --> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content={title} /> <meta name="twitter:description" content={description} /> <meta name="twitter:image" content={new URL(image, Astro.site)} /> <!-- 规范链接 --> <link rel="canonical" href={url} /> <!-- Favicon --> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <!-- 结构化数据 --> <script type="application/ld+json" set:html={JSON.stringify({ "@context": "https://schema.org", "@type": "WebSite", "name": title, "url": url.toString(), "description": description })} /> </head> <body> <slot /> </body></html>动态 SEO 组件:---// src/components/SEO.astrointerface Props { title: string; description: string; image?: string; type?: 'website' | 'article'; publishedTime?: Date; modifiedTime?: Date; author?: string;}const { title, description, image = '/og-default.jpg', type = 'website', publishedTime, modifiedTime, author} = Astro.props;const url = new URL(Astro.url.pathname, Astro.site);const imageUrl = new URL(image, Astro.site);---<meta charset="UTF-8" /><meta name="viewport" content="width=device-width" /><meta name="description" content={description} /><meta name="robots" content="index, follow" /><!-- Open Graph --><meta property="og:type" content={type} /><meta property="og:title" content={title} /><meta property="og:description" content={description} /><meta property="og:image" content={imageUrl} /><meta property="og:url" content={url} />{publishedTime && <meta property="article:published_time" content={publishedTime.toISOString()} />}{modifiedTime && <meta property="article:modified_time" content={modifiedTime.toISOString()} />}{author && <meta property="article:author" content={author} />}<!-- Twitter Card --><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:title" content={title} /><meta name="twitter:description" content={description} /><meta name="twitter:image" content={imageUrl} /><!-- Canonical --><link rel="canonical" href={url} /><!-- JSON-LD --><script type="application/ld+json" set:html={JSON.stringify({ "@context": "https://schema.org", "@type": type === 'article' ? 'Article' : 'WebSite', "headline": title, "description": description, "image": imageUrl.toString(), "url": url.toString(), "datePublished": publishedTime?.toISOString(), "dateModified": modifiedTime?.toISOString(), "author": { "@type": "Person", "name": author }})} />使用 SEO 组件:---// src/pages/blog/[slug].astroimport SEO from '../../components/SEO.astro';import { getEntry } from 'astro:content';const post = await getEntry('blog', Astro.params.slug);const { Content } = await post.render();---<SEO title={post.data.title} description={post.data.description} image={post.data.image} type="article" publishedTime={post.data.publishDate} modifiedTime={post.data.updatedDate} author={post.data.author}/><article> <h1>{post.data.title}</h1> <Content /></article>站点地图生成:// src/pages/sitemap.xml.tsimport { getCollection } from 'astro:content';export async function GET(context) { const posts = await getCollection('blog'); const site = context.site?.toString() || ''; const body = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> ${posts.map(post => ` <url> <loc>${site}blog/${post.slug}</loc> <lastmod>${post.data.updatedDate || post.data.publishDate}</lastmod> <changefreq>weekly</changefreq> <priority>0.8</priority> </url>`).join('')} <url> <loc>${site}</loc> <lastmod>${new Date().toISOString()}</lastmod> <changefreq>daily</changefreq> <priority>1.0</priority> </url></urlset>`; return new Response(body, { headers: { 'Content-Type': 'application/xml', 'Cache-Control': 'public, max-age=86400', }, });}Robots.txt 配置:// src/pages/robots.txt.tsexport async function GET(context) { const site = context.site?.toString() || ''; const body = `User-agent: *Allow: /Disallow: /api/Disallow: /admin/Sitemap: ${site}sitemap.xml`; return new Response(body, { headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'public, max-age=86400', }, });}结构化数据:---// src/components/ArticleSchema.astrointerface Props { title: string; description: string; image: string; publishDate: Date; author: string; url: string;}const { title, description, image, publishDate, author, url } = Astro.props;---<script type="application/ld+json" set:html={JSON.stringify({ "@context": "https://schema.org", "@type": "Article", "headline": title, "description": description, "image": image, "datePublished": publishDate.toISOString(), "dateModified": publishDate.toISOString(), "author": { "@type": "Person", "name": author }, "publisher": { "@type": "Organization", "name": "My Website", "logo": { "@type": "ImageObject", "url": "/logo.png" } }, "mainEntityOfPage": { "@type": "WebPage", "@id": url }})} />面包屑导航:---// src/components/Breadcrumb.astrointerface Props { items: Array<{ name: string; href: string; }>;}const { items } = Astro.props;---<nav aria-label="Breadcrumb"> <ol class="breadcrumb"> {items.map((item, index) => ( <li class="breadcrumb-item"> {index === items.length - 1 ? ( <span aria-current="page">{item.name}</span> ) : ( <a href={item.href}>{item.name}</a> )} </li> ))} </ol></nav><script type="application/ld+json" set:html={JSON.stringify({ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": items.map((item, index) => ({ "@type": "ListItem", "position": index + 1, "name": item.name, "item": new URL(item.href, Astro.site).toString() }))})} /><style> .breadcrumb { display: flex; list-style: none; padding: 0; margin: 1rem 0; } .breadcrumb-item:not(:last-child)::after { content: ' / '; margin: 0 0.5rem; } .breadcrumb-item a { color: #0066cc; text-decoration: none; } .breadcrumb-item a:hover { text-decoration: underline; }</style>性能与 SEO:---// src/pages/index.astroimport { Image } from 'astro:assets';import heroImage from '../assets/hero.jpg';---<!-- 优化图片加载 --><Image src={heroImage} alt="Hero Image" width={1200} height={630} format="webp" loading="eager" priority={true}/><!-- 预加载关键资源 --><link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin /><link rel="preconnect" href="https://api.example.com" /><!-- 内联关键 CSS --><style> /* 关键 CSS */ .hero { min-height: 60vh; }</style>最佳实践:Meta 标签:为每个页面设置唯一的 title 和 description使用 Open Graph 和 Twitter Card设置规范链接避免重复内容结构化数据:使用 JSON-LD 格式实现正确的 Schema 类型使用 Google 结构化数据测试工具验证性能优化:优化 Core Web Vitals使用图片优化实现代码分割内容优化:使用语义化 HTML优化标题层级(H1-H6)提供有意义的 alt 文本技术 SEO:生成站点地图配置 robots.txt实现面包屑导航Astro 的 SEO 功能帮助开发者构建搜索引擎友好的网站,提高在线可见性。
阅读 0·2月21日 16:13

什么是Expo EAS?它包含哪些核心服务?

Expo EAS (Expo Application Services) 是Expo官方提供的一套云服务,用于简化Expo应用的构建、提交和更新流程。EAS提供了从开发到部署的完整解决方案。EAS核心服务:EAS Build(构建服务)EAS Build是云端构建服务,可以构建Android APK/IPA和iOS IPA文件。主要功能:云端构建,无需本地配置原生环境支持开发和生产两种构建配置自动处理签名和证书构建历史记录和日志查看并行构建支持使用方法:# 安装EAS CLInpm install -g eas-cli# 配置EASeas build:configure# 构建Android应用eas build --platform android# 构建iOS应用(需要Apple开发者账号)eas build --platform ios# 构建开发版本eas build --profile development --platform androidEAS Submit(提交服务)EAS Submit自动将构建好的应用提交到应用商店。支持的平台:Google Play StoreApple App Store使用方法:# 提交到Google Playeas submit --platform android --latest# 提交到App Storeeas submit --platform ios --latestEAS Update(更新服务)EAS Update允许通过OTA (Over-the-Air)方式更新应用,无需重新提交应用商店。主要功能:即时推送更新支持回滚到之前版本细粒度的更新控制更新分组和发布策略使用方法:# 创建更新eas update --branch production --message "Fix bug"# 查看更新历史eas update:list# 回滚更新eas update:rollback --branch productionEAS配置文件:在项目根目录创建eas.json配置文件:{ "cli": { "version": ">= 5.2.0" }, "build": { "development": { "developmentClient": true, "distribution": "internal" }, "preview": { "distribution": "internal", "android": { "buildType": "apk" } }, "production": { "android": { "buildType": "app-bundle" }, "ios": { "autoIncrement": true } } }, "submit": { "production": { "android": { "serviceAccountKeyPath": "./google-service-account.json" }, "ios": { "appleId": "your-apple-id@email.com", "ascAppId": "YOUR_APP_STORE_CONNECT_APP_ID", "appleTeamId": "YOUR_TEAM_ID" } } }}环境变量管理:EAS支持在构建时注入环境变量:# 设置环境变量eas secret:create --name API_KEY --value "your-api-key"# 在代码中使用const apiKey = process.env.API_KEY;最佳实践:CI/CD集成:将EAS Build集成到GitHub Actions或其他CI/CD流程中版本管理:使用Git分支和EAS Update分支对应管理不同环境构建优化:合理配置构建配置,区分开发和生产环境监控和日志:定期查看构建日志,及时发现和解决问题权限管理:为团队成员分配适当的EAS权限限制和注意事项:iOS构建需要Apple开发者账号和付费开发者计划构建时间取决于项目大小和服务器负载免费账户有构建次数限制某些原生功能可能需要额外配置EAS大大简化了Expo应用的部署流程,使开发者能够更专注于应用开发本身。
阅读 0·2月21日 16:08

Expo CLI和Expo Go有什么区别?它们如何协同工作?

Expo CLI和Expo Go是Expo开发流程中的两个核心工具,它们各自承担不同的职责,协同工作以提供高效的开发体验。Expo CLI:Expo CLI是命令行工具,用于创建、构建和管理Expo项目。主要功能:项目初始化:通过npx create-expo-app命令快速创建新的Expo项目,支持TypeScript、JavaScript等多种模板。开发服务器:启动开发服务器,实时编译代码并提供热重载功能。构建配置:配置和管理项目的构建设置,包括应用图标、启动画面、权限配置等。打包发布:支持构建APK、IPA等安装包,或直接发布到Expo服务器。依赖管理:安装和更新Expo SDK版本及依赖包。常用命令:npx create-expo-app my-appnpx expo startnpx expo build:androidnpx expo build:iosExpo Go:Expo Go是一个移动应用,可在Android和iOS设备上安装,用于实时预览和测试Expo应用。主要功能:实时预览:通过扫描二维码或输入URL,在真实设备上查看应用效果。无需构建:开发过程中无需编译原生代码,大幅提升开发效率。跨设备测试:同时在多台设备上测试应用,验证不同屏幕尺寸和系统版本的兼容性。内置SDK:包含完整的Expo SDK,支持所有Expo组件和API。工作流程:使用Expo CLI创建项目并启动开发服务器在移动设备上安装Expo Go应用通过Expo Go连接到开发服务器实时查看代码修改效果限制:Expo Go不支持自定义原生代码,如果项目需要添加自定义原生模块,需要使用Expo Development Build或Eject流程。最佳实践:开发阶段优先使用Expo Go进行快速迭代测试阶段使用Development Build获得更接近生产环境的表现生产构建使用EAS Build生成优化的安装包这两个工具的结合使得Expo开发流程既快速又灵活,适合从原型到生产的完整开发周期。
阅读 0·2月21日 16:06

Expo如何支持Web平台?有哪些注意事项?

Expo支持Web平台,使开发者能够使用相同的代码库构建Web应用。这大大扩展了Expo的应用场景,实现了真正的跨平台开发。Expo for Web特点:单一代码库:使用相同的JavaScript/TypeScript代码响应式设计:自动适应不同屏幕尺寸Web API支持:访问浏览器原生APIPWA支持:可配置为渐进式Web应用快速开发:支持热重载和快速刷新配置Web支持:安装依赖:npx expo install react-dom react-native-web @expo/webpack-config配置app.json:{ "expo": { "web": { "bundler": "webpack", "output": "single", "favicon": "./assets/favicon.png" }, "experiments": { "typedRoutes": true } }}启动Web开发服务器:npx expo start --web平台特定代码:使用Platform模块处理平台差异:import { Platform } from 'react-native';function MyComponent() { if (Platform.OS === 'web') { return <div>Web specific content</div>; } return <View>Mobile specific content</View>;}Web特定API:窗口API:// 获取窗口尺寸const width = window.innerWidth;const height = window.innerHeight;// 监听窗口大小变化window.addEventListener('resize', handleResize);本地存储:// 使用localStoragelocalStorage.setItem('key', 'value');const value = localStorage.getItem('key');导航API:// 使用浏览器历史window.history.pushState({}, '', '/new-route');window.history.back();样式适配:响应式样式:import { StyleSheet, Dimensions } from 'react-native';const styles = StyleSheet.create({ container: { width: Dimensions.get('window').width > 768 ? '80%' : '100%', padding: 16, },});CSS媒体查询:// 使用expo-linear-gradient等库import { LinearGradient } from 'expo-linear-gradient';<LinearGradient colors={['#4c669f', '#3b5998']} style={{ flex: 1 }}/>Web特定组件:HTML元素:// 在Web上使用HTML元素import { View, Text } from 'react-native';// 在Web上渲染为div和span<View style={{ padding: 16 }}> <Text>Hello Web</Text></View>Web特定库:// 使用react-web-specific库import { useMediaQuery } from 'react-responsive';const isDesktop = useMediaQuery({ minWidth: 992 });性能优化:代码分割:// 使用React.lazy进行代码分割const LazyComponent = React.lazy(() => import('./LazyComponent'));懒加载:// 懒加载图片import { Image } from 'react-native';<Image source={{ uri: 'https://example.com/image.jpg' }} loading="lazy"/>缓存策略:// 配置Service Worker进行缓存// 在public/sw.js中配置PWA配置:创建manifest.json:{ "name": "My Expo App", "short_name": "MyApp", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", "icons": [ { "src": "/assets/icon-192.png", "sizes": "192x192", "type": "image/png" } ]}配置Service Worker:// public/sw.jsself.addEventListener('install', event => { event.waitUntil( caches.open('v1').then(cache => { return cache.addAll([ '/', '/index.html', '/static/js/main.js' ]); }) );});部署Web应用:构建生产版本:npx expo export:web部署到Vercel:# 安装Vercel CLInpm i -g vercel# 部署vercel部署到Netlify:# 安装Netlify CLInpm i -g netlify-cli# 部署netlify deploy --prod常见问题:样式差异:Web和移动端样式可能有所不同,需要测试和调整API兼容性:某些移动端API在Web上不可用,需要提供替代方案性能问题:Web版本可能比移动端慢,需要优化加载和渲染触摸事件:Web需要同时支持鼠标和触摸事件键盘导航:Web需要支持键盘导航和无障碍访问最佳实践:渐进增强:先实现核心功能,然后为Web添加特定优化响应式设计:确保应用在不同屏幕尺寸上都能良好显示性能监控:使用Web性能工具监控和优化加载速度SEO优化:添加meta标签和结构化数据测试覆盖:在多个浏览器和设备上测试Web版本Expo for Web使开发者能够用一套代码构建真正的跨平台应用,大大提高了开发效率和代码复用率。
阅读 0·2月21日 16:04

Expo有哪些常用的开发工具和调试技巧?

Expo提供了丰富的开发工具和调试功能,帮助开发者提高开发效率和代码质量。掌握这些工具对于Expo开发至关重要。核心开发工具:Expo CLIExpo CLI是主要的命令行工具,提供项目创建、开发服务器启动、构建等功能。常用命令:# 创建新项目npx create-expo-app my-app# 启动开发服务器npx expo start# 清除缓存npx expo start -c# 查看设备信息npx expo start --tunnel# 生成应用图标npx expo install expo-app-iconExpo Dev ToolsExpo Dev Tools是基于Web的开发工具界面,提供可视化的项目管理功能。功能:设备连接管理日志查看性能监控快速刷新控制网络请求监控React Native DebuggerReact Native Debugger是一个独立的调试工具,集成了React DevTools和Redux DevTools。使用方法:# 安装npm install -g react-native-debugger# 启动react-native-debugger调试技巧:Console调试使用console.log输出调试信息:console.log('Debug info');console.warn('Warning message');console.error('Error message');// 使用console.group组织日志console.group('User Data');console.log('Name:', user.name);console.log('Age:', user.age);console.groupEnd();React DevToolsReact DevTools提供组件树查看、props检查、状态监控等功能。使用方法:// 在开发环境中启用if (__DEV__) { const DevTools = require('react-devtools'); DevTools.connectToDevTools({ host: 'localhost', port: 8097, });}FlipperFlipper是Facebook开发的移动应用调试工具,支持网络请求、数据库、布局检查等。配置Flipper:// 在metro.config.js中const { getDefaultConfig } = require('expo/metro-config');const config = getDefaultConfig(__dirname);module.exports = config;ReactotronReactotron是一个强大的调试工具,支持Redux、API调用、日志等功能。配置Reactotron:import Reactotron from 'reactotron-react-native';if (__DEV__) { const tron = Reactotron .configure() .useReactNative() .connect(); console.tron = tron;}性能优化工具:性能监控使用React Native Performance API:import { Performance } from 'react-native';// 记录性能标记Performance.mark('component-start');// 组件渲染完成后Performance.mark('component-end');// 测量性能Performance.measure('component-render', 'component-start', 'component-end');内存分析使用Flipper或React Native Debugger查看内存使用情况:监控内存泄漏分析组件渲染性能优化图片和资源加载Bundle分析使用webpack-bundle-analyzer分析bundle大小:npm install --save-dev @expo/webpack-config webpack-bundle-analyzer错误处理:错误边界使用React错误边界捕获组件错误:class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error('Error caught by boundary:', error, errorInfo); } render() { if (this.state.hasError) { return <Text>Something went wrong.</Text>; } return this.props.children; }}全局错误处理监听全局错误事件:// JavaScript错误const defaultErrorHandler = ErrorUtils.getGlobalHandler();ErrorUtils.setGlobalHandler((error, isFatal) => { console.error('Global error:', error); defaultErrorHandler(error, isFatal);});// Promise错误process.on('unhandledRejection', (error) => { console.error('Unhandled promise rejection:', error);});测试工具:JestJest是Expo默认的测试框架:// 示例测试import { render } from '@testing-library/react-native';import MyComponent from './MyComponent';test('renders correctly', () => { const { getByText } = render(<MyComponent />); expect(getByText('Hello')).toBeTruthy();});DetoxDetox是端到端测试框架:describe('Login Flow', () => { it('should login successfully', async () => { await element(by.id('username')).typeText('user@example.com'); await element(by.id('password')).typeText('password'); await element(by.id('login-button')).tap(); await expect(element(by.id('welcome'))).toBeVisible(); });});最佳实践:开发环境配置:使用不同的环境变量配置开发、测试和生产环境日志管理:在生产环境中禁用详细的日志输出错误追踪:集成Sentry或Bugsnag等错误追踪服务性能监控:定期监控应用性能指标自动化测试:建立完善的测试体系掌握这些调试工具和技巧,可以大大提高Expo开发的效率和质量。
阅读 0·2月21日 16:04

Expo提供了哪些常用的原生组件和API?如何使用它们?

Expo提供了丰富的原生组件和API,使开发者能够轻松访问移动设备的各种功能。这些组件和API经过精心设计,提供了统一的跨平台接口。核心原生组件:Camera(相机)import { Camera } from 'expo-camera';// 请求相机权限const { status } = await Camera.requestCameraPermissionsAsync();// 使用相机组件<Camera style={{ flex: 1 }} type={type} />Location(位置服务)import * as Location from 'expo-location';// 请求位置权限const { status } = await Location.requestForegroundPermissionsAsync();// 获取当前位置const location = await Location.getCurrentPositionAsync({});Notifications(推送通知)import * as Notifications from 'expo-notifications';// 请求通知权限const { status } = await Notifications.requestPermissionsAsync();// 发送本地通知await Notifications.scheduleNotificationAsync({ content: { title: 'Hello!', body: 'This is a notification', }, trigger: { seconds: 2 },});Audio(音频)import { Audio } from 'expo-av';// 播放音频const { sound } = await Audio.Sound.createAsync( { uri: 'https://example.com/audio.mp3' });await sound.playAsync();FileSystem(文件系统)import * as FileSystem from 'expo-file-system';// 读取文件const content = await FileSystem.readAsStringAsync(fileUri);// 写入文件await FileSystem.writeAsStringAsync(fileUri, 'Hello World');ImagePicker(图片选择器)import * as ImagePicker from 'expo-image-picker';// 选择图片const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true,});SecureStore(安全存储)import * as SecureStore from 'expo-secure-store';// 保存敏感数据await SecureStore.setItemAsync('token', 'user-token');// 读取敏感数据const token = await SecureStore.getItemAsync('token');Sensors(传感器)import { Accelerometer } from 'expo-sensors';// 监听加速度计Accelerometer.addListener(accelerometerData => { console.log(accelerometerData);});常用API:Constants(常量)import Constants from 'expo-constants';// 获取设备信息const deviceName = Constants.deviceName;const platform = Constants.platform;Haptics(触觉反馈)import * as Haptics from 'expo-haptics';// 触发触觉反馈Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);Linking(链接处理)import * as Linking from 'expo-linking';// 打开URLawait Linking.openURL('https://expo.dev');// 处理深度链接const url = await Linking.getInitialURL();ScreenOrientation(屏幕方向)import * as ScreenOrientation from 'expo-screen-orientation';// 锁定屏幕方向await ScreenOrientation.lockAsync( ScreenOrientation.OrientationLock.PORTRAIT);权限管理:Expo提供了统一的权限请求API:import * as Permissions from 'expo-permissions';// 请求权限const { status, granted } = await Permissions.askAsync( Permissions.CAMERA, Permissions.LOCATION);最佳实践:权限请求时机:在用户需要使用功能时再请求权限,提供清晰的说明错误处理:妥善处理权限被拒绝的情况,提供友好的用户提示性能优化:及时释放资源,如停止传感器监听、取消音频播放等平台差异:注意不同平台的API差异,使用条件渲染处理平台特定功能异步操作:所有原生API调用都是异步的,使用async/await处理这些原生组件和API大大简化了跨平台开发,使开发者能够专注于业务逻辑而不是原生实现细节。
阅读 0·2月21日 16:03

Expo应用的可访问性(Accessibility)如何实现?有哪些最佳实践?

Expo应用的可访问性(Accessibility)是确保所有用户,包括有视觉、听觉、运动或认知障碍的用户,都能有效使用应用的重要方面。Expo和React Native提供了丰富的可访问性API和属性。可访问性基础:accessibilityLabel为屏幕阅读器提供元素的描述。<Image source={{ uri: 'https://example.com/image.jpg' }} accessibilityLabel="用户头像" style={{ width: 50, height: 50 }}/><Button title="提交" accessibilityLabel="提交表单" onPress={handleSubmit}/>accessibilityHint提供有关元素行为的额外信息。<TouchableOpacity accessibilityLabel="查看详情" accessibilityHint="点击查看用户详细信息" onPress={handlePress}> <Text>查看</Text></TouchableOpacity>accessibilityRole指定元素的UI角色。<View accessibilityRole="button" accessibilityLabel="确认" onClick={handleConfirm}> <Text>确认</Text></View>常用可访问性角色:button:按钮link:链接header:标题text:文本image:图片search:搜索框adjustable:可调节控件accessibilityState描述元素的当前状态。<CheckBox accessibilityLabel="同意条款" accessibilityState={{ checked: isChecked, disabled: false, }} value={isChecked} onValueChange={setIsChecked}/>可访问性状态:disabled:禁用状态selected:选中状态checked:勾选状态busy:忙碌状态expanded:展开状态accessibilityValue描述元素的值。<Slider accessibilityLabel="音量" accessibilityValue={{ min: 0, max: 100, now: volume }} value={volume} onValueChange={setVolume}/><ProgressBar accessibilityLabel="下载进度" accessibilityValue={{ min: 0, max: 100, now: progress }} progress={progress / 100}/>可访问性操作:accessibilityActions定义元素支持的可访问性操作。<View accessibilityLabel="播放控制" accessibilityActions={[ { name: 'increment', label: '增加音量' }, { name: 'decrement', label: '减少音量' }, { name: 'magicTap', label: '双击播放/暂停' }, ]} onAccessibilityAction={(event) => { switch (event.nativeEvent.actionName) { case 'increment': setVolume((v) => Math.min(v + 10, 100)); break; case 'decrement': setVolume((v) => Math.max(v - 10, 0)); break; case 'magicTap': togglePlayPause(); break; } }}> <Text>音量: {volume}</Text></View>onAccessibilityEscape定义转义操作。<Modal visible={isVisible} onAccessibilityEscape={() => setIsVisible(false)} accessibilityViewIsModal={true}> <View> <Text>模态框内容</Text> <Button title="关闭" onPress={() => setIsVisible(false)} /> </View></Modal>可访问性属性:accessible标记元素为可访问的。<View accessible={true}> <Text>这个视图可以被屏幕阅读器访问</Text></View>accessibilityElementsHidden隐藏子元素的可访问性。<View accessible={true} accessibilityLabel="容器" accessibilityElementsHidden={isHidden}> <Text>子元素1</Text> <Text>子元素2</Text></View>accessibilityIgnoresInvertColors忽略颜色反转设置。<Image source={{ uri: 'https://example.com/chart.jpg' }} accessibilityIgnoresInvertColors={true} style={{ width: 200, height: 200 }}/>焦点管理:focusable使元素可聚焦。<TextInput focusable={true} accessibilityLabel="用户名输入框" placeholder="请输入用户名"/>accessibilityLiveRegion标记动态内容区域。<Text accessibilityLiveRegion="polite" accessibilityLabel="状态信息"> {statusMessage}</Text>可访问性事件:function useAccessibilityFocus() { const [isFocused, setIsFocused] = useState(false); const handleFocus = () => { setIsFocused(true); console.log('Element focused'); }; const handleBlur = () => { setIsFocused(false); console.log('Element blurred'); }; return { isFocused, handleFocus, handleBlur };}语义化组件:使用语义化HTML标签(Web)// 在Web平台上使用语义化标签if (Platform.OS === 'web') { return ( <nav accessibilityRole="navigation"> <ul> <li><a href="/home">首页</a></li> <li><a href="/about">关于</a></li> </ul> </nav> );}使用正确的可访问性角色// 按钮使用button角色<TouchableOpacity accessibilityRole="button" accessibilityLabel="提交" onPress={handleSubmit}> <Text>提交</Text></TouchableOpacity>// 链接使用link角色<TouchableOpacity accessibilityRole="link" accessibilityLabel="查看详情" onPress={handlePress}> <Text>查看详情</Text></TouchableOpacity>最佳实践:提供清晰的标签// 好的实践<Button title="提交表单" accessibilityLabel="提交用户注册表单" onPress={handleSubmit}/>// 避免重复<Button title="提交" accessibilityLabel="提交" // 与title重复 onPress={handleSubmit}/>使用有意义的提示<TouchableOpacity accessibilityLabel="删除项目" accessibilityHint="此操作无法撤销,请谨慎操作" onPress={handleDelete}> <Text>删除</Text></TouchableOpacity>支持键盘导航function KeyboardNavigation() { const [focusedIndex, setFocusedIndex] = useState(0); const handleKeyDown = (event) => { if (event.key === 'ArrowDown') { setFocusedIndex((i) => Math.min(i + 1, items.length - 1)); } else if (event.key === 'ArrowUp') { setFocusedIndex((i) => Math.max(i - 1, 0)); } else if (event.key === 'Enter') { items[focusedIndex].onPress(); } }; return ( <View onKeyDown={handleKeyDown}> {items.map((item, index) => ( <TouchableOpacity key={index} accessibilityLabel={item.label} focusable={true} style={[ styles.item, focusedIndex === index && styles.focused, ]} onPress={item.onPress} > <Text>{item.label}</Text> </TouchableOpacity> ))} </View> );}支持屏幕阅读器function ScreenReaderSupport() { const isScreenReaderEnabled = useAccessibilityInfo(); return ( <View> {isScreenReaderEnabled ? ( <Text>屏幕阅读器已启用</Text> ) : ( <Text>屏幕阅读器未启用</Text> )} </View> );}测试可访问性import { AccessibilityInfo } from 'react-native';async function testAccessibility() { // 检查屏幕阅读器是否启用 const isScreenReaderEnabled = await AccessibilityInfo.isScreenReaderEnabled(); console.log('Screen reader enabled:', isScreenReaderEnabled); // 检查减少动画设置 const isReduceMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled(); console.log('Reduce motion enabled:', isReduceMotionEnabled); // 监听可访问性变化 AccessibilityInfo.addEventListener( 'screenReaderChanged', (isEnabled) => { console.log('Screen reader changed:', isEnabled); } );}可访问性工具:AccessibilityInfo APIimport { AccessibilityInfo } from 'react-native';// 获取可访问性信息const isScreenReaderEnabled = await AccessibilityInfo.isScreenReaderEnabled();const isReduceMotionEnabled = await AccessibilityInfo.isReduceMotionEnabled();// 监听变化AccessibilityInfo.addEventListener('screenReaderChanged', (isEnabled) => { console.log('Screen reader:', isEnabled);});AccessibilityInfo.addEventListener('reduceMotionChanged', (isEnabled) => { console.log('Reduce motion:', isEnabled);});可访问性检查工具iOS:VoiceOverAndroid:TalkBackWeb:屏幕阅读器(NVDA、JAWS等)常见可访问性问题:缺少可访问性标签为所有交互元素添加accessibilityLabel为图片提供描述性标签焦点管理不当确保键盘导航顺序合理提供清晰的焦点指示器颜色对比度不足确保文本和背景有足够的对比度支持高对比度模式动态内容未通知使用accessibilityLiveRegion标记动态内容及时通知屏幕阅读器内容变化通过实施这些可访问性实践,可以确保Expo应用对所有用户都是友好和可用的。
阅读 0·2月21日 16:03

什么是Expo Router?它如何实现文件系统路由?

Expo Router是Expo官方提供的路由解决方案,基于文件系统路由,专为Expo应用设计。它简化了导航管理,提供了类型安全的路由和深度链接支持。核心特性:文件系统路由基于文件和文件夹结构自动生成路由支持动态路由和嵌套路由类似Next.js的路由体验类型安全自动生成TypeScript类型编译时路由检查智能代码补全深度链接原生深度链接支持Web URL兼容自动处理链接参数安装和配置:# 安装Expo Routernpx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar# 配置app.json{ "expo": { "scheme": "myapp", "experiments": { "typedRoutes": true } }}项目结构:app/├── _layout.tsx # 根布局├── index.tsx # 首页 (/)├── about.tsx # 关于页面 (/about)├── user/│ ├── [id].tsx # 用户详情页 (/user/:id)│ └── settings.tsx # 用户设置 (/user/settings)├── (tabs)/│ ├── _layout.tsx # Tab布局│ ├── home.tsx # Tab首页│ └── profile.tsx # Tab个人页└── (modal)/ └── _layout.tsx # Modal布局路由类型:静态路由// app/index.tsxexport default function HomeScreen() { return <Text>Home</Text>;}动态路由// app/user/[id].tsximport { useLocalSearchParams } from 'expo-router';export default function UserScreen() { const { id } = useLocalSearchParams<{ id: string }>(); return <Text>User: {id}</Text>;}嵌套路由// app/(tabs)/_layout.tsximport { Tabs } from 'expo-router';export default function TabLayout() { return ( <Tabs> <Tabs.Screen name="home" options={{ title: 'Home' }} /> <Tabs.Screen name="profile" options={{ title: 'Profile' }} /> </Tabs> );}分组路由// (tabs)和(modal)是路由组,不影响URL// app/(tabs)/home.tsx -> /home// app/(modal)/settings.tsx -> /settings导航API:import { useRouter, useSegments } from 'expo-router';function MyComponent() { const router = useRouter(); const segments = useSegments(); // 导航到页面 const navigateToUser = () => { router.push('/user/123'); }; // 替换当前页面 const replacePage = () => { router.replace('/settings'); }; // 返回上一页 const goBack = () => { router.back(); }; // 检查当前路由 const isHome = segments[0] === 'home'; return ( <View> <Button title="Go to User" onPress={navigateToUser} /> <Button title="Replace" onPress={replacePage} /> <Button title="Back" onPress={goBack} /> </View> );}链接API:import { Link, useLocalSearchParams } from 'expo-router';// 使用Link组件<Link href="/user/123"> <Text>Go to User</Text></Link>// 使用useLocalSearchParams获取参数const { id } = useLocalSearchParams<{ id: string }>();深度链接配置:// app/_layout.tsximport { Stack } from 'expo-router';import * as Linking from 'expo-linking';const linking = { prefixes: [Linking.createURL('/')], config: { screens: { index: '/', user: '/user/:id', }, },};export default function RootLayout() { return <Stack />;}最佳实践:路由组织:合理使用路由组和嵌套布局,保持结构清晰类型安全:启用typedRoutes实验性功能,获得完整的类型支持性能优化:使用懒加载减少初始包大小错误处理:实现404页面和错误边界测试覆盖:为路由逻辑编写单元测试与React Navigation的对比:Expo Router基于React Navigation构建,提供了更高级的抽象:更简单的配置自动类型生成文件系统路由更好的深度链接支持Expo Router是构建Expo应用导航的理想选择,特别适合需要类型安全和深度链接的项目。
阅读 0·2月21日 16:03