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

面试题手册

SVG 性能优化的具体策略有哪些

SVG 的性能优化对于提升网页加载速度和用户体验至关重要。以下是 SVG 性能优化的详细策略:1. 文件大小优化移除不必要的代码:# 使用 SVGO 优化 SVGnpx svgo input.svg -o output.svg# 批量优化npx svgo -f ./icons -o ./optimized# 配置优化选项npx svgo --config svgo.config.js input.svg -o output.svgSVGO 配置示例:// svgo.config.jsmodule.exports = { plugins: [ 'removeDoctype', 'removeXMLProcInst', 'removeComments', 'removeMetadata', 'removeUselessDefs', 'cleanupIDs', 'minifyStyles', 'convertPathData', 'mergePaths', 'removeUnusedNS', 'sortDefsChildren', 'removeEmptyAttrs', 'removeEmptyContainers', 'cleanupNumericValues', 'convertColors', 'removeUnknownsAndDefaults' ]};2. 路径优化简化路径命令:<!-- 优化前 --><path d="M 10.123456 20.654321 L 30.987654 40.321098" /><!-- 优化后 --><path d="M10.12 20.65L30.99 40.32" />使用相对坐标:<!-- 使用绝对坐标 --><path d="M 10 10 L 20 10 L 20 20 L 10 20 Z" /><!-- 使用相对坐标(更简洁)--><path d="M10 10h10v10h-10z" />合并路径:<!-- 优化前 --><rect x="10" y="10" width="50" height="50" fill="blue" /><rect x="70" y="10" width="50" height="50" fill="blue" /><!-- 优化后(使用 path)--><path d="M10 10h50v50H10z M70 10h50v50H70z" fill="blue" />3. 渲染性能优化减少元素数量:<!-- 优化前:多个独立元素 --><circle cx="10" cy="10" r="5" fill="blue" /><circle cx="20" cy="10" r="5" fill="blue" /><circle cx="30" cy="10" r="5" fill="blue" /><!-- 优化后:使用 group --><g fill="blue"> <circle cx="10" cy="10" r="5" /> <circle cx="20" cy="10" r="5" /> <circle cx="30" cy="10" r="5" /></g>使用 CSS 代替 SVG 属性:<!-- 优化前 --><circle cx="50" cy="50" r="40" fill="blue" stroke="red" stroke-width="2" /><!-- 优化后:使用 CSS --><style>.circle { fill: blue; stroke: red; stroke-width: 2px;}</style><circle class="circle" cx="50" cy="50" r="40" />4. 动画性能优化优先使用 CSS 动画:/* CSS 动画(GPU 加速)*/.animated { animation: rotate 2s linear infinite; transform-origin: center;}@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}避免动画 width/height:/* 优化前:动画 width/height(性能差)*/.bad { animation: scale 1s ease;}@keyframes scale { from { width: 50px; height: 50px; } to { width: 100px; height: 100px; }}/* 优化后:动画 transform(性能好)*/.good { animation: scale 1s ease;}@keyframes scale { from { transform: scale(1); } to { transform: scale(2); }}5. 滤镜性能优化避免过度使用滤镜:<!-- 避免复杂的滤镜链 --><filter id="complex"> <feGaussianBlur stdDeviation="5" /> <feOffset dx="5" dy="5" /> <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" /> <feMerge> <feMergeNode in="SourceGraphic" /> <feMergeNode /> </feMerge></filter><!-- 简化滤镜或使用 CSS --><style>.shadow { filter: drop-shadow(3px 3px 3px rgba(0,0,0,0.3));}</style>6. 加载优化内联关键 SVG:<!-- 首屏关键 SVG 内联 --><header> <svg viewBox="0 0 24 24" width="24" height="24"> <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" /> </svg></header><!-- 非关键 SVG 延迟加载 --><img src="non-critical.svg" loading="lazy" alt="Non-critical" />使用 SVG Sprite:<!-- 合并多个图标到一个文件 --><svg style="display: none;"> <symbol id="icon1" viewBox="0 0 24 24">...</symbol> <symbol id="icon2" viewBox="0 0 24 24">...</symbol> <symbol id="icon3" viewBox="0 0 24 24">...</symbol></svg><!-- 使用图标 --><svg><use href="#icon1" /></svg><svg><use href="#icon2" /></svg>7. 压缩和缓存服务器配置:# Nginx 配置location ~* \.(svg)$ { gzip on; gzip_vary on; gzip_min_length 1000; gzip_types image/svg+xml; expires 1y; add_header Cache-Control "public, immutable";}8. 监控和测试性能测试工具:Lighthouse:测试整体性能WebPageTest:分析加载性能Chrome DevTools:监控渲染性能最佳实践:定期使用 SVGO 优化 SVG 文件优先使用 CSS 动画减少元素数量和复杂度合理使用滤镜和渐变内联关键 SVG,延迟加载其他启用 gzip 压缩设置适当的缓存策略监控性能指标
阅读 0·2月21日 15:19

SVG 的裁剪和蒙版如何使用

