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

前端面试题手册

什么是 Rspack,它与 Webpack 有什么区别?

Rspack 是一个基于 Rust 语言开发的高性能前端构建工具,旨在提供比传统 Webpack 更快的构建速度和更好的开发体验。它利用 Rust 的高性能和安全特性,实现了极致的构建性能,同时保持了与 Webpack 生态的兼容性。Rspack 的核心特点包括:高性能构建:使用 Rust 编写,利用 Rust 的零成本抽象和内存安全特性,大幅提升构建速度。相比 Webpack,Rspack 在大型项目中可以实现 10-100 倍的构建速度提升。Webpack 兼容:Rspack 设计时充分考虑了与 Webpack 的兼容性,支持大部分 Webpack 的配置和插件,开发者可以无缝迁移现有项目。模块热更新(HMR):提供快速的 HMR 支持,在开发过程中实现毫秒级的热更新,提升开发效率。代码分割:支持智能代码分割,自动识别公共依赖,优化打包体积,提升应用加载性能。Tree Shaking:实现高效的 Tree Shaking,自动移除未使用的代码,减少最终打包体积。增量构建:支持增量构建,只重新构建发生变化的模块,进一步提升构建速度。TypeScript 支持:内置 TypeScript 支持,无需额外配置即可处理 TypeScript 文件。CSS 处理:提供强大的 CSS 处理能力,支持 CSS Modules、PostCSS 等。Rspack 的架构设计使其能够充分利用多核 CPU 的优势,通过并行处理构建任务,显著提升构建效率。同时,Rspack 的插件系统设计灵活,开发者可以轻松扩展其功能。在实际应用中,Rspack 特别适合大型前端项目和需要快速构建的场景,能够显著缩短构建时间,提升开发体验。
阅读 0·2月21日 15:35

Rspack 如何支持 TypeScript?

Rspack 对 TypeScript 提供了原生支持,这使得开发者无需额外配置即可处理 TypeScript 文件,大大简化了开发流程。以下是 Rspack TypeScript 支持的详细说明:原生 TypeScript 支持Rspack 内置了 TypeScript 支持,这意味着:无需额外 Loader:不需要安装 ts-loader 或其他 TypeScript loader直接导入 .ts 和 .tsx 文件即可自动处理 TypeScript 编译类型检查:支持类型检查(可选)可以配置是否进行类型检查提供类型错误提示类型声明文件:支持 .d.ts 类型声明文件自动解析类型声明提供完整的类型支持基本配置最小配置module.exports = { entry: './src/index.ts', module: { rules: [ { test: /\.ts$/, use: 'builtin:swc-loader', type: 'javascript/auto' } ] }}完整配置module.exports = { entry: './src/index.tsx', module: { rules: [ { test: /\.(ts|tsx)$/, use: { loader: 'builtin:swc-loader', options: { jsc: { parser: { syntax: 'typescript', tsx: true }, transform: { react: { runtime: 'automatic' } } } } }, type: 'javascript/auto' } ] }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }}SWC LoaderRspack 使用内置的 SWC Loader 来处理 TypeScript,SWC 是一个用 Rust 编写的超快 TypeScript/JavaScript 编译器:SWC 的优势极快的编译速度:比 Babel 快 20-70 倍比 tsc 快 10-30 倍显著提升构建速度完整的 TypeScript 支持:支持所有 TypeScript 语法支持最新的 ECMAScript 特性兼容 TypeScript 配置低内存占用:比 Babel 占用更少内存适合大型项目更好的资源利用率SWC 配置选项{ loader: 'builtin:swc-loader', options: { jsc: { parser: { syntax: 'typescript', tsx: true, decorators: true, dynamicImport: true }, transform: { react: { runtime: 'automatic', importSource: '@emotion/react' }, optimizer: { globals: { vars: { 'process.env.NODE_ENV': 'production' } } } }, target: 'es2015', loose: false, externalHelpers: true }, env: { targets: 'defaults', coreJs: 3 }, sourceMaps: true, inlineSourcesContent: true }}tsconfig.json 集成Rspack 可以读取和使用 tsconfig.json 配置:{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "jsx": "react-jsx", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["src"], "exclude": ["node_modules"]}类型检查类型检查配置Rspack 支持两种类型检查方式:构建时类型检查: module.exports = { builtins: { pluginImport: [ { libraryName: 'lodash', libraryDirectory: '', camel2DashComponentName: false } ] }, plugins: [ new rspack.ForkTsCheckerWebpackPlugin({ typescript: { configFile: './tsconfig.json', memoryLimit: 4096 } }) ] }独立类型检查:使用 tsc --noEmit 单独运行类型检查在 CI/CD 流程中集成分离类型检查和构建过程类型检查最佳实践开发环境:可以在开发时禁用类型检查以提升速度使用编辑器的类型检查功能保存时进行快速类型检查构建环境:在生产构建时启用完整类型检查确保代码质量阻止类型错误的代码部署CI/CD:在持续集成中运行类型检查作为代码质量门禁结合其他检查工具React + TypeScriptRspack 对 React + TypeScript 提供了完整的支持:module.exports = { module: { rules: [ { test: /\.(ts|tsx)$/, use: { loader: 'builtin:swc-loader', options: { jsc: { parser: { syntax: 'typescript', tsx: true }, transform: { react: { runtime: 'automatic', importSource: '@emotion/react' } } } } }, type: 'javascript/auto' } ] }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'] }}性能优化增量编译:利用 Rspack 的增量构建能力只重新编译变化的 TypeScript 文件大幅提升开发体验类型缓存:缓存类型检查结果避免重复的类型检查提升构建速度并行处理:并行处理多个 TypeScript 文件充分利用多核 CPU缩短构建时间常见问题类型声明文件找不到:确保 @types 包已安装检查 tsconfig.json 配置验证类型声明文件路径类型检查错误:检查 tsconfig.json 配置确保类型定义正确使用 // @ts-ignore 或 // @ts-expect-error 临时忽略编译速度慢:确保使用 SWC Loader启用增量构建优化 tsconfig.json 配置Rspack 的 TypeScript 支持为开发者提供了快速、高效的开发体验,通过合理的配置和优化,可以充分发挥 TypeScript 的类型安全优势,同时保持极快的构建速度。
阅读 0·2月21日 15:35

Rspack 如何处理 CSS?

Rspack 的 CSS 处理能力是其前端构建功能的重要组成部分,提供了强大的 CSS 处理和优化功能。以下是 Rspack CSS 处理的详细说明:CSS 处理方式Rspack 提供了多种 CSS 处理方式,可以根据项目需求选择合适的方案:CSS Modules:支持模块化的 CSS自动生成唯一的类名避免样式冲突CSS-in-JS:支持各种 CSS-in-JS 库如 styled-components、emotion 等保持样式和组件的紧密关联原生 CSS:支持标准 CSS 文件支持 CSS 预处理器支持 PostCSSCSS 配置基本配置module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] }}提取 CSS 到单独文件const MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = { module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash].css', chunkFilename: 'css/[id].[contenthash].css' }) ]}CSS Modules 配置module.exports = { module: { rules: [ { test: /\.module\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]' } } } ] } ] }}CSS 预处理器支持Rspack 支持多种 CSS 预处理器:Sass/SCSSmodule.exports = { module: { rules: [ { test: /\.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] } ] }}Lessmodule.exports = { module: { rules: [ { test: /\.less$/, use: [ 'style-loader', 'css-loader', 'less-loader' ] } ] }}Stylusmodule.exports = { module: { rules: [ { test: /\.styl$/, use: [ 'style-loader', 'css-loader', 'stylus-loader' ] } ] }}PostCSS 集成PostCSS 是一个强大的 CSS 处理工具,Rspack 可以轻松集成:module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader', { loader: 'postcss-loader', options: { postcssOptions: { plugins: [ require('autoprefixer'), require('cssnano') ] } } } ] } ] }}PostCSS 配置文件创建 postcss.config.js:module.exports = { plugins: [ require('autoprefixer')({ overrideBrowserslist: ['> 1%', 'last 2 versions'] }), require('cssnano')({ preset: 'default' }) ]}CSS 优化Rspack 提供了多种 CSS 优化功能:代码压缩:使用 cssnano 压缩 CSS移除注释和空格优化选择器去重:移除重复的样式规则合并相同的声明减少最终体积Tree Shaking:移除未使用的 CSS分析 JavaScript 中的类名引用只保留使用的样式CSS 优化配置const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin({ minimizerOptions: { preset: [ 'default', { discardComments: { removeAll: true }, normalizeWhitespace: false } ] } }) ] }}CSS-in-JS 支持Rspack 对各种 CSS-in-JS 库提供了良好的支持:styled-components// 安装依赖// npm install styled-components babel-plugin-styled-components// 配置module.exports = { module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, use: { loader: 'builtin:swc-loader', options: { jsc: { transform: { react: { runtime: 'automatic' } } } } } } ] }}emotion// 安装依赖// npm install @emotion/react @emotion/styled// 配置module.exports = { module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, use: { loader: 'builtin:swc-loader', options: { jsc: { transform: { react: { runtime: 'automatic', importSource: '@emotion/react' } } } } } } ] }}CSS 加载优化按需加载:使用动态导入加载 CSS减少初始加载的 CSS 体积提升首屏加载速度关键 CSS:提取关键 CSS 内联到 HTML非关键 CSS 异步加载优化渲染性能CSS 缓存:使用 contenthash 生成文件名利用浏览器缓存减少重复下载最佳实践选择合适的 CSS 方案:小型项目:使用原生 CSS 或 CSS Modules中型项目:考虑使用 CSS Modules 或 CSS-in-JS大型项目:推荐使用 CSS-in-JS 或设计系统优化 CSS 体积:移除未使用的 CSS压缩 CSS 文件合并重复的样式提升加载性能:按需加载 CSS内联关键 CSS使用 CDN 加速维护性考虑:使用 CSS Modules 避免冲突使用预处理器提高可维护性建立统一的 CSS 规范Rspack 的 CSS 处理功能为开发者提供了灵活而强大的样式处理能力,通过合理的配置和优化,可以构建出高性能、易维护的前端应用。
阅读 0·2月21日 15:34

