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

前端面试题手册

RxJS 6 和 RxJS 7 有什么区别?

RxJS 6 与 RxJS 7 的主要变化1. 导入方式的变化RxJS 6:// 创建操作符import { of, from, interval } from 'rxjs';// 操作符(使用 pipe)import { map, filter, switchMap } from 'rxjs/operators';// 工具函数import { Subscription } from 'rxjs';RxJS 7:// 导入方式基本相同,但更加模块化import { of, from, interval } from 'rxjs';import { map, filter, switchMap } from 'rxjs/operators';// 新增了一些操作符import { debounceTime, distinctUntilChanged } from 'rxjs/operators';2. 新增的操作符RxJS 7 新增了多个有用的操作符:1. partition根据谓词函数将 Observable 分成两个。import { of, partition } from 'rxjs';const source$ = of(1, 2, 3, 4, 5, 6);const [evens$, odds$] = partition(source$, x => x % 2 === 0);evens$.subscribe(x => console.log('Even:', x)); // 2, 4, 6odds$.subscribe(x => console.log('Odd:', x)); // 1, 3, 52. tap 的改进tap 现在接受一个对象,可以分别处理不同的通知类型。import { of } from 'rxjs';import { tap } from 'rxjs/operators';of(1, 2, 3).pipe( tap({ subscribe: () => console.log('Subscribed'), next: value => console.log('Next:', value), error: error => console.log('Error:', error), complete: () => console.log('Completed'), unsubscribe: () => console.log('Unsubscribed') })).subscribe();3. connectable 操作符简化了多播 Observable 的创建。import { interval, connectable } from 'rxjs';import { take } from 'rxjs/operators';const source$ = interval(1000).pipe(take(5));const connectable$ = connectable(source$);connectable$.subscribe(value => console.log('Subscriber 1:', value));connectable$.subscribe(value => console.log('Subscriber 2:', value));connectable$.connect();4. shareReplay 的改进shareReplay 现在支持配置对象。import { interval } from 'rxjs';import { shareReplay, take } from 'rxjs/operators';const shared$ = interval(1000).pipe( take(5), shareReplay({ bufferSize: 2, refCount: true }));5. filter 的类型推断改进更好的 TypeScript 类型推断。import { of } from 'rxjs';import { filter } from 'rxjs/operators';const source$ = of(1, 2, 3, 4, 5);// RxJS 7 中类型推断更准确const even$ = source$.pipe( filter(x => x % 2 === 0) // x 被推断为 number);even$.subscribe(x => console.log(x)); // x 是 number 类型3. 废弃的操作符以下操作符在 RxJS 7 中被废弃:1. throwErrorRxJS 6:import { throwError } from 'rxjs';throwError('Error message');RxJS 7:import { throwError } from 'rxjs';// 推荐使用工厂函数throwError(() => new Error('Error message'));2. concat 和 merge 的静态方法虽然仍然可用,但推荐使用 concatWith 和 mergeWith。import { of, concat, merge } from 'rxjs';// 仍然可用concat(of(1), of(2)).subscribe();merge(of(1), of(2)).subscribe();// 推荐使用of(1).pipe(concatWith(of(2))).subscribe();of(1).pipe(mergeWith(of(2))).subscribe();4. 性能优化1. 更小的包体积RxJS 7 通过树摇优化减少了包体积。// RxJS 7 只导入需要的操作符import { of } from 'rxjs';import { map } from 'rxjs/operators';// 打包时只会包含 of 和 map2. 更快的执行速度某些操作符的执行速度得到了优化。import { of } from 'rxjs';import { map, filter } from 'rxjs/operators';// RxJS 7 中这些操作符执行更快of(1, 2, 3, 4, 5).pipe( map(x => x * 2), filter(x => x > 5)).subscribe();5. TypeScript 改进1. 更好的类型推断import { of } from 'rxjs';import { map, filter } from 'rxjs/operators';// RxJS 7 中类型推断更准确const result$ = of(1, 2, 3).pipe( map(x => x.toString()), // 推断为 Observable<string> filter(x => x.length > 0));2. 严格类型检查import { of } from 'rxjs';import { map } from 'rxjs/operators';// RxJS 7 中类型检查更严格of(1, 2, 3).pipe( map(x => x.toUpperCase()) // TypeScript 错误:number 没有 toUpperCase);6. 错误处理改进1. 更好的错误信息import { of } from 'rxjs';import { map } from 'rxjs/operators';of(1, 2, 3).pipe( map(x => { if (x === 2) throw new Error('Custom error'); return x; })).subscribe({ error: error => { console.log(error.message); // 更清晰的错误信息 }});2. onErrorResumeNext 的改进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, 67. 调度器改进1. animationFrameScheduler 的改进import { interval, animationFrameScheduler } from 'rxjs';import { take } from 'rxjs/operators';// RxJS 7 中 animationFrameScheduler 更稳定interval(0, animationFrameScheduler).pipe( take(60)).subscribe(frame => { console.log('Frame:', frame);});2. asapScheduler 的改进import { of, asapScheduler } from 'rxjs';// RxJS 7 中 asapScheduler 性能更好of(1, 2, 3, asapScheduler).subscribe(value => { console.log(value);});8. 测试工具改进1. TestScheduler 的改进import { TestScheduler } from 'rxjs/testing';// RxJS 7 中 TestScheduler 更易用const testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected);});testScheduler.run(({ cold, expectObservable }) => { const source$ = cold('-a-b-c|'); const expected = '-a-b-c|'; expectObservable(source$).toBe(expected);});2. marble testing 的改进import { TestScheduler } from 'rxjs/testing';// RxJS 7 中 marble testing 更直观testScheduler.run(({ cold, hot, expectObservable, expectSubscriptions }) => { const source$ = cold('-a-b-c|'); const expected = '-a-b-c|'; expectObservable(source$).toBe(expected);});9. 文档和示例改进1. 更好的文档RxJS 7 提供了更详细的文档和示例。// 官方文档提供了更多实用的示例import { of } from 'rxjs';import { map, filter } from 'rxjs/operators';of(1, 2, 3, 4, 5).pipe( map(x => x * 2), filter(x => x > 5)).subscribe(console.log);2. 更多的最佳实践官方文档中包含了更多的最佳实践建议。// 推荐使用 pipe 而不是链式调用import { of } from 'rxjs';import { map, filter } from 'rxjs/operators';// ✅ 推荐of(1, 2, 3).pipe( map(x => x * 2), filter(x => x > 5)).subscribe();// ❌ 不推荐of(1, 2, 3).pipe( map(x => x * 2)).pipe( filter(x => x > 5)).subscribe();10. 迁移指南1. 从 RxJS 6 迁移到 RxJS 7# 升级 RxJSnpm install rxjs@7# 检查废弃的 APInpm run lint2. 常见迁移问题问题 1: throwError 的使用// ❌ RxJS 6throwError('Error message');// ✅ RxJS 7throwError(() => new Error('Error message'));问题 2: 类型推断问题// 可能需要显式类型import { of } from 'rxjs';import { map } from 'rxjs/operators';const result$ = of(1, 2, 3).pipe( map(x => x.toString())) as Observable<string>;总结RxJS 7 相比 RxJS 6 的主要改进:新增操作符: partition、connectable 等性能优化: 更小的包体积,更快的执行速度TypeScript 改进: 更好的类型推断和严格类型检查错误处理: 更好的错误信息和处理机制调度器改进: 更稳定和高效的调度器测试工具: 更易用的 TestScheduler文档改进: 更详细的文档和示例迁移到 RxJS 7 可以带来更好的性能和开发体验,但需要注意一些 API 的变化。
阅读 0·2月21日 16:54

