原文链接: blog.thoughtram.io/angular/201…

本文为 RxJS 中文社区 翻译文章,如需转载,请注明出处,谢谢合作!

如果你也想和我们一起,翻译更多优质的 RxJS 文章以奉献给大家,请点击【这里】

温馨提示: 文章较长,原文中写的是40分钟阅读,建议大家午后有大把空闲时间再慢慢读来

开发 Web 应用时,性能始终都是重中之重。要想提升 Angular 应用的速度,我们可以做一些工作,比如要摇树优化 (tree-shaking)、AoT (ahead-of-time)、模块的懒加载以及缓存。想要对 Angular 应用的性能提升的实战技巧有一个全面了解的话,我们强烈推荐你参考由 Minko Gechev 撰写的 Angular 性能检测表。在本文中,我们将专注于缓存。

实际上,缓存是提升网站用户体验的最有效的方式之一,尤其是当用户使用宽带受限的设备或网络环境较差。

缓存数据或资源的方式有很多种。静态资源通常都是由标准的浏览器缓存或 Service Workers 来进行缓存。虽然 Service Workers 也可以缓存 API 请求,但是对于图像、HTML、JS 或 CSS 文件等资源的缓存,它们通常更为有用。我们通常使用自定义机制来缓存应用的数据。

无论我们使用的是什么机制,缓存通常都是提升应用的响应能力减少网络花销,并具有内容在网络中断时可用的优势。换句话说,当内容被缓存的更接近消费者时,比如在客户端,请求将不会导致额外的网络活动,并且可以更快速地检索缓存数据,从而节省了网络往返的整个过程。

在本文中,我们将使用 RxJS 和 Angular 提供的工具来开发一个高级缓存机制。

目录

  • 动机
  • 需求
  • 实现基础缓存
  • 自动更新
  • 发送更新通知
  • 按需拉取新数据
  • 展望
  • 特别鸣谢

动机

不时地就会有人问,如何在大量使用 Observables 的 Angular 应用中缓存数据?大多数人对于如何使用 Promises 来缓存数据有不错的理解,但当切换至响应式编程时,便会因为它的复杂度 (庞大的 API)、思维转化 (从命令式到声明式) 和众多概念而感到不知所措。因此,很难将一个基于 Promises 的现有缓存机制转换成基于 Observables 的,当你想要缓存机制变得更高级点时更是如此。

在 Angular 应用中通常使用 HttpClientModule 中的 HttpClient 来执行 HTTP 请求。HttpClient 的所有 API 都是基于 Observable 的,也就是说像 getpostputdelete 等方法返回的都是 Observable 。因为 Observables 天生是惰性的,所以只有当我们调用 subscribe 时才会真正发起请求。但是,对同一个 Observable 调用多次 subscribe 会导致源 Observable 一遍又一遍地重新创建,每个订阅 (subscription) 上执行一个请求。我们称之为冷的 Observables 。

如果你对此完全没有概念的话,我们之前写过一篇此主题的文章: 冷的 vs 热的 Observables 。(译者注: 想了解冷的 vs 热的 Observables,还可以推荐阅读这篇文章)

这种行为将导致使用 Observables 来实现缓存机制变得很棘手。简单的方法往往就需要相当数量的样板代码, 我们可能会选择绕过 RxJS, 这也是可行的,但如果我们想要最终驾驭 Observables 的强大力量时,这种方式是不推荐的。说白了就是我们不想开配备小型摩托车引擎的法拉利,对吧?

需求

在深入代码之前,我们先来为要实现的高级缓存机制制定需求。

我们想要开发的应用名为笑话世界。这是一个简单的应用,它只是根据制定的分类来随机展示笑话。为了让应用更简单、更专注,我们只设定一个分类。

应用有三个组件: AppComponentDashboardComponentJokeListComponent

AppComponent 组件是应用的入口,它渲染工具栏和 <router-outlet>,后者会根据当前路由器状态来填充内容。

DashboardComponent 组件只展示分类的列表。在这可以导航至 JokeListComponent 组件,它负责将笑话列表渲染到屏幕中。

笑话是使用 Angular 的 HttpClient 服务从服务器拉取的。要保持组件的职责单一和概念分离,我们想创建一个 JokeService 来负责请求数据。然后组件只需通过注入此服务便可以通过它的公有 API 来访问数据。

以上就是我们这个应用的架构,目前还没有涉及到缓存。

当从分类列表页导航至笑话列表页时,我们更倾向于请求缓存中的最新数据,而不是每次都向服务器发起请求。而缓存的底层数据会每10秒钟自动更新。

当然,对于生产级应用来说,每隔10秒轮询新数据并非是个好选择,一般来说会使用一种更成熟的方式来更新缓存 (例如 Web Socket 推送更新)。但在这里我们将保持简单性,以便于专注于缓存本身。

我们将会以某种形式来接收更新通知。对于这个应用来说,当缓存更新时,我们不想 UI (JokeListComponent) 中的数据自动更新,而是等待用户来执行 UI 的更新。为什么要这样做?想象一下,用户可能正在读某条笑话,然后突然间因为数据的自动更新这条笑话就消失了。这样的结果就是由于这种较差的用户体验,让用户很生气。因此,我们的做法是每当有新数据时提示用户更新。

为了更好玩一些,我们还想要用户能够强制缓存更新。这与仅更新 UI 不同,因为强制更新意味着从服务器请求最新数据、更新缓存,然后相应地更新 UI 。