Rspack 的开发服务器(Dev Server)有哪些功能?

Rspack 的开发服务器(Dev Server)为开发者提供了强大的本地开发环境,支持热更新、代理、HTTPS 等功能。以下是 Rspack Dev Server 的详细说明:基本配置启动开发服务器// rspack.config.jsmodule.exports = { mode: 'development', devServer: { static: { directory: path.join(__dirname, 'public') }, compress: true, port: 9000 }}命令行启动# 开发模式启动npx rspack serve# 指定配置文件npx rspack serve --config rspack.config.js# 指定端口npx rspack serve --port 8080核心功能1. 模块热更新(HMR)Rspack Dev Server 内置了强大的 HMR 功能:module.exports = { devServer: { hot: true, // 启用 HMR liveReload: false // 禁用页面自动刷新 }}2. 静态文件服务module.exports = { devServer: { static: { directory: path.join(__dirname, 'public'), publicPath: '/', serveIndex: true, watch: true } }}3. 代理配置解决开发环境跨域问题:module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' } }, '/images': { target: 'http://example.com', changeOrigin: true } } }}4. HTTPS 支持module.exports = { devServer: { https: { key: fs.readFileSync('path/to/private.key'), cert: fs.readFileSync('path/to/certificate.pem'), ca: fs.readFileSync('path/to/ca.pem') } }}高级配置1. 压缩配置module.exports = { devServer: { compress: true, client: { overlay: { errors: true, warnings: false } } }}2. 开启 Gzipmodule.exports = { devServer: { compress: true, devMiddleware: { stats: 'errors-only' } }}3. 历史 API 回退module.exports = { devServer: { historyApiFallback: { index: '/index.html', rewrites: [ { from: /^\/api/, to: '/404.html' } ] } }}4. 打开浏览器module.exports = { devServer: { open: true, open: { app: { name: 'google chrome' } } }}性能优化1. 缓存配置module.exports = { devServer: { devMiddleware: { index: true, writeToDisk: false, stats: 'minimal' } }}2. 监听选项module.exports = { devServer: { watchFiles: { paths: ['src/**/*.php', 'public/**/*'], options: { usePolling: false, interval: 1000 } } }}3. 构建延迟module.exports = { devServer: { devMiddleware: { index: true, writeToDisk: false }, client: { logging: 'warn', overlay: { errors: true, warnings: false } } }}错误处理1. 错误覆盖module.exports = { devServer: { client: { overlay: { errors: true, warnings: true } } }}2. 错误日志module.exports = { devServer: { devMiddleware: { stats: { colors: true, hash: false, version: false, timings: true, assets: false, chunks: false, modules: false, reasons: false, children: false, source: false, errors: true, errorDetails: true, warnings: true, publicPath: false } } }}中间件配置Rspack Dev Server 支持自定义中间件:const express = require('express');const app = express();app.get('/some/path', function(req, res) { res.json({ custom: 'response' });});module.exports = { devServer: { setupMiddlewares: (middlewares, devServer) => { if (!devServer) { throw new Error('devServer is not defined'); } devServer.app.get('/some/path', function(req, res) { res.json({ custom: 'response' }); }); return middlewares; } }}WebSocket 配置module.exports = { devServer: { client: { webSocketURL: 'auto://0.0.0.0:0/ws' }, webSocketServer: { type: 'ws', options: { host: 'localhost', port: 8080 } } }}最佳实践环境分离:开发环境使用 Dev Server生产环境使用静态文件服务器配置不同的环境变量代理配置:合理配置代理解决跨域使用环境变量管理代理地址考虑使用 Mock 服务性能优化:启用 HMR 提升开发体验合理配置监听选项避免不必要的文件监听错误处理:启用错误覆盖快速定位问题配置合适的日志级别使用 source maps 调试安全考虑:开发环境不要暴露敏感信息使用 HTTPS 测试安全功能配置适当的 CORS 策略Rspack Dev Server 为开发者提供了功能强大、配置灵活的开发环境,通过合理配置可以极大提升开发效率和体验。
阅读 0·2月21日 15:34