Prettier 支持哪些编程语言和文件类型?

Prettier 支持的编程语言和文件类型Prettier 是一款支持多种编程语言的代码格式化工具,几乎涵盖了现代前端开发中使用的所有主要语言。完全支持的语言JavaScript 生态系统JavaScript (ES6+)TypeScriptJSX (React)TSX (TypeScript + React)FlowVue 单文件组件 (.vue)Angular 模板样式语言CSSSCSS/SASSLessStylus模板语言HTMLVue 模板Angular 模板HandlebarsMustache数据格式JSONJSON5JSONC (带注释的 JSON)配置文件YAMLTOMLINI文档语言Markdown (.md)MDX其他语言GraphQLMarkdownXML部分支持的语言Java (通过插件)PHP (通过插件)Python (通过插件)Ruby (通过插件)Go (通过插件)Rust (通过插件)语言支持原理Prettier 通过解析器支持不同语言:解析器选择: 根据文件扩展名自动选择对应的解析器AST 生成: 使用语言特定的解析器生成抽象语法树格式化: 应用统一的格式化规则代码生成: 将格式化后的 AST 转换回源代码指定解析器如果 Prettier 无法自动识别文件类型,可以在配置中指定解析器:{ "overrides": [ { "files": "*.vue", "options": { "parser": "vue" } } ]}多语言项目配置对于包含多种语言的项目,可以使用 overrides 配置为不同文件类型设置不同的格式化规则:{ "semi": true, "overrides": [ { "files": "*.css", "options": { "singleQuote": false } }, { "files": "*.json", "options": { "tabWidth": 4 } } ]}Prettier 的多语言支持使其成为全栈项目的理想格式化工具,可以统一整个项目的代码风格。
阅读 0·2月21日 16:54

RxJS 中的 Observable 和 Promise 有什么区别?

核心区别Observable 和 Promise 都是处理异步操作的工具,但它们在设计理念和使用方式上有显著差异:1. 执行时机Promise: 一旦创建就会立即执行,无法取消const promise = new Promise((resolve) => { console.log('Promise 立即执行'); resolve('done');});Observable: 只有订阅时才会执行,可以取消订阅const observable = new Observable((observer) => { console.log('Observable 订阅时执行'); observer.next('data');});observable.subscribe();2. 数据流Promise: 只能发出一个值,然后完成或失败promise.then(value => console.log(value)); // 只接收一个值Observable: 可以发出多个值,随时间推移持续推送数据observable.subscribe(value => console.log(value)); // 可以接收多个值3. 可取消性Promise: 无法取消,一旦创建就会执行到底const promise = fetch('/api/data');// 无法中途取消这个请求Observable: 可以通过 unsubscribe() 取消订阅const subscription = observable.subscribe();subscription.unsubscribe(); // 取消订阅4. 操作符支持Promise: 只有链式调用 then/catch/finallypromise .then(data => processData(data)) .then(result => console.log(result)) .catch(error => handleError(error));Observable: 丰富的操作符生态系统observable .pipe( map(data => processData(data)), filter(result => result.isValid), catchError(error => handleError(error)) ) .subscribe();5. 懒加载 vs 急切加载Promise: 急切执行,创建时就开始工作Observable: 懒加载,直到订阅才开始执行6. 同步/异步Promise: 总是异步的Observable: 可以是同步或异步的实际应用场景使用 Promise 的场景单次异步操作不需要取消只需要一个结果简单的异步流程使用 Observable 的场景需要处理多个值需要取消操作复杂的数据流处理事件处理WebSocket 连接实时数据流性能考虑Observable 在处理复杂异步流程时提供了更好的性能和灵活性,特别是在需要处理多个异步操作或需要取消操作的场景中。Promise 更适合简单的异步操作,代码更简洁直观。
阅读 0·2月21日 16:28

