© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
V. K. KotaruBuilding Offline Applications with Angularhttps://doi.org/10.1007/978-1-4842-7930-4_5

5. Cache Data with Service Workers

Venkata Keerti Kotaru1  
(1)
-, Hyderabad, Telangana, India
 

Service workers are used to cache data responses. So far, you have seen how to create a new Angular application, configure the application to be installable, and cache the application so that it is accessible even when offline. This chapter introduces how to cache a data response from an HTTP service.

This chapter begins by creating a new component to retrieve and show data from an HTTP service. Next, it discusses how to create an interface that acts as a contract between the service and the Angular application. Next, you will learn how to create a Node.js Express mock service that provides data to the Angular application. It runs in a separate process outside the Angular application. The chapter details how to create an Angular service, which uses an out-of-the-box HttpClient service to invoke the HTTP service.

Now that you have integrated with an HTTP service and accessed the data, the chapter details how to configure Web Arcade to cache data responses. It elaborates on the configuration and showcases a cached data response with a simulated offline browser.

Remember, Web Arcade is an online system for games. Imagine a screen that lists board games available on the application, as shown in Figure 5-1. Follow the instructions to build this component. It shows the data in an HTML table. On loading the page, the component invokes the service to retrieve the Web Arcade board games.
Figure 5-1

Board games list

Adding a Component to List Board Games

Begin by creating a component for listing the board games. Remember the “Angular Components” section in Chapter 3. Create a new component by running the following command. It will scaffold the new component.
% ng generate component components/board-games
Earlier, you used the dice component in the App component . Update it to use the new component, as shown in Listing 5-1. Notice the dice component, called wade-dice , has been commented.
<div class="container align-center">
   <!-- <wade-dice></wade-dice> -->
   <wade-board-games></wade-board-games>
</div>
Listing 5-1

Use the Board Component

Note

Angular single-page applications (SPAs) use routing to navigate between two pages with separate components. Listing 5-1 is temporary so that the focus stays on data caching in this chapter. Chapter 8 introduces Angular routing.

Define a Data Structure for Board Games

Next, define a data structure for the board games page. You create a TypeScript interface for defining the data structure. It defines a shape for the board games data objects. TypeScript uses an interface to define a contract, which is useful within the Angular application and with the external, remote service that serves the board games data.

The TypeScript interface enforces the required list of fields for board games. You will notice an error if a needed field is missing because of a problem in the remote service or a bug in the Angular application. An interface acts as a contract between the Angular application and the external HTTP service.

Run the following command to create the interface. It creates a new file called board-games-entity.ts in a new directory called common. Typically, data structures/entities are used across the Angular application. Hence, name the directory common.
ng generate interface common/board-games-entity
Listing 5-2 defines the specific fields for board games. The remote service is expected to return the same fields. The component uses this shape and structure for the data. Add the code to board-games-entity.ts.
export interface BoardGamesEntity {
   title: string;
   description: string;
   age: string;
   players: string;
   origin: string;
   link: string;
   alternateNames: string;
}
/* Multiple games data returned, hence creating an Array */
export interface GamesEntity {
   boardGames: Array<BoardGamesEntity>;
}
Listing 5-2

Interfaces for Board Games

BoardGamesEntity represents a single board game. Considering Web Arcade will have multiple games, GamesEntity includes an array of board games. Later, GamesEntity can extend to other categories of games in the Web Arcade system.

Mock Data Service

A typical service retrieves and updates data from/to a database or a back-end system, which is out of scope for this book. However, to integrate with a RESTful data service, this section details how to develop mock responses and data objects. The mock service returns board games data in JavaScript Object Notation (JSON) format. It can be readily integrated with the Angular component created in the earlier section “Adding a Component to List Board Games.

You will use Node.js’s Express server to develop the mock service. Follow these instructions to create a new service.