Rspack 如何管理环境变量?

Rspack 的环境变量管理是前端开发中的重要功能,能够帮助开发者在不同环境下使用不同的配置。以下是 Rspack 环境变量管理的详细说明:环境变量基本概念环境变量是在构建时注入到代码中的变量,用于区分不同环境(开发、测试、生产)的配置。环境变量定义方式1. DefinePlugin使用 DefinePlugin 定义环境变量:const { DefinePlugin } = require('@rspack/core');module.exports = { plugins: [ new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), 'process.env.API_URL': JSON.stringify('https://api.example.com'), 'process.env.VERSION': JSON.stringify('1.0.0') }) ]}2. .env 文件使用 .env 文件管理环境变量:# .env.developmentNODE_ENV=developmentAPI_URL=http://localhost:3000DEBUG=true# .env.productionNODE_ENV=productionAPI_URL=https://api.example.comDEBUG=false3. 命令行参数通过命令行传递环境变量:# Unix/LinuxNODE_ENV=production API_URL=https://api.example.com npx rspack build# Windowsset NODE_ENV=production&& set API_URL=https://api.example.com&& npx rspack build# 使用 cross-env(跨平台)npx cross-env NODE_ENV=production API_URL=https://api.example.com npx rspack build环境变量加载1. dotenv-webpack-plugin使用 dotenv-webpack-plugin 加载 .env 文件:const Dotenv = require('dotenv-webpack');module.exports = { plugins: [ new Dotenv({ path: './.env.production', safe: true, systemvars: true, silent: true }) ]}2. 自定义环境变量加载const fs = require('fs');const path = require('path');function loadEnv(mode) { const envPath = path.resolve(__dirname, `.env.${mode}`); const envContent = fs.readFileSync(envPath, 'utf8'); const envVars = {}; envContent.split('\n').forEach(line => { const [key, value] = line.split('='); if (key && value) { envVars[key.trim()] = value.trim(); } }); return envVars;}const envVars = loadEnv(process.env.NODE_ENV || 'development');module.exports = { plugins: [ new DefinePlugin({ 'process.env': JSON.stringify(envVars) }) ]}环境变量使用1. 在代码中使用// 使用环境变量const apiUrl = process.env.API_URL;const isDevelopment = process.env.NODE_ENV === 'development';const version = process.env.VERSION;console.log('API URL:', apiUrl);console.log('Is Development:', isDevelopment);2. TypeScript 类型定义// env.d.tsdeclare namespace NodeJS { interface ProcessEnv { NODE_ENV: 'development' | 'production' | 'test'; API_URL: string; VERSION: string; DEBUG: string; }}环境配置1. 多环境配置const { merge } = require('webpack-merge');const commonConfig = require('./rspack.common.js');const devConfig = require('./rspack.dev.js');const prodConfig = require('./rspack.prod.js');module.exports = (env) => { const mode = env.mode || 'development'; if (mode === 'production') { return merge(commonConfig, prodConfig); } return merge(commonConfig, devConfig);};2. 环境特定配置// rspack.dev.jsmodule.exports = { mode: 'development', devtool: 'eval-cheap-module-source-map', devServer: { hot: true, port: 3000 }}// rspack.prod.jsmodule.exports = { mode: 'production', devtool: 'source-map', optimization: { minimize: true }}环境变量最佳实践1. 敏感信息处理// 不要在代码中硬编码敏感信息// ❌ 错误const API_KEY = 'your-secret-key';// ✅ 正确const API_KEY = process.env.API_KEY;// 使用 .env.local 存储敏感信息(不提交到版本控制)// .env.localAPI_KEY=your-secret-key2. 环境变量验证// 验证必需的环境变量const requiredEnvVars = ['API_URL', 'API_KEY'];requiredEnvVars.forEach(envVar => { if (!process.env[envVar]) { throw new Error(`Missing required environment variable: ${envVar}`); }});3. 默认值设置// 设置默认值const apiUrl = process.env.API_URL || 'http://localhost:3000';const timeout = parseInt(process.env.TIMEOUT || '5000', 10);const debug = process.env.DEBUG === 'true';环境变量与构建1. 条件构建module.exports = (env) => { const isProduction = env.mode === 'production'; return { mode: isProduction ? 'production' : 'development', plugins: [ new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(env.mode), 'process.env.IS_PRODUCTION': JSON.stringify(isProduction) }) ] };};2. 环境特定插件const isProduction = process.env.NODE_ENV === 'production';const plugins = [ new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) })];if (isProduction) { plugins.push( new TerserPlugin(), new CompressionPlugin() );}module.exports = { plugins};.env 文件管理1. 文件命名约定.env # 所有环境的默认值.env.local # 本地覆盖(不提交).env.development # 开发环境.env.test # 测试环境.env.production # 生产环境2. .gitignore 配置# 忽略所有 .env.local 文件.env.local.env.*.local# 可以提交其他环境配置.env.development.env.test.env.production3. 环境变量优先级命令行参数.env.local.env.[mode].local.env.[mode].env环境变量与 CI/CD1. GitHub Actionsname: Buildon: [push]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: npm install - name: Build env: NODE_ENV: production API_URL: ${{ secrets.API_URL }} run: npm run build2. Docker# DockerfileFROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .ARG NODE_ENV=productionARG API_URLENV NODE_ENV=$NODE_ENVENV API_URL=$API_URLRUN npm run build最佳实践安全性:不要在代码中硬编码敏感信息使用 .env.local 存储本地配置在 CI/CD 中使用 secrets可维护性:为不同环境创建独立的配置文件使用清晰的变量命名提供默认值类型安全:为 TypeScript 项目提供类型定义验证环境变量的有效性处理缺失的环境变量文档化:记录所有环境变量说明每个变量的用途提供示例配置Rspack 的环境变量管理为开发者提供了灵活的配置方式,通过合理使用环境变量,可以轻松管理不同环境的配置,提升开发效率和代码质量。
阅读 0·2月21日 15:34