RxJS 中常用的操作符有哪些?如何使用?

常用操作符分类1. 创建操作符 (Creation Operators)of创建一个发出指定值的 Observableimport { of } from 'rxjs';of(1, 2, 3).subscribe(console.log);// 输出: 1, 2, 3from将数组、Promise、Iterable 等转换为 Observableimport { from } from 'rxjs';from([1, 2, 3]).subscribe(console.log);// 输出: 1, 2, 3from(Promise.resolve('Hello')).subscribe(console.log);// 输出: Hellointerval / timer创建定时发出的 Observableimport { interval, timer } from 'rxjs';interval(1000).subscribe(console.log);// 每秒发出一个递增数字: 0, 1, 2, 3...timer(2000, 1000).subscribe(console.log);// 2秒后开始,每秒发出一个数字: 0, 1, 2, 3...2. 转换操作符 (Transformation Operators)map转换每个发出的值import { of } from 'rxjs';import { map } from 'rxjs/operators';of(1, 2, 3).pipe( map(x => x * 2)).subscribe(console.log);// 输出: 2, 4, 6switchMap取消之前的内部 Observable,只处理最新的import { fromEvent } from 'rxjs';import { switchMap } from 'rxjs/operators';fromEvent(document, 'click').pipe( switchMap(() => fetch('/api/data'))).subscribe(response => console.log(response));mergeMap并行处理所有内部 Observableimport { of } from 'rxjs';import { mergeMap } from 'rxjs/operators';of(1, 2, 3).pipe( mergeMap(x => of(x, x * 2))).subscribe(console.log);// 输出: 1, 2, 2, 4, 3, 6concatMap顺序处理内部 Observable,一个完成后再处理下一个import { of } from 'rxjs';import { concatMap } from 'rxjs/operators';of(1, 2, 3).pipe( concatMap(x => of(x, x * 2))).subscribe(console.log);// 输出: 1, 2, 2, 4, 3, 6 (顺序执行)3. 过滤操作符 (Filtering Operators)filter过滤符合条件的值import { of } from 'rxjs';import { filter } from 'rxjs/operators';of(1, 2, 3, 4, 5).pipe( filter(x => x % 2 === 0)).subscribe(console.log);// 输出: 2, 4take / takeLast / takeUntil只取前几个、后几个或直到某个条件import { interval, fromEvent } from 'rxjs';import { take, takeUntil } from 'rxjs/operators';interval(1000).pipe( take(3)).subscribe(console.log);// 输出: 0, 1, 2interval(1000).pipe( takeUntil(fromEvent(document, 'click'))).subscribe(console.log);// 点击时停止debounceTime在指定时间内只发出最后一个值import { fromEvent } from 'rxjs';import { debounceTime } from 'rxjs/operators';fromEvent(inputElement, 'input').pipe( debounceTime(300)).subscribe(event => console.log(event.target.value));throttleTime在指定时间内只发出第一个值import { fromEvent } from 'rxjs';import { throttleTime } from 'rxjs/operators';fromEvent(window, 'scroll').pipe( throttleTime(200)).subscribe(event => console.log('Scroll event'));4. 组合操作符 (Combination Operators)merge合并多个 Observable,并行发出值import { merge, interval } from 'rxjs';merge( interval(1000).pipe(map(x => `A${x}`)), interval(1500).pipe(map(x => `B${x}`))).subscribe(console.log);concat顺序连接多个 Observableimport { concat, of } from 'rxjs';concat( of(1, 2), of(3, 4), of(5, 6)).subscribe(console.log);// 输出: 1, 2, 3, 4, 5, 6combineLatest组合多个 Observable 的最新值import { combineLatest, of } from 'rxjs';combineLatest([ of(1, 2, 3), of('a', 'b', 'c')]).subscribe(([num, char]) => console.log(num, char));// 输出: [3, 'a'], [3, 'b'], [3, 'c']zip按索引组合多个 Observable 的值import { zip, of } from 'rxjs';zip( of(1, 2, 3), of('a', 'b', 'c')).subscribe(([num, char]) => console.log(num, char));// 输出: [1, 'a'], [2, 'b'], [3, 'c']5. 错误处理操作符 (Error Handling Operators)catchError捕获错误并返回新的 Observableimport { of } from 'rxjs';import { map, catchError } from 'rxjs/operators';of(1, 2, 3, 4).pipe( map(x => { if (x === 3) throw new Error('Error!'); return x; }), catchError(error => of('default value'))).subscribe(console.log);// 输出: 1, 2, 'default value'retry重试失败的 Observableimport { of } from 'rxjs';import { map, retry } from 'rxjs/operators';let count = 0;of(1, 2, 3).pipe( map(x => { count++; if (count < 3) throw new Error('Error!'); return x; }), retry(2)).subscribe(console.log);6. 工具操作符 (Utility Operators)tap执行副作用,不修改值import { of } from 'rxjs';import { tap, map } from 'rxjs/operators';of(1, 2, 3).pipe( tap(x => console.log('Before:', x)), map(x => x * 2), tap(x => console.log('After:', x))).subscribe();delay延迟发出值import { of } from 'rxjs';import { delay } from 'rxjs/operators';of(1, 2, 3).pipe( delay(1000)).subscribe(console.log);// 1秒后输出: 1, 2, 3实际应用示例搜索框防抖fromEvent(searchInput, 'input').pipe( debounceTime(300), map(event => event.target.value), filter(query => query.length > 2), switchMap(query => searchAPI(query))).subscribe(results => displayResults(results));自动保存form.valueChanges.pipe( debounceTime(1000), distinctUntilChanged(), switchMap(formData => saveAPI(formData))).subscribe();并行请求merge( getUserData(userId), getUserPosts(userId), getUserComments(userId)).pipe( combineLatestAll()).subscribe(([user, posts, comments]) => { renderUserProfile(user, posts, comments);});最佳实践合理使用 pipe() 链式调用注意操作符的执行顺序及时取消订阅避免内存泄漏根据场景选择合适的操作符使用 TypeScript 获得更好的类型推断
阅读 0·2月21日 16:28