Use the Express application generator to easily generate a Node.js Express service. Run the following command to install it:
npm install --save-dev express-generator
# (or)
yarn add --dev express-generator
Note

Notice the --save-dev option with the npm command and the --dev option with the yarn command. It installs the package in dev-dependencies in package.json, qualifying it as a developer tool. It will not be included in the production builds, which helps reduce the footprint. See Listing 5-3, line 15.

01: {
02:  "name": "web-arcade",
03:  "version": "0.0.0", /* removed code for brevity */
04:  "dependencies": {
05:    "@angular/animations": "~12.0.1",
06:    /* removed code for brevity */
07:    "zone.js": "~0.11.4"
08:  },
09:  "devDependencies": {
10:    "@angular-devkit/build-angular": "~12.0.1",
11:    "@angular/cli": "~12.0.1",
12:    "@angular/compiler-cli": "~12.0.1",
13:    "@types/jasmine": "~3.6.0",
14:    "@types/node": "^12.11.1",
15:    "express-generator": "^4.16.1",
16:    "jasmine-core": "~3.7.0",
17:    "karma": "~6.3.0",
18:    "karma-chrome-launcher": "~3.1.0",
19:    "karma-coverage": "~2.0.3",
20:    "karma-jasmine": "~4.0.0",
21:    "karma-jasmine-html-reporter": "^1.5.0",
22:    "typescript": "~4.2.3"
23:  }
24: }
Listing 5-3

Package.json dev-dependencies

Next, create a new directory for mock services; name it mock-services (an arbitrary name). Change the directory to mock-services. Run the following command to create a new Express service. It scaffolds a new Node.js Express application.
npx express-generator
Note

The npx command first checks the local node_modules for the package. If it is not found, the command downloads the package to the local cache and run the command.

The previous command runs, even without the dev-dependency installation in the previous step (npm install --save-dev express-generator). If you do not intend to run this command often, you may skip the dev-dependency installation.

Next, run npm install (or yarn install) in the mock-services directory.

Create and save board games data in a JSON file. The code sample saves it to [application-directory]/mock-services/data/board-games.json. The server-side Node.js service returns these fields and values to the Angular application. The structure matches the Angular interface structure defined in Listing 5-2. See Listing 5-4.
{
   "boardGames": [
       {
           "title": "Scrabble",
           "description": "A crossword game commonly played with English alphabets and words",
           "age": "5+",
           "players": "2 to 5",
           "origin": "Started by an architect named Alfred Mosher Butts in the year 1938",
           "link": "https://simple.wikipedia.org/wiki/Scrabble",
           "alternateNames": "Scrabulous (a version of the game on Facebook)"
       },
       {
           "title": "Checkers",
           "description": "Two players start with dark and light colored pieces. The pieces move diagonally.",
           "age": "3+",
           "players": "Two players",
           "origin": "12th century France",
           "link": "https://simple.wikipedia.org/wiki/Checkers",
           "alternateNames": "Draughts"
       }
/* You may extend additional mock games data*/
   ]
}
Listing 5-4

Board Games Mock Data

Next, update the mock service application to return the previous board games data. Create a new file called board-games.js under mock-services/routes. Add the code in Listing 5-5.
01: var express = require('express'); // import express
02: var router = express.Router(); // create a route
03: var boardGames = require('../data/board-games.json');
04:
05: /* GET board games listing. */
06: router.get('/', function(req, res, next) {
07:     res.setHeader('Content-Type', 'application/json');
08:     res.send(boardGames);
09: });
10:
11: module.exports = router;
Listing 5-5

New API Endpoint That Returns Mock Board Games Data

Consider the following explanation:
  • Line 3 imports and sets the board games mock data on a variable.

  • Lines 6 to 9 create the endpoint that returns the board games data.

  • Notice the get() function in line 6. The endpoint responds to an HTTP GET call, which is typically used to retrieve data (as opposed to create, update, or delete).

  • Line 7 sets the response content type to application/json ensuring the client browsers interpret the response format accurately.

  • Line 8 responds to the client with the board games data.

  • Line 11 exports a router instance that encapsulates the service endpoint.