SVG 裁剪和蒙版是创建复杂图形效果的重要工具。以下是 SVG 裁剪和蒙版的使用方法:1. 裁剪路径(clipPath)使用 <clipPath> 元素定义裁剪区域,只有裁剪区域内的内容才会显示。<svg width="200" height="200"> <defs> <clipPath id="myClip"> <circle cx="100" cy="100" r="80" /> </clipPath> </defs> <rect x="0" y="0" width="200" height="200" fill="blue" clip-path="url(#myClip)" /></svg>2. 裁剪路径示例使用不同形状作为裁剪路径。<svg width="300" height="200"> <defs> <clipPath id="circleClip"> <circle cx="100" cy="100" r="80" /> </clipPath> <clipPath id="rectClip"> <rect x="20" y="20" width="160" height="160" rx="20" /> </clipPath> <clipPath id="starClip"> <polygon points="100,10 40,198 190,78 10,78 160,198" /> </clipPath> </defs> <rect x="0" y="0" width="200" height="200" fill="blue" clip-path="url(#circleClip)" /> <rect x="200" y="0" width="200" height="200" fill="green" clip-path="url(#rectClip)" /> <rect x="400" y="0" width="200" height="200" fill="red" clip-path="url(#starClip)" /></svg>3. 蒙版(mask)使用 <mask> 元素创建蒙版效果,蒙版的透明度决定内容的显示程度。<svg width="200" height="200"> <defs> <mask id="myMask"> <rect x="0" y="0" width="200" height="200" fill="white" /> <circle cx="100" cy="100" r="50" fill="black" /> </mask> </defs> <rect x="0" y="0" width="200" height="200" fill="blue" mask="url(#myMask)" /></svg>4. 蒙版透明度使用不同的透明度创建渐变蒙版效果。<svg width="200" height="200"> <defs> <linearGradient id="maskGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="white" /> <stop offset="100%" stop-color="black" /> </linearGradient> <mask id="fadeMask"> <rect x="0" y="0" width="200" height="200" fill="url(#maskGradient)" /> </mask> </defs> <rect x="0" y="0" width="200" height="200" fill="blue" mask="url(#fadeMask)" /></svg>5. 裁剪与蒙版的区别裁剪(clipPath):只显示裁剪区域内的内容,区域外完全隐藏蒙版(mask):根据蒙版的透明度控制内容的显示程度,可以创建渐变效果6. 组合使用结合裁剪和蒙版创建复杂效果。<svg width="200" height="200"> <defs> <clipPath id="circleClip"> <circle cx="100" cy="100" r="80" /> </clipPath> <mask id="fadeMask"> <rect x="0" y="0" width="200" height="200" fill="url(#maskGradient)" /> </mask> <linearGradient id="maskGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="white" /> <stop offset="100%" stop-color="black" /> </linearGradient> </defs> <rect x="0" y="0" width="200" height="200" fill="blue" clip-path="url(#circleClip)" mask="url(#fadeMask)" /></svg>7. 动态裁剪使用 JavaScript 动态修改裁剪路径。<svg width="200" height="200"> <defs> <clipPath id="dynamicClip"> <circle id="clipCircle" cx="100" cy="100" r="50" /> </clipPath> </defs> <rect id="clippedRect" x="0" y="0" width="200" height="200" fill="blue" clip-path="url(#dynamicClip)" /></svg><script>const clipCircle = document.getElementById('clipCircle');let radius = 50;let growing = true;function animateClip() { if (growing) { radius += 1; if (radius >= 80) growing = false; } else { radius -= 1; if (radius <= 50) growing = true; } clipCircle.setAttribute('r', radius); requestAnimationFrame(animateClip);}animateClip();</script>8. 文本裁剪使用文本作为裁剪路径。<svg width="300" height="200"> <defs> <clipPath id="textClip"> <text x="150" y="100" font-size="48" font-weight="bold" text-anchor="middle"> SVG </text> </clipPath> </defs> <rect x="0" y="0" width="300" height="200" fill="url(#gradient)" clip-path="url(#textClip)" /> <defs> <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#ff6b6b" /> <stop offset="100%" stop-color="#4ecdc4" /> </linearGradient> </defs></svg>9. 图片裁剪使用 SVG 裁剪外部图片。<svg width="200" height="200"> <defs> <clipPath id="imageClip"> <circle cx="100" cy="100" r="80" /> </clipPath> </defs> <image href="photo.jpg" x="0" y="0" width="200" height="200" clip-path="url(#imageClip)" /></svg>最佳实践:合理选择裁剪或蒙版,根据需求决定使用裁剪实现简单的形状裁剪使用蒙版实现渐变和透明度效果考虑性能,避免过度使用复杂裁剪测试跨浏览器兼容性为裁剪和蒙版添加有意义的 ID
阅读 0·2月21日 15:19

如何理解和使用 SVG 路径命令

SVG 路径是 SVG 中最强大和灵活的元素,使用 <path> 标签和 d 属性来定义。路径命令由字母和数字组成,可以分为以下几类:1. 移动命令(M, m)M x y:移动到绝对坐标 (x, y)m dx dy:移动到相对坐标(相对于当前位置)用途:开始新的路径段,不绘制线条2. 直线命令(L, l, H, h, V, v)L x y:绘制直线到绝对坐标 (x, y)l dx dy:绘制直线到相对坐标H x:水平线到绝对 x 坐标h dx:水平线到相对 x 坐标V y:垂直线到绝对 y 坐标v dy:垂直线到相对 y 坐标3. 曲线命令三次贝塞尔曲线(C, c, S, s)C x1 y1, x2 y2, x y:使用两个控制点 (x1,y1) 和 (x2,y2) 绘制到 (x,y)c dx1 dy1, dx2 dy2, dx dy:相对坐标版本S x2 y2, x y:平滑曲线,自动推断第一个控制点s dx2 dy2, dx dy:相对坐标版本二次贝塞尔曲线(Q, q, T, t)Q x1 y1, x y:使用一个控制点 (x1,y1) 绘制到 (x,y)q dx1 dy1, dx dy:相对坐标版本T x y:平滑曲线,自动推断控制点t dx dy:相对坐标版本椭圆弧(A, a)A rx ry x-axis-rotation large-arc-flag sweep-flag x y参数:rx, ry(椭圆半径)、x-axis-rotation(旋转角度)、large-arc-flag(大弧标志)、sweep-flag(绘制方向)、x, y(终点坐标)4. 闭合命令(Z, z)Z 或 z:闭合路径,连接到起点自动绘制一条直线回到起点示例路径:<svg viewBox="0 0 100 100"> <!-- 心形路径 --> <path d="M 50 90 C 20 60, 0 30, 30 30 C 40 30, 50 40, 50 50 C 50 40, 60 30, 70 30 C 100 30, 80 60, 50 90 Z" fill="red" /></svg>最佳实践:大写字母表示绝对坐标,小写字母表示相对坐标可以省略命令字母之间的空格和逗号连续的相同命令可以省略命令字母使用相对坐标可以简化路径定义
阅读 0·2月21日 15:19

如何理解 SVG 的坐标系统和变换

SVG 的坐标系统和变换是理解 SVG 图形定位和操作的关键概念:1. 坐标系统SVG 使用笛卡尔坐标系,原点 (0,0) 位于左上角:x 轴向右为正方向y 轴向下为正方向单位可以是 px、em、rem、cm、mm 等2. viewBox 属性viewBox 是 SVG 中最重要的属性之一,定义了 SVG 内容的坐标系和可视区域。<svg viewBox="0 0 100 100" width="200" height="200"> <circle cx="50" cy="50" r="40" fill="red" /></svg>viewBox 参数:min-x, min-y:左上角坐标width, height:可视区域的宽度和高度viewBox 的作用:定义内部坐标系实现响应式缩放控制显示比例3. preserveAspectRatio 属性控制 viewBox 如何在 SVG 视口中缩放和对齐。<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" width="200" height="200"> <circle cx="50" cy="50" r="40" fill="blue" /></svg>参数说明:align:对齐方式(xMin, xMid, xMax + YMin, YMid, YMax)meetOrSlice:缩放方式(meet:完整显示,slice:填充整个区域)常用值:xMidYMid meet:居中显示,保持比例(默认)xMidYMid slice:居中显示,填充整个区域none:不保持比例,拉伸填充4. transform 属性SVG 元素可以通过 transform 属性进行变换。变换类型:平移(translate)<rect x="0" y="0" width="50" height="50" fill="red" transform="translate(100, 100)" />旋转(rotate)<rect x="0" y="0" width="50" height="50" fill="blue" transform="rotate(45, 25, 25)" />参数:角度,旋转中心 x,旋转中心 y缩放(scale)<rect x="0" y="0" width="50" height="50" fill="green" transform="scale(2)" /><rect x="0" y="0" width="50" height="50" fill="yellow" transform="scale(1.5, 0.5)" />倾斜(skew)<rect x="0" y="0" width="50" height="50" fill="purple" transform="skewX(30)" /><rect x="0" y="0" width="50" height="50" fill="orange" transform="skewY(20)" />组合变换<rect x="0" y="0" width="50" height="50" fill="red" transform="translate(100, 100) rotate(45) scale(1.5)" />5. 坐标系统嵌套使用 <g> 元素创建局部坐标系统。<svg viewBox="0 0 200 200"> <g transform="translate(50, 50)"> <circle cx="0" cy="0" r="20" fill="red" /> <circle cx="50" cy="0" r="20" fill="blue" /> <circle cx="25" cy="50" r="20" fill="green" /> </g></svg>6. 实际应用示例响应式图标:<svg viewBox="0 0 24 24" width="100%" height="100%"> <path d="M12 2L2 22h20L12 2zm0 4l7 14H5l7-14z" fill="currentColor" /></svg>居中对齐:<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" width="300" height="200"> <rect x="0" y="0" width="100" height="100" fill="blue" /></svg>最佳实践:始终使用 viewBox 实现响应式设计使用 preserveAspectRatio 控制缩放行为合理使用 <g> 分组和变换简化代码注意变换顺序会影响最终结果
阅读 0·2月21日 15:19

Gin 框架中如何实现模板渲染和静态文件服务?

Gin 框架中的模板渲染和静态文件服务如下:1. 模板渲染Gin 支持多种模板引擎,包括 HTML、Pug、Ace 等。1.1 加载模板import "github.com/gin-gonic/gin"func main() { r := gin.Default() // 加载模板文件 r.LoadHTMLGlob("templates/*") // 或者加载指定模板 r.LoadHTMLFiles("templates/index.html", "templates/about.html") r.Run(":8080")}1.2 渲染 HTML 模板func renderHTML(c *gin.Context) { c.HTML(200, "index.html", gin.H{ "title": "Home Page", "message": "Welcome to Gin!", })}1.3 模板继承// 基础模板 templates/base.html<!DOCTYPE html><html><head> <title>{{ .title }}</title></head><body> {{ block "content" . }}{{ end }}</body></html>// 子模板 templates/index.html{{ define "content" }}<h1>{{ .message }}</h1>{{ end }}// 渲染继承模板func renderInherited(c *gin.Context) { c.HTML(200, "index.html", gin.H{ "title": "Home", "message": "Welcome!", })}1.4 自定义模板函数func main() { r := gin.Default() // 创建模板引擎 t := template.Must(template.New("").Funcs(template.FuncMap{ "upper": strings.ToUpper, "formatDate": func(t time.Time) string { return t.Format("2006-01-02") }, }).ParseGlob("templates/*")) // 设置自定义模板引擎 r.SetHTMLTemplate(t) r.GET("/", func(c *gin.Context) { c.HTML(200, "index.html", gin.H{ "name": "john", "date": time.Now(), }) }) r.Run(":8080")}2. 静态文件服务2.1 基本静态文件服务func main() { r := gin.Default() // 提供静态文件服务 r.Static("/static", "./static") // 或者 r.Static("/assets", "./assets") r.Run(":8080")}2.2 单个静态文件func main() { r := gin.Default() // 提供单个静态文件 r.StaticFile("/favicon.ico", "./resources/favicon.ico") r.Run(":8080")}2.3 静态文件服务到根路径func main() { r := gin.Default() // 将静态文件服务到根路径 r.StaticFS("/", http.Dir("./public")) r.Run(":8080")}3. 模板和静态文件的最佳实践3.1 目录结构project/├── main.go├── templates/│ ├── base.html│ ├── index.html│ └── about.html├── static/│ ├── css/│ │ └── style.css│ ├── js/│ │ └── app.js│ └── images/│ └── logo.png└── uploads/ └── files/3.2 模板组织func setupTemplates(r *gin.Engine) { // 加载所有模板 r.LoadHTMLGlob("templates/**/*.html") // 或者分别加载不同目录的模板 r.LoadHTMLGlob("templates/*.html") r.LoadHTMLGlob("templates/layouts/*.html") r.LoadHTMLGlob("templates/components/*.html")}3.3 静态文件缓存func setupStaticFiles(r *gin.Engine) { // 使用文件系统缓存 fs := http.Dir("./static") fileServer := http.FileServer(fs) // 添加缓存头 r.GET("/static/*filepath", func(c *gin.Context) { c.Header("Cache-Control", "public, max-age=3600") fileServer.ServeHTTP(c.Writer, c.Request) })}4. 前端资源优化4.1 压缩静态资源import "github.com/gin-contrib/gzip"func main() { r := gin.Default() // 启用 gzip 压缩 r.Use(gzip.Gzip(gzip.DefaultCompression)) r.Static("/static", "./static") r.Run(":8080")}4.2 版本控制静态资源func getVersionedPath(path string) string { info, err := os.Stat(path) if err != nil { return path } return fmt.Sprintf("%s?v=%d", path, info.ModTime().Unix())}func renderPage(c *gin.Context) { c.HTML(200, "index.html", gin.H{ "cssPath": getVersionedPath("/static/css/style.css"), "jsPath": getVersionedPath("/static/js/app.js"), })}5. 模板安全5.1 防止 XSS 攻击// Gin 默认会转义 HTML,防止 XSSfunc renderSafe(c *gin.Context) { // 自动转义 c.HTML(200, "index.html", gin.H{ "content": "<script>alert('xss')</script>", }) // 如果需要输出原始 HTML,使用 template.HTML c.HTML(200, "index.html", gin.H{ "content": template.HTML("<div>Safe HTML</div>"), })}5.2 CSRF 保护import "github.com/utrack/gin-csrf"func main() { r := gin.Default() // 配置 CSRF 中间件 r.Use(csrf.New(csrf.Options{ Secret: "csrf-secret-key", ErrorFunc: func(c *gin.Context) { c.String(400, "CSRF token mismatch") }, })) r.GET("/form", func(c *gin.Context) { c.HTML(200, "form.html", gin.H{ "csrf": csrf.GetToken(c), }) }) r.POST("/submit", func(c *gin.Context) { // 处理表单提交 }) r.Run(":8080")}6. 响应式设计支持6.1 移动端检测func isMobile(c *gin.Context) bool { userAgent := c.GetHeader("User-Agent") mobileRegex := regexp.MustCompile(`(Android|iPhone|iPad|iPod)`) return mobileRegex.MatchString(userAgent)}func renderResponsive(c *gin.Context) { templateName := "index.html" if isMobile(c) { templateName = "mobile.html" } c.HTML(200, templateName, gin.H{ "isMobile": isMobile(c), })}7. 最佳实践总结模板管理使用模板继承减少重复代码合理组织模板目录结构使用自定义模板函数提高复用性静态文件启用 gzip 压缩设置合理的缓存策略使用 CDN 加速静态资源安全性默认转义 HTML 防止 XSS实现 CSRF 保护验证和过滤用户输入性能优化使用模板缓存压缩静态资源实现资源版本控制开发体验支持热重载提供清晰的错误信息使用模板调试工具通过以上方法,可以在 Gin 框架中高效地实现模板渲染和静态文件服务。
阅读 0·2月21日 15:19

Expo应用的测试策略有哪些?如何进行单元测试和端到端测试?

Expo应用的测试是确保代码质量和应用稳定性的重要环节。Expo支持多种测试框架和工具,从单元测试到端到端测试都有完善的解决方案。测试框架选择:Jest(单元测试和集成测试)Jest是Expo默认的测试框架,适合单元测试和组件测试。安装和配置:npm install --save-dev jest @testing-library/react-native @testing-library/jest-nativejest.config.js配置:module.exports = { preset: 'jest-expo', setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], transformIgnorePatterns: [ 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)' ], testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'],};单元测试示例:// __tests__/utils.test.tsimport { formatDate, calculateTotal } from '../utils';describe('Utils', () => { describe('formatDate', () => { it('should format date correctly', () => { const date = new Date('2024-01-15'); expect(formatDate(date)).toBe('2024-01-15'); }); it('should handle null date', () => { expect(formatDate(null)).toBe(''); }); }); describe('calculateTotal', () => { it('should calculate total correctly', () => { const items = [{ price: 10 }, { price: 20 }]; expect(calculateTotal(items)).toBe(30); }); });});组件测试示例:// __tests__/components/Button.test.tsximport { render, fireEvent } from '@testing-library/react-native';import Button from '../components/Button';describe('Button', () => { it('renders correctly', () => { const { getByText } = render(<Button title="Click me" />); expect(getByText('Click me')).toBeTruthy(); }); it('calls onPress when pressed', () => { const onPress = jest.fn(); const { getByText } = render( <Button title="Click me" onPress={onPress} /> ); fireEvent.press(getByText('Click me')); expect(onPress).toHaveBeenCalledTimes(1); }); it('disables button when disabled prop is true', () => { const { getByText } = render( <Button title="Click me" disabled /> ); const button = getByText('Click me'); expect(button.props.disabled).toBe(true); });});Detox(端到端测试)Detox是灰盒端到端测试框架,适合测试完整的用户流程。安装和配置:npm install --save-dev detox detox-clidetox initdetox.config.js配置:module.exports = { testRunner: { args: { '$0': 'jest', config: 'e2e/config.json', }, jest: { setupTimeout: 120000, }, }, apps: { 'ios.debug': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/ExpoApp.app', build: 'xcodebuild -workspace ios/ExpoApp.xcworkspace -scheme ExpoApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', }, 'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..', }, }, devices: { simulator: { type: 'ios.simulator', device: { type: 'iPhone 14' }, }, emulator: { type: 'android.emulator', device: { avdName: 'Pixel_5_API_33' }, }, }, configurations: { 'ios.sim.debug': { device: 'simulator', app: 'ios.debug', }, 'android.emu.debug': { device: 'emulator', app: 'android.debug', }, },};端到端测试示例:// e2e/login.e2e.tsdescribe('Login Flow', () => { beforeAll(async () => { await device.launchApp(); }); beforeEach(async () => { await device.reloadReactNative(); }); it('should login successfully with valid credentials', async () => { await element(by.id('email-input')).typeText('user@example.com'); await element(by.id('password-input')).typeText('password123'); await element(by.id('login-button')).tap(); await expect(element(by.id('welcome-screen'))).toBeVisible(); }); it('should show error with invalid credentials', async () => { await element(by.id('email-input')).typeText('invalid@example.com'); await element(by.id('password-input')).typeText('wrongpassword'); await element(by.id('login-button')).tap(); await expect(element(by.text('Invalid credentials'))).toBeVisible(); });});React Native Testing Library专注于测试用户行为,而不是实现细节。安装:npm install --save-dev @testing-library/react-native @testing-library/jest-native使用示例:import { render, fireEvent, waitFor } from '@testing-library/react-native';import LoginForm from '../components/LoginForm';describe('LoginForm', () => { it('should submit form with valid data', async () => { const onSubmit = jest.fn(); const { getByPlaceholderText, getByText } = render( <LoginForm onSubmit={onSubmit} /> ); fireEvent.changeText( getByPlaceholderText('Email'), 'user@example.com' ); fireEvent.changeText( getByPlaceholderText('Password'), 'password123' ); fireEvent.press(getByText('Login')); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ email: 'user@example.com', password: 'password123', }); }); });});测试最佳实践:测试金字塔单元测试:70%集成测试:20%端到端测试:10%测试命名// 清晰的测试描述describe('User Component', () => { it('should display user name when user data is provided', () => { // 测试代码 }); it('should show loading state when fetching user data', () => { // 测试代码 }); it('should display error message when fetch fails', () => { // 测试代码 });});Mock和Stub// Mock API调用jest.mock('../api/user', () => ({ fetchUser: jest.fn(),}));import { fetchUser } from '../api/user';describe('UserScreen', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should fetch and display user', async () => { const mockUser = { id: 1, name: 'John' }; (fetchUser as jest.Mock).mockResolvedValue(mockUser); // 测试代码 });});测试异步代码it('should handle async operations', async () => { const { getByText, findByText } = render(<AsyncComponent />); // 等待异步操作完成 await findByText('Loaded Data'); expect(getByText('Loaded Data')).toBeTruthy();});快照测试it('should match snapshot', () => { const tree = renderer.create(<MyComponent />).toJSON(); expect(tree).toMatchSnapshot();});CI/CD集成:GitHub Actions配置name: Testson: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: '18' - run: npm ci - run: npm test -- --coverage - uses: codecov/codecov-action@v2测试覆盖率{ "collectCoverage": true, "coverageReporters": ["text", "lcov", "html"], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } }}常见测试场景:导航测试import { NavigationContainer } from '@react-navigation/native';const renderWithNavigation = (component) => { return render( <NavigationContainer> {component} </NavigationContainer> );};状态管理测试import { renderHook, act } from '@testing-library/react-hooks';import { useUserStore } from '../store/user';it('should update user state', () => { const { result } = renderHook(() => useUserStore()); act(() => { result.current.setUser({ name: 'John' }); }); expect(result.current.user.name).toBe('John');});网络请求测试import { rest } from 'msw';import { setupServer } from 'msw/node';const server = setupServer( rest.get('/api/user', (req, res, ctx) => { return res(ctx.json({ id: 1, name: 'John' })); }));beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());通过建立完善的测试体系,可以显著提高Expo应用的质量和可维护性。
阅读 0·2月21日 15:19