如何在 RxJS 中防止内存泄漏?

内存泄漏的原因在 RxJS 中,内存泄漏主要发生在以下几种情况:1. 未取消订阅最常见的内存泄漏原因是订阅了 Observable 但没有取消订阅。// ❌ 错误示例:内存泄漏class MyComponent { constructor() { this.data$ = http.get('/api/data').subscribe(data => { console.log(data); }); }}// 组件销毁时,订阅仍然存在,导致内存泄漏2. 长期运行的 Observableinterval、fromEvent 等会持续发出值的 Observable,如果不取消订阅会持续占用内存。// ❌ 错误示例setInterval(() => { console.log('Running...');}, 1000);// ✅ 正确示例const subscription = interval(1000).subscribe();subscription.unsubscribe();3. 闭包引用订阅回调中引用了外部变量,导致这些变量无法被垃圾回收。// ❌ 错误示例function createSubscription() { const largeData = new Array(1000000).fill('data'); return interval(1000).subscribe(() => { console.log(largeData.length); // largeData 被闭包引用 });}const sub = createSubscription();// 即使 sub 不再使用,largeData 也不会被释放4. 事件监听器未移除使用 fromEvent 创建的订阅如果不取消,事件监听器会一直存在。// ❌ 错误示例fromEvent(document, 'click').subscribe(event => { console.log('Clicked');});// 事件监听器永远不会被移除防止内存泄漏的方法1. 手动取消订阅最直接的方法是在适当的时候调用 unsubscribe()。class MyComponent { private subscriptions: Subscription[] = []; ngOnInit() { const sub1 = http.get('/api/data').subscribe(data => { this.data = data; }); const sub2 = interval(1000).subscribe(() => { this.update(); }); this.subscriptions.push(sub1, sub2); } ngOnDestroy() { this.subscriptions.forEach(sub => sub.unsubscribe()); }}2. 使用 takeUntiltakeUntil 是最常用的取消订阅方式之一。import { Subject, takeUntil } from 'rxjs';class MyComponent { private destroy$ = new Subject<void>(); ngOnInit() { http.get('/api/data').pipe( takeUntil(this.destroy$) ).subscribe(data => { this.data = data; }); interval(1000).pipe( takeUntil(this.destroy$) ).subscribe(() => { this.update(); }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }}3. 使用 take、takeWhile、takeLast根据条件自动取消订阅。// take: 只取前 N 个值interval(1000).pipe( take(5)).subscribe(value => console.log(value));// 输出: 0, 1, 2, 3, 4 然后自动取消订阅// takeWhile: 满足条件时继续订阅interval(1000).pipe( takeWhile(value => value < 5)).subscribe(value => console.log(value));// 输出: 0, 1, 2, 3, 4 然后自动取消订阅// takeLast: 只取最后 N 个值of(1, 2, 3, 4, 5).pipe( takeLast(2)).subscribe(value => console.log(value));// 输出: 4, 54. 使用 first只取第一个值,然后自动取消订阅。http.get('/api/data').pipe( first()).subscribe(data => { console.log(data);});// 只发出第一个值就完成5. 使用 AsyncPipe(Angular)在 Angular 中,AsyncPipe 会自动管理订阅。@Component({ template: ` <div *ngIf="data$ | async as data"> {{ data }} </div> `})export class MyComponent { data$ = http.get('/api/data'); // AsyncPipe 会自动取消订阅}6. 使用 finalize在取消订阅时执行清理操作。http.get('/api/data').pipe( finalize(() => { console.log('Cleaning up...'); // 执行清理操作 })).subscribe(data => { console.log(data);});最佳实践1. 组件级别的订阅管理import { Component, OnDestroy } from '@angular/core';import { Subject, takeUntil } from 'rxjs';@Component({ selector: 'app-my', template: '...'})export class MyComponent implements OnDestroy { private destroy$ = new Subject<void>(); constructor() { this.setupSubscriptions(); } private setupSubscriptions() { // 所有订阅都使用 takeUntil this.http.get('/api/user').pipe( takeUntil(this.destroy$) ).subscribe(user => { this.user = user; }); this.route.params.pipe( takeUntil(this.destroy$) ).subscribe(params => { this.loadPage(params.id); }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }}2. 创建可重用的取消订阅工具import { Subject, Observable } from 'rxjs';import { takeUntil } from 'rxjs/operators';export class AutoUnsubscribe { private destroy$ = new Subject<void>(); protected autoUnsubscribe<T>(observable: Observable<T>): Observable<T> { return observable.pipe(takeUntil(this.destroy$)); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }}// 使用class MyComponent extends AutoUnsubscribe { ngOnInit() { this.autoUnsubscribe(http.get('/api/data')).subscribe(data => { console.log(data); }); }}3. 使用 Subscription 集合import { Subscription } from 'rxjs';class MyService { private subscriptions = new Subscription(); startMonitoring() { const sub1 = interval(1000).subscribe(); const sub2 = fromEvent(document, 'click').subscribe(); this.subscriptions.add(sub1); this.subscriptions.add(sub2); } stopMonitoring() { this.subscriptions.unsubscribe(); }}4. 避免在回调中创建订阅// ❌ 错误示例interval(1000).subscribe(() => { http.get('/api/data').subscribe(data => { console.log(data); }); // 每次都创建新订阅,无法取消});// ✅ 正确示例interval(1000).pipe( switchMap(() => http.get('/api/data'))).subscribe(data => { console.log(data);});// switchMap 会自动取消之前的订阅检测内存泄漏1. 使用 Chrome DevTools// 在组件中添加标记class MyComponent { private id = Math.random(); ngOnDestroy() { console.log(`Component ${this.id} destroyed`); }}// 观察控制台,确认组件销毁时是否真的清理了订阅2. 使用 RxJS 调试工具import { tap } from 'rxjs/operators';http.get('/api/data').pipe( tap({ subscribe: () => console.log('Subscribed'), unsubscribe: () => console.log('Unsubscribed'), next: value => console.log('Next:', value), complete: () => console.log('Completed'), error: error => console.log('Error:', error) })).subscribe();常见陷阱1. 忘记取消嵌套订阅// ❌ 错误示例http.get('/api/user').subscribe(user => { http.get(`/api/posts/${user.id}`).subscribe(posts => { console.log(posts); }); // 内层订阅没有被管理});// ✅ 正确示例http.get('/api/user').pipe( switchMap(user => http.get(`/api/posts/${user.id}`))).subscribe(posts => { console.log(posts);});2. 在服务中创建订阅// ❌ 错误示例@Injectable()export class DataService { constructor(private http: HttpClient) { this.http.get('/api/data').subscribe(data => { this.data = data; }); // 服务中的订阅很难取消 }}// ✅ 正确示例@Injectable()export class DataService { private data$ = this.http.get('/api/data'); getData() { return this.data$; }}3. 忽略错误处理// ❌ 错误示例http.get('/api/data').subscribe(data => { console.log(data);});// 错误没有被处理,可能导致订阅无法正常完成// ✅ 正确示例http.get('/api/data').pipe( catchError(error => { console.error(error); return of([]); })).subscribe(data => { console.log(data);});总结防止 RxJS 内存泄漏的关键是:始终取消订阅:特别是对于长期运行的 Observable使用 takeUntil:这是最推荐的取消订阅方式避免嵌套订阅:使用 switchMap、concatMap 等操作符使用 AsyncPipe:在 Angular 中优先使用 AsyncPipe定期检查:使用 DevTools 检测内存泄漏错误处理:确保错误被正确处理,避免订阅卡住遵循这些最佳实践,可以有效地防止 RxJS 应用中的内存泄漏问题。
阅读 0·2月21日 16:28