来总结下我们将要开发的内容点:

  • 应用有两个组件 A 和 B,当从 A 导航至 B 时应该从缓存中请求 B 的数据,而不是每次都请求服务器
  • 缓存每隔10秒自动更新
  • UI 中的数据不会自动更新,而是需要用户执行更新操作
  • 用户可以强制更新,这将会发起 HTTP 请求以更新缓存和 UI

下面是应用的预览图:

实现基础缓存

我们先从简单的开始,然后一步步地打造出最终成熟的解决方案。

第一步是创建一个新的服务。

接下来,我们来添加两个接口,一个是描述 Joke 的数据结构,另一个是用来强化 HTTP 请求响应的类型。这会让 TypeScript 很开心,但最重要的是开发人员使用起来也更加方便和清晰。

export interface Joke {id: number;joke: string;categories: Array<string>;
}export interface JokeResponse {type: string;value: Array<Joke>;
}
复制代码

现在我们来实现 JokeService 。对于数据是来自缓存还是服务器,我们并不想暴露实现的细节,因此,我们只暴露一个 jokes 的属性,它返回的是包含笑话列表的 Observable 。

为了发起 HTTP 请求,我们需要确保在服务的构造函数中注入 HttpClien 服务。

下面是 JokeService 的框架:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';@Injectable()
export class JokeService {constructor(private http: HttpClient) { }get jokes() {...}
}
复制代码

接下来,我们将实现一个私有方法 requestJokes(),它会使用 HttpClient 来发起 GET 请求以获取笑话列表。

import { map } from 'rxjs/operators';@Injectable()
export class JokeService {constructor(private http: HttpClient) { }get jokes() {...}private requestJokes() {return this.http.get<JokeResponse>(API_ENDPOINT).pipe(map(response => response.value));}
}
复制代码

完成这一步后,我们只剩 jokes 的 getter 方法没有完成了。

一个简单的方法就是直接返回 this.requestJokes(),但这样并不会生效。从文章开头中我们已经得知 HttpClient 暴露出的所有方法,例如 get 返回的是冷的 Observables 。这意味着为每次订户都会重新发出整个数据流,从而导致多次的 HTTP 请求。毕竟,缓存的理念是提升应用的加载速度并将网络请求的数量限制到最小。

相反的,我们想让流变成热的。不仅如此,我们还想让每个新订阅者都接收到最新的缓存数据。有一个非常方便的操作符叫做 shareReplay 。它返回的 Observable 会共享底层数据源的单个订阅,在这里也就是 this.requestJokes() 所返回的 Observable 。

除此之外,shareReplay 还接收一个可选参数 bufferSize,对于我们这个案例它是相当便利的。bufferSize 决定了重放缓冲区的最大元素数量,也就是缓存和为每个新订阅者重放的元素数量。对于我们这个场景来说,我们只想要重放最新的一个至,所以 bufferSize 将设定为 1 。

我们来看下代码,并使用刚刚所学习到的知识:

import { Observable } from 'rxjs/Observable';
import { shareReplay, map } from 'rxjs/operators';const API_ENDPOINT = 'https://api.icndb.com/jokes/random/5?limitTo=[nerdy]';
const CACHE_SIZE = 1;@Injectable()
export class JokeService {private cache$: Observable<Array<Joke>>;constructor(private http: HttpClient) { }get jokes() {if (!this.cache$) {this.cache$ = this.requestJokes().pipe(shareReplay(CACHE_SIZE));}return this.cache$;}private requestJokes() {return this.http.get<JokeResponse>(API_ENDPOINT).pipe(map(response => response.value));}
}
复制代码

Ok,上面代码中的大部分我们都已经讨论过了。但是等下,那个私有属性 cache$ 和 getter 方法中的 if 语句是做什么的?答案很简单。如果直接返回 this.requestJokes().pipe(shareReplay(CACHE_SIZE)) 的话,那么每次订阅都将创建一个缓存实例。但我们想要的是所有订阅者都共享同一个实例。因此,我们将这个共享的实例保存在私有属性 cache$ 中,并在首次调用 getter 方法时对其进行初始化。后续的所有消费者都将共享此实例而无需每次都重新创建缓存。

通过下面的图来更直观地看下我们刚刚实现的内容:

在上图中,我们可以看到描述我们场景中所涉及到的对象的序列图,即请求笑话列表和在对象之间交换消息的队列。我们分解来看,以便更好地了解我们正在做什么。

我们从 DashboardComponent 导航至 JokeListComponent 开始说起。

组件初始化后 Angular 会调用 ngOnInit 生命周期钩子,这里我们将调用 JokeService 暴露的 jokes 的 getter 方法来请求笑话列表。因为这是首次请求数据,所以缓存本身还未初始化,也就是说 JokeService.cache$undefined 。在内部我们会调用 requestJokes(),它会返回一个将会发出服务端数据的 Observable 。同时我们还应用了 shareReplay 操作符来获取预期效果。

shareReplay 操作符会自动在原始数据源和所有后来的订阅者之间创建一个 ReplaySubject 。一旦订阅者的数量从 0 增加至 1,就会将 Subject 与底层源 Observable 进行连接,然后广播出它的所有重放值。后续的所有订阅者都将与中间人 Subject 进行连接,因此底层的冷的 Observable 只有一个订阅。这就是多播,它是我们这个简单缓存机制的基础。(译者注: 想深入了解多播,推荐这篇文章)

