feat(docs-infra): Convert AIO to use the new Service Worker 5.0.0. (#19795)

AIO is currently using a beta version of @angular/service-worker.
Since that was implemented, the SW has been rewritten and released
as part of Angular 5.0.0. This commit updates AIO to use the latest
implementation, with an appropriate configuration file that caches
the various AIO assets in useful ways.

PR Close #19795
This commit is contained in:
Alex Rickabaugh 2017-11-28 10:13:52 -08:00 committed by Matias Niemelä
parent 66ffa360df
commit be24f9f0cb
19 changed files with 254 additions and 181 deletions

View File

@ -56,14 +56,9 @@ It's necessary to remove the temporary files, because otherwise they're displaye
## Using ServiceWorker locally ## Using ServiceWorker locally
Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which Running `yarn start` (even when explicitly targeting production mode) does not set up the
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible ServiceWorker. If you want to test the ServiceWorker locally, you can use `yarn build` and then
with webpack serving the files from memory). serve the files in `dist/` with `yarn http-server dist -p 4200`.
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
with `yarn http-server dist -p 4200`.
For more details see #16745.
## Guide to authoring ## Guide to authoring

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -9,7 +9,7 @@
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
// README: // README:
// Redirects must also be handled by the ServiceWorker. If you add a redirect rule here, // Redirects must also be handled by the ServiceWorker. If you add a redirect rule here,
// make sure the routing RegExp in `ngsw-manifest.json` is updated accordingly. // make sure it is compatible with the configuration in `ngsw-config.json`.
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
// A random bad indexed page that used `api/api` // A random bad indexed page that used `api/api`

View File

@ -1,26 +0,0 @@
{
"external": {
"urls": [
{"url": "https://fonts.googleapis.com/css?family=Droid+Sans+Mono"},
{"url": "https://fonts.gstatic.com/s/droidsansmono/v7/ns-m2xQYezAtqh7ai59hJYdJ2JT0J65PSe7wdxAnx_I.woff2"},
{"url": "https://fonts.googleapis.com/icon?family=Material+Icons"},
{"url": "https://fonts.gstatic.com/s/materialicons/v22/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2"},
{"url": "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"}
]
},
"static.ignore": [
"\\.js\\.map$",
"^(?:/|\\\\)generated(?:/|\\\\)(?:docs(?:/|\\\\)(?!api(?:/|\\\\)api-list\\.json).*|images(?:/|\\\\)(?!marketing(?:/|\\\\)).*|live-examples|zips)(?:/|\\\\)"
],
"static.versioned": [
"\\.[0-9a-z]{20}\\."
],
"routing": {
"index": "/index.html",
"routes": {
"^(?!/styleguide|/docs/.|(?:/guide/(?:cli-quickstart|metadata|ngmodule|service-worker-(?:getstart|comm|configref)|learning-angular|webpack)|/news)(?:\\.html|/)?$|/testing|/api/(?:.+/[^/]+-|platform-browser/AnimationDriver|testing/|api/|animate/|(?:common/(?:NgModel|Control|MaxLengthValidator))|(?:[^/]+/)?(?:NgFor(?:$|-)|AnimationStateDeclarationMetadata|CORE_DIRECTIVES|PLATFORM_PIPES|DirectiveMetadata|HTTP_PROVIDERS))|.*/stackblitz(?:\\.html)?(?:\\?.*)?$|.*\\.[^\/.]+$)": {
"match": "regex"
}
}
}
}

View File