Puppeteer 如何与测试框架集成?有哪些 E2E 测试和 CI/CD 集成的最佳实践?

Puppeteer 可以与各种测试框架集成,实现端到端测试、单元测试和集成测试。以下是常见的集成方式和最佳实践。1. 与 Jest 集成安装依赖:npm install --save-dev puppeteer jest jest-puppeteer @types/puppeteer配置 Jest:// jest.config.jsmodule.exports = { preset: 'jest-puppeteer', testMatch: ['**/*.test.js'], setupFilesAfterEnv: ['./jest.setup.js']};设置文件:// jest.setup.jsbeforeEach(async () => { await page.goto('http://localhost:3000');});编写测试:describe('Puppeteer with Jest', () => { test('page title', async () => { await expect(page.title()).resolves.toMatch('My App'); }); test('user can login', async () => { await page.type('#username', 'testuser'); await page.type('#password', 'password'); await page.click('#login-button'); await expect(page).toMatch('Welcome'); });});2. 与 Mocha 集成安装依赖:npm install --save-dev puppeteer mocha chai配置 Mocha:// mocha.config.jsmodule.exports = { timeout: 10000, require: ['mocha-setup.js']};设置文件:// mocha-setup.jsconst puppeteer = require('puppeteer');const { expect } = require('chai');let browser;let page;before(async () => { browser = await puppeteer.launch(); page = await browser.newPage();});after(async () => { await browser.close();});beforeEach(async () => { await page.goto('http://localhost:3000');});global.page = page;global.expect = expect;编写测试:describe('Puppeteer with Mocha', () => { it('should display correct title', async () => { const title = await page.title(); expect(title).to.equal('My App'); }); it('should allow user to login', async () => { await page.type('#username', 'testuser'); await page.type('#password', 'password'); await page.click('#login-button'); const welcomeText = await page.$eval('.welcome', el => el.textContent); expect(welcomeText).to.include('Welcome'); });});3. 与 Playwright 集成安装依赖:npm install --save-dev @playwright/test配置 Playwright:// playwright.config.jsmodule.exports = { testDir: './tests', use: { headless: true, screenshot: 'only-on-failure' }};编写测试:const { test, expect } = require('@playwright/test');test.describe('Puppeteer migration', () => { test('basic navigation', async ({ page }) => { await page.goto('http://localhost:3000'); await expect(page).toHaveTitle('My App'); }); test('form submission', async ({ page }) => { await page.goto('http://localhost:3000'); await page.fill('#username', 'testuser'); await page.fill('#password', 'password'); await page.click('#login-button'); await expect(page.locator('.welcome')).toBeVisible(); });});4. 与 Cypress 集成安装 Cypress:npm install --save-dev cypress配置 Cypress:// cypress.config.jsconst { defineConfig } = require('cypress');module.exports = defineConfig({ e2e: { baseUrl: 'http://localhost:3000', setupNodeEvents(on, config) { on('task', { puppeteer({ url, action }) { return require('./puppeteer-task')(url, action); } }); } }});Puppeteer 任务:// puppeteer-task.jsconst puppeteer = require('puppeteer');module.exports = async (url, action) => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); let result; if (action === 'screenshot') { result = await page.screenshot({ encoding: 'base64' }); } else if (action === 'pdf') { result = await page.pdf({ encoding: 'base64' }); } await browser.close(); return result;};Cypress 测试:// cypress/e2e/puppeteer.spec.jsdescribe('Puppeteer integration', () => { it('should take screenshot with Puppeteer', () => { cy.task('puppeteer', { url: 'http://localhost:3000', action: 'screenshot' }).then(screenshot => { cy.log('Screenshot taken'); }); });});5. 端到端测试最佳实践测试结构:describe('User Flow', () => { before(async () => { // 设置测试环境 await setupTestDatabase(); }); after(async () => { // 清理测试环境 await cleanupTestDatabase(); }); beforeEach(async () => { // 每个测试前的准备 await page.goto('http://localhost:3000'); }); test('complete user registration flow', async () => { // 测试步骤 await page.click('#register-button'); await page.type('#username', 'newuser'); await page.type('#email', 'newuser@example.com'); await page.type('#password', 'password123'); await page.click('#submit-button'); // 验证结果 await expect(page).toMatch('Registration successful'); });});测试数据管理:// test-data.jsmodule.exports = { validUser: { username: 'testuser', email: 'test@example.com', password: 'password123' }, invalidUser: { username: '', email: 'invalid-email', password: '123' }};// 使用测试数据const { validUser } = require('./test-data');test('register with valid data', async () => { await page.type('#username', validUser.username); await page.type('#email', validUser.email); await page.type('#password', validUser.password); await page.click('#submit-button'); await expect(page).toMatch('Success');});6. 视觉回归测试使用 Percy:npm install --save-dev @percy/puppeteerconst percy = require('@percy/puppeteer');describe('Visual regression tests', () => { beforeAll(async () => { await percy.start(); }); afterAll(async () => { await percy.stop(); }); test('homepage visual', async () => { await page.goto('http://localhost:3000'); await percy.snapshot(page, 'Homepage'); });});使用 BackstopJS:// backstop.config.jsmodule.exports = { scenarios: [ { label: 'Homepage', url: 'http://localhost:3000', selectors: ['#header', '#main', '#footer'] } ], paths: { bitmaps_reference: 'backstop_data/bitmaps_reference', bitmaps_test: 'backstop_data/bitmaps_test', html_report: 'backstop_data/html_report' }};7. 性能测试使用 Lighthouse:npm install --save-dev lighthouse puppeteerconst lighthouse = require('lighthouse');const puppeteer = require('puppeteer');describe('Performance tests', () => { test('page performance score', async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); const runnerResult = await lighthouse('http://localhost:3000', { port: new URL(browser.wsEndpoint()).port, output: 'json' }); const score = runnerResult.lhr.categories.performance.score * 100; expect(score).toBeGreaterThan(80); await browser.close(); });});8. 测试报告使用 Allure:npm install --save-dev allure-commandlineconst allure = require('allure-js-commons');describe('Allure reporting', () => { test('with Allure steps', async () => { const epic = new allure.Epic('User Management'); const feature = new allure.Feature('Registration'); const story = new allure.Story('User can register'); epic.addFeature(feature); feature.addStory(story); await page.click('#register-button'); await page.type('#username', 'testuser'); await page.click('#submit-button'); story.addStep('Click register button'); story.addStep('Enter username'); story.addStep('Submit form'); });});9. CI/CD 集成GitHub Actions 配置:name: Puppeteer Testson: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: '16' - name: Install dependencies run: npm ci - name: Install Chrome run: | sudo apt-get update sudo apt-get install -y chromium-browser - name: Run tests run: npm test env: CI: trueDocker 配置:FROM node:16-alpine# 安装 ChromeRUN apk add --no-cache chromium# 安装依赖COPY package*.json ./RUN npm ci# 复制测试文件COPY . .# 运行测试CMD ["npm", "test"]10. 最佳实践总结1. 测试隔离:// 每个测试使用独立的上下文beforeEach(async () => { const context = await browser.createIncognitoBrowserContext(); page = await context.newPage();});afterEach(async () => { await context.close();});2. 等待策略:// 使用明确的等待await page.waitForSelector('.element', { visible: true });// 避免硬编码延迟// await page.waitForTimeout(5000); // 不推荐3. 错误处理:test('with error handling', async () => { try { await page.click('#button'); } catch (error) { // 保存失败截图 await page.screenshot({ path: 'failure.png' }); throw error; }});4. 测试数据清理:afterEach(async () => { // 清理测试数据 await cleanupTestData();});5. 并行测试:// Jest 配置module.exports = { maxWorkers: 4, // 并行运行测试 preset: 'jest-puppeteer'};
阅读 0·2月19日 19:55