Next, the endpoint needs to be associated with a route so that the previous code is invoked when the client requests data. Edit app.js in the root directory of the service application (mock-services/app.js). Add the lines of code in bold (lines 9 and 25) in Listing 5-6 to the file.
07: var indexRouter = require('./routes/index');
08: var usersRouter = require('./routes/users');
09: var boardGames = require('./routes/board-games');
10:
11: var app = express();
12:
13: // view engine setup
14: app.set('views', path.join(__dirname, 'views'));
15: app.set('view engine', 'jade');
16:
17: app.use(logger('dev'));
18: app.use(express.json());
19: app.use(express.urlencoded({ extended: false }));
20: app.use(cookieParser());
21: app.use(express.static(path.join(__dirname, 'public')));
22:
23: app.use('/', indexRouter);
24: app.use('/users', usersRouter);
25: app.use('/api/board-games', boardGames);
Listing 5-6

Integrate the New Board Games Endpoint

Consider the following explanation:
  • Line 9 imports the board games route instance exported in the earlier Listing 5-5.

  • Line 25 adds the route /api/board-games to the application. The new service is invoked when the client invokes this endpoint.

Run the mock service with the command npm start. By default, it runs the Node.js Express service application on port 3000. Access the new endpoint by accessing http://localhost:3000/api/board-games. See Figure 5-2.
Figure 5-2

Board games endpoint accessed on a browser

Note

Notice that you are running the service application on a separate port, which is 3000. Remember, in the earlier examples, the Angular application runs on ports 8080 (with Http-Server) and 4200 (with the ng serve command that uses Webpack internally). The Angular application running on one of these ports is expected to connect to the service instance running on port 3000.

Call the Service in the Angular Application

This section details how to update the Angular application to consume data from the Node.js service. In a typical application, Node.js services are server-side, remote services that access data from a database or another service.

Configure the Service in an Angular Application

Angular provides an easy way to configure various values including remote service URLs. In the Angular project, notice the directory src/environment. By default, you will see the following:
  • environment.ts: This is for the debug build configuration used by the developer on localhost. Typically, the ng serve command uses it.

  • environment.prod.ts: This is for production deployments. Running ng build (or yarn build or npm run build) uses this configuration file.

Edit the file src/environments/environment.ts and add the code in Listing 5-7. It has a relative path to the service endpoint.
1: export const environment = {
2:     boardGameServiceUrl: `/api/board-games`,
3:     production: false,
4:   };
5:
Listing 5-7

Integrate the New Board Games Endpoint

Consider the following explanation:
  • Line 2 adds a relative path to the service endpoint. You will import and use the configuration field boardGameServiceUrl while making a call to the service.

  • Line 3 set production to false. Remember, the file environment.ts is used with the ng serve command, which runs a debug build with the help of Webpack. It is set to true in the alternate environment file environment.prod.ts.

Create an Angular Service

Angular services are reusable units of code. Angular provides ways to create a service and instantiate and inject services into components and other services. The Angular services help separate concerns. Angular components primarily focus on the presentation logic. On the other hand, you may use services for other reusable functions that do not include presentation. Consider the following examples:
  • A service can be used for sharing data among components. Imagine a screen with a list of users. Say the list is shown by a UserList component. Users can select a user. The application navigates to another screen, which loads another component, say UserDetails. The user details component shows additional information about the user in your system. The user details component needs data about the selected user so that it can retrieve and show the additional information.

You may use a service to share the selected user information. The first component updates the selected user details to a common service. The second component retrieves the data from the same service.

Note

A service is an easy and a simple way to share data among components. However, for a large application, it is advisable to adapt the Redux pattern. It helps maintain application state, ensures unidirectional data flows, provides selectors for easy access to the state in the Redux store, and has many more features. For Angular, NgRx is a popular library that implements the Redux pattern and its concepts.

