Angular PWA 安装和配置

本篇文章是关于如何在Angular应用中开启PWA功能的完整介绍, 一步步带你从初始化项目到完整的PWA功能实现。

什么是PWA

渐进式 Web 应用程序(PWA) 是一种使用 Web 平台技术构建的应用程序,但它提供类似于平台特定应用程序的用户体验。

与网站一样,PWA 可以通过单一代码库在多个平台和设备上运行。与平台专用应用一样,它可以安装在设备上,可以在离线和后台运行,并且可以与设备以及其他已安装的应用集成。
本篇文章主要讲解如何在Angular项目中使用PWA.

在Angular项目中使用PWA

按照以下步骤在 Angular 中创建 PWA。

1.创建 Angular 应用程序
2.添加@angular/pwa包
3.@angular/pwa了解包中添加/修改的文件
4.在本地运行应用程序
5.将 PWA 部署到生产环境

创建一个 Angular 应用程序

1.首先我们使用Angular Cli来创建一个新的Angular项目

cmd 复制代码
ng new angular-pwa

项目初始化后通过快捷命令打开项目

cmd 复制代码
cd angular-pwa && code .

2.添加@angular/pwa包

git bash 复制代码
ng add @angular/pwa

上述快捷指令可以帮助我们添加PWA必备的文件和相关的配置。

3.了解 @angular/pwa 包添加/修改的文件

ngsw-config.json

PWA最核心的 JSON 格式的配置文件。这个文件主要负责 (serviceworker.js) 的生成。您不必为此编写代码,只需设置某个配置即可完成。配置特定的缓存策略缓存资源/资产。

index — 指定入口 HTML文件
assetGroups — 可以指定需要缓存的资产或资源,并指定缓存策略,是应为网络优先、缓存优先,还是两者的组合。
当然还有 dataGroups,默认配置文件没有帮我们生成,后续我们会详细介绍。

manifest.webmanifest

这个文件用来配置 PWA 应用打开时的显示效果。可以在这里设置启动画面图标、背景颜色、显示等选项。

app.config.ts

注册了ServiceWorker 服务,ServiceWorker也是实现PWA的核心。


注意代码中enabled: !isDevMode(),PWA是在生成环境下生效的。需要注意的是由于本项目使用新版的Angular Cli初始化,所以配置在app.config.ts文件。如果你使用的是旧版的Angular项目,默认配置在,App.Module.ts文件中。

angular.json

在production 的配置中添加了"serviceWorker": "ngsw-config.json"

接下来我们用一个完整的例子来看一下PWA的效果。

使用express起一个服务

新建一个server.js文件。

javascript 复制代码
import express from 'express';
import cors from 'cors';

// mock data
const categories = [
    { id: 1, name: 'skills' },
    { id: 2, name: 'life' },
    { id: 3, name: 'notes' },
    { id: 4, name: 'others' }
];

const posts = [
    {
        id: 1,
        title: 'Angular',
        content: 'Angular is a framework for building client applications...',
        categoryId: 1,
        createdAt: '2024-03-20'
    },
    {
        id: 2,
        title: 'React',
        content: 'React is a library for building user interfaces...',
        categoryId: 2,
        createdAt: '2024-03-19'
    },
    {
        id: 3,
        title: 'Vue',
        content: 'Vue is a progressive framework for building user interfaces...',
        categoryId: 3,
        createdAt: '2024-03-18'
    }
];
const app = express();
app.use(cors({
    origin: 'http://localhost:4200',
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
}));

app.get('/api/posts', (req, res) => {
    res.send(posts);
});

app.get("/api/categories", (req, res) => {
    res.send(categories);
});

app.listen(8080, () => {
    console.log('Server is running on port 8080');
});

我们创建了一个简单的接口服务.

修改前端页面

创建接口文件
typescript 复制代码
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { Category, Post } from "../models/blog";

@Injectable({
    providedIn: 'root'
})
export class DataService {
    private http = inject(HttpClient);
    private baseUrl = 'http://localhost:8080/api';

    getPosts() {
        return this.http.get<Post[]>(`${this.baseUrl}/posts`);
    }