whistle 如何支持 WebSocket 代理和调试?

答案Whistle 提供了 WebSocket 代理功能,可以捕获、调试和修改 WebSocket 连接和消息。WebSocket 代理基础1. 基本 WebSocket 代理配置规则:ws://example.com host 127.0.0.1:8080wss://example.com host 127.0.0.1:8080或者使用 forward 操作符:ws://example.com forward ws://127.0.0.1:8080wss://example.com forward wss://127.0.0.1:80802. WebSocket 消息捕获Whistle 会自动捕获 WebSocket 连接和消息:连接建立:记录 WebSocket 握手信息消息发送:记录客户端发送的消息消息接收:记录服务器返回的消息连接关闭:记录连接关闭原因WebSocket 调试功能1. 查看消息详情在 whistle 管理界面中:点击 "Network" 标签筛选 "WS" 类型的请求点击 WebSocket 连接查看详情查看 "Messages" 标签中的消息历史2. 消息格式化Whistle 会自动格式化 JSON 消息:{ "type": "message", "data": { "id": 1, "content": "Hello" }}3. 消息过滤可以使用过滤器快速查找特定消息:按消息类型过滤按消息内容搜索按时间范围过滤WebSocket 消息修改1. 使用插件修改消息创建插件:websocket-modifier.jsmodule.exports = function(server, options) { server.on('upgrade', function(req, socket, head) { console.log('WebSocket upgrade:', req.url); }); server.on('connection', function(ws, req) { ws.on('message', function(message) { console.log('Received message:', message.toString()); // 修改消息 const modifiedMessage = modifyMessage(message); ws.send(modifiedMessage); }); });};function modifyMessage(message) { const data = JSON.parse(message.toString()); data.timestamp = Date.now(); return JSON.stringify(data);}配置规则:ws://example.com plugin://websocket-modifier2. 拦截和修改握手创建脚本:websocket-handshake.jsmodule.exports = function(req, res) { // 修改 WebSocket 握手头 if (req.headers['upgrade'] === 'websocket') { req.headers['X-Custom-Header'] = 'Custom Value'; }};配置规则:ws://example.com reqScript://{websocket-handshake.js}WebSocket 性能监控1. 连接时间监控Whistle 会记录 WebSocket 连接的各个阶段时间:DNS 解析时间TCP 连接时间SSL/TLS 握手时间(对于 wss)连接建立时间2. 消息统计消息数量:发送和接收的消息总数消息大小:消息的平均大小和总大小消息频率:消息发送和接收的频率3. 连接状态监控连接状态:连接是否活跃连接时长:连接持续时间重连次数:连接重连的次数WebSocket 调试技巧1. 模拟服务器响应创建脚本:websocket-mock.jsmodule.exports = function(server, options) { server.on('upgrade', function(req, socket, head) { console.log('Mock WebSocket server'); // 模拟服务器行为 socket.on('data', function(data) { console.log('Client message:', data.toString()); // 返回模拟响应 const response = JSON.stringify({ type: 'response', data: 'Mock response', timestamp: Date.now() }); socket.write(response); }); });};2. 消息延迟模拟创建脚本:websocket-delay.jsmodule.exports = function(server, options) { server.on('connection', function(ws, req) { ws.on('message', function(message) { // 模拟延迟 setTimeout(() => { ws.send(message); }, 1000); // 延迟 1 秒 }); });};3. 消息丢失模拟创建脚本:websocket-drop.jsmodule.exports = function(server, options) { let messageCount = 0; server.on('connection', function(ws, req) { ws.on('message', function(message) { messageCount++; // 每 10 条消息丢弃 1 条 if (messageCount % 10 !== 0) { ws.send(message); } else { console.log('Dropped message:', messageCount); } }); });};常见问题解决1. WebSocket 连接失败检查项:确认代理规则正确检查目标服务器是否支持 WebSocket确认防火墙设置检查 SSL 证书(对于 wss)2. 消息乱码解决方法:确认消息编码格式检查消息是否为二进制数据使用正确的解码方式3. 连接频繁断开原因:网络不稳定服务器超时设置心跳机制问题解决方法:增加超时时间实现心跳机制优化网络环境最佳实践使用插件进行复杂处理插件提供更强大的功能便于复用和维护记录消息日志便于问题排查分析消息模式测试异常场景测试网络中断测试消息丢失测试连接超时性能优化减少不必要的消息处理使用消息压缩优化消息格式
阅读 0·2月21日 16:27