How are the components sharing the same instance of a service? See the next section for details of how an Angular service is provided and how the service instances are managed in an Angular application.
  • A service can be used to aggregate and transform JSON data. The Angular application might obtain data from various data sources. Create a service with a reusable function to aggregate and return the data. This enables a component to readily use the JSON objects for the presentation.

  • A service is used to retrieve data from a remote HTTP service. In this chapter, you have already built a service to share the board games data with an Angular application. The Node.js Express server running in a separate process (ideally on a remote server) shares this data over an HTTP GET call.

Create a new service by running the following Angular CLI command. You will use this service to invoke the api/board-games service built in the previous section.
ng generate service common/games
The CLI command creates a new games service. It creates the following files in the directory common:
  • common/games.services.ts: A TypeScript file for adding Angular service code that makes HTTP calls for games data

  • common/games.services.spec.ts: A unit test file for the functions in games.service.ts

Consider Listing 5-8 for the games service. Add a new function called getBoardNames() to invoke the HTTP service.
01: @Injectable({
02:     providedIn: 'root'
03: })
04: export class GamesService {
05:
06:     constructor() { }
07:
08:     getBoardGames(){
09:     }
10: }
Listing 5-8

Angular Service Skeleton Code

Provide a Service

Notice the code statement in lines 1 to 3. Those lines contain the Injectable decorator, with provideIn at the root level. Angular shares a single instance for the entire application. The following are the alternatives:
  • Provide at the module level: The service instance is available and shared within the module. Later sections give more details about Angular modules.

  • Provide at the component level: The service instance is created and available for the component and all its child components.

Once a service is provided, it needs to be injected. A service can be injected into a component or another service. In the current example, the board games component needs data so that the games are listed for users to view. Notice in the earlier Listing 5-8 that the code creates a new function called getBoardGames() intended to retrieve the list from the remote HTTP service.

Inject GamesService into BoardGamesComponent, as shown in Listing 5-9, line 5. The constructor creates a new field called gameService of type GamesService. This statement injects the service into the component.
01: export class BoardGamesComponent implements OnInit {
02:
03:     games = new Observable<GamesEntity>();
04:
05:     constructor(private gameService: GamesService) { }
06:
07:     ngOnInit(): void {
08:       this.games = this.gameService.getBoardGames();
09:     }
10:
11:   }
Listing 5-9

Inject Games Service into a Component

Note

The ngOnInit() function on line 7 is an Angular lifecycle hook. It is invoked after the framework completes initializing the component and its properties. This function is a good place in a component for additional initializations, including service calls.

Line 8 in Listing 5-9 calls the service function that retrieves board games data. This data is required as part of component initialization as the primary functionality of the component is to show a list of games.

HttpClient Service

Next, invoke the remote HTTP service . Angular provides the HttpClient service as part of the package @angular/common/http. It provides an API to invoke various HTTP methods including GET, POST, PUT, and DELETE.

As a prerequisite, import HttpClientModule from @angular/common/http. Add it (HttpClientModule) to the imports list on the Angular module, as shown in Listing 5-10, lines 7 and 13.
01: import {HttpClientModule} from '@angular/common/http';
02:
03: @NgModule({
04:   declarations: [
05:  // pre-existing declaratoins
06:   ],
07:   imports: [
08:    // pre-existing imports
09:     BrowserModule,
10:     HttpClientModule,
11:     AppRoutingModule,
12:
13:   ],
14:   providers: [],
15:   bootstrap: [AppComponent]
16: })
17: export class AppModule { }
18:
Listing 5-10

Import HttpClientModule

Remember from Listing 5-5 (line 6) that the service returns data to the Angular application with a GET call. Hence, we will use the get() function on the HttpClient instance to invoke the service. Remember, we already created the function getBoardGames() as part of GamesService (see Listing 5-8, line 8).

