PWA天气应用

https://codelabs.developers.google.com/codelabs/your-first-pwapp/#0

1.介绍

  这里将使用PWA技术来构建一个天气web应用,这个app将会:

  1. 使用以及验证PWA的特性
  2. 使用API获取最新的天气数据
  3. 添加城市时,可以提供类似原生应用的交互

我们将会学到

  1. 怎么使用app shell来设计一个应用
  2. 怎么使app离线工作
  3. 怎么储存数据用于离线工作

我们需要什么

  1. 最新版本的chrome。其实用其他浏览器也可以,只不过我们想用chrome的devTools来体验一些新版本浏览器的特性
  2. 自己做一个web服务器,或者用web server for chrome(ps:这是一个方便快捷的静态文件服务器,访问chrome://apps/或者书签最左边进入应用,进入web server,选择一个目录,启动服务器即可)
  3. 下载示例代码
  4. 一个文本编辑器
  5. 基础的web知识

2.开始

  下载解压以上的实例代码,然后打开静态文件服务器,以实例代码中的work为根目录,然后通过服务器访问里面的index.html(把chrome改为手机模式)。

  访问可以看到有个圆形进度条在转。work目录中仅仅是一个骨架,后续会添加剩余的功能

3.app shell

  app shell是html、css和js的最小集合,用于为用户提供WAP接口以及保证了良好的性能。它的第一次加载是非常快的而且马上进行缓存。任意时刻用户打开app,app都会从本地缓存中加载shell,这使app打开的速度非常快。

  shell的结构将数据从核心的公共结构和UI中分离开来,所有的公共结构和UI都使用service worker进行本地缓存,PWA仅仅请求必须的数据。可以理解为shell就是app的架子(包括UI以及公共的结构),而数据则显示在这个架子上,数据经常会发生变化,所以必要的数据需要都次都去请求获取。用另一种说法解释就是shell就是应用商店中的原生应用,运行的时候再请求数据来显示。

  service worker是一个浏览器运行在后台的脚本,用于提供各种特性

为什么要使用app shell结构

  这可以使我们专注于速度,提供原生应用的用户体验:瞬间完成加载、实时更新,而且不需要应用商店

设计app shell

首先是把核心组件从设计中拆分出来,需要明白:

  • 界面上什么需要马上显示?
  • 其他关键的UI组件是什么?
  • 什么资源是app shell所需的?如图片、脚本和样式等。

在这个天气app中,关键的组件如下:

  • 头部组件:标题、添加和刷新按钮
  • 天气预报版块的容器组件
  • 天气预报版块的模板
  • 一个用于添加城市的对话框
  • 用于显示loading的指示器

4.实现app shell

  有很多种方式可以初始化一个项目,我们推荐使用web starker kit,因为在这个例子我们希望尽可能的简单,所以已经提供好了必备的资源。

为shell创建html

  index.html已经在work目录中了,而且样式也已经写好了

检查关键的JS代码

  以上界面已经准备好了。在scripts/app.js中可以发现:

  • app对象包含了一些应用关键的信息
  • 四个监听器:头部组件的添加和刷新、添加城市的对话框的添加和取消
  • app.updateForcecastCard用于添加或更新天气预报
  • app.getForecast用于获取最新的天气预报信息
  • app.updateForecasts用于更新所有的天气预报信息
  • initialWeatherForecast用于mock数据,能快速测试界面

测试

  以上JS和界面都准备好了,解除以下两端代码的注释(分别在html和js文件底部位置):

<!--<script src="scripts/app.js" async></script>-->
// app.updateForecastCard(initialWeatherForecast);

  重新运行,即可看到天气预报效果

5.快速初始化

  PWA应该是快速启动而且马上可以使用,以上app可以快速打开,但是还不可用,因为没有数据,需要通过ajax来获取数据,但这额外的请求会导致初始的加载变慢,所以应该在app第一次加载的时候,服务端进行一次数据直出,来提高速度。

注入数据(数据直出)

  服务端直接把天气数据注入到JS中,但是在生产环境,注入的天气数据要基于用于的IP地址。这里假设initialWeatherForecast就是服务器已经注入的数据,我们直接拿来用

区分是否是第一次运行

  什么时候才需要展示缓存中可能已经过时的天气数据呢?

  对于用户所添加的城市,应该本地保存到一个存储系统中,为了尽可能简单,这里使用localStorage,这对于生产环境不是非常好,因为它是阻塞的,对于某些设备可能非常慢。

  首先,需要保存用户的选项,添加代码如下:

  // TODO add saveSelectedCities function here
  // Save list of cities to localStorage.
  app.saveSelectedCities = function() {
    var selectedCities = JSON.stringify(app.selectedCities);
    localStorage.selectedCities = selectedCities;
  };

  接着,添加初始化的代码,用来检查用户是否本地保存了一些城市(如果是则渲染出来),否则使用注入的数据:

// TODO add startup code here
  app.selectedCities = localStorage.selectedCities;
  if (app.selectedCities) {
    app.selectedCities = JSON.parse(app.selectedCities);
    app.selectedCities.forEach(function(city) {
      app.getForecast(city.key, city.label);
    });
  } else {
    /* The user is using the app for the first time, or the user has not
     * saved any cities, so show the user some fake data. A real app in this
     * scenario could guess the user's location via IP lookup and then inject
     * that data into the page.
     */
    app.updateForecastCard(initialWeatherForecast);
    app.selectedCities = [
      {key: initialWeatherForecast.key, label: initialWeatherForecast.label}
    ];
    app.saveSelectedCities();
  }

保存城市信息

  最后,需要修改添加城市butAddCity的监听器,来保存被选择的城市到localStorage中:

document.getElementById('butAddCity').addEventListener('click', function() {
    // Add the newly selected city
    var select = document.getElementById('selectCityToAdd');
    var selected = select.options[select.selectedIndex];
    var key = selected.value;
    var label = selected.textContent;
    if (!app.selectedCities) {
      app.selectedCities = [];
    }
    app.getForecast(key, label);
    app.selectedCities.push({key: key, label: label});
    app.saveSelectedCities();
    app.toggleAddDialog(false);
  });

6.使用service worker来预缓存app shell

  PWA应该支持离线工作,而且对于断续的,缓慢的网络环境,也可以正常工作。实现这一点需要通过service worker来缓存app shell和data

注册sw

  先进行判断,支持的话再进行sw的注册

  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
             .register('./service-worker.js')
             .then(function() { console.log('Service Worker Registered'); });
  }