一旦服务端返回数据,数据就会被缓存。

注意,在序列图中 Cache 是一个独立的对象,它被表示成一个 ReplaySubject,它位于消费者 (订阅者) 和底层数据源 (HTTP 请求) 之间。

当再次为 JokeListComponent 组件请求数据时,缓存将会重放最新值并将其发送给消费者。这样就不会再发起额外的 HTTP 请求。

很简单,是吧?

要想了解更多细节,我们还需更进一步,来看看在 Observable 级别缓存是如何工作的。因此,我们将使用弹珠图 (marble diagram) 来对流的工作原理进行可视化展示:

弹珠图看上去十分清晰,底层的 Observable 确实只有一个订阅,所有消费者订阅的都是这个共享 Observable,即 ReplaySubject 。我们还可以看到只有第一个订阅者触发了 HTTP 请求,而其他订阅者获得的只是缓存重放的最新值。

最后,我们来看看 JokeListComponent 以及如何展现数据。首先是注入 JokeService 。然后在 ngOnInit 生命周期中对 jokes$ 属性进行初始化,初始值为由服务所暴露的 getter 方法所返回的 Observable, Observable 的类型为 Array<Joke>,这正是我们想要的数据。

@Component({...
})
export class JokeListComponent implements OnInit {jokes$: Observable<Array<Joke>>;constructor(private jokeService: JokeService) { }ngOnInit() {this.jokes$ = this.jokeService.jokes;}...
}
复制代码

注意,我们并没有命令式地去订阅 jokes$,而是在模板中使用 async 管道,这样做是因为这个管道让人爱不释手。很好奇?可以参考这篇文章: 关于 AsyncPipe 你需要知道的三件事

<mat-card *ngFor="let joke of jokes$ | async">...</mat-card>
复制代码

酷!这就是我们的简单缓存了。想要验证请求是否只发起一次,可以打开 Chrome 的开发者工具,然后点击 Network 标签页并选择 XHR 。从分类列表页开始,导航至笑话列表页,然后再返回分类列表页,反反复复几次。

第 1 阶段在线 Demo: 点击查看 。

自动更新

到目前为止,我们已经通过了少量的代码开发出了一个简单的缓存机制,大部分的脏活都是由 shareReplay 操作符完成的,它负责缓存和重放最新值。

目前完全可以正常运行,但是在后台的数据源却永远不会更新。如果数据可能每隔几分钟就发生变化怎么办?我们可不想强迫用户去刷新整个页面才能从服务器获得最新数据。

如果我们的缓存可以在后台每10秒更新一次岂不是很好?完全同意!作为用户,我们不必重新加载页面,如果数据发生变化的话,UI 会相应地更新。重申下,在真实的应用中我们基本上不会使用轮询,而是使用服务器推送通知。但对于我们这个小 Demo 应用来说,间隔 10 秒的定时器已经足够了。

实现起来也相当简单。总而言之,我们想要创建一个 Observable,它发出一系列根据给定时间间隔隔开的值,或者简单点说,我们想要每 x 毫秒就生成一个值。我们有几种实现方式。

第一种选择是使用 interval 。此操作符接收一个可选参数 period,它定义了每次发出值间的时间间隔。参考下面的示例:

import { interval } from 'rxjs/observable/interval';interval(10000).subscribe(console.log);
复制代码

这里我们设置的 Observable 会发出无限序列的整数,每次发出值会间隔 10 秒。也就是说第一个值将会在 10 秒发出。为了更好地演示,我们来看下 interval 操作符的弹珠图:

呃,果真如此。第一个值是“延迟”发出的,而这并非我们想要的效果。为什么这么说?因为如果我们从分类列表页导航至笑话列表页时,我们必须等待 10 秒后才会向服务器发起数据请求以渲染页面。

我们可以通过引入另一个名为 startWith(value) 的操作符来修复此问题,这样一开始就会先发出给定的 value,即初始值。但是,我们可以做的更好!

如果我告诉你还有另外一个操作符,它可以先根据给定的时间 (初始延迟) 发出值,然后再根据时间间隔 (常规的定时器) 来不停地发出值。timer 了解一下。

弹珠图时刻!

酷,但是它真的解决了我们问题了吗?是的,没错。如果我们将初始延迟设置为 0,并将时间间隔设置为 10 秒,这样它的行为就和 interval(10000).pipe(startWith(0)) 是一样的,但却只使用了一个操作符。

我们来使用 timer 操作符并将其运用在我们现有的缓存机制当中。

我们需要设置一个定时器,然后每次时间一到就发起 HTTP 请求来从服务器拉取最新数据。也就是说,对于每个时间点我们都需要使用 switchMap 来切换成一个获取笑话列表的 Observable 。使用 swtichMap 有一个好的副作用就是可以避免条件竞争。这是由于这个操作符的本质,它会取消对前一个内部 Observable 的订阅,然后只发出最新内部 Observable 中的值。

我们缓存的其余部分都保持原样,我们的流仍然是多播的,所有的订阅者都共享同一个底层数据源。

同样的,shareReplay 会将最新值发送给现有的订阅者,并为后来的订阅者重放最新值。

正如在弹珠图中所展示的,timer 每 10 秒发出一个值。每个值都将转换成拉取数据的内部 Observable 。因为使用的是 switchMap,我们可以避免竞争条件,因此消费者只会收到值 13 。第二个内部 Observable 的值会被“跳过”,这是因为当新值发出时我们其实已经取消对它的订阅了。