Next, inject the HttpClient service into GamesService and use the get() API to make an HTTP call. See Listing 5-11 .
01: import { Injectable } from '@angular/core';
02: import { HttpClient } from '@angular/common/http';
03: import { environment } from 'src/environments/environment';
04: import { GamesEntity } from './board-games-entity';
05: import { Observable } from 'rxjs';
06:
07:
08: @Injectable({
09:   providedIn: 'root'
10: })
11: export class GamesService {
12:
13:   constructor(private client: HttpClient) { }
14:
15:   getBoardGames(): Observable<GamesEntity>{
16:     return this
17:       .client
18:       .get<GamesEntity>(environment.boardGameServiceUrl);
19:   }
20: }
21:
Listing 5-11

GamesService Injects and Uses HttpClient

Consider the following explanation:
  • Line 13 injects HttpClient into GamesService. Notice that the name of the field (an instance of HttpClient) is client. It is a private field and hence accessible only within the service class.

  • The statement in lines 16 to 18 invokes the client.get() API. As a client is a field of the class, it is accessed using the this keyword.

  • The get() function accepts one parameter, the URL for the service. Notice the import statement for the environment object on line 3. It imports the object exported from the environment configuration file. See Listing 5-7. It is one of the environment configuration files. Use the boardGameServiceUrl field from the configuration (Listing 5-11, line 18). You may have more than one URL configured in an environment file.

  • Notice that the get() function is expected to retrieve GamesEntity. It was created in Listing 5-2.

  • The getBoardGames() function returns an Observable<GamesEntity>. Observable is useful with asynchronous function calls. A remote service might take some time, such as a few milliseconds or sometimes a few seconds, to return the data. Hence, the service function returns an observable. The subscriber provides a function callback. The observable executes the function callback once data is available.

  • Notice that line 16 returns the output of the get() function call. It returns an Observable of the type specified. You specified the type GamesEntity on line 18. Hence, it returns an Observable of type GamesEntity. It matches with the return type of getBoardGames() on line 15.

Now, the service function is ready. Review Listing 5-9 again, which is a component TypeScript class. It calls the service function and sets the return value of type Observable<GamesEntity> to a class field. The class field uses the returned object in the HTML template. The template file renders the board games list on a page. See Listing 5-12.
01: <div>
02:     <table>
03:         <tr>
04:             <th> Title </th>
05:             <th> History </th>
06:         </tr>
07:         <ng-container *ngFor="let game of (games | async)?.boardGames">
08:             <tr>
09:                 <td>
10:                     <strong>
11:                         {{game.title}}
12:                     </strong>
13:                     <span>{{game.alternateNames}}</span>
14:                 </td>
15:                 <td>{{game.origin}}</td>
16:             </tr>
17:             <tr >
18:                 <td class="last-cell" colspan="2">{{game.description}}</td>
19:             </tr>
20:         </ng-container>
21:
22:     </table>
23: </div>
Listing 5-12

Board Games Component Template Shows List of Games

Consider the following explanation:
  • The template renders the list as an HTML table.

  • Notice, in line 7, that the *ngFor directive iterates through boardGames. See Listing 5-2. Notice that boardGames is an array on the interface GamesEntity.

  • The template shows fields on each game in the entity. See lines 11, 13, 15, and 18. They show the fields title, alternateNames, origin, and description.

  • Remember, the class field games is set with the values returned from the service. This field is used in the template. See line 7.

  • Notice the pipe with async (| async) on line 7. It is applied on Observable. Remember, the service returns an Observable. As mentioned earlier, an Observable is useful with asynchronous function calls. A remote service might take time, a few milliseconds or sometimes a few seconds, to return the data. The template uses the field boardGames on games Observable, when the data is available, in other words, when it is obtained from the service.

Cache the Board Games Data