如何在 Jest 中测试 API 调用和网络请求?如何 Mock fetch 和 Axios?

在 Jest 中测试 API 调用和网络请求需要使用 Mock 来隔离外部依赖:1. Mock fetch API:global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ data: 'test' }) }));test('fetches data from API', async () => { const data = await fetchData(); expect(data).toEqual({ data: 'test' }); expect(fetch).toHaveBeenCalledTimes(1);});2. Mock Axios:import axios from 'axios';jest.mock('axios');test('fetches user data', async () => { const mockData = { name: 'John', age: 30 }; axios.get.mockResolvedValue({ data: mockData }); const result = await getUser(1); expect(result).toEqual(mockData); expect(axios.get).toHaveBeenCalledWith('/users/1');});3. Mock 模块导出:// api.jsexport const fetchData = () => fetch('/api/data');// api.test.jsimport { fetchData } from './api';import fetch from 'node-fetch';jest.mock('node-fetch');test('fetches data', async () => { fetch.mockResolvedValue({ json: () => ({ data: 'test' }) }); const result = await fetchData(); expect(result).toEqual({ data: 'test' });});4. 测试错误处理:test('handles API errors', async () => { axios.get.mockRejectedValue(new Error('Network Error')); await expect(getUser(1)).rejects.toThrow('Network Error');});5. 测试不同的响应状态:test('handles 404 error', async () => { axios.get.mockRejectedValue({ response: { status: 404, data: { message: 'Not found' } } }); await expect(getUser(1)).rejects.toMatchObject({ response: { status: 404 } });});6. 使用 MSW(Mock Service Worker):import { rest } from 'msw';import { setupServer } from 'msw/node';const server = setupServer( rest.get('/api/users', (req, res, ctx) => { return res(ctx.json({ name: 'John' })); }));beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());test('fetches user with MSW', async () => { const user = await fetchUser(); expect(user).toEqual({ name: 'John' });});最佳实践:使用 Mock 隔离外部 API 依赖测试成功和失败场景验证请求参数和响应处理清理 Mock 以避免测试污染考虑使用 MSW 进行更真实的 API 模拟
阅读 0·2月19日 19:55

如何在 Jest 中测试 React 组件?常用的测试工具和查询方法有哪些?

在 Jest 中测试 React 组件需要结合测试工具和渲染方法:常用测试工具:@testing-library/react:官方推荐的 React 测试库react-test-renderer:用于快照测试enzyme:传统的 React 组件测试工具(较少使用)基本测试示例:import { render, screen, fireEvent } from '@testing-library/react';import Button from './Button';test('renders button with text', () => { render(<Button>Click me</Button>); expect(screen.getByText('Click me')).toBeInTheDocument();});test('calls onClick when clicked', () => { const handleClick = jest.fn(); render(<Button onClick={handleClick}>Click me</Button>); fireEvent.click(screen.getByText('Click me')); expect(handleClick).toHaveBeenCalledTimes(1);});常用查询方法:getByText():通过文本查找元素getByRole():通过角色查找元素getByTestId():通过 data-testid 属性查找queryByText():查找元素,不存在时返回 nullfindByText():异步查找元素测试异步组件:test('loads and displays data', async () => { render(<UserList />); expect(screen.getByText('Loading...')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('John')).toBeInTheDocument(); });});最佳实践:测试用户行为,而不是实现细节使用 @testing-library/react 而不是 enzyme使用 data-testid 作为最后的选择避免测试内部状态,测试可见输出保持测试简单和可读
阅读 0·2月19日 19:53

Puppeteer 如何处理动态网页和单页应用(SPA)?有哪些处理异步加载和路由变化的技巧?

