威尼斯888_www.wns888.com_威尼斯wns888官网
做最好的网站
威尼斯888 > 计算机网络 / Web前端 > PWA 不是单纯的某项技术

原标题:PWA 不是单纯的某项技术

浏览次数:74 时间:2019-12-08

React 同构应用 PWA 进级指南

2018/05/25 · JavaScript · PWA, React

初稿出处: 林东洲   

前言

新近在给自己的博客网站 PWA 进级,顺便就记下下 React 同构应用在运用 PWA 时蒙受的标题,这里不会从头起初介绍如何是 PWA,若是你想深造 PWA 相关知识,能够看下下边笔者收藏的部分稿子:

  • 您的首先个 Progressive Web App
  • 【ServiceWorker】生命周期那一个事情
  • 【PWA学习与推行】(1卡塔尔国2018,早先你的PWA学习之旅
  • Progressive Web Apps (PWA) 中文版

PWA 特性

PWA 不是意气风发味的某项本领,而是一群本事的聚合,比如:ServiceWorker,manifest 加多到桌面,push、notification api 等。

而就在近来时间,IOS 11.3 刚刚支持 Service worker 和好像 manifest 增加到桌面包车型大巴特性,所以此番 PWA 改动主要依然落到实处这两局地机能,至于别的的风味,等 iphone 援助了再升格吗。

Service Worker

service worker 在小编眼里,相仿于三个跑在浏览器后台的线程,页面第一遍加载的时候会加载这几个线程,在线程激活之后,通过对 fetch 事件,能够对每个拿到的能源拓宽支配缓存等。

综上所述什么财富供给被缓存?

那就是说在初阶选取 service worker 早前,首先必要掌握怎可以源要求被缓存?

缓存静态财富

第一是像 CSS、JS 那些静态财富,因为小编的博客里引用的剧本样式都以经过 hash 做长久化缓存,雷同于:main.ac62dexx.js 那样,然后张开强缓存,那样下一次客户后一次再拜会笔者的网址的时候就不要再行诉求能源。直接从浏览器缓存中读取。对于那大器晚成部分能源,service worker 没供给再去管理,直接放行让它去读取浏览器缓存就能够。

笔者感觉若是您的站点加载静态能源的时候小编未有开启强缓存,並且你只想通过前端去达成缓存,而没有供给后端在参加举行调节,那能够采用service worker 来缓存静态财富,不然就有一些多此一举了。

缓存页面

缓存页面显明是必得的,那是最宗旨的片段,当你在离线的状态下加载页面会之后现身:

图片 1

究其原因正是因为您在离线状态下不能够加载页面,现在有了 service worker,纵然你在没互联网的图景下,也得以加载从前缓存好的页面了。

缓存后端接口数据

缓存接口数据是急需的,但亦非必需经过 service worker 来促成,前端寄放数据的地点有过多,比方通过 localstorage,indexeddb 来开展仓库储存。这里自身也是经过 service worker 来落到实处缓存接口数据的,若是想通过其它措施来达成,只需求静心好 url 路径与数据对应的映照关系就可以。

缓存战术

分明了何等财富要求被缓存后,接下去将要讨论缓存战术了。

页面缓存战术

因为是 React 单页同构应用,每一趟加载页面包车型大巴时候数据都是动态的,所以小编动用的是:

  1. 互连网优先的办法,即优先拿到网络上风行的财富。当互联网央求失利的时候,再去赢得 service worker 里以前缓存的能源
  2. 当网络加载成功之后,就立异 cache 中对应的缓存财富,保险下一次每一次加载页面,都以上次探问的流行财富
  3. 风华正茂经找不到 service worker 中 url 对应的能源的时候,则去赢得 service worker 对应的 /index.html 默许首页

// sw.js self.addEventListener('fetch', (e卡塔尔(英语:State of Qatar) => { console.log('以后正在号令:' + e.request.url卡塔尔(英语:State of Qatar); const currentUrl = e.request.url; // 相称上页面路线 if (matchHtml(currentUrl卡塔尔(قطر‎卡塔尔(英语:State of Qatar) { const requestToCache = e.request.clone(卡塔尔; e.respondWith( // 加载互连网上的资源fetch(requestToCache卡塔尔国.then((response卡塔尔(قطر‎ => { // 加载失利 if (!response || response.status !== 200卡塔尔国 { throw Error('response error'卡塔尔; } // 加载成功,更新缓存 const responseToCache = response.clone(卡塔尔; caches.open(cacheName卡塔尔(قطر‎.then((cache卡塔尔(英语:State of Qatar) => { cache.put(requestToCache, responseToCache卡塔尔国; }卡塔尔(英语:State of Qatar); console.log(response卡塔尔(英语:State of Qatar); return response; }卡塔尔(英语:State of Qatar).catch(function(卡塔尔(قطر‎ { // 获取对应缓存中的数据,获取不到则失利到收获私下认可首页 return caches.match(e.request卡塔尔国.then((response卡塔尔国 => { return response || caches.match('/index.html'卡塔尔(قطر‎; }卡塔尔国; }卡塔尔(قطر‎ 卡塔尔; } }卡塔尔(قطر‎;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// sw.js
self.addEventListener('fetch', (e) => {
  console.log('现在正在请求:' + e.request.url);
  const currentUrl = e.request.url;
  // 匹配上页面路径
  if (matchHtml(currentUrl)) {
    const requestToCache = e.request.clone();
    e.respondWith(
      // 加载网络上的资源
      fetch(requestToCache).then((response) => {
        // 加载失败
        if (!response || response.status !== 200) {
          throw Error('response error');
        }
        // 加载成功,更新缓存
        const responseToCache = response.clone();
        caches.open(cacheName).then((cache) => {
          cache.put(requestToCache, responseToCache);
        });
        console.log(response);
        return response;
      }).catch(function() {
        // 获取对应缓存中的数据,获取不到则退化到获取默认首页
        return caches.match(e.request).then((response) => {
           return response || caches.match('/index.html');
        });
      })
    );
  }
});

何以存在命中持续缓存页面包车型大巴意况?

  1. 先是供给鲜明的是,顾客在首先次加载你的站点的时候,加载页面后才会去运转sw,所以首先次加载不可能因此 fetch 事件去缓存页面
  2. 本人的博客是单页应用,可是客商并不一定会经过首页步向,有希望会透过其余页面路径步入到自家的网址,那就以致笔者在 install 事件中一贯不能够钦赐须要缓存那八个页面
  3. 末尾兑现的功能是:顾客率先次张开页面,登时断掉互联网,照旧得以离线访谈笔者的站点

整合方面三点,作者的法子是:第二次加载的时候会缓存 /index.html 那一个财富,并且缓存页面上的多少,借使顾客马上离线加载的话,这个时候并不曾缓存对应的门径,举个例子 /archives 能源访谈不到,那重回 /index.html 走异步加载页面包车型客车逻辑。

在 install 事件缓存 /index.html,保障了 service worker 第三回加载的时候缓存暗中同意页面,留下退路。

import constants from './constants'; const cacheName = constants.cacheName; const apiCacheName = constants.apiCacheName; const cacheFileList = ['/index.html']; self.addEventListener('install', (e) => { console.log('Service Worker 状态: install'); const cacheOpenPromise = caches.open(cacheName).then((cache) => { return cache.addAll(cacheFileList); }); e.waitUntil(cacheOpenPromise); });

1
2
3
4
5
6
7
8
9
10
11
12
import constants from './constants';
const cacheName = constants.cacheName;
const apiCacheName = constants.apiCacheName;
const cacheFileList = ['/index.html'];
 
self.addEventListener('install', (e) => {
  console.log('Service Worker 状态: install');
  const cacheOpenPromise = caches.open(cacheName).then((cache) => {
    return cache.addAll(cacheFileList);
  });
  e.waitUntil(cacheOpenPromise);
});

在页面加载完后,在 React 组件中登时缓存数据:

// cache.js import constants from '../constants'; const apiCacheName = constants.apiCacheName; export const saveAPIData = (url, data卡塔尔 => { if ('caches' in window卡塔尔(英语:State of Qatar) { // 伪造 request/response 数据 caches.open(apiCacheName卡塔尔国.then((cache卡塔尔(英语:State of Qatar) => { cache.put(url, new Response(JSON.stringify(data卡塔尔(قطر‎, { status: 200 }卡塔尔(قطر‎卡塔尔(英语:State of Qatar); }卡塔尔; } }; // React 组件 import constants from '../constants'; export default class extends PureComponent { componentDidMount(卡塔尔国 { const { state, data } = this.props; // 异步加载数据 if (state === constants.INITIAL_STATE || state === constants.FAILURE_STATE卡塔尔 { this.props.fetchData(卡塔尔; } else { // 服务端渲染成功,保存页面数据 saveAPIData(url, data卡塔尔国; } } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// cache.js
import constants from '../constants';
const apiCacheName = constants.apiCacheName;
 
export const saveAPIData = (url, data) => {
  if ('caches' in window) {
    // 伪造 request/response 数据
    caches.open(apiCacheName).then((cache) => {
      cache.put(url, new Response(JSON.stringify(data), { status: 200 }));
    });
  }
};
 
// React 组件
import constants from '../constants';
export default class extends PureComponent {
  componentDidMount() {
    const { state, data } = this.props;
    // 异步加载数据
    if (state === constants.INITIAL_STATE || state === constants.FAILURE_STATE) {
      this.props.fetchData();
    } else {
        // 服务端渲染成功,保存页面数据
      saveAPIData(url, data);
    }
  }
}

那样就保证了客商率先次加载页面,马上离线访问站点后,就算不能够像第一次同样能够服务端渲染数据,但是随后能透过得到页面,异步加载数据的不二等秘书技创设离线应用。

图片 2

客户率先次访谈站点,要是在不刷新页面包车型地铁情形切换路由到别的页面,则会异步获取到的数码,当后一次访谈对应的路由的时候,则失败到异步获取数据。

图片 3

当客户第三遍加载页面包车型大巴时候,因为 service worker 已经调整了站点,已经持有了缓存页面包车型客车力量,之后在做客的页面都将会被缓存或然更新缓存,当客户离线访谈的的时候,也能访谈到服务端渲染的页面了。

图片 4

接口缓存攻略

谈完页面缓存,再来说讲接口缓存,接口缓存就跟页面缓存超级帅似了,唯风度翩翩的两样在于:页面第一遍加载的时候不自然有缓存,但是会有接口缓存的留存(因为杜撰了 cache 中的数据卡塔尔(英语:State of Qatar),所以缓存攻略跟页面缓存肖似:

  1. 互联网优先的法子,即优先获得互联网上接口数据。当网络央求战败的时候,再去得到service worker 里早前缓存的接口数据
  2. 当互连网加载成功未来,就更新 cache 中对应的缓存接口数据,保险后一次每趟加载页面,都以上次拜谒的最新接口数据

故此代码就像是那样(代码相似,不再赘述卡塔尔(英语:State of Qatar):

self.add伊夫ntListener('fetch', (e卡塔尔(قطر‎ => { console.log('今后正在号召:'

  • e.request.url); const currentUrl = e.request.url; if (matchHtml(currentUrl)) { // ... } else if (matchApi(currentUrl)) { const requestToCache = e.request.clone(); e.respondWith( fetch(requestToCache).then((response) => { if (!response || response.status !== 200) { return response; } const responseToCache = response.clone(); caches.open(apiCacheName).then((cache) => { cache.put(requestToCache, responseToCache); }); return response; }).catch(function() { return caches.match(e.request); }) ); } });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
self.addEventListener('fetch', (e) => {
  console.log('现在正在请求:' + e.request.url);
  const currentUrl = e.request.url;
  if (matchHtml(currentUrl)) {
    // ...
  } else if (matchApi(currentUrl)) {
    const requestToCache = e.request.clone();
    e.respondWith(
      fetch(requestToCache).then((response) => {
        if (!response || response.status !== 200) {
          return response;
        }
        const responseToCache = response.clone();
        caches.open(apiCacheName).then((cache) => {
          cache.put(requestToCache, responseToCache);
        });
        return response;
      }).catch(function() {
        return caches.match(e.request);
      })
    );
  }
});

此地其实能够再扩充优化的,例如在获取数据接口的时候,能够先读取缓存中的接口数据开展渲染,当真正的网络接口数据再次来到之后再打开沟通,这样也能立竿见影收缩客户的首屏渲染时间。当然那只怕会生出页面闪烁的功能,能够增添一些动漫来实行连接。

别的难题

到近期甘休,已经基本上能够兑现 service worker 离线缓存应用的成效了,但是还只怕有依然存在部分主题素材:

高速度与刺激活 service worker

暗中同意情状下,页面包车型地铁乞请(fetch)不会透过 sw,除非它自身是由此 sw 获取的,也正是说,在安装 sw 之后,必要刷新页面技艺有机能。sw 在设置成功并激活在此以前,不会响应 fetch或push等事件。

因为站点是单页面应用,那就招致了您在切换路由(未有刷新页面)的时候未有缓存接口数据,因为那时service worker 还平素不从头专门的学问,所以在加载 service worker 的时候供给迅速地激活它。代码如下:

self.addEventListener('activate', (e) => { console.log('Service Worker 状态: activate'); const cachePromise = caches.keys().then((keys) => { return Promise.all(keys.map((key) => { if (key !== cacheName && key !== apiCacheName卡塔尔(قطر‎ { return caches.delete(key卡塔尔(قطر‎; } return null; }卡塔尔(قطر‎卡塔尔(قطر‎; }卡塔尔(英语:State of Qatar); e.waitUntil(cachePromise卡塔尔(英语:State of Qatar); // 急忙度与刺激活 sw,使其能够响应 fetch 事件 return self.clients.claim(); }卡塔尔(英语:State of Qatar);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.addEventListener('activate', (e) => {
  console.log('Service Worker 状态: activate');
  const cachePromise = caches.keys().then((keys) => {
    return Promise.all(keys.map((key) => {
      if (key !== cacheName && key !== apiCacheName) {
        return caches.delete(key);
      }
      return null;
    }));
  });
  e.waitUntil(cachePromise);
  // 快速激活 sw,使其能够响应 fetch 事件
  return self.clients.claim();
});

一些小说说还索要在 install 事件中加多 self.skipWaiting(); 来跳过等待时间,可是笔者在施行中开采便是不增加也能够健康激活 service worker,原因未知,有读者通晓的话能够沟通下。

后天当你首先次加载页面,跳转路由,马上离线访谈的页面,也得以顺遂地加载页面了。

无须强缓存 sw.js

客户每一次访谈页面包车型客车时候都会去重新得到sw.js,根据文件内容跟此前的本子是或不是相仿来判定 service worker 是不是有匡正。所以若是您对 sw.js 开启强缓存的话,就将深陷死循环,因为老是页面取获得的 sw.js 都是生机勃勃律,那样就无法升迁你的 service worker。

除此以外对 sw.js 开启强缓存也是还未供给的:

  1. 本人 sw.js 文件本人就超级小,浪费不了多少带宽,感觉浪费可以利用左券缓存,但附加扩展开销肩负
  2. sw.js 是在页面空闲的时候才去加载的,并不会影响客户首屏渲染速度

制止改换 sw 的 U索罗德L

在 sw 中如此做是“最差实行”,要在原地点上改善 sw。

举个例证来验证为啥:

  1. index.html 注册了 sw-v1.js 作为 sw
  2. sw-v1.js 对 index.html 做了缓存,也便是缓存优先(offline-first)
  3. 你更新了 index.html 重新注册了在新鸿基土地资金财产方的 sw sw-v2.js

生龙活虎经您像上边那么做,客商恒久也拿不到 sw-v2.js,因为 index.html 在 sw-v1.js 缓存中,那样的话,假若您想翻新为 sw-v2.js,还亟需转移原本的 sw-v1.js。

测试

后来,大家曾经完成了利用 service worker 对页面进行离线缓存的作用,要是想体会效果的话,访谈小编的博客:

随便浏览自便的页面,然后关掉互联网,再一次做客,从前您浏览过的页面都可以在离线的事态下开展访谈了。

IOS 供给 11.3 的版本才支撑,使用 Safari 进行拜望,Android 请选择支持service worker 的浏览器

manifest 桌面应用

眼下说完了何等利用 service worker 来离线缓存你的同构应用,不过 PWA 不唯有限于此,你还能运用安装 manifest 文件来将您的站点加多到活动端的桌面上,进而达到趋近于原生应用的体验。

使用 webpack-pwa-manifest 插件

自己的博客站点是由此 webpack 来构建前端代码的,所以自个儿在社区里找到 webpack-pwa-manifest 插件用来生成 manifest.json。

首先安装好 webpack-pwa-manifest 插件,然后在您的 webpack 配置文件中加上:

// webpack.config.prod.js const WebpackPwaManifest = require('webpack-pwa-manifest'); module.exports = webpackMerge(baseConfig, { plugins: [ new WebpackPwaManifest({ name: 'Lindz's Blog', short_name: 'Blog', description: 'An isomorphic progressive web blog built by React & Node', background_color: '#333', theme_color: '#333', filename: 'manifest.[hash:8].json', publicPath: '/', icons: [ { src: path.resolve(constants.publicPath, 'icon.png'), sizes: [96, 128, 192, 256, 384, 512], // multiple sizes destination: path.join('icons') } ], ios: { 'apple-mobile-web-app-title': 'Lindz's Blog', 'apple-mobile-web-app-status-bar-style': '#000', 'apple-mobile-web-app-capable': 'yes', 'apple-touch-icon': '//xxx.com/icon.png', }, }) ] })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.config.prod.js
const WebpackPwaManifest = require('webpack-pwa-manifest');
module.exports = webpackMerge(baseConfig, {
  plugins: [
    new WebpackPwaManifest({
      name: 'Lindz's Blog',
      short_name: 'Blog',
      description: 'An isomorphic progressive web blog built by React & Node',
      background_color: '#333',
      theme_color: '#333',
      filename: 'manifest.[hash:8].json',
      publicPath: '/',
      icons: [
        {
          src: path.resolve(constants.publicPath, 'icon.png'),
          sizes: [96, 128, 192, 256, 384, 512], // multiple sizes
          destination: path.join('icons')
        }
      ],
      ios: {
        'apple-mobile-web-app-title': 'Lindz's Blog',
        'apple-mobile-web-app-status-bar-style': '#000',
        'apple-mobile-web-app-capable': 'yes',
        'apple-touch-icon': '//xxx.com/icon.png',
      },
    })
  ]
})

回顾地阐释下安顿消息:

  1. name: 应用名称,正是Logo下边的彰显名称
  2. short_name: 应用名称,但 name 不可能显示完全时候则显示这么些
  3. background_color、theme_color:看名称就会想到其意义,相应的颜料
  4. publicPath: 设置 cdn 路径,跟 webpack 里的 publicPath 一样
  5. icons: 设置Logo,插件会活动帮您转移区别 size 的图纸,不过图片大小必需超越最大 sizes
  6. ios: 设置在 safari 中哪些去增添桌面应用

设置完以往,webpack 会在创设进程中变化对应的 manifest 文件,并在 html 文件中援引,上面正是生成 manifest 文件:

{ "icons": [ { "src": "/icons/icon_512x512.79ddc5874efb8b481d9a3d06133b6213.png", "sizes": "512x512", "type": "image/png" }, { "src": "/icons/icon_384x384.09826bd1a5d143e05062571f0e0e86e7.png", "sizes": "384x384", "type": "image/png" }, { "src": "/icons/icon_256x256.d641a3644ce20c06855db39cfb2f7b40.png", "sizes": "256x256", "type": "image/png" }, { "src": "/icons/icon_192x192.8f11e077242cccd9c42c0cbbecd5149c.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon_128x128.cc0714ab18fa6ee6de42ef3d5ca8fd09.png", "sizes": "128x128", "type": "image/png" }, { "src": "/icons/icon_96x96.dbfccb1a5cef8093a77c079f761b2d63.png", "sizes": "96x96", "type": "image/png" } ], "name": "Lindz's Blog", "short_name": "Blog", "orientation": "portrait", "display": "standalone", "start_url": ".", "description": "An isomorphic progressive web blog built by React & Node", "background_color": "#333", "theme_color": "#333" }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
  "icons": [
    {
      "src": "/icons/icon_512x512.79ddc5874efb8b481d9a3d06133b6213.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_384x384.09826bd1a5d143e05062571f0e0e86e7.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_256x256.d641a3644ce20c06855db39cfb2f7b40.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_192x192.8f11e077242cccd9c42c0cbbecd5149c.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_128x128.cc0714ab18fa6ee6de42ef3d5ca8fd09.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_96x96.dbfccb1a5cef8093a77c079f761b2d63.png",
      "sizes": "96x96",
      "type": "image/png"
    }
  ],
  "name": "Lindz's Blog",
  "short_name": "Blog",
  "orientation": "portrait",
  "display": "standalone",
  "start_url": ".",
  "description": "An isomorphic progressive web blog built by React & Node",
  "background_color": "#333",
  "theme_color": "#333"
}

html 中会援引这些文件,并且增进对 ios 增加桌面应用的支撑,就好像这么。

<!DOCTYPE html> <html lang=en> <head> <meta name=apple-mobile-web-app-title content="Lindz's Blog"> <meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-status-bar-style content=#838a88> <link rel=apple-touch-icon href=xxxxx> <link rel=manifest href=/manifest.21d63735.json> </head> </html>

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang=en>
<head>
  <meta name=apple-mobile-web-app-title content="Lindz's Blog">
  <meta name=apple-mobile-web-app-capable content=yes>
  <meta name=apple-mobile-web-app-status-bar-style content=#838a88>
  <link rel=apple-touch-icon href=xxxxx>
  <link rel=manifest href=/manifest.21d63735.json>
</head>
</html>

就如此轻易,你就足以选取 webpack 来增多你的桌面应用了。

测试

增添完之后您能够通过 chrome 开采者工具 Application – Manifest 来查看你的 mainfest 文件是或不是见到成效:

图片 5

这么表达您的结构生效了,安卓机遇自动识别你的计划文件,并问询客户是还是不是丰硕。

结尾

讲到那大约就完了,等未来 IOS 援助 PWA 的此外功效的时候,届期候笔者也会相应地去实践别的 PWA 的性状的。今后 IOS 11.3 也只是协理 PWA 中的 service worker 和 app manifest 的成效,但是相信在不久的今后,其余的效劳也会相应获得扶植,届期候相信 PWA 将会在活动端绽开异彩的。

1 赞 收藏 评论

图片 6

本文由威尼斯888发布于计算机网络 / Web前端,转载请注明出处:PWA 不是单纯的某项技术

关键词:

上一篇:前端优化带来的思考,浅谈前端工程化

下一篇:本文作者