如何在 Astro 项目中进行测试?如何使用 Vitest、Playwright 等测试框架?

Astro 的测试策略对于确保代码质量和应用稳定性至关重要。了解如何在 Astro 项目中进行单元测试、集成测试和端到端测试是开发者必备的技能。测试框架选择:Vitest(推荐):与 Vite 深度集成快速的测试执行支持 TypeScriptJest:成熟的测试框架丰富的生态系统广泛使用Playwright:端到端测试跨浏览器支持现代化的 API安装测试依赖:# 安装 Vitestnpm install -D vitest @vitest/ui# 安装测试工具npm install -D @testing-library/react @testing-library/vue @testing-library/svelte# 安装 Playwrightnpm install -D @playwright/test配置测试环境:// vitest.config.tsimport { defineConfig } from 'vitest/config';import astro from 'astro/vitest';export default defineConfig({ plugins: [astro()], test: { environment: 'jsdom', globals: true, setupFiles: ['./src/test/setup.ts'], },});// src/test/setup.tsimport { expect, afterEach } from 'vitest';import { cleanup } from '@testing-library/react';// 清理测试环境afterEach(() => { cleanup();});// 扩展 expectexpect.extend({});单元测试 Astro 组件:// src/components/__tests__/Button.astro.test.tsimport { describe, it, expect } from 'vitest';import { render } from '@testing-library/react';import Button from '../Button.astro';describe('Button Component', () => { it('renders button with correct text', () => { const { getByText } = render(Button, { props: { text: 'Click me' }, }); expect(getByText('Click me')).toBeInTheDocument(); }); it('applies correct variant class', () => { const { container } = render(Button, { props: { variant: 'primary' }, }); const button = container.querySelector('button'); expect(button).toHaveClass('btn-primary'); });});测试 React 组件:// src/components/__tests__/Counter.test.tsximport { describe, it, expect, vi } from 'vitest';import { render, screen, fireEvent } from '@testing-library/react';import userEvent from '@testing-library/user-event';import Counter from '../Counter';describe('Counter Component', () => { it('renders initial count', () => { render(<Counter initialCount={0} />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('increments count when button is clicked', async () => { const user = userEvent.setup(); render(<Counter initialCount={0} />); const button = screen.getByRole('button', { name: 'Increment' }); await user.click(button); expect(screen.getByText('Count: 1')).toBeInTheDocument(); });});测试 Vue 组件:// src/components/__tests__/TodoList.test.tsimport { describe, it, expect } from 'vitest';import { mount } from '@vue/test-utils';import TodoList from '../TodoList.vue';describe('TodoList Component', () => { it('renders todo items', () => { const todos = [ { id: 1, text: 'Learn Astro', completed: false }, { id: 2, text: 'Build app', completed: true }, ]; const wrapper = mount(TodoList, { props: { todos }, }); expect(wrapper.findAll('.todo-item')).toHaveLength(2); expect(wrapper.text()).toContain('Learn Astro'); }); it('emits complete event when checkbox is clicked', async () => { const wrapper = mount(TodoList, { props: { todos: [{ id: 1, text: 'Task', completed: false }] }, }); await wrapper.find('input[type="checkbox"]').setValue(true); expect(wrapper.emitted('complete')).toBeTruthy(); expect(wrapper.emitted('complete')[0]).toEqual([1]); });});测试 API 路由:// src/pages/api/__tests__/users.test.tsimport { describe, it, expect, beforeEach, vi } from 'vitest';import { GET, POST } from '../users';describe('Users API', () => { beforeEach(() => { vi.clearAllMocks(); }); it('GET returns list of users', async () => { const request = new Request('http://localhost/api/users'); const response = await GET({ request } as any); const data = await response.json(); expect(response.status).toBe(200); expect(data).toHaveProperty('users'); expect(Array.isArray(data.users)).toBe(true); }); it('POST creates new user', async () => { const userData = { name: 'John Doe', email: 'john@example.com' }; const request = new Request('http://localhost/api/users', { method: 'POST', body: JSON.stringify(userData), }); const response = await POST({ request } as any); const data = await response.json(); expect(response.status).toBe(201); expect(data).toHaveProperty('id'); expect(data.name).toBe(userData.name); });});测试内容集合:// src/content/__tests__/blog.test.tsimport { describe, it, expect } from 'vitest';import { getCollection } from 'astro:content';describe('Blog Content Collection', () => { it('has required frontmatter fields', async () => { const posts = await getCollection('blog'); posts.forEach(post => { expect(post.data).toHaveProperty('title'); expect(post.data).toHaveProperty('publishDate'); expect(post.data).toHaveProperty('description'); }); }); it('has valid publish dates', async () => { const posts = await getCollection('blog'); posts.forEach(post => { expect(post.data.publishDate).toBeInstanceOf(Date); expect(post.data.publishDate.getTime()).not.toBeNaN(); }); });});端到端测试(Playwright):// e2e/home.spec.tsimport { test, expect } from '@playwright/test';test.describe('Home Page', () => { test('loads successfully', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/My Astro App/); await expect(page.locator('h1')).toContainText('Welcome'); }); test('navigation works', async ({ page }) => { await page.goto('/'); await page.click('text=About'); await expect(page).toHaveURL(/\/about/); await expect(page.locator('h1')).toContainText('About Us'); }); test('form submission', async ({ page }) => { await page.goto('/contact'); await page.fill('input[name="name"]', 'John Doe'); await page.fill('input[name="email"]', 'john@example.com'); await page.fill('textarea[name="message"]', 'Hello!'); await page.click('button[type="submit"]'); await expect(page.locator('.success-message')).toBeVisible(); });});测试中间件:// src/middleware/__tests__/auth.test.tsimport { describe, it, expect, vi } from 'vitest';import { onRequest } from '../middleware';describe('Auth Middleware', () => { it('redirects to login without token', async () => { const request = new Request('http://localhost/dashboard'); const redirectSpy = vi.fn(); await onRequest({ request, redirect: redirectSpy } as any); expect(redirectSpy).toHaveBeenCalledWith('/login'); }); it('allows access with valid token', async () => { const request = new Request('http://localhost/dashboard', { headers: { 'Authorization': 'Bearer valid-token' }, }); const nextSpy = vi.fn().mockResolvedValue(new Response()); await onRequest({ request, next: nextSpy } as any); expect(nextSpy).toHaveBeenCalled(); });});测试配置脚本:// package.json{ "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:run": "vitest run", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed" }}测试覆盖率:// vitest.config.tsimport { defineConfig } from 'vitest/config';import astro from 'astro/vitest';export default defineConfig({ plugins: [astro()], test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'src/test/', '**/*.d.ts', '**/*.config.*', '**/mockData', ], }, },});最佳实践:测试金字塔:大量单元测试适量集成测试少量端到端测试测试组织:按功能组织测试使用清晰的测试名称保持测试独立Mock 和 Stub:隔离外部依赖使用 vi.mock() 模拟模块提供一致的测试数据持续集成:在 CI 中运行测试设置测试覆盖率阈值自动化测试报告测试性能:使用测试缓存并行运行测试优化测试执行时间Astro 的测试生态系统提供了全面的测试支持,帮助开发者构建可靠的应用。
阅读 0·2月21日 15:18

