避免并发的重复请求 for Angular

在项目的实际开发中偶然遇到了相同的GET请求被连续触发的问题,典型用例如CMS系统首页打开时导航栏需要加载栏目数据,页面中的栏目列表也同样请求该数据。当然,理想状态下可以要求导航栏先加载并缓存,然后其它组件从缓存中获取,然而实际上这些功能可能由不同的开发者编写,那么协调起来就麻烦一些了。而且越复杂的系统就更容易的出现这个问题,所以不得不解决一下了。最初遇到这个问题是在一个AngularJS(AngularJS1.6测试通过)项目中,所以先丢这个出来:/** * 这只是一个简单的例子,请自行扩展。 * 返回的值总是一个promise,这样就默默的拦截了重复的请求 * 注意:这里使用了本地缓存,这可能造成数据无法更新, * 而下一个例子则仅仅是过滤掉一个请求周期之内重复的请求 /function get(url) { var defer = $q.defer(); if (localStage.getItem(‘cachedRequest-’ + url) !== null) { if (localStage.getItem(‘cachedRequest-’ + url).then) { //then方法不是undefined那么这就是个promise对象,扔回去 return localStage.getItem(‘cachedRequest-’ + url); } else { //数据已经本地缓存了那就放到defer里面返回 defer.resolve(JSON.parse(localStage.getItem(‘cachedRequest-’ + url))); } } else { //不好解释,要打太多字…明白就好 var promise = $http.get(url).then(function(res){ localStage.setItem(‘cachedRequest-’ + url, JSON.stringify(res)); return defer.resolve(res); }); defer.resolve(promise); } return defer.promise();}Angular版本 (Angular6测试通过)/* * 这是最简代码,错误处理等是使用拦截器实现的 /import { Injectable } from ‘@angular/core’;import { throwError, Subject } from ‘rxjs’;import { HttpClient, HttpHeaders } from ‘@angular/common/http’;import { map } from ‘rxjs/operators’;//如果配置文件中设置了代理那么可以丢掉这个const BEServer = “http://localhost”;@Injectable({ providedIn: ‘root’})export class ApiRequestService { private apiSubjects = {}; constructor( private http: HttpClient ) { } private extractData(res: Response) { let body = res; return body || { }; } private buildUrl(url, params) { / 此处略去N行代码和迭代方法 / return url; } private getHttpOptions(type) { / 各种略 */ return {headers:{}}; } exec(type, url, data = null) { url = BEServer + url; let method = type.toLowerCase(); if (([‘get’, ‘post’, ‘put’, ‘delete’, ‘file’]).indexOf(method) < 0) { return throwError(‘Request method is invalid.’); } let httpOptions = this.getHttpOptions(method); if (method == ‘get’) { if (data) { url = this.buildUrl(url, data); } } if (method == ‘get’) { if (! this.apiSubjects[url]) { this.apiSubjects[url] = { subscribe: this.http[method](url, httpOptions).pipe(map(this.extractData)).subscribe(data => { this.apiSubjects[url].subject.next(data); //这个delete的处理感觉不顺,但是实测也找不到更好的办法 delete(this.apiSubjects[url]); }), subject: new Subject<Object>() }; } return this.apiSubjects[url].subject; } else if (method == ‘delete’) { return this.http[method](url, httpOptions).pipe(map(this.extractData)); } else { return this.http[method](url, data, httpOptions).pipe(map(this.extractData)); } }}//调用测试,不必要的代码全略掉instanceOfApiRequestService.exec(‘GET’, ‘/api/dashboard’).subscribe(data => { console.log(data);});instanceOfApiRequestService.exec(‘GET’, ‘/api/dashboard’).subscribe(data => { console.log(data);});instanceOfApiRequestService.exec(‘GET’, ‘/api/dashboard’).subscribe(data => { console.log(data);});//三次log都被触发,但是只有一次http请求。刚接触Angular6不久,不管是我这个想法本身有错误还是解决的方式有问题都请拍砖不要客气,只求大侠的砖头上绘制一下示例代码,不胜感激。 ...

November 5, 2018 · 2 min · jiezi

高级 Angular 组件模式 (7)

07 使用 Content Directives原文: Use Content Directives因为父组件会提供所有相关的 UI 元素(比如这里的 button),所以 toggle 组件的开发者可能无法满足组件使用者的一些附加需求,比如,在一个自定义的开关控制元素上增加 aria 属性。如果 toggle 组件能够提供一些 hooks 方法或指令给组件使用者,这些 hooks 方法或指令能够在自定义的开关元素上设置一些合理的默认值,那将是极好的。目标提供一些 hooks 方法或指令给组件使用者,使其可以与所提供的 UI 元素交互并修改它们。实现我们通过实现一个 [toggler] 指令来负责向组件使用者提供的自定义元素增加 role=“switch” 和 aria-pressed 属性。这个 [toggler] 指令拥有一个 [on] input 属性(并与 <switch> 组件共享),该属性将决定 aria-pressed 属性的值是 true 还是 false。成果stackblitz演示地址译者注到这里已经是第七篇了,也许你已经发现,Angular 中很多开发模式或者理念,都和 Directive 脱不了干系。Angular 中其本身推崇组件化开发,即把一切 UI 概念当做 Component 来看待,但仔细思考的话,这其实是有前提的,即这个 UI 概念一般是由一个或多个 html 元素组成的,比如一个按钮、一个表格等。但是在前端开发中,小于元素这个颗粒度的概念也是存在的,比如上文提及的 aira 属性便是其中之一,如果也为将这些 UI 概念抽象化为一个组件,就未免杀鸡用牛刀了,因此这里使用 Directive 才是最佳实践,其官方文章本身也有描述,Directive 即为没有模板的 Component。从组件开发者的角度来看的话,Directive 也会作为一种相对 Component 更加轻量的解决方案,因为与其提供封装良好、配置灵活、功能完备(这三点其实很难同时满足)的 Component,不如提供功能简单的 Directive,而将部分其他工作交付组件使用者来完成。比如文章中所提及的,作为组件开发者,无法预先得知组件使用者会怎样管理开关元素以及它的样式,因此提供一些 hooks 是很有必要的,而 hooks 这个概念,一般情况下,都会是相对简单的,比如生命周期 hook、调用过程 hook、自定义属性 hook 等,在这里,我们通过 Directive 为自定义开关元素增加 aria 属性来达到提供自定义属性 hook 的目标。 ...

October 9, 2018 · 1 min · jiezi

浅淡 RxJS WebSocket

引言中后台仪表盘是一个非常复杂,特别是当需要全面屏运用时,数据的实时性需求非常高。WebSocket 不管在什么环境中使用其实都是非常简单,各现代浏览器实现标准都很统一,而且接口也足够简单。即便是在 Angular 也是如此,只需要简单几行代码就能使用 WebSocket。const ws = new WebSocket(‘wss://echo.websocket.org’);ws.onmessage = (e) => { console.log(‘message’, e);}若需要向服务端发送消息,则:ws.send(content);在 Angular 里绝大多数的人都会根据上述代码进一步拓展,比如统一消息解析、错误处理、多路复用等,并最终将其封装成一个服务类。事实上,RxJS 也包裹了一个 WebSocket Subject,位于 rxjs/websocket。如何使用假如将上面的示例使用 RxJS 来写,则:import { webSocket, WebSocketSubject } from ‘rxjs/webSocket’;const ws = webSocket(‘wss://echo.websocket.org’);ws.subscribe(res => { console.log(‘message’, res);});ws.next(content);webSocket 是一个工厂函数,所生产出来的 WebSocketSubject 对象可被多次订阅,若未订阅或取消最后一个订阅时都会导致 WebSocket 连接中断,当再一次订阅时会重新自动连接。WebSocketSubjectConfigwebSocket 除了接收字符串(WebSocket服务远程地址)外,还允许指定更复杂的配置项。默认情况下,消息是使用 JSON.parse 和 JSON.stringify 对消息格式序列化和反序列化操作,所以不管消息发送或接收都以 JSON 为准,可通过 serializer、deserializer 属性来改变。若需要关心 WebSocket 什么时候开始或结束(closeObserver),则:const open$ = new Subject();const ws = webSocket({ url: ‘wss://echo.websocket.org’, openObserver: open$});// 订阅打开事件open$.subscribe(() => {});消息WebSocketSubject 也是 Subject 的变体之一,因此订阅它表示接收消息,反之则利用 next、complete、error 来维护消息的推送。使用 next 来发送消息使用 complete 会尝试检测是否最后一个订阅,若是将会关闭连接使用 error 相当于原始 close 方法且必须提供 { code: number, reason?: string} 参数,注意 code 务必遵守取值范围可被重放调用 next 发送消息时若 WebSocket 连接中断(例如:没人订阅时),消息会被缓存当下一次重新连接以后会按顺序发送。这对于异步世界里非常方便,我们只需要确保 Angular 启动前初始化好 WebSocket 不管什么时候订阅接收消息,都可以随时发送也无须等待。事实上这一点是 RxJS WebSocket 默认情况下是通过 webSocket 所生产的 WebSocketSubject 其本质上是 ReplaySubject 的“重放”能力。当然你可以通过 webSocket 的第二个参数改变这种行为。多路复用一般来说我们不太可能只会一个 Web Socket 服务完成所有的事,然而也不太可能针对每一个业务实例创建一个 webSocket。往往我们会增加一层网关并将这些业务 WebSocket 进行汇总,对于前端始终只需要一个连接,这就是多路复用存在的意义。而核心是必须要让后端知道,什么时候发送什么消息给什么样的服务。首先必须先使用 multiplex 方法来创建 Observable 以便订阅某一路消息,它有三个参数来帮助我们区分消息:subMsg 告知正在订阅哪一路消息unsubMsg 告知取消订阅哪一路消息messageFilter 过滤消息,使订阅者只接收哪一路消息const ws = webSocket(‘wss://echo.websocket.org’);const user$ = this.ws.multiplex( () => ({ type: ‘subscribe’, tag: ‘user’ }), () => ({ type: ‘unsubscribe’, tag: ‘user’ }), message => message.type === ‘user’);user$.subscribe(message => console.log(message));const todo$ = this.ws.multiplex( () => ({ type: ‘subscribe’, tag: ’todo’ }), () => ({ type: ‘unsubscribe’, tag: ’todo’ }), message => message.type === ’todo’);todo$.subscribe(message => console.log(message));user$ 流和 todo$ 流他们共用一个 WebSocket 连接,这便是多路复用。虽然订阅是通过 multiplex 创建的,然后消息的推送依然还是需要使用 ws.next()。总结这原本是对内部一个简单培训,然而我发现竟然极少人会讨论 RxJS 里面 Web Socket 的实现。其实一直有想着要给 ng-alain 内置 WebSocket,只是就封装角度来讲完全没有价值,因为已经足够优雅。 ...

September 24, 2018 · 1 min · jiezi

使用 ng-packagr 打包 Angular

写在前面为了让 Angular 类库应用范围更自由,Angular 提出一套打包格式建议名曰:Angular Package Format,包括 FESM2015、FESM5、UMD、ESM2015、ESM5、ES2015 格式,不同格式可以在不同的环境(Angular Cli、Webpack、SystemJS等)中使用。传统方式需要对这些格式逐一打包,一个示例打包脚本写法。这种写法只能针对不同项目的配置,而且除非你了解这些格式的本质否则很难维护;后来社区根据 APF 规范实现了类库 ng-packagr,通过简单的配置可以将你的类库打包成 APF 规范格式。至 V6 以后 Angular Cli 也基于 ng-packagr 实现了另一个 @angular-devkit/build-ng-packagr 应用构建器。如何使用既然 ng-packagr 被 Angular Cli 内置,这让我们进一步简化了生产一个 APF 规范格式的类库的成本。在 Angualr Cli 里使用 ng g library 来创建一个类库模板,例如在一个新的 Angular 应用里执行:ng g library <library name>而打包,则:ng build <library name>最终,将生成的 dist/<libary name> 目录下文件上传相应包管理服务器(例如:npm)提供给其他 人使用。配置说明由 Angular Cli 生成的类库模板大部分内容同 Angular 应用一样,只是多了一个 ng-package.json 的配置文件(对于生产环境是 ng-package.prod.json),它是专门针对 ng-packagr 的一个配置文件,如同 angular.json 一般也是基于 JSON Schema 格式,因此可以通过访问 ng-package.schema.json 了解所有细节,以下描述一些重点项。whitelistedNonPeerDependenciesng-packagr 默认会根据 package.json 的 peerDependencies 节点清单来决定类库所需要第三方依赖包,这些依赖包是不会被打包至类库。然而,所依赖包不存在 peerDependencies 节点里时(当然建议需要依赖的项应该在里面),就需要该属性的配置。lib/entryFile指定入口文件。lib/umdModuleIdsUMD 格式采用 rollup 打包,当类库需要引用一些无法猜出正确 UMD 标识符时,就需要你手动映射这些类库的标识。“umdModuleIds”: { “lodash”: “_"}angular.jsonAngular Cli 配置文件 angular.json 内会增加一个以 <libary name> 命名的构建配置,绝大多数配置性同普通 Angular 应用如出一辙,唯一不同的是 builder 节点为:“builder”: “@angular-devkit/build-ng-packagr:build"次级入口有时候一个类库可能会包含着多个二次入口,就像 @angular/core 类库包含着一个 @angular/core/testing 模块,它只是运用于测试,因此并不希望在项目中引入 @angular/core 时也包含测试代码,但同时二者又是同一个功能性时,这种次级导入显得非常重要。另一种像 ngx-bootstrap、@angular/cdk/ally 等都提供次级模块的导入,可以更好的优化体积。不论出于何种目的,都可以通过 Angular Cli 简单的文件组织进一步打包出主、次级分明的类库。ng g library 生成的结构大概如下:<libary name>├── src| ├── public_api.ts| └── lib/.ts├── ng-package.json├── ng-package.prod.json├── package.json├── tsconfig.lib.json└── tsconfig.spec.json当根目录下包含 README.md、LICENSE 时会自动被复制到 dist 目录中,Npm 规定必须包含 README.md 文件,否则访问已发布类库页时会有未找到描述文件错误提示。若想创建一个 <libary name>/testing 的次级入口,只需要在 <libary name> 根目录下创建一个 testing 目录:<libary name>├── src| ├── public_api.ts| └── lib/.ts├── ng-package.json├── ng-package.prod.json├── package.json├── tsconfig.lib.json├── tsconfig.spec.json└── testing ├── src | ├── public_api.ts | └── .ts └── package.json核心是需要提供一个 package.json 文件,而且内容简单到姥姥家。{ “ngPackage”: {}}最后,依然使用 ng build <libary name>,会产生一个次级导入模块。小结至此,基本上利用 Angular Cli 可以快速的构建一个可发布于 Npm Angular 类库,更复杂的可以构建像 ngx-bootstrap、@angular/cdk/ 类库。自定义构建Angular Cli 虽然提供非常便利的环境,但是对于一些复杂环境像 Delon 类库(ng-alain基建系列类库)包含着多个类库、类库又包含多个次级导入时,Angular Cli 会显得有点啰嗦,特别是对每个类库的 angular.json 配置。其实 @angular-devkit/build-ng-packagr 非常简单,如果将取进一步简化,整个实现差不多相当于:const path = require(‘path’);const ngPackage = require(’ng-packagr’);const target = path.resolve(__dirname, ‘./projects/<libary name>’);ngPackage .ngPackagr() .forProject(path.resolve(target, ng-package.prod.json)) .withTsConfig(path.resolve(target, ’tsconfig.lib.json’)) .build() .then(() => { // 构建完成后干点事 });将上面的代码放到 ./build.js,执行:node scripts/build.js其结果完成是等价。build() 返回的是一个 Promise 对象,意味着可以确保构建开始前和结束后做一点额外的事。总结ng-packagr 极大简化 Angular 类库被打包出一个 APF 规范建议,虽然它以 ng- 开头,但本质上并不一定非要在 Angular 中运用,也可以使用在 React、VUE。 ...

September 20, 2018 · 2 min · jiezi