缓存站点的资源

  当sw注册完成后的第一次访问页面,install事件就会被触发,在这个事件中对资源进行缓存,在sw.js内部执行如下代码:

var cacheName = 'weatherPWA-step-6-1';
var filesToCache = [];

self.addEventListener('install', function(e) {
  console.log('[ServiceWorker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('[ServiceWorker] Caching app shell');
      return cache.addAll(filesToCache);
    })
  );
});

   以上根据一个名字,打开一个cache,每个cache相当于一个缓存的集合,两两之间不会互相影响。addAll将一系列资源添加到cache中,这是一个原子操作。

  添加完以上代码后,刷新页面,在调试工具中可以看到当前域中有一个sw处于running状态(页面刷新前,这里是一片空白的):

  接着添加一个activate事件监听:

self.addEventListener('activate', function(e) {
  console.log('[ServiceWorker] Activate');
});

  再次刷新页面,以上的sw状态,变成

  这是因为旧的sw仍然控制着当前页面,新的sw无法生效,就处于wating状态了(添加的activate回调也没执行)。这里旧的sw是指最开始页面刷新后,处于running状态的sw,里面只监听了一个install事件。后来我们修改了sw的代码,添加了一个activate监听,这就属于一个新的sw了。

  为了使新的sw能够生效,即能够更新的sw。需要手动关闭页面,然后重新打开页面,或者点击上面的skipWaiting。但是对于调试环境下,为了更加方便,可以启用 update on reload 选项,这样每次刷新页面,sw都会被强制更新生效。启用这个选项后强制更新,控制台会报一个错误(这是可以忽略的):

  更新完成的第一件事情就是。将旧的sw的缓存,或者更新后用不到的缓存移除掉,需要被移除的cache的名字保存在cacheName中

self.addEventListener('activate', function(e) {
  console.log('[ServiceWorker] Activate');
  e.waitUntil(
    caches.keys().then(function(keyList) {
      return Promise.all(keyList.map(function(key) {
        if (key !== cacheName) {
          console.log('[ServiceWorker] Removing old cache', key);
          return caches.delete(key);
        }
      }));
    })
  );
  return self.clients.claim();
});

   claim函数处理一个边缘情况:(未完...)

其他

  只要sw缓存了数据,下次离线访问的时候,请求会被sw拦截,sw可以返回对应的数据,包括当前的页面html等。

原文地址:https://www.cnblogs.com/hellohello/p/8331304.html