Serverless 架构下的测试策略有哪些?

Serverless 架构下的测试策略需要考虑函数的无状态特性、外部依赖和冷启动等因素:测试类型:1. 单元测试测试框架:使用 Jest、Mocha、pytest 等测试框架Mock 外部依赖:Mock 数据库、API 等外部依赖测试覆盖率:确保关键逻辑有充分的测试覆盖2. 集成测试本地测试:使用 SAM CLI、Serverless Framework 本地运行函数测试环境:在独立的测试环境中进行集成测试端到端测试:测试完整的业务流程3. 性能测试冷启动测试:测试函数的冷启动时间并发测试:测试函数在高并发下的表现负载测试:测试函数在持续负载下的稳定性测试工具:1. 本地测试工具SAM CLI:AWS 官方的本地测试工具Serverless Offline:Serverless Framework 的本地模拟插件Docker:使用 Docker 容器模拟云端环境2. 测试框架Jest:JavaScript/TypeScript 测试框架Pytest:Python 测试框架Junit:Java 测试框架3. Mock 工具AWS SDK Mock:Mock AWS SDK 调用Nock:Mock HTTP 请求Sinon:JavaScript Mock 库测试最佳实践:1. 测试隔离独立测试:每个测试用例独立运行,不相互影响测试数据:使用测试数据,不影响生产数据环境隔离:使用独立的测试环境2. 持续集成自动化测试:在 CI/CD 流程中自动运行测试测试报告:生成测试报告,便于查看测试结果失败告警:测试失败时及时告警3. 测试覆盖率覆盖率目标:设置合理的测试覆盖率目标覆盖率报告:定期生成覆盖率报告持续改进:根据覆盖率报告持续改进测试面试者应能分享实际项目中的测试经验和最佳实践。
阅读 0·2月21日 15:18