下面来将我们刚刚所学到的应用到 JokeService 中:

import { timer } from 'rxjs/observable/timer';
import { switchMap, shareReplay } from 'rxjs/operators';const REFRESH_INTERVAL = 10000;@Injectable()
export class JokeService {private cache$: Observable<Array<Joke>>;constructor(private http: HttpClient) { }get jokes() {if (!this.cache$) {// 设置每 X 毫秒发出值的定时器const timer$ = timer(0, REFRESH_INTERVAL);// 每个时间点都会发起 HTTP 请求来获取最新数据this.cache$ = timer$.pipe(switchMap(_ => this.requestJokes()),shareReplay(CACHE_SIZE));}return this.cache$;}...
}
复制代码

酷!是否想自己试试呢?经常尝试下面的在线 Demo 吧。从分类列表页导航至笑话列表页,然后见证奇迹的诞生。耐心等待几秒后就能看见数据更新了。记住,虽然缓存是每 10 秒刷新一次,但你可以在在线 Demo 中自由更改 REFRESH_INTERVAL 的值。

第 2 阶段在线 Demo: 点击查看。

发送更新通知

我们来简单回顾下到目前为止我们所开发的内容。

当从 JokeService 请求数据时,我们总是希望请求缓存中的最新数据,而不是每次都请求服务器。缓存的底层数据每隔 10 秒刷新一次,数据传播到组件后将使得 UI 自动更新。

这是有些失败的。想象一下,我们就是用户,当我们正在看某条笑话时突然笑话就消失了,这是因为 UI 自动更新了。这种糟糕的用户体验会让用户很生气。

因此,当有新数据时应该发通知提醒用户。换句话说,我们想让用户来执行 UI 的更新操作。

事实上,要完成此功能我们都不需要去修改服务层。逻辑相当简单。毕竟,我们的服务层不应该关心发送通知以及何时、如何去更新屏幕上的数据,这些都应该是由视图层来负责。

首先,我们需要由初始值来展示给用户,否则, 在第一次更新缓存之前, 屏幕将是空白的。我们马上就会明白这样做的原因。设置初始值的流就像调用 getter 方法那样简单。此外,因为我们只对首个值感兴趣,所以我们可以使用 take 操作符。

为了让逻辑可以复用,我们创建一个辅助方法 getDataOnce()

import { take } from 'rxjs/operators';@Component({...
})
export class JokeListComponent implements OnInit {...ngOnInit() {const initialJokes$ = this.getDataOnce();...}getDataOnce() {return this.jokeService.jokes.pipe(take(1));}...
}
复制代码

根据需求,我们只想在用户真正执行更新时才更新 UI,而不是自动更新。那么用户如何实施你所要求的更新呢?当我们单击 UI 中表示“更新”的按钮时, 才会执行此操作。暂时,我们不必考虑通知,而应该专注于点击按钮时的更新逻辑。

要完成此功能,我们需要一种方式来创建来源于 DOM 事件的 Observable,在这里指按钮的点击事件。创建的方式有好几种,但最常用的是使用 Subject 作为模板和组件类中逻辑之间的桥梁。简而言之,Subject 是一种同时实现 Observer (观察者) 和 Observable 的类型。Observables 定义了数据流并生成数据,而观察者可以订阅 Observables 并接收数据。

Subject 好的方面是我们可以直接在模板使用事件绑定,然后当事件触发时调用 next 方法。这会将特定值广播给所有正在监听值的观察者们。注意,如果 Subject 的类型为 void 的话,我们还可以省略该值。事实上,这正是我们的实际场景。

我们来实例化一个新的 Subject 。

import { Subject } from 'rxjs/Subject';@Component({...
})
export class JokeListComponent implements OnInit {update$ = new Subject<void>();...
}
复制代码

之后我们就可以在模板中来使用它。

<div class="notification"><span>There's new data available. Click to reload the data.</span><button mat-raised-button color="accent" (click)="update$.next()"><div class="flex-row"><mat-icon>cached</mat-icon>UPDATE</div></button>
</div>
复制代码

来看下我们是如何使用事件绑定语法来捕获 <button> 上的点击事件的?当点击按钮时,我们只是传播一个幽灵值从而通知所有活动的观察者。我们称之为幽灵值是因为实际上并没有传任何值,或者说传递的值的类型为 void

另一种方式是使用 @ViewChild() 装饰器和 RxJS 的 fromEvent 操作符。但是,这需要我们在组件类中“混入” DOM 并从视图中查询 HTML 元素。使用 Subject 的话,我们只需要将两者桥接即可,除了我们在按钮上添加的事件绑定之外,根本不会触及 DOM 。

好了,设置好视图后,我们就可以切换至处理 UI 更新的逻辑了。

那么更新 UI 意味着什么?缓存是在后台自动更新的,而我们想要点击按钮时才渲染从缓存中拿到的最新值,是这样吧?这意味着我们的源头流是 Subject 。每次 update$ 上发出值时,我们就将其映射成给出最新缓存值的 Observable 。换句话说,我们使用的是 高阶 Observable ( Higher Order Observable ) ,即发出 Observables 的 Observable 。

在此之前,我们应该知道 switchMap 正好可以解决这种问题。但这次,我们将使用 mergeMap 。它的行为与 switchMap 很类似,它不会取消前一个内部 Observable 的订阅,而是将内部 Observable 的发出值合并到输出 Observable 中。

事实,从缓存中请求最新值时,HTTP 请求早已完成,缓存也已经成功更新。因此,我们并不会面临条件竞争的问题。虽然这看上去还是异步的,但某种程度上来说,它其实是同步的,因为值是在同一个 tick 中发出的。

import { Subject } from 'rxjs/Subject';
import { mergeMap } from 'rxjs/operators';@Component({...
})
export class JokeListComponent implements OnInit {update$ = new Subject<void>();...ngOnInit() {...const updates$ = this.update$.pipe(mergeMap(() => this.getDataOnce()));...}...
}
复制代码

酷!每次“更新”时我们都是从缓存中请求的最新值,而缓存使用的是我们之前实现的辅助方法。

到这里,还差一小步就可以完成负责将笑话渲染到屏幕上的流。我们所需要做的只是合并 initialJokes$update$ 这两个流。

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { mergeMap } from 'rxjs/operators';@Component({...
})
export class JokeListComponent implements OnInit {jokes$: Observable<Array<Joke>>;update$ = new Subject<void>();...ngOnInit() {const initialJokes$ = this.getDataOnce();const updates$ = this.update$.pipe(mergeMap(() => this.getDataOnce()));this.jokes$ = merge(initialJokes$, updates$);...}...
}
复制代码

我们使用辅助方法 getDataOnce() 来将每次更新事件映射成最新的缓存值,这点很重要。回想一下,在这个方法内部使用了 take(1),它只取第一个值然后就完成流。这是至关重要的,否则最终得到的是一个正在进行中或实时连接到缓存的流。在这种情况下,基本上会破坏我们仅通过点击“更新”按钮来执行 UI 更新的逻辑。

还有,因为底层的缓存是多播的,永远都重新订阅缓存以获取最新值是完全安全的。

在继续完成通知流之前,我们先暂停下来看看刚刚实现逻辑的弹珠图。

正如在图中所看到的,initialJokes$ 很关键,因为如果没有它的话我们只能在点击“更新”按钮后才能看到屏幕上的笑话列表。虽然数据在后台每 10 秒更新一次,但我们根本无法点击更新按钮。因为按钮本身也是通知的一部分,但我们却一直没有将其展示给用户。

那么,让我们填补这个空白并实现缺失的功能。

我们需要创建一个 Observable 来负责显示/隐藏通知。从本质上来说,我们需要一个发出 truefalse 的流。当更新时,我们想要的值是 true,当用户点击“更新”按钮时,我们想要的值是 false

此外,我们还想要跳过缓存发出的首个(初始)值,因为它并不是新数据。

如果使用流的思维,我们可以将其拆分为多个流,然后再将它们合并成单个的 Observable 。最终的流将具备显示或隐藏通知的所需行为。

理论到此为止!下面来看代码:

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { skip, mapTo } from 'rxjs/operators';@Component({...
})
export class JokeListComponent implements OnInit {showNotification$: Observable<boolean>;update$ = new Subject<void>();...ngOnInit() {...const initialNotifications$ = this.jokeService.jokes.pipe(skip(1));const show$ = initialNotifications$.pipe(mapTo(true));const hide$ = this.update$.pipe(mapTo(false));this.showNotification$ = merge(show$, hide$);}...
}
复制代码

这里,我们跳过了缓存的第一个值,然后监听它剩下所有的值,这样做的原因是第一个值不是新数据。我们将 initialNotifications$ 发出的每个值都映射成 true 以显示通知。一旦我们点击通知里的“更新”按钮,update$ 就会产生一个值,我们可以将这个值映射成 false 以关闭通知。

我们在 JokeListComponent 组件的模板中使用 showNotification$ 来切换 class 以显示/关闭通知。

<div class="notification" [class.visible]="showNotification$ | async">...
</div>
复制代码

耶!目前,我们已经十分接近最终的解决方案了。在继续前进之前,我们来试玩下在线 Demo 。不用着急,再来一步步地过遍代码。

第 3 阶段在线 Demo: 点击查看。

按需拉取新数据

酷!一路走来我们已经为我们的缓存实现了一些很酷的功能。要结束本文并将缓存再提升一个等级的话,我们还需要做一件事。作为用户,我们想要能够在任何时间点来强制更新数据。

这并没有什么复杂的,但要完成此功能我们需要同时修改组件和服务。

我们先从服务开始。我们需要一个面向公众的 API 来强制缓存重载数据。从技术上来说,我们会完成当前缓存,并将其设置为 null 。这意味着下次我们从服务中请求数据时会设置一个新的缓存,它会从服务器拉取数据并保存起来以便为后来的订阅者服务。每次强制更新时创建一个新缓存并不是什么大问题,因为旧的缓存将会完成并最终被垃圾收集。实际上,这样做还有一个有用的副作用,就是重置了定时器,这决定是我们想得到的效果。比如说,我们等待 9 秒后点击“强制更新”按钮。我们所期望的数据刷新了,但我们不想看到 1 秒后弹出更新通知。我们想要让计时器重新开始,这样当强制更新后再过 10 秒才应该触发自动更新

销毁缓存的另一个原因是相比于不销毁缓存的版本,它的复杂度要小得多。如果是后者的话,缓存需要知道重载数据是否是强制执行的。

我们来创建一个 Subject,它用来通知缓存以完成。这里我们利用了 takeUnitl 操作符并将其加入到 cache$ 流中。此外,我们还实现了一个公开的 API ,它使用 Subject 来广播事件,同时将缓存设置为 null

import { Subject } from 'rxjs/Subject';
import { timer } from 'rxjs/observable/timer';
import { switchMap, shareReplay, map, takeUntil } from 'rxjs/operators';const REFRESH_INTERVAL = 10000;@Injectable()
export class JokeService {private reload$ = new Subject<void>();...get jokes() {if (!this.cache$) {const timer$ = timer(0, REFRESH_INTERVAL);this.cache$ = timer$.pipe(switchMap(() => this.requestJokes()),takeUntil(this.reload$),shareReplay(CACHE_SIZE));}return this.cache$;}forceReload() {// 调用 `next` 以完成当前缓存流this.reload$.next();// 将缓存设置为 `null`,这样下次调用 `jokes` 时// 就会创建一个新的缓存this.cache$ = null;}...
}
复制代码

光在服务中实现并没有什么作用,我们还需要在 JokeListComponent 中来使用它。为此,我们将实现一个函数 forceReload(),当点击“强制更新”按钮时会调用此函数。此外,我们还需要创建一个 Subject 作为事件总线 ( Event Bus ),用于更新 UI 以及显示通知。我们很快就会看到它的作用。

import { Subject } from 'rxjs/Subject';@Component({...
})
export class JokeListComponent implements OnInit {forceReload$ = new Subject<void>();...forceReload() {this.jokeService.forceReload();this.forceReload$.next();}...
}
复制代码

这样我们就可以将 JokeListComponent 模板中按钮联系起来,以强制缓存重新加载数据。我们需要做的只是使用 Angular 的事件绑定语法来监听 click 事件,当点击按钮时调用 forceReload()

<button class="reload-button" (click)="forceReload()" mat-raised-button color="accent"><div class="flex-row"><mat-icon>cached</mat-icon>FETCH NEW JOKES</div>
</button>
复制代码

这样已经可以工作了,但前提是我们先返回到分类列表页,然后再回到笑话列表页。这肯定不是我们想要的结果。当强制缓存重载数据时我们希望能立即更新 UI 。

还记得我们已经实现好的流 update$ 吗?当我们点击“更新”按钮时,它会请求缓存中的最新数据。事实上,我们需要的也是同样的行为,因此我们可以继续使用并扩展此流。这意味着我们需要合并 update$forceReload$,因为这两个流都是 UI 更新的数据源。

import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { mergeMap } from 'rxjs/operators';@Component({...
})
export class JokeListComponent implements OnInit {update$ = new Subject<void>();forceReload$ = new Subject<void>();...ngOnInit() {...const updates$ = merge(this.update$, this.forceReload$).pipe(mergeMap(() => this.getDataOnce()));...}...
}
复制代码

就是这么简单,难道不是吗?是的,但还没有结束。实际上,我们这样做只会“破坏”通知。在我们点击“强制更新”按钮之前,一切都是好用的。一旦点击按钮后,屏幕和缓存中的数据依旧照常更新,但当等待了 10 秒后却并没有通知弹出。问题在于强制更新将会完成缓存流,这意味着在组件中不会再接收到值。通知流 ( initialNotifications$ ) 基本就是死掉了。这不是正确的结果,那么我们如何来修复它呢?

相当简单!我们监听 forceReload$ 发出的事件,将其每个发出的值都切换成一个新的通知流。这里取消前一个流的订阅很重要。耳边是否回荡起铃声?就好像在告诉我们这里需要使用 switchMap

我们来动手实现代码!

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { take, switchMap, mergeMap, skip, mapTo } from 'rxjs/operators';@Component({...
})
export class JokeListComponent implements OnInit {showNotification$: Observable<boolean>;update$ = new Subject<void>();forceReload$ = new Subject<void>();...ngOnInit() {...const reload$ = this.forceReload$.pipe(switchMap(() => this.getNotifications()));const initialNotifications$ = this.getNotifications();const show$ = merge(initialNotifications$, reload$).pipe(mapTo(true));const hide$ = this.update$.pipe(mapTo(false));this.showNotification$ = merge(show$, hide$);}getNotifications() {return this.jokeService.jokes.pipe(skip(1));}...
}
复制代码

就这样。每当 forceReload$ 发出值,我们就取消对前一个 Observable 的订阅,然后切换成一个全新的通知流。注意,这里有一行代码我们需要调用两次,就是 this.jokeService.jokes.pipe(skip(1)) 。为了避免重复,我们创建了函数 getNotifications(),它返回笑话列表的流,但会跳过第一个值。最后,我们将 initialNotifications$reload$ 合并成一个名为 show$ 的流。这个流负责在屏幕上显示通知。另外没有必要取消 initialNotifications$ 的订阅,因为它会在缓存重新创建之前完成。其余的都保持不变。

嗯,我们做到了。我们来花点时间看看我们刚刚实现内容的弹珠图。

正如在图中所看见的,对于显示通知来说,initialNotifications$ 十分重要。如果没有这个流的话,我们只能在强制缓存更新后才有机会看到通知。也就是说,当我们按需请求最新数据时,我们必须不断地切换成新的通知流,因为前一个(旧的) Observable 已经完成并不再发出任何值。

就是这样!我们使用 RxJS 和 Angular 提供的工具实现了一个复杂的缓存机制。简答回顾下,我们的服务暴露出一个流,它为我们提供笑话列表。每隔 10 秒会触发 HTTP 请求来更新缓存。为了提升用户体验,我们提供了更新通知,这样用户可以执行更新 UI 的操作。在此之上,我们还为用户提供了一种按需请求最新数据的方式。

太棒了!这就是完整的解决方案。花费几分钟再来看一遍代码。然后尝试不同的场景以确认是否一切都能正常运行。

第 4 阶段在线 Demo: 点击查看。

展望

如果你稍后想要做些课后作业或开发脑力的话,这有几点改进想法:

  • 添加错误处理
  • 将逻辑从组件中重构至服务中,以使其可复用

特别鸣谢

特别感谢 Kwinten Pisman 帮助我完成代码的编写。我还要感谢 Ben Lesh 和 Brian Troncone 给予我有价值的反馈和提出一些改进点。此外,还要非常感谢 Christoph Burgdorf 对于文章和代码的审查。

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. angularjs 页面缓存及动态刷新解决方案

    angularjs 页面缓存及动态刷新解决方案参考文章&#xff1a; &#xff08;1&#xff09;angularjs 页面缓存及动态刷新解决方案 &#xff08;2&#xff09;https://www.cnblogs.com/yang-shun/p/10191974.html &#xff08;3&#xff09;https://www.javazxz.com/thread-549…...

    2024/4/20 19:45:53
  2. angular2页面抓取,如何在Angular 2中获取当前页面的绝对路径?

    Ive essentially run into this problem, where I need a reference to the current route to use gradients, but have yet to figure out how to translate the solution into Angular 2.解决方案constructor(location:Location) {console.log(location.prepareExternalUrl(l…...

    2024/5/7 14:39:29
  3. angular-resource的url转义问题

    最近遇到这样一个问题&#xff0c;就是使用angular-resource后&#xff0c;拼接的url中 / 会被转义成 %2F &#xff0c;后台代码不能识别&#xff0c;查看$resource的源码后发现&#xff0c;拼接的url被转义了&#xff0c;现提供一种解决方案&#xff1a; 使用angular的拦截器…...

    2024/4/25 2:59:01
  4. angular7 路由路径中的 #

    一般我们习惯看的路由路径是 http://localhost:4200/home 可是有时候路径会变成 http://localhost:4200/#/home 路径中出现了 # &#xff0c;是angular中使用了 hash模式 设置hash模式有两种途径 在app.module.ts中 引入 import {HashLocationStrategy, LocationStrategy}…...

    2024/4/26 18:52:01
  5. angular 部署的项目自定义访问路径,不使用root根目录的配置

    配置方法如下&#xff08;以项目名称为admin为例&#xff09;&#xff1a; 需要注意的地方&#xff0c;html中引用的静态文件要从assets开始写&#xff0c;不可以使用相对路径&#xff0c;例如: <img src"../images/bg.png"/> 不可以&#xff0c;会出现路径引…...

    2024/5/7 15:02:45
  6. Java 中使用HttpURLConnection发起POST 请求

    private void httpUrlConnection() { try { String pathUrl = "http://172.20.0.206:8082/TestServelt/login.do"; // 建立连接 URL url = new URL(pathUrl); HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); // //设置连接属性 http…...

    2024/4/21 4:37:49
  7. angular 项目路径中去掉 #

    第一版项目上线了&#xff0c;今天总结了一些东西&#xff0c;不停总结&#xff0c;不断进步&#xff01; angular中使用ui-router配置路由时候会出现#&#xff0c;见图&#xff0c;如何去掉#呢&#xff1f; 1.router文件中设置html5模式 (别忘了注入$locationProvider) $loc…...

    2024/5/7 14:25:34
  8. angular中的路径问题

    我们在写项目时会遇到启动页调到引导页&#xff0c;引导页再调到首页&#xff0c; 那我们在用angular框架写这种东西的时候如果我们不细心的话就会遇到问题&#xff0c; 比如说找不到引导页的图片等等。 那我们怎么解决这个问题呢&#xff1f; 首先我们要明确&#xff0c;我们使…...

    2024/4/21 4:37:48
  9. Angular的自动化测试

    当Angular项目的规模到达一定的程度&#xff0c;就需要进行测试工作了。本文着重介绍关于ng的测试部分&#xff0c;主要包括以下三个方面&#xff1a; 框架的选择&#xff08;KarmaJasmine&#xff09;测试的分类和选择&#xff08;单元测试 端到端测试&#xff09;在ng中各个…...

    2024/4/21 4:37:47
  10. Flutter组件化开发方案

    作者&#xff1a;腾讯 - 小德&#xff08;koudleren 任晓帅&#xff09; 前言 前面讲了Flutter和Native的混合开发模式&#xff0c;Flutter作为Native工程的一个Module存在&#xff0c;这样可以有效的将Flutter和Native进行物理隔离&#xff0c;但随着Flutter承载的业务越来越多…...

    2024/4/21 4:37:45
  11. webpack+jquery 组件化、模块化开发的解决方案

    demo 基于webpack搭建纯静态页面型前端工程解决方案模板 按需加载模块&#xff0c;按需进行懒加载&#xff0c;在实际用到某些模块的时候再增量更新多入口文件&#xff0c;自动扫描入口。同时支持SPA和多页面型的项目静态资源按需自动注入到html中&#xff0c;并可自动加上has…...

    2024/4/21 4:37:45
  12. 什么是模块化,模块化开发如何实现?

    相信广大前端朋友们都遇到过这么一个问题&#xff1f; 什么是模块化&#xff0c;模块化开发如何实现&#xff1f; 那么什么是模块化呢&#xff0c;时下流行的库与框架又有哪些在利用模块化进行开发呢&#xff1f; 今天我从以下两个方向来进行描述&#xff0c;如果描述不够准…...

    2024/4/21 4:37:44
  13. Angular4.0_开发准备

    启动Angular过程介绍 启动时加载了哪个页面&#xff1f; 启动时加载了哪些脚本&#xff1f; 这些脚本做了什么事&#xff1f; 默认情况下是index对应的文件是启动时加载的页面 main.ts是启动时的起点文件 main.ts //核心模块提供的enableProdMode用来&#xff0c;用来关闭…...

    2024/4/21 4:37:44
  14. angularJS的模块化操作

    模块化 -减少全局污染 -减少模块之间的相互依赖 <!DOCTYPE HTML> <!--<html ng-app>模块化后,应声明哪个是初始模块--> <html ng-app"myApp"> <head><meta http-equiv"Content-Type" content"text/html" chars…...

    2024/4/21 4:37:41
  15. require'模块化jquery和angular问题

    require 模块化开发问题&#xff0c;正常自己写的模块 是exports 导出一个模块 //模块化引入jquery 不同和问题 require 引入jquery swiper .... 插件和库的时候需要 require.config({   baseUrl:"js/libs", //文件夹目录相对与html的位置   paths:{     jqu…...

    2024/4/21 4:37:40
  16. JS模块化规范详解

    JavaScript模块化规范详解 目录 为什么要模块化 模块化的好处 页面引入加载script存在的问题 模块化规范 CommonJS Node.js中实现 浏览器端实现 AMD CMD ES6模块化 1. 为什么要模块化&#xff1f; Web sites are turning into Web Apps. Code complexity(复杂度) g…...

    2024/4/21 4:37:40
  17. 前端开发——模块化(html模块化开发)

    web从进入2.0时代后&#xff0c;web开发人员更加注重模块化思想的运用&#xff0c;特别是有了SPA之后。 SPA——组件化 进入了spa时代的我们对于模块化有了新的称呼‘组件化’&#xff0c;spa既是我们所熟知的单页面应用。 spa 框架 1.vue.js&#xff08;推荐1&#xff09; 2.a…...

    2024/4/21 4:37:39
  18. angularjs——模块化与mvc

    车辆管理系统arcgis移植已经基本完成。开始下一件事情&#xff0c;搭建一个视频培训系统。这个系统准备用angularjs微服务架构的方式搭建&#xff0c;如果有可能再引入CDN。 先用一天时间大致看完了《angularjs权威教程》,由于js的高级编程还算比较扎实&#xff0c;对angularjs…...

    2024/4/20 19:46:01
  19. 【Angular4学习】模块化再理解-1

    Angular模块把组件、指令和管道打包成内聚的功能块&#xff0c;每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。 Angular模块是一个由NgModule装饰器提供元数据的类&#xff0c;元数据包括&#xff1a;   声明哪些组件、指令、管道属于该模块&#xff1b; 公开某些…...

    2024/4/20 19:45:59
  20. Angular 发布一个属于自己的Library,实现项目模块化

    发布一个属于自己的Library&#xff0c;实现项目模块化能做什么&#xff1f; 它可以实现你的组件从项目中脱离&#xff0c;在多个项目中灵活使用。 这篇文章主要在说什么&#xff1f; 这篇文章主要是从配置出发&#xff0c;扫描知识盲区&#xff0c;带你了解lib的实现过程&a…...

    2024/4/20 19:45:58

最新文章

  1. Json拼接

    package service.WebWh;import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject;public class a {public static void main(String[] args) {// 创建一个 JSONArray 对象用于存储多个 JSON 数据对象JSONArray jsons new JSONArray();// 创建第一个…...

    2024/5/7 16:00:43
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/5/7 10:36:02
  3. Redis分区

    Redis分区是一种数据分片技术&#xff0c;用于将数据分布到多个Redis实例&#xff08;节点&#xff09;上以提高性能和扩展性。分区使得Redis能够处理比单个实例更大的数据集&#xff0c;并允许并行处理客户端请求。 原理&#xff1a; Redis分区通过一致性哈希算法&#xff08;…...

    2024/5/5 1:23:35
  4. 理解 Golang 变量在内存分配中的规则

    为什么有些变量在堆中分配、有些却在栈中分配&#xff1f; 我们先看来栈和堆的特点&#xff1a; 简单总结就是&#xff1a; 栈&#xff1a;函数局部变量&#xff0c;小数据 堆&#xff1a;大的局部变量&#xff0c;函数内部产生逃逸的变量&#xff0c;动态分配的数据&#x…...

    2024/5/1 13:25:19
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/7 5:50:09
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/7 9:45:25
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/5/4 23:54:56
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/7 14:25:14
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/5/4 23:54:56
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/5/4 23:55:05
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/5/4 23:54:56
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/5/7 11:36:39
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/5/4 23:54:56
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/6 1:40:42
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/5/4 23:54:56
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/5/4 23:55:17
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/5/7 9:26:26
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/5/4 23:54:56
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/4 23:55:06
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/5 8:13:33
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/5/4 23:55:16
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/5/4 23:54:58
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/6 21:42:42
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/5/4 23:54:56
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57