So far, we have created an HTTP service to provide board games data, created an Angular service to use the HTTP service to obtain the data, and added a new component to show the list. Now, configure the service worker to cache the board games data (and even other HTTP service responses).

Remember, in the previous chapter, we listed various configurations for Angular service workers. As you have seen, Angular uses a file called ngsw-config.json for service worker configurations. In this section, you will add a dataGroups section to cache the HTTP service data. See Listing 5-13 for the new configuration to cache the board games data.
01: "dataGroups": [{
02:     "name": "data",
03:     "urls": [
04:       "api/board-games"
05:     ],
06:     "cacheConfig": {
07:       "maxAge": "36h",
08:       "timeout": "10s",
09:       "maxSize": 100,
10:       "strategy":"performance"
11:     }
12:   }]
Listing 5-13

Data Groups Configuration for a Service Worker in an Angular Application

Consider the following explanation:
  • Line 4 configures the service URLs to cache data. It is an array, and we can configure multiple URLs here.

  • The URLs support matching patterns. For example, you may use api/* to configure all the URLs.

  • As part of the cache configuration (cacheConfig), see line 10. Set strategy to performance. This instructs the service worker to use cached responses first for better performance. Alternatively, you may use freshness, which goes to the network first and uses the cache only when the application is offline.

  • Notice that maxAge is set to 36 hours, after which the service worker clears the cached responses (of board games). Caching data for too long could cause the application to use obsolete fields and records. The service worker configuration provides a mechanism to automatically clear the data at periodic intervals, ensuring the application does not use stale data.

  • The timeout is set to 10 seconds. This is dependent on strategy. Assuming strategy is set to freshness, after 10 seconds, the service worker uses cached responses.

  • maxSize is set to 100 records. It is a good practice to limit the size by design. Browsers (like any other platform) manage and allocate memory for each application. If the application exceeds the upper limit, the entire dataset and the cache could be evicted.

Listing 5-13 has a single data groups configuration object. As we further develop the application, the additional services might have slightly different cache requirements. For example, the list of gamers might need to be the latest ones. If your friend joins the arcade, you prefer to see her listed instead of showing the old list. Hence, you might change the strategy to freshness. Add this URL configuration as another object in the dataGroups array. On the other hand, for a service that fits the current configuration, add the URL to the urls field on line 4.

Run the Angular build and start Http-Server to see the changes. See the following command:
yarn build && http-server dist/web-arcade --proxy http://localhost:3000
See Figure 5-3 for cached service response with a service worker.
Figure 5-3

Cached service responses with the service worker

Angular Modules

Traditionally, Angular had its own modularity system. The new framework (Angular 2 and greater) uses NgModules to bring modularity to applications. An Angular module encapsulates directives including components, services, pipes, etc. Create Angular modules to logically group features. See Figure 5-4.
Figure 5-4

Angular modules

All Angular applications use at least one root module. Typically, the module is named AppModule and defined in src/app/app.module.ts. A module may export one or more functionalities. The other modules in the application can import the exported components and services.

Note

Angular modules are separate from JavaScript (ES6) modules. They complement each other. An Angular application uses both JavaScript modules and Angular modules.

Summary

This chapter provided instructions for creating a new component for listing board games. With this code sample, it demonstrated how service workers cache data responses from an HTTP service. It provided instructions to create a board games component with Angular CLI. You also updated the application to use this new component instead of dice.

It also defined data contracts between the Angular application and the external HTTP service, detailed how to create a Node.js Express service for providing data to the Angular application, and introduced Angular services.

Exercise
  • Create a new route in the Node.js Express application for exposing a list of jigsaw puzzles.

  • Create an Angular service to use the new jigsaw puzzles service endpoint and retrieve data.

  • Ensure the latest jigsaw puzzles data is available to the user. Cache only when the user is offline or lost connectivity.

  • For the new service, configure to use data from the cache if the service does not respond after one minute.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.253.123