Puppeteer 在处理动态网页和单页应用(SPA)时具有独特的优势,可以执行 JavaScript、等待异步加载、处理路由变化等。1. 处理动态内容加载等待元素出现:const puppeteer = require('puppeteer');async function scrapeDynamicContent() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); // 等待动态加载的元素 await page.waitForSelector('.dynamic-content', { visible: true }); const content = await page.$eval('.dynamic-content', el => el.textContent); console.log(content); await browser.close();}scrapeDynamicContent();等待特定条件:await page.waitForFunction(() => { return document.querySelectorAll('.item').length > 0;});等待网络请求完成:await page.goto('https://example.com', { waitUntil: 'networkidle2'});2. 处理无限滚动基本无限滚动:async function scrapeInfiniteScroll() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com/infinite-scroll'); const items = []; let previousHeight = 0; while (true) { // 滚动到底部 await page.evaluate(() => { window.scrollBy(0, window.innerHeight); }); // 等待新内容加载 await page.waitForTimeout(1000); // 检查是否有新内容 const currentHeight = await page.evaluate(() => document.body.scrollHeight); if (currentHeight === previousHeight) { break; // 没有新内容了 } previousHeight = currentHeight; // 收集数据 const newItems = await page.$$eval('.item', elements => { return elements.map(el => el.textContent); }); items.push(...newItems); } await browser.close(); return items;}优化的无限滚动:async function scrapeInfiniteScrollOptimized() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com/infinite-scroll'); const items = []; let noNewItemsCount = 0; while (noNewItemsCount < 3) { // 连续 3 次没有新内容就停止 const itemCountBefore = items.length; // 滚动到底部 await page.evaluate(() => { window.scrollTo(0, document.body.scrollHeight); }); // 等待加载指示器消失 try { await page.waitForSelector('.loading', { hidden: true, timeout: 3000 }); } catch (error) { // 加载指示器可能不存在 } // 收集新数据 const newItems = await page.$$eval('.item', elements => { return elements.map(el => el.textContent); }); if (newItems.length === itemCountBefore) { noNewItemsCount++; } else { noNewItemsCount = 0; items.push(...newItems); } } await browser.close(); return items;}3. 处理 SPA 路由监听路由变化:async function handleSPARoutes() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); // 监听路由变化 page.on('framenavigated', async (frame) => { console.log('Navigated to:', frame.url()); // 等待页面内容加载 await frame.waitForSelector('.content'); const title = await frame.$eval('.content', el => el.textContent); console.log('Page title:', title); }); // 点击导航链接 await page.click('#about-link'); await page.waitForTimeout(1000); await page.click('#contact-link'); await page.waitForTimeout(1000); await browser.close();}等待特定路由:async function waitForRoute(page, path) { return new Promise((resolve) => { const checkRoute = async () => { const currentPath = await page.evaluate(() => window.location.pathname); if (currentPath === path) { resolve(); } else { setTimeout(checkRoute, 100); } }; checkRoute(); });}// 使用await page.click('#about-link');await waitForRoute(page, '/about');4. 处理 AJAX 请求等待特定 API 响应:async function waitForAPIResponse(page, urlPattern) { return new Promise((resolve) => { page.on('response', (response) => { if (response.url().includes(urlPattern)) { resolve(response); } }); });}// 使用const apiResponse = await Promise.all([ waitForAPIResponse(page, '/api/data'), page.click('#load-data-button')]);const data = await apiResponse.json();console.log(data);拦截和修改 API 请求:await page.setRequestInterception(true);page.on('request', (request) => { if (request.url().includes('/api/data')) { // 修改请求 request.continue({ headers: { ...request.headers(), 'Authorization': 'Bearer token' } }); } else { request.continue(); }});5. 处理 WebSocket监听 WebSocket 消息:const client = await page.target().createCDPSession();await client.send('Network.enable');client.on('Network.webSocketFrameReceived', (params) => { console.log('WebSocket message:', params.response.payloadData);});client.on('Network.webSocketFrameSent', (params) => { console.log('WebSocket sent:', params.response.payloadData);});6. 处理客户端渲染等待客户端渲染完成:async function waitForClientRendering(page) { // 方法 1:等待特定元素 await page.waitForSelector('.rendered-content'); // 方法 2:等待渲染标志 await page.waitForFunction(() => { return window.__RENDER_COMPLETE__ === true; }); // 方法 3:等待网络空闲 await page.waitForFunction(() => { return performance.getEntriesByType('resource').length > 0; });}处理 React/Vue 应用:async function scrapeReactApp() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com/react-app'); // 等待 React 应用挂载 await page.waitForSelector('#root'); // 等待数据加载完成 await page.waitForFunction(() => { return window.__INITIAL_STATE__?.loaded === true; }); // 与 React 应用交互 await page.click('#load-more-button'); await page.waitForSelector('.new-items'); const items = await page.$$eval('.item', elements => { return elements.map(el => el.textContent); }); await browser.close(); return items;}7. 实际应用场景场景 1:抓取社交媒体动态内容async function scrapeSocialMediaPosts(username) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(`https://social-media.com/${username}`); const posts = []; // 滚动加载更多帖子 while (posts.length < 50) { // 滚动到底部 await page.evaluate(() => { window.scrollBy(0, window.innerHeight); }); // 等待新帖子加载 await page.waitForTimeout(2000); // 收集帖子数据 const newPosts = await page.$$eval('.post', elements => { return elements.map(post => ({ id: post.dataset.id, content: post.querySelector('.content')?.textContent, likes: post.querySelector('.likes')?.textContent, timestamp: post.querySelector('.timestamp')?.textContent })); }); // 只添加新帖子 const newPostIds = new Set(posts.map(p => p.id)); const uniqueNewPosts = newPosts.filter(p => !newPostIds.has(p.id)); posts.push(...uniqueNewPosts); } await browser.close(); return posts;}场景 2:抓取电商网站商品列表async function scrapeEcommerceProducts(categoryUrl) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(categoryUrl); const products = []; while (true) { // 等待商品加载 await page.waitForSelector('.product-card'); // 收集当前页商品 const pageProducts = await page.$$eval('.product-card', cards => { return cards.map(card => ({ id: card.dataset.id, title: card.querySelector('.title')?.textContent, price: card.querySelector('.price')?.textContent, rating: card.querySelector('.rating')?.textContent })); }); products.push(...pageProducts); // 检查是否有下一页 const nextButton = await page.$('.next-page:not(.disabled)'); if (!nextButton) { break; } // 点击下一页 await nextButton.click(); await page.waitForTimeout(1000); } await browser.close(); return products;}场景 3:抓取实时数据更新async function scrapeRealTimeData(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); const dataUpdates = []; // 监听 DOM 变化 await page.evaluate(() => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { window.__DATA_UPDATES__ = window.__DATA_UPDATES__ || []; window.__DATA_UPDATES__.push({ timestamp: Date.now(), addedNodes: mutation.addedNodes.length }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); }); // 等待一段时间收集数据 await page.waitForTimeout(30000); // 获取收集的数据 const updates = await page.evaluate(() => { return window.__DATA_UPDATES__ || []; }); await browser.close(); return updates;}8. 最佳实践1. 使用适当的等待策略:// 优先使用 waitForSelectorawait page.waitForSelector('.element');// 复杂条件使用 waitForFunctionawait page.waitForFunction(() => { return document.querySelectorAll('.item').length > 10;});// 网络请求使用 waitForResponseawait page.waitForResponse(response => response.url().includes('/api/data'));2. 避免硬编码等待时间:// 不好的做法await page.waitForTimeout(5000);// 好的做法await page.waitForSelector('.loaded-content');3. 处理加载失败:try { await page.waitForSelector('.content', { timeout: 10000 });} catch (error) { console.log('Content failed to load, using fallback'); // 使用备用策略}4. 优化性能:// 禁用不必要的资源await page.setRequestInterception(true);page.on('request', (request) => { if (['image', 'font', 'media'].includes(request.resourceType())) { request.abort(); } else { request.continue(); }});5. 处理反爬虫:// 设置真实的用户代理await page.setUserAgent('Mozilla/5.0 ...');// 添加随机延迟const randomDelay = () => Math.random() * 2000 + 1000;await page.waitForTimeout(randomDelay());// 模拟人类行为await page.evaluate(() => { window.scrollBy(0, Math.random() * 500);});
阅读 0·2月19日 19:49

