In the first part of this series on building a logistics application, we configured the Strapi backend and implemented authentication on the frontend using the Angular framework. In this series, we will add geolocation to enable order shipment tracking and convert our application into a Progressive Web Application(PWA)
This tutorial will be divided into two parts:
PWA(Progressive Web App) is a type of web app that is installable on a device and can function similarly to a native app downloaded from an app store, with added features and functionality (though it is still limited in some aspects compared to native applications).
Angular already supports PWAs. Converting an Angular project into a PWA requires little setup. The foundation of every PWA is a service worker, which Angular provides right out of the box. This allows for features like background synchronization, offline access, and caching.
Throughout this series, we will learn how to add geolocation to enable order shipment tracking and convert our application into a PWA.
Let's start.
We start by setting up our Geolocation service. Our GeolocationService
provides location data to our application by using the browser’s geolocation feature or falling back to IP-based location data when necessary. This two-tiered approach ensures that we can still retrieve the user’s location even if browser-based geolocation fails or isn’t supported.
To set up Geolation via HttpClient
, we inject Angular’s HttpClient
, which allows us to make HTTP requests to external APIs. Here’s how we set it up:
1private http = inject(HttpClient)
This injection allows us to call our IP-based location service ipinfo.io
when browser geolocation isn’t available or fails.
The main method, getCurrentLocation()
, attempts to retrieve the user’s latitude and longitude. We use an Rxjs Observable
here to handle the asynchronous nature of geolocation data retrieval.
1// geolocation.service.ts
2
3getCurrentLocation(): Observable<{ lat: number; lon: number }> {
4 return new Observable((observer) => {
5 // Attempt to get geolocation from the browser
6 if ('geolocation' in navigator) {
7 navigator.geolocation.getCurrentPosition(
8 (position) => {
9 observer.next({
10 lat: position.coords.latitude,
11 lon: position.coords.longitude,
12 });
13 observer.complete();
14 },
15 (error) => {
16 console.error('Geolocation failed:', error);
17 this.getLocationFromIP(observer);
18 }
19 );
20 } else {
21 // If geolocation is not supported, fall to IP-based location
22 this.getLocationFromIP(observer);
23 }
24 });
25}
First, we checked if the browser supports geolocation. If so, we use navigator.geolocation.getCurrentPosition()
to get the user’s location. When successful, we send the location coordinates to our observer, completing the Observable
. If there’s an error (e.g., the user denies permission or geolocation fails), we log the error and fall back to IP-based geolocation by calling getLocationFromIP()
.
When browser geolocation isn’t an option, getLocationFromIP()
takes over. This method uses ipinfo.io’s
API to retrieve the user’s location based on their IP address.
1// src/app/services/geolocation.service.ts
2private getLocationFromIP(observer: any): void {
3 this.http
4 .get<{ loc: string }>(this.ipGeoUrl)
5 .pipe(
6 catchError((error) => {
7 observer.error('Unable to retrieve location from IP.');
8 return of(null);
9 })
10 )
11 .subscribe({
12 next: (response) => {
13 if (response) {
14 const [lat, lon] = response.loc.split(',');
15 observer.next({
16 lat: parseFloat(lat),
17 lon: parseFloat(lon),
18 });
19 observer.complete();
20 } else {
21 observer.error('IP geolocation failed.');
22 }
23 },
24 });
25}
Above, we make a GET
request to ipinfo.io, which returns location data in the form { loc: 'latitude,longitude' }
. If the request is successful, we split the loc
string to retrieve the latitude and longitude values, converting them to numbers before sending them to our observer. If there’s an issue with the API or if no location data is returned, we throw an error to inform the user that IP-based geolocation failed.
Our UserService
provides an easy way to access user data that is stored locally in the browser. This service is helpful for quickly retrieving details like the user’s ID or other necessary information across our app.
The service is provided in the root of our app, meaning it can be used throughout our entire application wherever it’s needed.
1@Injectable({
2 providedIn: 'root',
3})
4export class UserService {}
We don’t need to inject any dependencies into UserService
. This service primarily focuses on handling local storage data related to the user.
The getUserData()
method is our main function when accessing user information. It checks if we have any userData
stored in the localStorage
and parses it from a JSON string to an object, which makes it easy to work with.
1// src/app/services/user.service.ts
2getUserData(): any {
3 const userData = localStorage.getItem('userData');
4 return userData ? JSON.parse(userData) : null;
5}
We try to get userData
from localStorage
. If userData
exists, we use JSON.parse()
to turn it into an object, which we can now access and use in our application. If there’s no userData
stored, getUserData()
returns null
, indicating that no user data is available. For example, this could happen if the user isn’t logged in.
The getDriverId()
method as the name implies gets the driverId
from our userData
. This is particularly useful in scenarios where we need to identify the current driver quickly.
1// src/app/services/user.service.ts
2getDriverId(): number | null {
3 const user = this.getUserData();
4 return user ? user.driverId : null;
5}
We call getUserData()
to retrieve the user object. If userData
is available, then we access driverId
from this object and return it. If no user data is found, getDriverId()
returns null
, showing that there is no driver ID available.
This is the page where a driver signs up and is automatically added to the Strapi backend user collection type.
The user is to input the email, password, and name. The email will be the identifier when attempting to log in some other time after registration.
1// src/app/pages/driver-register/driver-register.component.ts
2
3import { Component } from '@angular/core';
4import { FormBuilder, FormGroup, Validators } from '@angular/forms';
5import { Router } from '@angular/router';
6import { AuthService } from '../../services/auth.service';
7@Component({
8 selector: 'app-driver-register',
9 templateUrl: './driver-register.component.html',
10 styleUrl: './driver-register.component.scss',
11})
12export class DriverRegisterComponent {
13 registerForm: FormGroup;
14 error: string = '';
15 isLoading: boolean = false;
16
17 constructor(
18 private fb: FormBuilder,
19 private authService: AuthService,
20 private router: Router
21 ) {
22 this.registerForm = this.fb.group({
23 name: ['', Validators.required],
24 email: ['', [Validators.required, Validators.email]],
25 password: ['', [Validators.required]],
26 });
27 }
28
29 handleSubmit(): void {
30 if (this.registerForm.invalid) {
31 return;
32 }
33
34 const { name, email, password } = this.registerForm.value;
35 this.isLoading = true;
36
37 this.authService.register(name, email, password).subscribe({
38 next: (data) => {
39 this.isLoading = false;
40 if (data.error) {
41 this.error = data.error.message;
42 return;
43 }
44 this.authService.saveToken(data.jwt);
45 localStorage.setItem('userData', JSON.stringify(data.user));
46 this.router.navigate(['/driver/dashboard']);
47 },
48 error: (err) => {
49 this.isLoading = false;
50 this.error = 'Registration failed. Please try again.';
51 console.error(err);
52 },
53 });
54 }
55}
The DriverLoginComponent
handles the login process for drivers in the application.
1// src/app/pages/driver-login.component.ts
2
3import { Component } from '@angular/core';
4import { Router } from '@angular/router';
5import { FormBuilder, FormGroup, Validators } from '@angular/forms';
6import { AuthService } from '../../services/auth.service';
7@Component({
8 selector: 'app-driver-login',
9 templateUrl: './driver-login.component.html',
10 styleUrl: './driver-login.component.scss',
11})
12export class DriverLoginComponent {
13 loginForm: FormGroup;
14 error: string = '';
15 isLoading: boolean = false;
16
17
18 constructor(
19 private fb: FormBuilder,
20 private authService: AuthService,
21 private router: Router
22 ) {
23 this.loginForm = this.fb.group({
24 email: ['', [Validators.required, Validators.email]],
25 password: ['', [Validators.required]],
26 });
27 }
28
29 handleSubmit(): void {
30 if (this.loginForm.invalid) {
31 return;
32 }
33
34 const { email, password } = this.loginForm.value;
35 this.isLoading = true;
36 this.authService.login(email, password).subscribe({
37 next: (data) => {
38 this.isLoading = false;
39 if (data.error) {
40 this.error = data.error.message;
41 return;
42 }
43 this.authService.saveToken(data.jwt);
44 localStorage.setItem('userData', JSON.stringify(data.user));
45 this.router.navigate(['/driver/dashboard']);
46 },
47 error: (err) => {
48 this.isLoading = false;
49 this.error = 'Login failed. Please try again.';
50 console.log(err);
51 },
52 });
53 }
54}
A login form group is created using angular's formBuider
and an email and password form control are created. I've added some input validation logic to prevent the user from triggering the submit function unless they've passed validation.
We inject the authService
into the component to reach out to our Strapi backend and to save the JSON web token returned in the observable data upon successful login. We then redirect the driver to the dashboard page.
This is where all the information concerning a shipment and order assigned to a particular driver is showcased. It's where we allow the driver to start a pending shipment, and complete a shipment that's in transit. We conditionally render different content to a newly registered user who hasn't been assigned to an order or shipment yet based on the isRegistered property.
The newly registered user is told to wait while an existing user/driver who was redirected from the login page is shown the order and shipment details and the buttons to handle the shipment based on the shipment status.
1// src/app/pages/driver-dashboard/driver-dashboard.component.ts
2
3import { Component, OnInit, inject } from '@angular/core';
4import { GeolocationService } from '../../services/geolocation.service';
5import { UserService } from '../../services/user.service';
6import { ApiService } from '../../services/api.service';
7
8@Component({
9 selector: 'app-driver-dashboard',
10 templateUrl: './driver-dashboard.component.html',
11 styleUrl: './driver-dashboard.component.scss',
12})
13export class DriverDashboardComponent implements OnInit {
14 error: string = '';
15 isLoading: boolean = false;
16 data: any = null;
17 success: string = '';
18
19 isJustRegistered: boolean;
20
21 private geolocationService = inject(GeolocationService);
22 private apiService = inject(ApiService);
23 private userService = inject(UserService);
24
25 ngOnInit(): void {
26 const driverId = this.userService.getDriverId();
27 this.isJustRegistered = driverId ? false : true;
28
29 if (!this.isJustRegistered) {
30 this.loadAssignedShipment(driverId);
31 }
32 }
33
34 loadAssignedShipment(driverId: number): void {
35 if (!driverId) {
36 this.error = 'No driver ID found';
37 return;
38 }
39
40 this.isLoading = true;
41 this.apiService.getAssignedShipment(driverId).subscribe({
42 next: (response: any) => {
43 this.data = response.data[0].attributes;
44
45 if (!this.data) {
46 this.error = 'No shipment found for the assigned driver.';
47 }
48 },
49 error: (err) => {
50 this.isLoading = false;
51 this.error = 'Error fetching shipment data.';
52 },
53 complete: () => {
54 this.isLoading = false;
55 },
56 });
57 }
58
59 handleStartShipment(): void {
60 console.log('Starting shipment update process...');
61 if (!this.data) {
62 this.error = 'No shipment selected';
63 return;
64 }
65
66 this.isLoading = true;
67 const driverId = this.userService.getDriverId();
68 if (!driverId) {
69 this.error = 'Driver ID not found';
70 this.isLoading = false;
71 return;
72 }
73
74 this.geolocationService.getCurrentLocation().subscribe({
75 next: ({ lat, lon }) => {
76 console.log('Location obtained:', { lat, lon });
77
78 this.updateShipmentLocation(driverId, lat, lon);
79 },
80 error: (err) => {
81 console.error('Error obtaining location:', err);
82 this.error = 'Failed to get location.';
83 this.isLoading = false;
84 },
85 });
86 }
87
88 private updateShipmentLocation(
89 driverId: number,
90 lat: number,
91 lon: number
92 ): void {
93 this.apiService
94 .updateShipmentStatus(driverId, 'in_transit', lat, lon)
95 .subscribe({
96 next: () => {
97 this.success = 'Shipment is now in transit.';
98 this.data.shipmentStatus = 'in_transit';
99 this.loadAssignedShipment(driverId);
100 },
101 error: () => {
102 this.error = 'Error updating shipment status.';
103 },
104 complete: () => {
105 this.isLoading = false;
106 },
107 });
108 }
109
110 handleCompleteShipment(): void {
111 if (!this.data) {
112 this.error = 'No shipment selected';
113 return;
114 }
115
116 this.isLoading = true;
117 const driverId = this.userService.getDriverId();
118 if (!driverId) {
119 this.error = 'Driver ID not found';
120 this.isLoading = false;
121 return;
122 }
123
124 this.apiService.updateShipmentStatus(driverId, 'completed').subscribe({
125 next: () => {
126 this.success = 'Shipment completed successfully.';
127 this.data.shipmentStatus = 'completed';
128 this.loadAssignedShipment(driverId);
129 },
130 error: () => {
131 this.error = 'Error completing shipment.';
132 },
133 complete: () => {
134 this.isLoading = false;
135 },
136 });
137 }
138
139 getStatusColor(status: string): string {
140 switch (status.toLowerCase()) {
141 case 'pending':
142 return 'bg-yellow-400';
143 case 'in_transit':
144 return 'bg-blue-400';
145 case 'completed':
146 return 'bg-green-400';
147 default:
148 return 'bg-gray-400';
149 }
150 }
151}
The logistics application's landing page is designed to have a search input box where we input the orderId of an order. If an order isn't found, we display an error message.
1// src/app/pages/landing-page/landing-page.component.ts
2import { FormBuilder, FormGroup, Validators } from '@angular/forms';
3
4import { Component, inject } from '@angular/core';
5import { Router } from '@angular/router';
6import { Search } from 'lucide-angular';
7import { ApiService } from '../../services/api.service';
8@Component({
9 selector: 'app-landing-page',
10 templateUrl: './landing-page.component.html',
11 styleUrl: './landing-page.component.scss',
12})
13export class LandingPageComponent {
14 trackForm: FormGroup;
15 error: string = '';
16 Search = Search;
17 isLoading: boolean = false;
18
19 private apiService = inject(ApiService);
20
21 constructor(private fb: FormBuilder, private router: Router) {
22 this.trackForm = this.fb.group({
23 orderId: ['', Validators.required],
24 });
25 }
26
27 handleSubmit(): void {
28 if (this.trackForm.invalid) {
29 return;
30 }
31
32 const orderId = this.trackForm.value.orderId;
33 this.isLoading = true;
34
35 this.apiService.trackOrder(orderId).subscribe({
36 next: (data) => {
37 this.isLoading = false;
38 if (data.data.length === 0) {
39 this.error = 'Order with ID ' + orderId + ' does not exist !';
40 return;
41 }
42 this.router.navigate([`/track`], { queryParams: { orderId } });
43 },
44 error: (err) => {
45 this.isLoading = false;
46 this.error = `Order with ID "${orderId}" was not found.`;
47 console.error(err);
48 },
49 });
50 }
51}
This component also reaches out to the apiService where we have a defined method known as trackOrder where we use one of our custom routes and controller defined in part 1 of this series.
Upon loading, the component first finds the order ID from the URL. If no ID is found, it redirects the driver back to the homepage, ensuring the app doesn’t attempt to load tracking information without the required identifier(our email).
Once we have the order ID, the component starts the tracking process using the startPolling
method, This method calls up our apiService
. Here’s where our trackOrder
method, a special setup from Part 1 of this series, comes into play. It reaches out to our strapi backend and fetches the most current order data tied to that orderId
. This trackOrder method keeps an eye on the shipment data. It’s all about keeping the map up-to-date so users always know where their shipment is.
We use a third-party library known as a library to show the shipment location to the user.
1// src/app/pages/track-order/track-order.component.ts
2
3import { Component, OnInit, OnDestroy } from '@angular/core';
4import { ActivatedRoute, Router } from '@angular/router';
5import { Subscription } from 'rxjs';
6import * as L from 'leaflet';
7import { ApiService } from '../../services/api.service';
8
9@Component({
10 selector: 'app-track-order',
11 templateUrl: './track-order.component.html',
12 styleUrl: './track-order.component.scss',
13})
14export class TrackOrderComponent implements OnInit, OnDestroy {
15 orderId: string | null = null;
16 orderData = null;
17 error: string = '';
18 isLoading: boolean = false;
19 private subscription: Subscription | null = null;
20 private map: L.Map | null = null;
21 private marker: L.Marker | null = null;
22
23 constructor(
24 private route: ActivatedRoute,
25 private router: Router,
26 private apiService: ApiService
27 ) {}
28
29 ngOnInit(): void {
30 this.orderId = this.route.snapshot.queryParamMap.get('orderId');
31 if (!this.orderId) {
32 this.router.navigate(['/']);
33 return;
34 }
35 this.startPolling();
36 }
37
38 startPolling(): void {
39 this.isLoading = true;
40 this.subscription = this.apiService.trackOrder(+this.orderId).subscribe({
41 next: (data) => {
42 this.isLoading = false;
43 if (data.error) {
44 this.error = data.error.message;
45 return;
46 }
47 this.orderData = data.data[0].attributes;
48 if (this.orderData.shipment) {
49 const { latitude, longitude } =
50 this.orderData.shipment.data.attributes;
51 this.initializeMap(latitude, longitude);
52 }
53 },
54 error: () => {
55 this.isLoading = false;
56 this.error = 'Tracking failed. Please try again.';
57 },
58 });
59 }
60
61 initializeMap(lat: number, lng: number): void {
62 if (this.map) {
63 this.map.setView([lat, lng], 18);
64 this.marker?.setLatLng([lat, lng]);
65 this.marker.setPopupContent('Shipment Location');
66 return;
67 }
68
69 setTimeout(() => {
70 this.map = L.map('map', {
71 center: [lat, lng],
72 zoom: 18,
73 zoomControl: true,
74 scrollWheelZoom: true,
75 });
76
77 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
78 maxZoom: 19,
79 attribution: '© OpenStreetMap contributors',
80 }).addTo(this.map);
81
82 this.marker = L.marker([lat, lng])
83 .addTo(this.map)
84 .bindPopup('Shipment Location')
85 .openPopup();
86
87 L.control.scale().addTo(this.map);
88 }, 1000);
89 }
90
91 ngOnDestroy(): void {
92 this.subscription?.unsubscribe();
93 this.map?.remove();
94 }
95}
In our app-module file, we have these codes:
1// src/app/app.module.ts
2
3import { CUSTOM_ELEMENTS_SCHEMA, NgModule, isDevMode } from '@angular/core';
4import { BrowserModule } from '@angular/platform-browser';
5import { AppRoutingModule } from './app-routing.module';
6import { AppComponent } from './app.component';
7import { DriverDashboardComponent } from './pages/driver-dashboard/driver-dashboard.component';
8import { DriverLoginComponent } from './pages/driver-login/driver-login.component';
9import { DriverRegisterComponent } from './pages/driver-register/driver-register.component';
10import { LandingPageComponent } from './pages/landing-page/landing-page.component';
11import { TrackOrderComponent } from './pages/track-order/track-order.component';
12import { DriverComponent } from './pages/driver/driver.component';
13import {
14 HTTP_INTERCEPTORS,
15 provideHttpClient,
16 withFetch,
17} from '@angular/common/http';
18import { AuthInterceptor } from './interceptors/auth.interceptor';
19import { ReactiveFormsModule } from '@angular/forms';
20import { LucideAngularModule } from 'lucide-angular';
21import { ServiceWorkerModule } from '@angular/service-worker';
22import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
23import { SpinnerComponent } from './spinner/spinner.component';
24
25@NgModule({
26 declarations: [
27 AppComponent,
28 DriverDashboardComponent,
29 DriverLoginComponent,
30 DriverRegisterComponent,
31 LandingPageComponent,
32 TrackOrderComponent,
33 DriverComponent,
34 SpinnerComponent,
35 ],
36 imports: [
37 BrowserModule,
38 AppRoutingModule,
39 ReactiveFormsModule,
40 LucideAngularModule,
41 BrowserAnimationsModule,
42 ServiceWorkerModule.register('ngsw-worker.js', {
43 enabled: !isDevMode(),
44 // Register the ServiceWorker as soon as the application is stable
45 // or after 30 seconds (whichever comes first).
46 registrationStrategy: 'registerWhenStable:30000',
47 }),
48 ],
49 schemas: [CUSTOM_ELEMENTS_SCHEMA],
50 providers: [
51 { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
52 provideHttpClient(withFetch()),
53 ],
54 bootstrap: [AppComponent],
55})
56export class AppModule {}
In our app-routing module file, we have the following code:
1// src/app/app-routing.module.ts
2
3import { NgModule } from '@angular/core';
4import { RouterModule, Routes } from '@angular/router';
5import { LandingPageComponent } from './pages/landing-page/landing-page.component';
6import { TrackOrderComponent } from './pages/track-order/track-order.component';
7import { DriverLoginComponent } from './pages/driver-login/driver-login.component';
8import { DriverRegisterComponent } from './pages/driver-register/driver-register.component';
9import { DriverDashboardComponent } from './pages/driver-dashboard/driver-dashboard.component';
10import { DriverComponent } from './pages/driver/driver.component';
11
12const routes: Routes = [
13 {
14 path: '',
15 component: LandingPageComponent,
16 pathMatch: 'full',
17 },
18 {
19 path: 'track',
20 component: TrackOrderComponent,
21 },
22 {
23 path: 'driver',
24 component: DriverComponent,
25 children: [
26 {
27 path: 'login',
28 component: DriverLoginComponent,
29 },
30 {
31 path: 'register',
32 component: DriverRegisterComponent,
33 },
34 {
35 path: 'dashboard',
36 component: DriverDashboardComponent,
37 },
38 ],
39 },
40 { path: '**', redirectTo: '', pathMatch: 'full' },
41];
42
43@NgModule({
44 imports: [RouterModule.forRoot(routes)],
45 exports: [RouterModule],
46})
47export class AppRoutingModule {}
The next thing on our list is to convert our application to a progressive web application.
To turn our logistics application into a PWA, Angular CLI provides the @angular/pwa` package. It's a package that adds all the necessary files and configurations to make our app PWA-ready. These include a manifest file, service worker configuration, and icons for different devices.
Let's start
Let's install the Angular PWA package, using the Angular CLI.
ng add @angular/pwa
After @angular/pwa
has been added, a new ngsw-config.json
file will be created at the root of the project. This file is responsible for configuring how Angular's service worker mechanism will handle caching assets. The file looks like this:
1// src/ngsw-config.json
2{
3 "$schema": "./node_modules/@angular/service-worker/config/schema.json",
4 "index": "/index.html",
5 "assetGroups": [
6 {
7 "name": "app",
8 "installMode": "prefetch",
9 "resources": {
10 "files": [
11 "/favicon.ico",
12 "/index.csr.html",
13 "/index.html",
14 "/manifest.webmanifest",
15 "/*.css",
16 "/*.js"
17 ]
18 }
19 },
20 {
21 "name": "assets",
22 "installMode": "lazy",
23 "updateMode": "prefetch",
24 "resources": {
25 "files": [
26 "/**/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
27 ]
28 }
29 }
30 ]
31}
Once you're done adding the PWA to the application(by angular's CLI), build the project using:
ng build
After building the project, we need to serve the application. A good option to use for our logistics application is http-server
to see our PWA in action.
Let's install http-server
npm install -g http-server
http-server -p 8080 -c-1 dist/logistics/browser --cors
This command starts an HTTP server on port 8080, serving files from the dist/logistics/browser
directory with caching disabled (-c-1) and Cross-Origin Resource Sharing (CORS) enabled.
The logistics
in the above command should be replaced with the build output name in your angular.json
file. Here's an example below:
1// src/angular.json
2{
3 "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
4 "version": 1,
5 "newProjectRoot": "projects",
6 "projects": {
7 "logistics": {
8 "projectType": "application",
9 "schematics": {
10 "@schematics/angular:component": {
11 "style": "scss",
12 "skipTests": true,
13 "standalone": false
14 },
15 "@schematics/angular:class": {
16 "skipTests": true
17 },
18 "@schematics/angular:directive": {
19 "skipTests": true,
20 "standalone": false
21 },
22 "@schematics/angular:guard": {
23 "skipTests": true
24 },
25 "@schematics/angular:interceptor": {
26 "skipTests": true
27 },
28 "@schematics/angular:pipe": {
29 "skipTests": true,
30 "standalone": false
31 },
32 "@schematics/angular:resolver": {
33 "skipTests": true
34 },
35 "@schematics/angular:service": {
36 "skipTests": true
37 }
38 },
39 "root": "",
40 "sourceRoot": "src",
41 "prefix": "app",
42 "architect": {
43 "build": {
44 "builder": "@angular-devkit/build-angular:application",
45 "options": {
46 //// Here
47 "outputPath": "dist/logistics",
Install the app once you navigate to the URL in the browser.
Now it functions like an installed application.
Open the app in a browser and check the “Add to Home Screen” prompt. You can also go offline and see how the app still functions, thanks to the service worker.
Manifest: Customize manifest.webmanifest
to include our app’s name, icons, theme color, and other properties.
Service Worker: Modify ngsw-config.json
to fine-tune caching strategies for different resources.
A network proxy is what service workers do. They can decide how to react to every HTTP request that the application sends out. For instance, if a cached response is available, they can deliver it after querying a local cache. Proxying includes resources mentioned in HTML and even the original request to index.html
, so it's not just restricted to queries done using programmatic APIs like get
. As a result, service worker-based caching is entirely programmable and independent of caching headers given by the server.
Data requests are not versioned along with the application. They're cached according to manually configured policies that are more useful for situations such as API requests and other data dependencies.
This field contains an array of data groups, each of which defines a set of data resources and the policy by which they are cached.
1// src/ngsw-config.json
2
3"dataGroups": [
4 {
5 "name": "strapi-api-calls",
6 "urls": ["http://localhost:1337"],
7 "cacheConfig": {
8 "strategy": "freshness",
9 "maxSize": 100,
10 "maxAge": "1d",
11 "timeout": "10s"
12 }
13 }
14 ]
Whenever the ServiceWorker handles a request, it checks data groups in the order in which they appear in ngsw-config.json
.
Data groups follow this Typescript interface:
1export interface DataGroup {
2
3 name: string;
4 urls: string[];
5 version?: number;
6 cacheConfig: {
7 maxSize: number;
8 maxAge: string;
9 timeout?: string;
10 refreshAhead?: string;
11 strategy?: 'freshness' | 'performance';
12 };
13 cacheQueryOptions?: {
14 ignoreSearch?: boolean;
15 };
16}
So with this, our application can work in offline mode.
Below is the demo of the project. This demo summarizes the application's core functionality. It will also help you to understand the overall application workflow, starting from setup to creating your own orders and shipments.
In this article series, we learned how to implement real-time tracking for order shipments. Additionally, we learned about Progressive Web Apps (PWAs) which is one of the core bases of modern web development, and demonstrated how to integrate them into our application. Thank you for reading, and I hope this tutorial has been helpful in your journey toward building better web applications.
Full stack Developer proficient in Node JS, Angular, Nest JS