whistle 在实际开发中有哪些应用场景和实战技巧?

答案Whistle 在实际开发中有许多实用的应用场景,可以大大提高开发效率和问题排查能力。常见应用场景1. 本地开发调试场景描述:前端开发时需要调用后端接口,但后端服务在本地运行,需要将线上域名指向本地服务。解决方案:# 将线上域名指向本地服务www.example.com host 127.0.0.1:3000api.example.com host 127.0.0.1:3001# 或者使用 forward 操作符www.example.com forward http://127.0.0.1:3000优点:无需修改代码中的域名快速切换本地和线上环境便于前后端联调2. 解决跨域问题场景描述:前端开发时遇到跨域问题,需要添加 CORS 响应头。解决方案:# 添加 CORS 响应头www.example.com resHeaders://{cors-headers.json}cors-headers.json:{ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization"}优点:无需后端修改代码快速解决跨域问题适合开发环境使用3. 接口数据模拟场景描述:后端接口尚未开发完成,前端需要先开发功能,需要模拟接口返回数据。解决方案:# 模拟用户接口www.example.com/api/user resBody://{mock-user.json}# 模拟列表接口www.example.com/api/list resBody://{mock-list.json}# 使用脚本动态生成数据www.example.com/api/dynamic resScript://{dynamic-mock.js}mock-user.json:{ "code": 0, "data": { "id": 1, "name": "张三", "age": 25 }}优点:前后端并行开发提高开发效率便于测试各种场景4. 多环境切换场景描述:需要在开发、测试、生产环境之间快速切换,测试不同环境的功能。解决方案:创建不同环境的配置文件:rules-dev:www.example.com host 127.0.0.1:3000api.example.com host 127.0.0.1:3001rules-test:www.example.com host test.example.comapi.example.com host api-test.example.comrules-prod:www.example.com host prod.example.comapi.example.com host api-prod.example.com切换环境:# 切换到开发环境w2 restart -f rules-dev# 切换到测试环境w2 restart -f rules-test# 切换到生产环境w2 restart -f rules-prod优点:快速切换环境避免修改代码提高测试效率5. 性能优化测试场景描述:需要测试网站性能,找出性能瓶颈。解决方案:# 启用 gzip 压缩www.example.com resHeaders://{compression-headers.json}# 设置缓存策略www.example.com/static/* resHeaders://{cache-headers.json}# 模拟慢速网络www.example.com resScript://{slow-network.js}slow-network.js:module.exports = function(req, res) { setTimeout(() => { res.end(JSON.stringify({ code: 0, data: 'success' })); }, 2000); // 延迟2秒};优点:模拟真实网络环境发现性能问题优化用户体验6. 移动端调试场景描述:需要在移动设备上调试 Web 应用或混合应用。解决方案:配置手机代理手机和电脑连接同一 Wi-Fi配置手机代理指向电脑 IP 和 whistle 端口安装 HTTPS 证书添加移动端专用规则# 移动端接口模拟m.example.com/api/user resBody://{mobile-mock-user.json}# 移动端跨域处理m.example.com resHeaders://{cors-headers.json}优点:真实设备测试捕获移动端网络请求解决移动端特定问题7. 接口错误模拟场景描述:需要测试应用对各种错误情况的处理,如网络错误、服务器错误等。解决方案:# 模拟 500 错误www.example.com/api/error resBody://{"code":500,"message":"服务器错误"}# 模拟超时www.example.com/api/timeout resScript://{timeout.js}# 模拟网络错误www.example.com/api/network-error resScript://{network-error.js}timeout.js:module.exports = function(req, res) { // 不返回响应,模拟超时 setTimeout(() => { // 可选:返回超时错误 res.statusCode = 408; res.end(JSON.stringify({ code: 408, message: '请求超时' })); }, 30000);};优点:测试异常处理逻辑提高应用健壮性便于排查问题8. 接口数据修改场景描述:需要修改接口返回的数据,测试不同数据场景。解决方案:# 修改响应数据www.example.com/api/user resScript://{modify-data.js}# 替换响应内容www.example.com resReplace://old-string/new-stringmodify-data.js:module.exports = function(req, res) { const originalEnd = res.end; res.end = function(chunk, encoding) { if (chunk) { const body = chunk.toString(); const jsonData = JSON.parse(body); // 修改数据 jsonData.data.status = 'active'; jsonData.data.timestamp = Date.now(); originalEnd.call(res, JSON.stringify(jsonData), encoding); } else { originalEnd.call(res, chunk, encoding); } };};优点:快速测试不同数据场景无需后端修改提高测试效率9. 接口请求修改场景描述:需要修改请求参数或请求头,测试不同请求场景。解决方案:# 修改请求头www.example.com reqHeaders://{custom-headers.json}# 使用脚本修改请求www.example.com reqScript://{modify-request.js}custom-headers.json:{ "X-Custom-Header": "Custom Value", "X-Request-ID": "12345"}modify-request.js:module.exports = function(req, res) { // 添加请求参数 if (req.url.includes('/api/user')) { const separator = req.url.includes('?') ? '&' : '?'; req.url += separator + 'debug=true'; } // 修改请求头 req.headers['X-Debug-Mode'] = 'true';};优点:测试不同请求场景添加调试信息便于问题排查10. WebSocket 调试场景描述:需要调试 WebSocket 连接和消息。解决方案:# WebSocket 代理ws.example.com host 127.0.0.1:8080# 使用插件调试 WebSocketws.example.com plugin://websocket-debugger优点:捕获 WebSocket 消息调试实时通信解决连接问题实战技巧1. 规则分组管理使用注释和分组来管理大量规则:# ==================== 本地开发 ====================www.local.com host 127.0.0.1:3000api.local.com host 127.0.0.1:3001# ==================== 测试环境 ====================www.test.com host test.example.comapi.test.com host api-test.example.com# ==================== 数据模拟 ====================www.example.com/api/user resBody://{mock-user.json}www.example.com/api/list resBody://{mock-list.json}2. 快速启用/禁用规则在规则前添加 # 来快速禁用规则:# www.example.com host 127.0.0.1:3000 # 禁用www.example.com host test.example.com # 启用3. 使用 Values 文件创建 Values 文件存储常用配置:values.json:{ "local-ip": "127.0.0.1", "local-port": "3000", "test-host": "test.example.com"}在规则中使用:www.example.com host {{local-ip}}:{{local-port}}4. 导出和导入配置定期导出配置文件,便于备份和分享:# 导出配置cp ~/.whistle/rules ~/backup/whistle-rules-$(date +%Y%m%d)# 导入配置cp ~/backup/whistle-rules-20240101 ~/.whistle/rules最佳实践定期备份配置避免配置丢失便于回滚使用版本控制使用 Git 管理配置文件便于团队协作添加清晰注释说明规则用途便于维护定期清理规则删除不再使用的规则保持配置简洁安全考虑不要在公共网络使用调试完成后关闭代理保护证书安全
阅读 0·2月21日 16:27