Qwik 中的 `$` 符号有什么作用?

Qwik 的 $ 符号是其架构的核心,它不仅仅是一个命名约定,而是编译器处理代码的重要标识。理解 $ 符号的作用对于掌握 Qwik 至关重要。1. $ 符号的核心作用$ 符号告诉 Qwik 编译器这是一个需要特殊处理的函数或组件,编译器会对其进行代码分割、序列化和懒加载处理。2. $ 符号的使用场景component$ - 组件定义import { component$ } from '@builder.io/qwik';export const MyComponent = component$(() => { return <div>Hello Qwik</div>;});作用:标识这是一个 Qwik 组件编译器会自动将组件代码分割成独立的 chunk组件默认是懒加载的useSignal / useStore - 状态管理import { useSignal, useStore } from '@builder.io/qwik';export const Counter = component$(() => { const count = useSignal(0); // 不需要 $ const user = useStore({ // 不需要 $ name: 'John', age: 30 }); return <div>{count.value}</div>;});注意:useSignal 和 useStore 本身不需要 $,但它们创建的状态对象会被编译器特殊处理。onClick$ / onInput$ - 事件处理export const Button = component$(() => { const handleClick$ = () => { console.log('Clicked!'); }; return <button onClick$={handleClick$}>Click me</button>;});作用:标识这是一个可恢复的事件处理函数编译器会将事件处理函数独立分割只在用户触发事件时才加载和执行useTask$ / useVisibleTask$ - 生命周期export const DataComponent = component$(() => { useTask$(() => { console.log('Component mounted or updated'); }); useVisibleTask$(() => { console.log('Component is visible'); }); return <div>Data Component</div>;});作用:标识这是一个生命周期钩子useTask$ 在服务器和客户端都会执行useVisibleTask$ 只在客户端执行useResource$ - 异步数据export const UserList = component$(() => { const users = useResource$(({ track }) => { track(() => /* 依赖项 */); return fetch('https://api.example.com/users'); }); return ( <div> {users.value?.map(user => <div key={user.id}>{user.name}</div>)} </div> );});作用:标识这是一个异步数据获取函数编译器会处理加载状态和错误状态支持依赖追踪和重新获取action$ - 服务端操作import { action$ } from '@builder.io/qwik-city';export const useSubmitForm = action$(async (data, { requestEvent }) => { // 服务端逻辑 return { success: true };});作用:标识这是一个服务端操作编译器会自动处理表单提交和响应支持类型安全的数据验证3. $ 符号的编译器处理代码分割编译器会自动将带有 $ 的函数分割成独立的文件:// 原始代码export const App = component$(() => { const handleClick$ = () => { console.log('Clicked'); }; return <button onClick$={handleClick$}>Click</button>;});// 编译后的结构// App.js - 组件代码// handleClick.js - 事件处理函数(独立文件)序列化编译器会将函数引用序列化到 HTML 中:<!-- 编译后的 HTML --><button data-qwik="..." onClick$="./handleClick.js#handleClick"> Click</button>懒加载编译器会生成懒加载逻辑,只在需要时加载代码:// 自动生成的懒加载代码function loadHandler() { return import('./handleClick.js').then(m => m.handleClick);}4. $ 符号的命名约定组件名称// 推荐export const MyComponent = component$(() => {});// 不推荐(但有效)export const myComponent = component$(() => {});事件处理函数// 推荐const handleClick$ = () => {};const handleSubmit$ = () => {};// 不推荐(但有效)const handle_click$ = () => {};const clickHandler$ = () => {};生命周期函数// 推荐useTask$(() => {});useVisibleTask$(() => {});// 这些是内置函数,不需要自定义命名5. 常见错误和注意事项忘记使用 $// 错误:事件处理函数没有使用 $export const Button = component$(() => { const handleClick = () => { // 缺少 $ console.log('Clicked'); }; return <button onClick={handleClick}>Click</button>; // 错误});// 正确export const Button = component$(() => { const handleClick$ = () => { // 使用 $ console.log('Clicked'); }; return <button onClick$={handleClick$}>Click</button>; // 正确});混淆 $ 的使用位置// 错误:在 JSX 属性中错误使用 $export const Button = component$(() => { return <button onClick$={() => console.log('Clicked')}>Click</button>; // 内联箭头函数不应该使用 $});// 正确export const Button = component$(() => { const handleClick$ = () => { console.log('Clicked'); }; return <button onClick$={handleClick$}>Click</button>;});6. $ 符号的底层原理编译时转换Qwik 编译器在编译时会:识别所有带有 $ 的函数将这些函数提取到独立文件生成序列化元数据创建懒加载逻辑更新函数引用运行时恢复在运行时,Qwik 会:从 HTML 中读取序列化元数据按需加载对应的 JavaScript 文件恢复函数的执行上下文执行函数逻辑总结:$ 符号是 Qwik 架构的核心,它通过编译时优化实现了自动的代码分割、序列化和懒加载。理解 $ 符号的作用对于编写高性能的 Qwik 应用至关重要。
阅读 0·2月21日 15:18