Angular PWA 入坑指北

1,916 阅读6分钟

hello,各位小伙伴大家好啊,这篇文章终于是在我拖的不能在拖的情况下,更了出来。在这里十分感谢那些不辞辛苦提醒我更新文章的小伙伴。愿你们在新的一年里,远离病毒,远离降薪,远离加班,升职加薪与你们同在,阿门~

今天我们来水一篇文章,至于为什么说水一篇文章呢,因为关于PWA入门的基础教程其实网上是有很多的,大厂的系统教程也是有的,这里我们推荐两个。

第一个是google的code lab。非常系统的讲解了从一个普通的应用如何变成PWA应用。步骤详细,容易上手。

第二个是Angular官方的PWA文档,Angular的好处就是官方CLI提供了工具,能后快速的帮你完成基础的配置,省力但是存在一些难度和问题,需要自己处理。

下面开始正片环节!

什么是PWA

PWA全称Progressive web app,中文名称渐进式Web应用。

至于PWA的特点,优势,前世今生这些细节,我给各位小伙伴找了一篇文章,来自王玉略同学的简单介绍一下Progressive Web App(PWA).文章写的很详细,为了保证各位小伙伴看到的都是不重复的资源信息,我就不重复这些了。

ps: @王玉略 同学记得来点赞~

为什么我选择PWA

如果为我们将PWA用在真实的项目上,就需要有我们技术选型的依据。为什么选择PWA?

先说一下项目需求,我们需要开发一款跨端的应用,并且该应用很大部分情况下是在离线使用。

应用的主要功能在于用户输入数据的展示,并且数据量不大。

开发周期三个月,包含需求确定,UI/UX,开发,部署。

需要开发的内容, client APP, BFF(Back-end For Front-end), Back-end.

开发人员, 两名。 估算之后的开发实际时间大约一个半月。

我需要开发client APP和BFF。

再加上我之前并没有React Native的经验,所以如果选择React Native我并不能保证在计划时间内完成开发任务,更不要说Flutter或者原生开发了。

其实我们认真分析项目可以发现核心需求只有一个,那就是离线使用。所以传统的网页并能满足,但是PWA的特点就是可以离线运行,所以PWA可以完美的满足核心需求。

开发环境

  • windows: 10
  • node: 10.16.3
  • npm: 6.9.0
  • Angular CLI: 9.0.6
  • Angular: 9.0.6
  • Chrome: 80.0.3987.163

初始化Angular APP

ok, 下面进入正题,首先却创建项目。

ng new ng-pwa-app --prefix=slb

这样一个完整的Angular项目 GET!

接下来我们借助Angular CLI的支持执行下面这行命令。

ng add @angular/pwa --project *project-name*

对应到我们的项目就是

ng add @angular/pwa --project ng-pwa-app

到这里,我们得到了一个PWA的项目。

好,本期教程到此结束~

错了,错了,我们继续...

我们首先来看下这条命令都背着我们做了什么事情。

  • 安装 @angular/service-worker
  • 启用service worker build
  • 在app module 注册service worker
  • 在index.html 引入manifesttheme-color
  • 增加icon
  • 创建 ngsw-config.json

接下来我们将项目编译一下

ng build --prod

编译之后我们会得到dist/ng-pwa-app/目录下的编译结果。

至于为什么我们不直接使用 ng serve来测试呢?

答案当然是 ng serve 不能够支持PWA

这时候我们需要一个server 来 host 我们的app,这里我们使用http-serve.

全局安装 http-serve npm i http-server -g

接下来我们就可以运行一下我们的项目了~

执行下面这行命令

http-server -p 8080 -c-1 dist/ng-pwa-app

查看localhost:8080

告诉我,你看到了下图~

到这里我们先不继续往下深入,先看一下Angular CLI背着我们具体做的东西。

manifest