whistle 常见问题有哪些,如何排查和解决?

答案Whistle 在实际使用中可能会遇到各种问题,了解常见问题及其解决方法可以提高工作效率。安装和启动问题1. 安装失败问题:npm install -g whistle# 报错:EACCES: permission denied解决方法:方法一:使用 sudo(Mac/Linux)sudo npm install -g whistle方法二:修改 npm 目录权限sudo chown -R $(whoami) ~/.npmsudo chown -R $(whoami) /usr/local/lib/node_modules方法三:使用 nvmnvm install nodenvm use nodenpm install -g whistle2. 启动失败问题:w2 start# 报错:Port 8899 is already in use解决方法:方法一:查找并关闭占用端口的进程# Mac/Linuxlsof -i :8899kill -9 <PID># Windowsnetstat -ano | findstr :8899taskkill /PID <PID> /F方法二:使用其他端口w2 start -p 8080方法三:停止之前的 whistle 实例w2 stopw2 start3. 启动后无法访问问题:启动成功但无法访问 http://127.0.0.1:8899/解决方法:检查 whistle 是否正在运行:w2 status检查防火墙设置:Windows:允许 whistle 通过防火墙Mac:系统偏好设置 → 安全性与隐私 → 防火墙Linux:检查 iptables 或 ufw 设置检查端口是否正确:# 查看监听端口netstat -an | grep 8899配置问题1. 规则不生效问题:配置了规则但没有生效解决方法:检查规则语法:确保规则格式正确检查是否有语法错误查看规则是否被注释检查规则优先级:更具体的规则应该放在前面检查是否有规则冲突重启 whistle:w2 restart清除浏览器缓存:清除浏览器缓存和 Cookie使用隐私模式测试2. HTTPS 拦截失败问题:无法拦截 HTTPS 请求解决方法:检查 HTTPS 拦截是否启用:访问 http://127.0.0.1:8899/点击 "HTTPS" 标签勾选 "Capture HTTPS"检查证书是否正确安装:下载根证书安装到受信任的根证书颁发机构重启浏览器使用规则启用 HTTPS:pattern whistle.https://3. 代理配置错误问题:浏览器无法通过 whistle 代理访问网络解决方法:检查代理配置:确认代理地址正确:127.0.0.1:8899确认代理类型:HTTP 代理确认没有配置 PAC 文件测试代理连接:curl -x http://127.0.0.1:8899 http://www.example.com检查网络连接:确认电脑可以访问网络检查 DNS 设置性能问题1. whistle 运行缓慢问题:whistle 响应缓慢,影响开发效率解决方法:清除缓存:w2 clean cache减少规则数量:删除不必要的规则使用更精确的匹配模式增加内存限制:node --max-old-space-size=4096 $(which w2) start升级到最新版本:npm update -g whistle2. 内存占用过高问题:whistle 占用大量内存解决方法:查看内存使用:w2 memory限制日志大小:w2 log clear定期重启 whistle:w2 restart优化规则:避免使用复杂的正则表达式减少脚本处理3. CPU 占用过高问题:whistle 占用大量 CPU解决方法:查看 CPU 使用:w2 cpu检查插件:禁用不必要的插件更新插件到最新版本优化脚本:减少脚本中的复杂计算使用异步操作移动端问题1. 手机无法连接到代理问题:配置了手机代理但无法连接解决方法:检查网络连接:确认手机和电脑在同一 Wi-Fi测试手机能否访问电脑 IP检查代理配置:确认代理地址是电脑 IP确认代理端口是 8899确认代理类型是 HTTP检查防火墙:允许 whistle 通过防火墙允许 8899 端口入站连接2. HTTPS 证书安装失败问题:手机无法安装或信任 HTTPS 证书解决方法:iOS 设备:下载证书后打开"设置" → "已下载描述文件"安装证书进入"设置" → "通用" → "关于本机" → "证书信任设置"启用"完全信任"Android 设备:下载证书后打开按照提示安装进入"设置" → "安全" → "加密与凭据" → "受信任的凭据"确认证书已安装重启手机浏览器3. 某些应用无法拦截问题:某些应用的请求无法被 whistle 拦截解决方法:检查应用是否使用系统代理:某些应用不使用系统代理需要使用 VPN 模式检查证书绑定:某些应用使用证书绑定需要使用 Frida 等工具检查网络库:某些应用使用自定义网络库需要逆向分析WebSocket 问题1. WebSocket 连接失败问题:WebSocket 无法建立连接解决方法:检查代理规则:ws://example.com host 127.0.0.1:8080检查服务器支持:确认服务器支持 WebSocket检查 WebSocket 端口是否开放检查防火墙:允许 WebSocket 端口检查代理设置2. WebSocket 消息丢失问题:WebSocket 消息部分丢失解决方法:检查网络稳定性:使用稳定的网络避免频繁切换网络检查服务器负载:服务器可能过载增加服务器资源检查心跳机制:实现心跳检测自动重连机制插件问题1. 插件安装失败问题:无法安装 whistle 插件解决方法:检查 npm 源:npm config get registry# 如果不是官方源,切换到官方源npm config set registry https://registry.npmjs.org/检查网络连接:确保可以访问 npm 仓库使用代理或镜像使用淘宝镜像:npm config set registry https://registry.npmmirror.com/2. 插件运行错误问题:插件安装后运行报错解决方法:查看错误日志:w2 log检查插件版本:确认插件版本与 whistle 版本兼容更新插件到最新版本检查插件依赖:安装插件依赖npm install数据问题1. 配置丢失问题:whistle 配置意外丢失解决方法:恢复备份:cp ~/.whistle/rules.backup ~/.whistle/rules从 Git 恢复:git checkout ~/.whistle/rules重新配置:重新添加规则重新安装插件2. 日志过大问题:whistle 日志文件过大解决方法:清空日志:w2 log clear设置日志轮转:w2 log rotate定期清理:# 创建定时任务清理日志crontab -e# 添加:0 0 * * * w2 log clear最佳实践定期备份配置使用 Git 管理配置定期导出配置文件保留历史版本保持更新定期更新 whistle更新插件到最新版本关注官方公告监控资源使用定期检查内存和 CPU及时清理缓存优化规则和脚本文档化配置添加规则注释编写配置文档记录问题解决方案使用脚本自动化自动化常用操作减少手动操作提高工作效率
阅读 0·2月21日 16:27