    getCategories() {
        return this.http.get<Category[]>(`${this.baseUrl}/categories`);
    }
}
创建组件

首先我们在app目录下新建components文件夹。

git bash 复制代码
cd src/app/components
ng g component post

cli会帮我们创建post组件,修改post组件

typescript 复制代码
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from "@angular/core";
import { Category, Post } from "../../models/blog";

import { Subscription } from "rxjs";
import { CommonModule, isPlatformBrowser } from "@angular/common";
import { DataService } from "../../services/data.service";
@Component({
    selector: 'app-post',
    templateUrl: './post.component.html',
    styleUrls: ['./post.component.scss'],
    imports: [CommonModule],
    standalone: true
})
export class PostComponent implements OnInit, OnDestroy {
    posts: Post[] = [];
    categories: Category[] = [];
    subs = new Subscription();
    offline = true;

    constructor(private dataService: DataService,
        @Inject(PLATFORM_ID) private platformId: Object
    ) {
        if (isPlatformBrowser(platformId)) {
            this.offline = !navigator.onLine;
            window.addEventListener("online", () => {
                this.offline = false;
                this.loadData();
            })
            window.addEventListener("offline", () => {
                this.offline = true;
            })
        }
    }

    ngOnInit(): void {
        this.loadData();
    }

    loadData() {
        this.subs.add(this.dataService.getPosts().subscribe((posts) => {
            this.posts = posts;
        }));
        this.subs.add(this.dataService.getCategories().subscribe((categories) => {
            this.categories = categories;
        }));
    }

    ngOnDestroy() {
        this.subs.unsubscribe();
    }
}

由于组件代码过多,只展示了部分代码,如果想看完整代码请下载完整code

配置路由
typescript 复制代码
import { Routes } from '@angular/router';
import { PostComponent } from './components/post/post.component';

export const routes: Routes = [
    {
        path: '',
        component: PostComponent
    }
];
配置缓存接口

打开ngsw-config.json文件,新增配置

json 复制代码
  "dataGroups": [
    {
      "name": "api-freshness",
      "urls": ["/api/posts", "/api/categories"],
      "cacheConfig": {
        "strategy": "freshness",
        "maxSize": 100,
        "maxAge": "1h",
        "timeout": "10s"
      }
    }
  ]

以上配置我们指定了需要缓存的 API 路径(["/api/posts", "/api/categories"])。所有匹配这些路径的网络请求都会被 Service Worker 按照下面的 cacheConfig 策略处理。

"strategy": "freshness"采用了网络优先的策略。先尝试从网络获取数据,如果网络响应在 timeout 时间内返回,则使用网络数据并更新缓存。如果网络超时或失败,则使用缓存中的数据.

"maxSize": 100, 缓存中最多存储 100 条请求的响应。超过后会自动移除最旧的缓存项.

"maxAge": "1h", 缓存的最大有效期为 1 小时。超过 1 小时的数据会被视为过期,不再使用。
"timeout": "10s", 网络请求的超时时间为 10 秒。如果 10 秒内没有响应,就会回退到缓存。

基本的准备工作我们已经做完了,接下来就可以测试我们的PWA了。

启动项目

首先我们先启动我们的服务,运行已经编写好的启动脚本

git bash 复制代码
npm run pwa-server

我们的服务就在8080端口跑起来了,接着启动我们的angular项目

git bash 复制代码
ng serve
// npm run start

然后我们打开浏览器控制台,关掉网络,刷新页面

发现PWA缓存并没有生效,再次印证了在开发模式下是不生效的。

接下来我们关掉我们的前端进程,在生产模式下测试。

git bash 复制代码
npm run pwa


我们可以看到我们的两个接口都被service worker 拦截和请求了。我们再次将网络置为offline并刷新页面。

与之前不同的是我们的页面仍然可以正常显示, 尽管接口的请求都是失败的,但是还有缓存的数据兜底。

此外我们的浏览器多出了一个安装PWA应用的按钮,点击安装就可以将我们的网页应用安装到本地像app那样打开使用。至此关于Angular PWA应用的介绍就告一段落了。