{
  "name": "ng-pwa-app",
  "short_name": "ng-pwa-app",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "./",
  "start_url": "./",
  "icons": [
    {
      "src": "assets/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "assets/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
    {
      "purpose": "maskable"
    },
    {
      "src": "assets/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "maskable"
    },
      "src": "assets/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "assets/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "assets/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "assets/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "assets/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ]
}

上面的manifest 文件是CLI帮我们生成的

  • name: 项目名称
  • theme_color: 主题颜色
  • display: 运行方式
  • start_url: 加载目录或者说入口地址
  • icons: 图标数组,针对不同尺寸

ngsw-config.json

如果你之前接触过Angular的worker的话,就会知道Angular引入worker并不是我们创建一个worker.js,而是提供一个配置json,然后根据你的配置自动帮你生成worker js 文件。

下面就是这个配置文件的内容:

{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/manifest.webmanifest",
          "/*.css",
          "/*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

针对这个文件,我们讲两部分

assetGroups

关于assetGroups,它出现在了上面的json文件中,从上面的内容你可以发现,这部分是与应用一起加载的资源,同时也会一起更新。因为我们在开始就已经知道了这些具体的资源,所以我们可以将这些明确的资源配置在这里。

主要包括了应用的运行类资源,html,js,css等等资源,还有就是一些文件资源,svg,png,ttf等资源。

具体细节如下:

interface AssetGroup {
  name: string;
  installMode?: 'prefetch' | 'lazy';
  updateMode?: 'prefetch' | 'lazy';
  resources: {
    files?: string[];
    urls?: string[];
  };
}

dataGroups

关于dataGroups的配置我们在上面并没有体现,那是因为这部分的缓存的资源是随应用执行而产生和发生变化的。

比如接口返回的缓存。

具体细节如下:

interface DataGroup {
  name: string;
  urls: string[];
  version?: number;
  cacheConfig: {
    maxSize: number;
    maxAge: string;
    timeout?: string;
    strategy?: 'freshness' | 'performance';
  };
}

说明一点,其中的urls的内容是一个匹配策略。你可以将你希望缓存的所有地址写在这里。

运行lighthouse

打开chrome 的dev tool, 选择 Audits页。

点击 Generate report, 等待结果。

向下滑动页面,查看Progressive Web App相关的内容。如下:

从这里的结果上我们可以发现我们目前存在两个问题。

  • 我们没使用https,先忽略这个问题,真实的生产环境你一定需要部署到https的server上。
  • 缺少app-touch-icon

我们需要处理的就是第二项。

处理的方法其实很简单,就是需要在index.html页面添加

<link rel="apple-touch-icon" href="assets/icons/icon-144x144.png">

添加之后重新编译,和重启server.

重新运行Lighthouse.

重新执行之后我们发现,错误只剩下https的问题了。但是我们依然没有在地址栏的右边看到安装按钮。这是为什么?

既然这里Lighthouse不能发现问题,那么我们切换到Application页面看一下。

进入Application tab下,左侧选择Manifest.

我们发现了下面的警告!

上面的警告说我们的给的图片不合适?可是,这不是官方的图标吗???

网上搜索了一番,发现有人也遇到了这个问题,但是没有看到合适的回答。

一番折腾之后我发现了一个解决方案,去manifest里面修改144X144这个图片的perpose.

从默认的maskable 修改为 any.

然后重新编译,重启server,刷新页面。

点击安装:

变样子了对吗?

到这里,各位小伙伴现在请站起来给自己鼓鼓掌。你已经得到了一个可以安装的PWA应用的~

处理更新

上面我们得到了应用之后,用户就可以安装到桌面了,但是如果我们服务端更新了,但是用户这个没有同步怎么办?

Angular的SwUpdate给我们提供了这些功能。

SwUpdate 服务支持四个独立的操作:

  • 获取出现可用更新的通知。如果要刷新页面,这些就是可加载的新版本。

  • 获取更新被激活的通知。这时候 Service Worker 就可以立即使用这些新版本提供服务了。

  • 要求 Service Worker 向服务器查询是否有新版本。

  • 要求 Service Worker

我们在 app.component.ts里面创建两个方法。

监听更新:

private _listenOnUpdate() {
    this.updates.available.subscribe(event => {
      console.log('current version is', event.current);
      console.log('available version is', event.available);
    });
    this.updates.activated.subscribe(event => {
      console.log('old version was', event.previous);
      console.log('new version is', event.current);
    });
  }

如果发现可用更新或者已激活更新,我们就会看到上面的Log,具体发现更新之后的处理,你可以根据实际需求来决定。

比如你可以弹框提醒用户刷新页面。

触发更新:

private _checkForUpdate() {
    const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
    const everySixHours$ = interval(1000 * 60 * 60 * 6);
    const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);

    everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate());
  }

这里设置的是每6小时触发一个检测更新。

完整代码:

import { Component, ApplicationRef, ChangeDetectionStrategy } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { interval, concat } from 'rxjs';
import { first } from 'rxjs/operators';

@Component({
  selector: 'slb-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  constructor(private updates: SwUpdate, private appRef: ApplicationRef) {
    this._checkForUpdate();
    this._listenOnUpdate();
  }

  private _listenOnUpdate() {
    this.updates.available.subscribe(event => {
      console.log('current version is', event.current);
      console.log('available version is', event.available);
    });
    this.updates.activated.subscribe(event => {
      console.log('old version was', event.previous);
      console.log('new version is', event.current);
    });
  }

  private _checkForUpdate() {
    const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
    const everySixHours$ = interval(1000 * 60 * 60 * 6);
    const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);

    everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate());
  }
}

到这里的话,传统的教程应该已经结束了,但是我们没有,我们还有补充教程。如何手动处理worker事件。

通过上面的教程我们已经知道了,Angular会帮我们生成worker.js 文件,但是在我们实际的项目中,我们有可能需要自己监听一些事件,然后自定义一些处理。但是我们没有worker.js 文件,这时候怎么做??

动一下你聪明的小脑瓜,想一下?

其实很简单,那就是我们自己写一个worker.js替换默认的。

当然,并不是简单的全部替换。

先上代码:

mien-worker.js

'use strict';

self.onmessage = (e) => {
  console.log(e, 'message from mien\'s custom worker service...')
}
importScripts('./ngsw-worker.js');

在我们自定义的worker.js 内,我们自己注册事件。在最后要做的就是把Angular帮我们做的事情拿过来用。

importScripts('./ngsw-worker.js')

我们再到 app.module.ts去修改worker的注册。

从之前的:

ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),

修改为:

ServiceWorkerModule.register('mien-worker.js', { enabled: environment.production }),

再有把我们worker文件加入到资源列表,这样这个js 就不会被编译进main.js内了。

Angular.json

"assets": [
              "src/favicon.ico",
              "src/assets",
              "src/manifest.webmanifest",
              "src/mien-worker.js"
            ],

新增的内容是 src/mien-worker.js.

来看效果:

OK~

到这里,本期的更新 Angular PWA 入坑指北 就全部结束了。

在这里我公布一个计划,我计划开一个系列专栏。从头学习javascript。在现在所有人只关心三大框架,小程序,Flutter的时候,我觉得基础才是最重要的基石,良好的基础能让你在学习新知识的时候信手拈来。哪怕是看源代码的时候也能得心应手。

如果你感兴趣,请留言告诉我吧。

Mien, 5年前端开发经验,目前就职于斯伦贝谢

涉猎范围: Angular, Vue, React, 小程序, electron, nodeJS以及一些乱七八糟的技术

欢迎留言提问和指正。

我们下周再见~