whistle 和 Charles 有什么区别,如何选择使用?

答案Whistle 和 Charles 都是常用的网络调试代理工具,但它们在设计理念、功能特性和使用场景上有所不同。核心差异对比| 特性 | Whistle | Charles ||------|---------|---------|| 开源性质 | 完全开源免费 | 商业软件(有免费版) || 开发语言 | Node.js | Java || 配置方式 | 规则配置,更灵活 | 图形界面,更直观 || 插件系统 | 支持插件扩展 | 支持扩展 || 跨平台 | 完美支持 Windows/Mac/Linux | 主要支持 Windows/Mac || 学习曲线 | 需要学习规则语法 | 图形界面易上手 |Whistle 的优势规则配置更灵活支持复杂的规则组合可以通过脚本实现自定义逻辑规则可以版本控制开源免费无需付费即可使用全部功能社区活跃,问题解决快可以根据需求修改源码更适合团队协作规则配置可以共享支持配置文件导入导出适合集成到开发流程中性能更好基于 Node.js,启动速度快内存占用相对较小处理大量请求时更稳定Charles 的优势图形界面更友好可视化操作,无需记忆命令适合不熟悉命令行的用户界面美观,操作直观功能更全面内置更多调试工具支持更多协议提供更详细的请求分析适合快速调试启动即可使用界面操作快速适合临时调试任务选择建议选择 Whistle 的场景:需要长期使用网络代理团队协作开发需要自定义复杂的代理逻辑预算有限,需要免费工具熟悉命令行和脚本选择 Charles 的场景:偶尔需要调试网络请求更喜欢图形界面操作需要快速上手使用对可视化分析有更高要求预算充足,愿意付费总结Whistle 更适合专业开发者和技术团队,特别是需要长期、深度使用网络代理的场景。Charles 更适合偶尔使用或偏好图形界面的用户。两者都是优秀的工具,选择哪个主要取决于个人习惯和具体需求。
阅读 0·2月21日 16:27