diff --git a/aio/content/examples/service-worker-getstart/e2e/app.e2e-spec.ts b/aio/content/examples/service-worker-getstart/e2e/app.e2e-spec.ts new file mode 100755 index 0000000000..02b2f1dcff --- /dev/null +++ b/aio/content/examples/service-worker-getstart/e2e/app.e2e-spec.ts @@ -0,0 +1,37 @@ +import { AppPage } from './app.po'; +import { browser, element, by } from 'protractor'; + +describe('sw-example App', () => { + let page: AppPage; + let logo = element(by.css('img')); + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('Welcome to Service Workers!'); + }); + + it('should display the Angular logo', () => { + page.navigateTo(); + expect(logo.isPresent()).toBe(true); + }); + + it('should show a header for the list of links', function () { + const listHeader = element(by.css('app-root > h2')); + expect(listHeader.getText()).toEqual('Here are some links to help you start:'); + }); + + it('should show a list of links', function () { + element.all(by.css('ul > li > h2 > a')).then(function(items) { + expect(items.length).toBe(4); + expect(items[0].getText()).toBe('Angular Service Worker Intro'); + expect(items[1].getText()).toBe('Tour of Heroes'); + expect(items[2].getText()).toBe('CLI Documentation'); + expect(items[3].getText()).toBe('Angular blog'); + }); + }); + +}); diff --git a/aio/content/examples/service-worker-getstart/example-config.json b/aio/content/examples/service-worker-getstart/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/service-worker-getstart/src/app/app.component.css b/aio/content/examples/service-worker-getstart/src/app/app.component.css new file mode 100755 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/service-worker-getstart/src/app/app.component.html b/aio/content/examples/service-worker-getstart/src/app/app.component.html new file mode 100755 index 0000000000..6d21987a20 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/app/app.component.html @@ -0,0 +1,23 @@ + +
+

+ Welcome to {{title}}! +

+ Angular Logo +
+

Here are some links to help you start:

+ + diff --git a/aio/content/examples/service-worker-getstart/src/app/app.component.spec.ts b/aio/content/examples/service-worker-getstart/src/app/app.component.spec.ts new file mode 100755 index 0000000000..bcbdf36b3e --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/app/app.component.spec.ts @@ -0,0 +1,27 @@ +import { TestBed, async } from '@angular/core/testing'; +import { AppComponent } from './app.component'; +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); + it(`should have as title 'app'`, async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('app'); + })); + it('should render title in a h1 tag', async(() => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); + })); +}); diff --git a/aio/content/examples/service-worker-getstart/src/app/app.component.ts b/aio/content/examples/service-worker-getstart/src/app/app.component.ts new file mode 100755 index 0000000000..6b80f360d2 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'Service Workers'; +} diff --git a/aio/content/examples/service-worker-getstart/src/app/app.module.ts b/aio/content/examples/service-worker-getstart/src/app/app.module.ts new file mode 100755 index 0000000000..daaf81e652 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/app/app.module.ts @@ -0,0 +1,31 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { AppComponent } from './app.component'; + +// #docregion sw-import +import { ServiceWorkerModule } from '@angular/service-worker'; +import { environment } from '../environments/environment'; +// #enddocregion sw-import + +import { CheckForUpdateService } from './check-for-update.service'; +import { LogUpdateService } from './log-update.service'; +import { PromptUpdateService } from './prompt-update.service'; + +// #docregion sw-module +@NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}) + ], + providers: [ + CheckForUpdateService, + LogUpdateService, + PromptUpdateService, + ], + bootstrap: [AppComponent] +}) +export class AppModule { } +// #enddocregion sw-module diff --git a/aio/content/examples/service-worker-getstart/src/app/check-for-update.service.ts b/aio/content/examples/service-worker-getstart/src/app/check-for-update.service.ts new file mode 100755 index 0000000000..81ca74fff9 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/app/check-for-update.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { SwUpdate } from '@angular/service-worker'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/interval'; + +function promptUser(event): boolean { + return true; +} + +// #docregion sw-check-update +@Injectable() +export class CheckForUpdateService { + + constructor(updates: SwUpdate) { + Observable.interval(6 * 60 * 60).subscribe(() => updates.checkForUpdate()); + } +} +// #enddocregion sw-check-update diff --git a/aio/content/examples/service-worker-getstart/src/app/log-update.service.ts b/aio/content/examples/service-worker-getstart/src/app/log-update.service.ts new file mode 100755 index 0000000000..e38961d4f5 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/app/log-update.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { SwUpdate } from '@angular/service-worker'; + +// #docregion sw-update +@Injectable() +export class LogUpdateService { + + constructor(updates: SwUpdate) { + updates.available.subscribe(event => { + console.log('current version is', event.current); + console.log('available version is', event.available); + }); + updates.activated.subscribe(event => { + console.log('old version was', event.previous); + console.log('new version is', event.current); + }); + } +} +// #enddocregion sw-update diff --git a/aio/content/examples/service-worker-getstart/src/app/prompt-update.service.ts b/aio/content/examples/service-worker-getstart/src/app/prompt-update.service.ts new file mode 100755 index 0000000000..9e3e56012e --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/app/prompt-update.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { SwUpdate } from '@angular/service-worker'; + +function promptUser(event): boolean { + return true; +} + +// #docregion sw-activate +@Injectable() +export class PromptUpdateService { + + constructor(updates: SwUpdate) { + updates.available.subscribe(event => { + if (promptUser(event)) { + updates.activateUpdate().then(() => document.location.reload()); + } + }); + } +} +// #enddocregion sw-activate diff --git a/aio/content/examples/service-worker-getstart/src/index.html b/aio/content/examples/service-worker-getstart/src/index.html new file mode 100755 index 0000000000..2f9232b839 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/index.html @@ -0,0 +1,14 @@ + + + + + SwExample + + + + + + + + + diff --git a/aio/content/examples/service-worker-getstart/src/main.ts b/aio/content/examples/service-worker-getstart/src/main.ts new file mode 100755 index 0000000000..91ec6da5f0 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.log(err)); diff --git a/aio/content/examples/service-worker-getstart/src/ngsw-config.json b/aio/content/examples/service-worker-getstart/src/ngsw-config.json new file mode 100755 index 0000000000..8b78a4f893 --- /dev/null +++ b/aio/content/examples/service-worker-getstart/src/ngsw-config.json @@ -0,0 +1,28 @@ + +{ + "index": "/index.html", + "assetGroups": [{ + "name": "app", + "installMode": "prefetch", + "resources": { + "files": [ + "/favicon.ico", + "/index.html" + ], + "versionedFiles": [ + "/*.bundle.css", + "/*.bundle.js", + "/*.chunk.js" + ] + } + }, { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": [ + "/assets/**" + ] + } + }] +} \ No newline at end of file diff --git a/aio/content/guide/service-worker-comm.md b/aio/content/guide/service-worker-comm.md new file mode 100644 index 0000000000..8c65444e83 --- /dev/null +++ b/aio/content/guide/service-worker-comm.md @@ -0,0 +1,41 @@ +# Communicating with service workers + +Importing `ServiceWorkerModule` into your `AppModule` doesn't just register the service worker, it also provides a few services you can use to interact with the service worker and control the caching of your app. + +## `SwUpdate` service + +The `SwUpdate` service gives you access to events that indicate when the service worker has discovered an available update for your app or when it has activated such an update—meaning it is now serving content from that update to your app. + +The `SwUpdate` service supports four separate operations: +* Getting notified of *available* updates. These are new versions of the app to be loaded if the page is refreshed. +* Getting notified of update *activation*. This is when the service worker starts serving a new version of the app immediately. +* Asking the service worker to check the server for new updates. +* Asking the service worker to activate the latest version of the app for the current tab. + +### Available and activated updates + +The two update events, `available` and `activated`, are `Observable` properties of `SwUpdate`: + + + + +You can use these events to notify the user of a pending update or to refresh their pages when the code they are running is out of date. + +### Checking for updates + +It's possible to ask the service worker to check if any updates have been deployed to the server. You might choose to do this if you have a site that changes frequently or want updates to happen on a schedule. + +Do this with the `checkForUpdate()` method: + + + + +This method returns a `Promise` which indicates that the update check has completed successfully, though it does not indicate whether an update was discovered as a result of the check. Even if one is found, the service worker must still successfully download the changed files, which can fail. If successful, the `available` event will indicate availability of a new version of the app. + +### Forcing update activation + +If the current tab needs to be updated to the latest app version immediately, it can ask to do so with the `activateUpdate()` method: + + + +Doing this could break lazy-loading into currently running apps, especially if the lazy-loaded chunks use filenames with hashes, which change every version. diff --git a/aio/content/guide/service-worker-configref.md b/aio/content/guide/service-worker-configref.md new file mode 100644 index 0000000000..e77bc78861 --- /dev/null +++ b/aio/content/guide/service-worker-configref.md @@ -0,0 +1,161 @@ +{@a glob} + +# Reference: Configuration file + +The `src/ngsw-config.json` configuration file specifies which files and data URLs the Angular +service worker should cache and how it should update the cached files and data. The +CLI processes the configuration file during `ng build --prod`. Manually, you can process +it with the `ngsw-config` tool: + +```sh +ngsw-config dist src/ngswn-config.json /base/href +``` + +The configuration file uses the JSON format. All file paths must begin with `/`, which is the deployment directory—usually `dist` in CLI projects. + +Patterns use a limited glob format: + +* `**` matches 0 or more path segments. +* `*` matches exactly one path segment or filename segment. +* The `!` prefix marks the pattern as being negative, meaning that only files that don't match the pattern will be included. + +Example patterns: + +* `/**/*.html` specifies all HTML files. +* `/*.html` specifies only HTML files in the root. +* `!/**/*.map` exclude all sourcemaps. + +Each section of the configuration file is described below. + +## `appData` + +This section enables you to pass any data you want that describes this particular version of the app. +The `SwUpdate` service includes that data in the update notifications. Many apps use this section to provide additional information for the display of UI popups, notifying users of the available update. + +## `index` + +Specifies the file that serves as the index page to satisfy navigation requests. Usually this is `/index.html`. + +## `assetGroups` + +*Assets* are resources that are part of the app version that update along with the app. They can include resources loaded from the page's origin as well as third-party resources loaded from CDNs and other external URLs. As not all such external URLs may be known at build time, URL patterns can be matched. + +This field contains an array of asset groups, each of which defines a set of asset resources and the policy by which they are cached. + +```json +{ + "assetGroups": [{ + ... + }, { + ... + }] +} +``` + +Each asset group specifies both a group of resources and a policy that governs them. This policy determines when the resources are fetched and what happens when changes are detected. + +Asset groups follow the Typescript interface shown here: + +```typescript +interface AssetGroup { + name: string; + installMode?: 'prefetch' | 'lazy'; + updateMode?: 'prefetch' | 'lazy'; + resources: { + files?: string[]; + versionedFiles?: string[]; + urls?: string[]; + }; +} +``` + +### `name` + +A `name` is mandatory. It identifies this particular group of assets between versions of the configuration. + +### `installMode` + +The `installMode` determines how these resources are initially cached. The `installMode` can be either of two values: + +* `prefetch` tells the Angular service worker to fetch every single listed resource while it's caching the current version of the app. This is bandwidth-intensive but ensures resources are available whenever they're requested, even if the browser is currently offline. + +* `lazy` does not cache any of the resources up front. Instead, the Angular service worker only caches resources for which it receives requests. This is an on-demand caching mode. Resources that are never requested will not be cached. This is useful for things like images at different resolutions, so the service worker only caches the correct assets for the particular screen and orientation. + +### `updateMode` + +For resources already in the cache, the `updateMode` determines the caching behavior when a new version of the app is discovered. Any resources in the group that have changed since the previous version are updated in accordance with `updateMode`. + +* `prefetch` tells the service worker to download and cache the changed resources immediately. + +* `lazy` tells the service worker to not cache those resources. Instead, it treats them as unrequested and waits until they're requested again before updating them. An `updateMode` of `lazy` is only valid if the `installMode` is also `lazy`. + +### `resources` + +This section describes the resources to cache, broken up into three groups. + +* `files` lists patterns that match files in the distribution directory. These can be single files or glob-like patterns that match a number of files. + +* `versionedFiles` is like `files` but should be used for build artifacts that already include a hash in the filename, which is used for cache busting. The Angular service worker can optimize some aspects of its operation if it can assume file contents are immutable. + +* `urls` includes both URLs and URL patterns that will be matched at runtime. These resources are not fetched directly and do not have content hashes, but they will be cached according to their HTTP headers. This is most useful for CDNs such as the Google Fonts service. + +## `dataGroups` + +Unlike asset resources, data requests are not versioned along with the app. They're cached according to manually-configured policies that are more useful for situations such as API requests and other data dependencies. + +Data groups follow this Typescript interface: + +```typescript +export interface DataGroup { + name: string; + urls: string[]; + version?: number; + cacheConfig: { + maxSize: number; + maxAge: string; + timeout?: string; + strategy?: 'freshness' | 'performance'; + }; +} +``` + +### `name` +Similar to `assetGroups`, every data group has a `name` which uniquely identifies it. + +### `urls` +A list of URL patterns. URLs that match these patterns will be cached according to this data group's policy. + +### `version` +Occasionally APIs change formats in a way that is not backward-compatible. A new version of the app may not be compatible with the old API format and thus may not be compatible with existing cached resources from that API. + +`version` provides a mechanism to indicate that the resources being cached have been updated in a backwards-incompatible way, and that the old cache entries—those from previous versions—should be discarded. + +`version` is an integer field and defaults to `0`. + +### `cacheConfig` +This section defines the policy by which matching requests will be cached. + +#### `maxSize` +(required) The maximum number of entries, or responses, in the cache. Open-ended caches can grow in unbounded ways and eventually exceed storage quotas, calling for eviction. + +#### `maxAge` +(required) The `maxAge` parameter indicates how long responses are allowed to remain in the cache before being considered invalid and evicted. `maxAge` is a duration string, using the following unit suffixes: + +* `d`: days +* `h`: hours +* `m`: minutes +* `s`: seconds +* `u`: milliseconds + +For example, the string `3d12h` will cache content for up to three and a half days. + +#### `timeout` +This duration string specifies the network timeout. The network timeout is how long the Angular service worker will wait for the network to respond before using a cached response, if configured to do so. + +#### `strategy` + +The Angular service worker can use either of two caching strategies for data resources. + +* `performance`, the default, optimizes for responses that are as fast as possible. If a resource exists in the cache, the cached version is used. This allows for some staleness, depending on the `maxAge`, in exchange for better performance. This is suitable for resources that don't change often; for example, user avatar images. + +* `freshness` optimizes for currency of data, preferentially fetching requested data from the network. Only if the network times out, according to `timeout`, does the request fall back to the cache. This is useful for resources that change frequently; for example, account balances. diff --git a/aio/content/guide/service-worker-devops.md b/aio/content/guide/service-worker-devops.md new file mode 100644 index 0000000000..1935a2d9c0 --- /dev/null +++ b/aio/content/guide/service-worker-devops.md @@ -0,0 +1,302 @@ +# DevOps: Angular service worker in production + +This page is a reference for deploying and supporting production apps that use the Angular service worker. It explains how the Angular service worker fits into the larger production environment, the service worker's behavior under various conditions, and available recourses and fail-safes. + +## Service worker and caching of app resources + +Conceptually, you can imagine the Angular service worker as a forward cache or a CDN edge that is installed in the end user's web browser. The service worker's job is to satisfy requests made by the Angular app for resources or data from a local cache, without needing to wait for the network. Like any cache, it has rules for how content is expired and updated. + +{@a versions} + +### App versions + +In the context of an Angular service worker, a "version" is a collection of resources that represent a specific build of the Angular app. Whenever a new build of the app is deployed, the service worker treats that build as a new version of the app. This is true even if only a single file is updated. At any given time, the service worker may have multiple versions of the app in its cache and it may be serving them simultaneously. For more information, see the [App tabs](guide/service-worker-devops#tabs) section below. + +To preserve app integrity, the Angular service worker groups all files into a version together. The files grouped into a version usually include HTML, JS, and CSS files. Grouping of these files is essential for integrity because HTML, JS, and CSS files frequently refer to each other and depend on specific content. For example, an `index.html` file might have a `