@ -8,7 +8,7 @@
"scripts": { "scripts": {
"preinstall": "node ../tools/yarn/check-yarn.js", "preinstall": "node ../tools/yarn/check-yarn.js",
"postinstall": "node tools/cli-patches/patch.js && uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map", "postinstall": "node tools/cli-patches/patch.js && uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map",
"aio-use-local": "node tools/ng-packages-installer overwrite . --debug --ignore-packages @angular/service-worker", "aio-use-local": "node tools/ng-packages-installer overwrite . --debug",
"aio-use-npm": "node tools/ng-packages-installer restore .", "aio-use-npm": "node tools/ng-packages-installer restore .",
"aio-check-local": "node tools/ng-packages-installer check .", "aio-check-local": "node tools/ng-packages-installer check .",
"ng": "yarn check-env && ng", "ng": "yarn check-env && ng",
@ -55,14 +55,12 @@
"boilerplate:test": "node tools/examples/test.js", "boilerplate:test": "node tools/examples/test.js",
"generate-stackblitz": "node ./tools/stackblitz-builder/generateStackblitz", "generate-stackblitz": "node ./tools/stackblitz-builder/generateStackblitz",
"generate-zips": "node ./tools/example-zipper/generateZips", "generate-zips": "node ./tools/example-zipper/generateZips",
"sw-manifest": "ngu-sw-manifest --dist dist --in ngsw-manifest.json --out dist/ngsw-manifest.json",
"sw-copy": "cp node_modules/@angular/service-worker/bundles/worker-basic.min.js dist/",
"build-404-page": "node scripts/build-404-page", "build-404-page": "node scripts/build-404-page",
"build-ie-polyfills": "yarn webpack-cli src/ie-polyfills.js -o src/generated/ie-polyfills.min.js --mode production", "build-ie-polyfills": "yarn webpack-cli src/ie-polyfills.js -o src/generated/ie-polyfills.min.js --mode production",
"update-webdriver": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG", "update-webdriver": "webdriver-manager update --standalone false --gecko false $CHROMEDRIVER_VERSION_ARG",
"~~check-env": "node scripts/check-environment", "~~check-env": "node scripts/check-environment",
"~~build": "ng build", "~~build": "ng build",
"post~~build": "yarn build-404-page && yarn sw-manifest && yarn sw-copy" "post~~build": "yarn build-404-page"
}, },
"engines": { "engines": {
"node": ">=8.9.1 <9.0.0", "node": ">=8.9.1 <9.0.0",
@ -82,7 +80,7 @@
"@angular/platform-browser-dynamic": "6.0.0", "@angular/platform-browser-dynamic": "6.0.0",
"@angular/platform-server": "6.0.0", "@angular/platform-server": "6.0.0",
"@angular/router": "6.0.0", "@angular/router": "6.0.0",
"@angular/service-worker": "^1.0.0-beta.16", "@angular/service-worker": "6.0.0",
"@webcomponents/custom-elements": "^1.0.8", "@webcomponents/custom-elements": "^1.0.8",
"classlist.js": "^1.1.20150312", "classlist.js": "^1.1.20150312",
"core-js": "^2.4.1", "core-js": "^2.4.1",

View File

@ -2,8 +2,8 @@
"aio": { "aio": {
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime": 2768, "runtime": 2712,
"main": 475855, "main": 458226,
"polyfills": 38453, "polyfills": 38453,
"prettify": 14913 "prettify": 14913
} }

View File

@ -2,6 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core'; import { ErrorHandler, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
@ -40,6 +41,8 @@ import { CustomElementsModule } from 'app/custom-elements/custom-elements.module
import { SharedModule } from 'app/shared/shared.module'; import { SharedModule } from 'app/shared/shared.module';
import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module'; import { SwUpdatesModule } from 'app/sw-updates/sw-updates.module';
import {environment} from '../environments/environment';
// These are the hardcoded inline svg sources to be used by the `<mat-icon>` component // These are the hardcoded inline svg sources to be used by the `<mat-icon>` component
export const svgIconProviders = [ export const svgIconProviders = [
{ {
@ -99,6 +102,7 @@ export const svgIconProviders = [
MatToolbarModule, MatToolbarModule,
SwUpdatesModule, SwUpdatesModule,
SharedModule, SharedModule,
ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}),
], ],
declarations: [ declarations: [
AppComponent, AppComponent,

View File

@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
import { from as fromPromise, Observable } from 'rxjs'; import { from as fromPromise, Observable } from 'rxjs';
import { first, map, share } from 'rxjs/operators'; import { first, map, share } from 'rxjs/operators';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/share';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';

View File

@ -1,6 +1,7 @@
import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
import { asapScheduler as asap, combineLatest, Subject } from 'rxjs'; import { asapScheduler as asap, combineLatest, Subject } from 'rxjs';
import { startWith, subscribeOn, takeUntil } from 'rxjs/operators'; import { startWith, subscribeOn, takeUntil } from 'rxjs/operators';
import 'rxjs/add/operator/startWith';
import { ScrollService } from 'app/shared/scroll.service'; import { ScrollService } from 'app/shared/scroll.service';
import { TocItem, TocService } from 'app/shared/toc.service'; import { TocItem, TocService } from 'app/shared/toc.service';

View File

@ -3,6 +3,7 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { AsyncSubject, Observable, of } from 'rxjs'; import { AsyncSubject, Observable, of } from 'rxjs';
import { catchError, switchMap, tap } from 'rxjs/operators'; import { catchError, switchMap, tap } from 'rxjs/operators';
import 'rxjs/add/operator/do';
import { DocumentContents } from './document-contents'; import { DocumentContents } from './document-contents';
export { DocumentContents } from './document-contents'; export { DocumentContents } from './document-contents';

View File

@ -5,7 +5,7 @@
var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json'; var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
// NOTE: This needs to be kept in sync with `ngsw-manifest.json`. // NOTE: This needs to be kept in sync with `ngsw-config.json`.
importScripts('/assets/js/lunr.min.js'); importScripts('/assets/js/lunr.min.js');
var index; var index;

View File

@ -1,13 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ServiceWorkerModule } from '@angular/service-worker';
import { SwUpdatesService } from './sw-updates.service'; import { SwUpdatesService } from './sw-updates.service';
@NgModule({ @NgModule({
imports: [
ServiceWorkerModule
],
providers: [ providers: [
SwUpdatesService SwUpdatesService
] ]

View File

@ -1,41 +1,41 @@
import { ApplicationRef, ReflectiveInjector } from '@angular/core'; import { ApplicationRef, ReflectiveInjector } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing'; import { discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing';
import { NgServiceWorker } from '@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
import { SwUpdatesService } from './sw-updates.service'; import { SwUpdatesService } from './sw-updates.service';
describe('SwUpdatesService', () => { describe('SwUpdatesService', () => {
let injector: ReflectiveInjector; let injector: ReflectiveInjector;
let appRef: MockApplicationRef; let appRef: MockApplicationRef;
let service: SwUpdatesService; let service: SwUpdatesService;
let sw: MockNgServiceWorker; let swu: MockSwUpdate;
let checkInterval: number; let checkInterval: number;
// Helpers // Helpers
// NOTE: // NOTE:
// Because `SwUpdatesService` uses the `debounceTime` operator, it needs to be instantiated and // Because `SwUpdatesService` uses the `interval` operator, it needs to be instantiated and
// destroyed inside the `fakeAsync` zone (when `fakeAsync` is used for the test). Thus, we can't // destroyed inside the `fakeAsync` zone (when `fakeAsync` is used for the test). Thus, we can't
// run `setup()`/`tearDown()` in `beforeEach()`/`afterEach()` blocks. We use the `run()` helper // run `setup()`/`tearDown()` in `beforeEach()`/`afterEach()` blocks. We use the `run()` helper
// to call them inside each test's zone. // to call them inside each test's zone.
const setup = () => { const setup = (isSwUpdateEnabled: boolean) => {
injector = ReflectiveInjector.resolveAndCreate([ injector = ReflectiveInjector.resolveAndCreate([
{ provide: ApplicationRef, useClass: MockApplicationRef }, { provide: ApplicationRef, useClass: MockApplicationRef },
{ provide: Logger, useClass: MockLogger }, { provide: Logger, useClass: MockLogger },
{ provide: NgServiceWorker, useClass: MockNgServiceWorker }, { provide: SwUpdate, useFactory: () => new MockSwUpdate(isSwUpdateEnabled) },
SwUpdatesService SwUpdatesService
]); ]);
appRef = injector.get(ApplicationRef); appRef = injector.get(ApplicationRef);
service = injector.get(SwUpdatesService); service = injector.get(SwUpdatesService);
sw = injector.get(NgServiceWorker); swu = injector.get(SwUpdate);
checkInterval = (service as any).checkInterval; checkInterval = (service as any).checkInterval;
}; };
const tearDown = () => service.ngOnDestroy(); const tearDown = () => service.ngOnDestroy();
const run = (specFn: VoidFunction) => () => { const run = (specFn: VoidFunction, isSwUpdateEnabled = true) => () => {
setup(); setup(isSwUpdateEnabled);
specFn(); specFn();
tearDown(); tearDown();
}; };
@ -46,109 +46,153 @@ describe('SwUpdatesService', () => {
})); }));
it('should start checking for updates when instantiated (once the app stabilizes)', run(() => { it('should start checking for updates when instantiated (once the app stabilizes)', run(() => {
expect(sw.checkForUpdate).not.toHaveBeenCalled(); expect(swu.checkForUpdate).not.toHaveBeenCalled();
appRef.isStable.next(false); appRef.isStable.next(false);
expect(sw.checkForUpdate).not.toHaveBeenCalled(); expect(swu.checkForUpdate).not.toHaveBeenCalled();
appRef.isStable.next(true); appRef.isStable.next(true);
expect(sw.checkForUpdate).toHaveBeenCalled(); expect(swu.checkForUpdate).toHaveBeenCalled();
})); }));
it('should schedule a new check if there is no update available', fakeAsync(run(() => { it('should periodically check for updates', fakeAsync(run(() => {
appRef.isStable.next(true); appRef.isStable.next(true);
sw.checkForUpdate.calls.reset(); swu.checkForUpdate.calls.reset();
sw.$$checkForUpdateSubj.next(false);
expect(sw.checkForUpdate).not.toHaveBeenCalled();
tick(checkInterval); tick(checkInterval);
expect(sw.checkForUpdate).toHaveBeenCalled(); expect(swu.checkForUpdate).toHaveBeenCalledTimes(1);
expect(sw.activateUpdate).not.toHaveBeenCalled();
tick(checkInterval);
expect(swu.checkForUpdate).toHaveBeenCalledTimes(2);
appRef.isStable.next(false);
tick(checkInterval);
expect(swu.checkForUpdate).toHaveBeenCalledTimes(3);
discardPeriodicTasks();
}))); })));
it('should activate new updates immediately', fakeAsync(run(() => { it('should activate available updates immediately', fakeAsync(run(() => {
appRef.isStable.next(true); appRef.isStable.next(true);
sw.checkForUpdate.calls.reset(); expect(swu.activateUpdate).not.toHaveBeenCalled();
sw.$$checkForUpdateSubj.next(true); swu.$$availableSubj.next({available: {hash: 'foo'}});
expect(sw.checkForUpdate).not.toHaveBeenCalled(); expect(swu.activateUpdate).toHaveBeenCalled();
tick(checkInterval);
expect(sw.checkForUpdate).not.toHaveBeenCalled();
expect(sw.activateUpdate).toHaveBeenCalled();
}))); })));
it('should not pass a specific version to `NgServiceWorker.activateUpdate()`', fakeAsync(run(() => { it('should keep periodically checking for updates even after one is available/activated', fakeAsync(run(() => {
appRef.isStable.next(true); appRef.isStable.next(true);
sw.$$checkForUpdateSubj.next(true); swu.checkForUpdate.calls.reset();
tick(checkInterval);
expect(sw.activateUpdate).toHaveBeenCalledWith(null);
})));
it('should schedule a new check after activating the update', fakeAsync(run(() => {
appRef.isStable.next(true);
sw.checkForUpdate.calls.reset();
sw.$$checkForUpdateSubj.next(true);
tick(checkInterval); tick(checkInterval);
expect(sw.checkForUpdate).not.toHaveBeenCalled(); expect(swu.checkForUpdate).toHaveBeenCalledTimes(1);
sw.$$activateUpdateSubj.next(); swu.$$availableSubj.next({available: {hash: 'foo'}});
expect(sw.checkForUpdate).not.toHaveBeenCalled();
tick(checkInterval); tick(checkInterval);
expect(sw.checkForUpdate).toHaveBeenCalled(); expect(swu.checkForUpdate).toHaveBeenCalledTimes(2);
tick(checkInterval);
expect(swu.checkForUpdate).toHaveBeenCalledTimes(3);
discardPeriodicTasks();
}))); })));
it('should emit on `updateActivated` when an update has been activated', run(() => { it('should emit on `updateActivated` when an update has been activated', run(() => {
const activatedVersions: (string|undefined)[] = []; const activatedVersions: (string|undefined)[] = [];
service.updateActivated.subscribe(v => activatedVersions.push(v)); service.updateActivated.subscribe(v => activatedVersions.push(v));
sw.$$updatesSubj.next({type: 'pending', version: 'foo'}); swu.$$availableSubj.next({available: {hash: 'foo'}});
sw.$$updatesSubj.next({type: 'activation', version: 'bar'}); swu.$$activatedSubj.next({current: {hash: 'bar'}});
sw.$$updatesSubj.next({type: 'pending', version: 'baz'}); swu.$$availableSubj.next({available: {hash: 'baz'}});
sw.$$updatesSubj.next({type: 'activation', version: 'qux'}); swu.$$activatedSubj.next({current: {hash: 'qux'}});
expect(activatedVersions).toEqual(['bar', 'qux']); expect(activatedVersions).toEqual(['bar', 'qux']);
})); }));
describe('when `SwUpdate` is not enabled', () => {
const runDeactivated = (specFn: VoidFunction) => run(specFn, false);
it('should not check for updates', fakeAsync(runDeactivated(() => {
appRef.isStable.next(true);
tick(checkInterval);
tick(checkInterval);
swu.$$availableSubj.next({available: {hash: 'foo'}});
swu.$$activatedSubj.next({current: {hash: 'bar'}});
tick(checkInterval);
tick(checkInterval);
expect(swu.checkForUpdate).not.toHaveBeenCalled();
})));
it('should not activate available updates', fakeAsync(runDeactivated(() => {
swu.$$availableSubj.next({available: {hash: 'foo'}});
expect(swu.activateUpdate).not.toHaveBeenCalled();
})));
it('should never emit on `updateActivated`', runDeactivated(() => {
const activatedVersions: (string|undefined)[] = [];
service.updateActivated.subscribe(v => activatedVersions.push(v));
swu.$$availableSubj.next({available: {hash: 'foo'}});
swu.$$activatedSubj.next({current: {hash: 'bar'}});
swu.$$availableSubj.next({available: {hash: 'baz'}});
swu.$$activatedSubj.next({current: {hash: 'qux'}});
expect(activatedVersions).toEqual([]);
}));
});
describe('when destroyed', () => { describe('when destroyed', () => {
it('should not schedule a new check for update (after current check)', fakeAsync(run(() => { it('should not schedule a new check for update (after current check)', fakeAsync(run(() => {
appRef.isStable.next(true); appRef.isStable.next(true);
sw.checkForUpdate.calls.reset(); expect(swu.checkForUpdate).toHaveBeenCalled();
service.ngOnDestroy(); service.ngOnDestroy();
sw.$$checkForUpdateSubj.next(false); swu.checkForUpdate.calls.reset();
tick(checkInterval);
tick(checkInterval); tick(checkInterval);
expect(sw.checkForUpdate).not.toHaveBeenCalled(); expect(swu.checkForUpdate).not.toHaveBeenCalled();
}))); })));
it('should not schedule a new check for update (after activating an update)', fakeAsync(run(() => { it('should not schedule a new check for update (after activating an update)', fakeAsync(run(() => {
appRef.isStable.next(true); appRef.isStable.next(true);
sw.checkForUpdate.calls.reset(); expect(swu.checkForUpdate).toHaveBeenCalled();
sw.$$checkForUpdateSubj.next(true);
expect(sw.activateUpdate).toHaveBeenCalled();
service.ngOnDestroy(); service.ngOnDestroy();
sw.$$activateUpdateSubj.next(); swu.checkForUpdate.calls.reset();
swu.$$availableSubj.next({available: {hash: 'foo'}});
swu.$$activatedSubj.next({current: {hash: 'baz'}});
tick(checkInterval);
tick(checkInterval); tick(checkInterval);
expect(sw.checkForUpdate).not.toHaveBeenCalled(); expect(swu.checkForUpdate).not.toHaveBeenCalled();
})));
it('should not activate available updates', fakeAsync(run(() => {
service.ngOnDestroy();
swu.$$availableSubj.next({available: {hash: 'foo'}});
expect(swu.activateUpdate).not.toHaveBeenCalled();
}))); })));
it('should stop emitting on `updateActivated`', run(() => { it('should stop emitting on `updateActivated`', run(() => {
const activatedVersions: (string|undefined)[] = []; const activatedVersions: (string|undefined)[] = [];
service.updateActivated.subscribe(v => activatedVersions.push(v)); service.updateActivated.subscribe(v => activatedVersions.push(v));
sw.$$updatesSubj.next({type: 'pending', version: 'foo'}); swu.$$availableSubj.next({available: {hash: 'foo'}});
sw.$$updatesSubj.next({type: 'activation', version: 'bar'}); swu.$$activatedSubj.next({current: {hash: 'bar'}});
service.ngOnDestroy(); service.ngOnDestroy();
sw.$$updatesSubj.next({type: 'pending', version: 'baz'}); swu.$$availableSubj.next({available: {hash: 'baz'}});
sw.$$updatesSubj.next({type: 'activation', version: 'qux'}); swu.$$activatedSubj.next({current: {hash: 'qux'}});
expect(activatedVersions).toEqual(['bar']); expect(activatedVersions).toEqual(['bar']);
})); }));
@ -164,16 +208,18 @@ class MockLogger {
log = jasmine.createSpy('MockLogger.log'); log = jasmine.createSpy('MockLogger.log');
} }
class MockNgServiceWorker { class MockSwUpdate {
$$activateUpdateSubj = new Subject<boolean>(); $$availableSubj = new Subject<{available: {hash: string}}>();
$$checkForUpdateSubj = new Subject<boolean>(); $$activatedSubj = new Subject<{current: {hash: string}}>();
$$updatesSubj = new Subject<{type: string, version: string}>();
updates = this.$$updatesSubj.asObservable(); available = this.$$availableSubj.asObservable();
activated = this.$$activatedSubj.asObservable();
activateUpdate = jasmine.createSpy('MockNgServiceWorker.activateUpdate') activateUpdate = jasmine.createSpy('MockSwUpdate.activateUpdate')
.and.callFake(() => this.$$activateUpdateSubj.pipe(take(1))); .and.callFake(() => Promise.resolve());
checkForUpdate = jasmine.createSpy('MockNgServiceWorker.checkForUpdate') checkForUpdate = jasmine.createSpy('MockSwUpdate.checkForUpdate')
.and.callFake(() => this.$$checkForUpdateSubj.pipe(take(1))); .and.callFake(() => Promise.resolve());
constructor(public isEnabled: boolean) {}
} }

View File

@ -1,7 +1,7 @@
import { ApplicationRef, Injectable, OnDestroy } from '@angular/core'; import { ApplicationRef, Injectable, OnDestroy } from '@angular/core';
import { NgServiceWorker } from '@angular/service-worker'; import { SwUpdate } from '@angular/service-worker';
import { concat, Subject } from 'rxjs'; import { concat, interval, NEVER, Observable, Subject } from 'rxjs';
import { debounceTime, defaultIfEmpty, filter, first, map, startWith, takeUntil, tap } from 'rxjs/operators'; import { first, map, takeUntil, tap } from 'rxjs/operators';
import { Logger } from 'app/shared/logger.service'; import { Logger } from 'app/shared/logger.service';
@ -11,63 +11,55 @@ import { Logger } from 'app/shared/logger.service';
* *
* @description * @description
* 1. Checks for available ServiceWorker updates once instantiated. * 1. Checks for available ServiceWorker updates once instantiated.
* 2. As long as there is no update available, re-checks every 6 hours. * 2. Re-checks every 6 hours.
* 3. As soon as an update is detected, it activates the update and notifies interested parties. * 3. Whenever an update is available, it activates the update.
* 4. It continues to check for available updates.
* *
* @property * @property
* `updateActivated` {Observable<string>} - Emit the version hash whenever an update is activated. * `updateActivated` {Observable<string>} - Emit the version hash whenever an update is activated.
*/ */
@Injectable() @Injectable()
export class SwUpdatesService implements OnDestroy { export class SwUpdatesService implements OnDestroy {
private checkInterval = 1000 * 60 * 60 * 6; // 6 hours private checkInterval = 1000 * 60 * 60 * 6; // 6 hours
private onDestroy = new Subject<void>(); private onDestroy = new Subject<void>();
private checkForUpdateSubj = new Subject<void>(); updateActivated: Observable<string>;
updateActivated = this.sw.updates.pipe(
takeUntil(this.onDestroy),
tap(evt => this.log(`Update event: ${JSON.stringify(evt)}`)),
filter(({type}) => type === 'activation'),
map(({version}) => version),
);
constructor(appRef: ApplicationRef, private logger: Logger, private sw: NgServiceWorker) { constructor(appRef: ApplicationRef, private logger: Logger, private swu: SwUpdate) {
const appIsStable$ = appRef.isStable.pipe(first(v => v)); if (!swu.isEnabled) {
const checkForUpdates$ = this.checkForUpdateSubj.pipe(debounceTime(this.checkInterval), startWith<void>(undefined)); this.updateActivated = NEVER.pipe(takeUntil(this.onDestroy));
return;
}
concat(appIsStable$, checkForUpdates$) // Periodically check for updates (after the app is stabilized).
.pipe(takeUntil(this.onDestroy)) const appIsStable = appRef.isStable.pipe(first(v => v));
.subscribe(() => this.checkForUpdate()); concat(appIsStable, interval(this.checkInterval))
.pipe(
tap(() => this.log('Checking for update...')),
takeUntil(this.onDestroy),
)
.subscribe(() => this.swu.checkForUpdate());
// Activate available updates.
this.swu.available
.pipe(
tap(evt => this.log(`Update available: ${JSON.stringify(evt)}`)),
takeUntil(this.onDestroy),
)
.subscribe(() => this.swu.activateUpdate());
// Notify about activated updates.
this.updateActivated = this.swu.activated.pipe(
tap(evt => this.log(`Update activated: ${JSON.stringify(evt)}`)),
map(evt => evt.current.hash),
takeUntil(this.onDestroy),
);
} }
ngOnDestroy() { ngOnDestroy() {
this.onDestroy.next(); this.onDestroy.next();
} }
private activateUpdate() {
this.log('Activating update...');
this.sw.activateUpdate(null as any) // expects a non-null string
.subscribe(() => this.scheduleCheckForUpdate());
}
private checkForUpdate() {
this.log('Checking for update...');
this.sw.checkForUpdate()
.pipe(
// Temp workaround for https://github.com/angular/mobile-toolkit/pull/137.
// TODO (gkalpak): Remove once #137 is fixed.
defaultIfEmpty(false),
first(),
tap(v => this.log(`Update available: ${v}`)),
)
.subscribe(v => v ? this.activateUpdate() : this.scheduleCheckForUpdate());
}
private log(message: string) { private log(message: string) {
const timestamp = (new Date).toISOString(); const timestamp = (new Date).toISOString();
this.logger.log(`[SwUpdates - ${timestamp}]: ${message}`); this.logger.log(`[SwUpdates - ${timestamp}]: ${message}`);
} }
private scheduleCheckForUpdate() {
this.checkForUpdateSubj.next();
}
} }

View File

@ -20,7 +20,7 @@
<link rel="apple-touch-icon" sizes="144x144" href="assets/images/favicons/favicon-144x144.png"> <link rel="apple-touch-icon" sizes="144x144" href="assets/images/favicons/favicon-144x144.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="assets/images/favicons/favicon-144x144.png"> <link rel="apple-touch-icon-precomposed" sizes="144x144" href="assets/images/favicons/favicon-144x144.png">
<!-- NOTE: These need to be kept in sync with `ngsw-manifest.json`. --> <!-- NOTE: These need to be kept in sync with `ngsw-config.json`. -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

View File

@ -1,6 +1,5 @@
import { enableProdMode, ApplicationRef } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { first } from 'rxjs/operators';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
@ -9,11 +8,5 @@ if (environment.production) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => { platformBrowserDynamic().bootstrapModule(AppModule);
if (environment.production && 'serviceWorker' in (navigator as any)) {
const appRef: ApplicationRef = ref.injector.get(ApplicationRef);
appRef.isStable.pipe(first(v => v)).subscribe(() => {
(navigator as any).serviceWorker.register('/worker-basic.min.js');
});
}
});

75
aio/src/ngsw-config.json Normal file
View File

@ -0,0 +1,75 @@
{
"index": "/index.html",
"assetGroups": [
{
"name": "app-shell",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/index.html",
"/pwa-manifest.json",
"/app/search/search-worker.js",
"/assets/images/favicons/favicon.ico",
"/assets/js/*.js"
],
"urls": [
"https://fonts.googleapis.com/**",
"https://fonts.gstatic.com/s/**",
"https://maxcdn.bootstrapcdn.com/**"
],
"versionedFiles": [
"/*.bundle.css",
"/*.bundle.js",
"/*.chunk.js"
]
}
}, {
"name": "assets-eager",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/**",
"/generated/images/marketing/**",
"!/assets/images/favicons/**",
"!/**/_unused/**"
]
}
}, {
"name": "assets-lazy",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/images/favicons/**",
"/generated/ie-polyfills.min.js",
"!/**/_unused/**"
]
}
}, {
"name": "docs-index",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": [
"/generated/*.json",
"/generated/docs/*.json",
"/generated/docs/api/api-list.json",
"/generated/docs/app/search-data.json"
]
}
}, {
"name": "docs-lazy",
"installMode": "lazy",
"updateMode": "lazy",
"resources": {
"files": [
"/generated/docs/**/*.json",
"/generated/images/**",
"!/**/_unused/**"
]
}
}
]
}

View File

@ -1,6 +1,7 @@
import { loadLegacyUrls, loadLocalSitemapUrls, loadSWRoutes } from '../shared/helpers'; import { loadLegacyUrls, loadLocalSitemapUrls, loadSWRoutes } from '../shared/helpers';
describe('service-worker routes', () => { // NOTE: The new `@angular/service-worker` does not support configurable routes.
xdescribe('service-worker routes', () => {
loadLocalSitemapUrls().forEach(url => { loadLocalSitemapUrls().forEach(url => {
it('should process URLs in the Sitemap', () => { it('should process URLs in the Sitemap', () => {

View File

@ -200,12 +200,11 @@
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@angular/service-worker@^1.0.0-beta.16": "@angular/service-worker@6.0.0":
version "1.0.0-beta.16" version "6.0.0"
resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-1.0.0-beta.16.tgz#cb4fcd1d5b311195136fd284bcf2dbb870544d64" resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-6.0.0.tgz#35a187554d33e05911544080fafc281ff1b322e0"
dependencies: dependencies:
base64-js "^1.1.2" tslib "^1.9.0"
jshashes "^1.0.5"
"@google-cloud/common@^0.13.0": "@google-cloud/common@^0.13.0":
version "0.13.6" version "0.13.6"
@ -1514,7 +1513,7 @@ base64-arraybuffer@0.1.5:
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
base64-js@^1.0.2, base64-js@^1.1.2: base64-js@^1.0.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801"
@ -5998,10 +5997,6 @@ jsesc@~0.5.0:
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
jshashes@^1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/jshashes/-/jshashes-1.0.7.tgz#bed8c97a0e9632fd0513916f55f76dd5486be59f"
json-buffer@3.0.0: json-buffer@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"