Puppeteer 在实际项目中有哪些应用场景?请举例说明网页爬虫、自动化测试等具体实现。

Puppeteer 在实际项目中有广泛的应用场景,从网页爬虫到自动化测试,从数据采集到性能监控。以下是一些典型的实际应用案例。1. 网页爬虫和数据采集案例 1:电商商品价格监控const puppeteer = require('puppeteer');async function monitorProductPrices(productUrls) { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const results = []; for (const url of productUrls) { const page = await browser.newPage(); // 设置用户代理,避免被识别为爬虫 await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); await page.goto(url, { waitUntil: 'networkidle2' }); // 等待价格元素加载 await page.waitForSelector('.price', { timeout: 5000 }); const productData = await page.evaluate(() => { return { title: document.querySelector('.product-title')?.textContent, price: document.querySelector('.price')?.textContent, availability: document.querySelector('.availability')?.textContent, rating: document.querySelector('.rating')?.textContent }; }); results.push({ url, ...productData, timestamp: new Date().toISOString() }); await page.close(); } await browser.close(); return results;}// 使用示例const products = [ 'https://example.com/product/1', 'https://example.com/product/2'];monitorProductPrices(products).then(data => { console.log(JSON.stringify(data, null, 2));});案例 2:社交媒体数据抓取async function scrapeSocialMedia(username) { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); // 模拟登录 await page.goto('https://social-media.com/login'); await page.type('#username', 'your_username'); await page.type('#password', 'your_password'); await page.click('#login-button'); await page.waitForNavigation(); // 访问用户页面 await page.goto(`https://social-media.com/${username}`); // 滚动加载更多内容 while (true) { await page.evaluate(() => { window.scrollBy(0, window.innerHeight); }); try { await page.waitForSelector('.new-post', { timeout: 2000 }); } catch { break; } } // 抓取帖子数据 const posts = await page.evaluate(() => { return Array.from(document.querySelectorAll('.post')).map(post => ({ content: post.querySelector('.content')?.textContent, likes: post.querySelector('.likes')?.textContent, comments: post.querySelector('.comments')?.textContent, date: post.querySelector('.date')?.textContent })); }); await browser.close(); return posts;}2. 自动化测试案例 3:E2E 测试const { expect } = require('expect-puppeteer');async function runE2ETest() { const browser = await puppeteer.launch({ headless: 'new', slowMo: 50 // 减慢操作速度,便于观察 }); const page = await browser.newPage(); try { // 测试用户注册流程 await page.goto('https://example.com/register'); // 填写注册表单 await page.type('#username', 'testuser'); await page.type('#email', 'test@example.com'); await page.type('#password', 'password123'); await page.type('#confirm-password', 'password123'); // 提交表单 await Promise.all([ page.waitForNavigation(), page.click('#register-button') ]); // 验证注册成功 await expect(page).toMatch('Welcome, testuser!'); // 测试登录流程 await page.click('#logout-button'); await page.waitForNavigation(); await page.type('#login-email', 'test@example.com'); await page.type('#login-password', 'password123'); await page.click('#login-button'); await page.waitForNavigation(); // 验证登录成功 await expect(page).toMatch('Welcome back!'); console.log('E2E test passed!'); } catch (error) { console.error('E2E test failed:', error); // 保存失败截图 await page.screenshot({ path: 'test-failure.png' }); } finally { await browser.close(); }}runE2ETest();案例 4:视觉回归测试const fs = require('fs');const pixelmatch = require('pixelmatch');const { PNG } = require('pngjs');async function visualRegressionTest(url, baselinePath) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); // 截取当前页面 const screenshot = await page.screenshot(); await browser.close(); // 如果没有基线图片,保存当前截图作为基线 if (!fs.existsSync(baselinePath)) { fs.writeFileSync(baselinePath, screenshot); console.log('Baseline image created'); return true; } // 读取基线图片 const baseline = PNG.sync.read(fs.readFileSync(baselinePath)); const current = PNG.sync.read(screenshot); // 比较图片差异 const diff = new PNG({ width: baseline.width, height: baseline.height }); const numDiffPixels = pixelmatch( baseline.data, current.data, diff.data, baseline.width, baseline.height, { threshold: 0.1 } ); // 保存差异图片 fs.writeFileSync('diff.png', PNG.sync.write(diff)); const totalPixels = baseline.width * baseline.height; const diffPercentage = (numDiffPixels / totalPixels) * 100; console.log(`Difference: ${diffPercentage.toFixed(2)}%`); // 如果差异超过阈值,测试失败 if (diffPercentage > 0.5) { console.log('Visual regression detected!'); return false; } console.log('Visual regression test passed!'); return true;}visualRegressionTest('https://example.com', 'baseline.png');3. PDF 生成和文档处理案例 5:动态报表生成async function generateReport(data, outputPath) { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 生成 HTML 报表 const html = ` <!DOCTYPE html> <html> <head> <style> body { font-family: Arial, sans-serif; padding: 40px; } h1 { color: #333; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } th { background-color: #f2f2f2; } .summary { margin-top: 30px; padding: 20px; background-color: #f9f9f9; } </style> </head> <body> <h1>销售报表</h1> <p>生成时间: ${new Date().toLocaleString()}</p> <table> <thead> <tr> <th>产品</th> <th>数量</th> <th>单价</th> <th>总价</th> </tr> </thead> <tbody> ${data.map(item => ` <tr> <td>${item.product}</td> <td>${item.quantity}</td> <td>$${item.price.toFixed(2)}</td> <td>$${(item.quantity * item.price).toFixed(2)}</td> </tr> `).join('')} </tbody> </table> <div class="summary"> <h2>总计: $${data.reduce((sum, item) => sum + item.quantity * item.price, 0).toFixed(2)}</h2> </div> </body> </html> `; await page.setContent(html); // 生成 PDF await page.pdf({ path: outputPath, format: 'A4', printBackground: true, margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' } }); await browser.close(); console.log(`Report generated: ${outputPath}`);}// 使用示例const salesData = [ { product: '产品 A', quantity: 10, price: 99.99 }, { product: '产品 B', quantity: 5, price: 149.99 }, { product: '产品 C', quantity: 8, price: 79.99 }];generateReport(salesData, 'sales-report.pdf');案例 6:发票批量生成async function generateInvoices(invoices) { const browser = await puppeteer.launch(); const page = await browser.newPage(); for (const invoice of invoices) { const html = ` <!DOCTYPE html> <html> <head> <style> body { font-family: Arial, sans-serif; padding: 40px; } .header { text-align: center; margin-bottom: 40px; } .invoice-info { margin-bottom: 30px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ddd; padding: 10px; text-align: left; } th { background-color: #f2f2f2; } .total { text-align: right; font-weight: bold; margin-top: 20px; } </style> </head> <body> <div class="header"> <h1>发票</h1> <p>发票号: ${invoice.number}</p> </div> <div class="invoice-info"> <p>日期: ${invoice.date}</p> <p>客户: ${invoice.customer}</p> </div> <table> <thead> <tr> <th>项目</th> <th>数量</th> <th>单价</th> <th>总价</th> </tr> </thead> <tbody> ${invoice.items.map(item => ` <tr> <td>${item.name}</td> <td>${item.quantity}</td> <td>$${item.price}</td> <td>$${item.quantity * item.price}</td> </tr> `).join('')} </tbody> </table> <div class="total"> 总计: $${invoice.total} </div> </body> </html> `; await page.setContent(html); await page.pdf({ path: `invoices/invoice-${invoice.number}.pdf`, format: 'A4', printBackground: true }); console.log(`Generated invoice: ${invoice.number}`); } await browser.close();}// 使用示例const invoices = [ { number: 'INV-001', date: '2024-01-15', customer: '客户 A', items: [ { name: '服务 A', quantity: 1, price: 500 }, { name: '服务 B', quantity: 2, price: 300 } ], total: 1100 }];generateInvoices(invoices);4. 性能监控和分析案例 7:页面性能分析async function analyzePagePerformance(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 启用性能监控 const client = await page.target().createCDPSession(); await client.send('Performance.enable'); await client.send('Network.enable'); // 记录开始时间 const startTime = Date.now(); await page.goto(url, { waitUntil: 'networkidle2' }); const loadTime = Date.now() - startTime; // 获取性能指标 const metrics = await client.send('Performance.getMetrics'); // 获取关键性能指标 const performanceData = { loadTime, domContentLoaded: await page.evaluate(() => performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart ), firstPaint: await page.evaluate(() => performance.getEntriesByType('paint')[0]?.startTime ), firstContentfulPaint: await page.evaluate(() => performance.getEntriesByType('paint')[1]?.startTime ), resources: metrics.metrics }; // 生成性能报告 console.log('Performance Report:'); console.log(`Load Time: ${performanceData.loadTime}ms`); console.log(`DOM Content Loaded: ${performanceData.domContentLoaded}ms`); console.log(`First Paint: ${performanceData.firstPaint}ms`); console.log(`First Contentful Paint: ${performanceData.firstContentfulPaint}ms`); await browser.close(); return performanceData;}analyzePagePerformance('https://example.com');5. SEO 工具案例 8:SEO 检查工具async function seoAudit(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); const seoData = await page.evaluate(() => { const issues = []; const warnings = []; // 检查标题 const title = document.querySelector('title'); if (!title) { issues.push('Missing title tag'); } else if (title.textContent.length > 60) { warnings.push('Title too long (> 60 characters)'); } // 检查描述 const description = document.querySelector('meta[name="description"]'); if (!description) { issues.push('Missing meta description'); } else if (description.content.length > 160) { warnings.push('Meta description too long (> 160 characters)'); } // 检查 H1 标签 const h1Tags = document.querySelectorAll('h1'); if (h1Tags.length === 0) { issues.push('Missing H1 tag'); } else if (h1Tags.length > 1) { warnings.push('Multiple H1 tags found'); } // 检查图片 alt 属性 const images = document.querySelectorAll('img'); let missingAlt = 0; images.forEach(img => { if (!img.alt) missingAlt++; }); if (missingAlt > 0) { warnings.push(`${missingAlt} images missing alt attributes`); } // 检查链接 const links = document.querySelectorAll('a[href]'); let brokenLinks = 0; links.forEach(link => { if (link.getAttribute('href').startsWith('#')) brokenLinks++; }); return { title: title?.textContent, description: description?.content, h1Count: h1Tags.length, imageCount: images.length, linkCount: links.length, issues, warnings }; }); console.log('SEO Audit Results:'); console.log(JSON.stringify(seoData, null, 2)); await browser.close(); return seoData;}seoAudit('https://example.com');6. 最佳实践总结1. 错误处理:try { // 操作代码} catch (error) { console.error('Error:', error); // 保存错误截图 await page.screenshot({ path: 'error.png' });} finally { await browser.close();}2. 资源管理:// 及时清理资源await page.close();await browser.close();3. 性能优化:// 禁用不必要的资源await page.setRequestInterception(true);page.on('request', (request) => { if (['image', 'font'].includes(request.resourceType())) { request.abort(); } else { request.continue(); }});4. 反爬虫策略:// 设置真实的用户代理await page.setUserAgent('Mozilla/5.0 ...');// 添加延迟await new Promise(resolve => setTimeout(resolve, 1000));// 使用代理const browser = await puppeteer.launch({ args: ['--proxy-server=http://proxy.example.com:8080']});
阅读 0·2月19日 19:48