软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

React微前端实战教程(PWA篇)

渐进式Web应用(PWA)的好处:添加到主屏幕、缓存加速启动、离线运行、渐进式更新、通知等功能,不支持PWA的浏览器可以像以前一样使用网页。PWA上线时需要开启HTTPS协议,本地调试可以是HTTP协议。

在构建React微前端之后,相关资源都是动态选择,造成PWA安装时不好确定缓存哪些内容。本文例程使用postMessage()的方式在主线程与Service Worker线程之间传递数据,让Service Worker线程取得微前端元信息。

给React微前端Demo网站添加PWA功能的操作步骤如下:

1. 添加 PWA manifest

在网站的根目录中添加文件rmf-pwa.webmanifest,内容如下:

{
  "short_name": "Micro Frontends",
  "name": "React Micro Frontends Demo",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

在网站配置文件site_config.yamlhtmlBegin添加:

<link rel="manifest" href="/rmf-pwa.webmanifest"/>

有了这份清单,Chrome浏览器就可以识别我们的网站为PWA,就能安装并添加到主屏幕或桌面。

2. 添加Service Worker程序

PWA的一个大特点是充分使用Service Worker来缓存资源文件。在网站的根目录,添加service-worker.js,其内容如下:

// service-worker.js
var cacheApps = {
  'app-home': true,
  'app-example': true
};
var cacheName = 'rmf-cache';
var rmfMetadataJSONP = {apps: [], extra: {}};

function isImageAddress(addr) {
  return (addr.pathname.endsWith('.png') || addr.pathname.endsWith('.svg')
    || addr.pathname.endsWith('.jpg') || addr.pathname.endsWith('.ico'));
}

function cacheAppEntries(metadata) {
  metadata = metadata || rmfMetadataJSONP;
  return caches.open(cacheName).then(function (cache) {
    var apps = metadata.apps.filter(function(app) {
      return cacheApps[app.id];
    });
    var entries = apps.reduce(function (acc, cur) {
      return acc.concat(cur.entries);
    }, []).filter(function(entry) {
      var url = new URL(entry, self.location.origin);
      return url.origin === self.location.origin;
    });

    console.log('[Service Worker] Caching app entries');
    return cache.addAll(entries).catch(function(e) {
      console.log("[Service Worker] Error in caching ", e)
    });
  });
}

self.addEventListener('install', function (e) {
  console.log('[Service Worker] Install');
  e.waitUntil(
    self.clients.matchAll({
      includeUncontrolled: true,
      type: 'all',
    }).then(function(clients) {
      clients.forEach(function(client) {
        client.postMessage({type: 'rmf-cache-require', payload: null});
      });
    })
  );
});

self.addEventListener('fetch', function (e) {
  // console.log(e.request.url);
  var addr = new URL(e.request.url);
  var isCacheType = addr.pathname.startsWith('/rmf-') || isImageAddress(addr);

  if (e.request.method != 'GET' || !isCacheType) {
    return;
  }

  e.respondWith(
    caches.match(e.request).then(function (r) {
      return r || fetch(e.request).then(function (response) {
        return caches.open(cacheName).then(function (cache) {
          console.log('[Service Worker] Caching new resource: ' + e.request.url);
          cache.put(e.request, response.clone());
          return response;
        });
      });
    })
  );
});

self.addEventListener('message', function (e) {
  if (e.data && e.data.type == 'rmf-cache-prefetch') {
    console.log('[Service Worker] Message "rmf-cache-prefetch" received');
    var metadata = Object.assign({apps: [], extra: {}}, e.data.payload);

    if (e.waitUntil) {
      e.waitUntil(cacheAppEntries(metadata));
    } else {
      cacheAppEntries(metadata);
    }
  }
});

以上代码设计要求:

  1. install事件处理时,Service Worker线程向主界面线程(Client)发送'rmf-cache-require'消息。主界面线程响应消息再向Service Worker线程发送'rmf-cache-prefetch'消息,并携带微前端元信息,最后Service Worker线程开始缓存资源文件。只处理同域名下的资源,Service Worker线程不允许跨域访问。
  2. fetch事件处理时,只缓存GET与/rmf-开头的资源。从缓存中查询不到时,再向后端请求。

首次安装时,触发install事件,fetch事件的监听是无效的。后续再打开网站或PWA时,向后端发送请求时才能触发fetch事件。

当前这个service-worker.jsinstall时并没有缓存所有的资源文件,原因有二:1、小水管Demo网站做了并发限制,并发连接过多就503错误,造成当前查看的页面图片请求失败;2、后端返回的微前端metadata中已去除了所有文件清单,只保留入口文件。真实提供PWA网站服务时,并发限制要设置比较大,微前端metadata中要返回所有文件清单。

3. 注册Service Worker

在HTML文档中,添加如下的代码:

// Register service worker to control making site work offline
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js').then(function () {
    console.log('Service Worker Registered');
    });

    navigator.serviceWorker.addEventListener('message', function (e) {
    if (e.data && e.data.type == 'rmf-cache-require') {
        console.log('Message "rmf-cache-require" received');
        e.source.postMessage({ type: 'rmf-cache-prefetch', payload: rmfMetadataJSONP });
    }
    });

    navigator.serviceWorker.startMessages();
}

其中的最后一行代码可以解决Service Worker发送'rmf-cache-require'未能及时响应的问题。

在Demo网站中,我们使用site_config.yaml文件定制HTML模板,把上面的代码搬到配置文件中:

htmlMiddle: >-
  </head><body><noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div><script>var rmfMetadataJSONP = {apps:[], extra: {}};
  function rmfMetadataCallback(data) { rmfMetadataJSONP = data }
  if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js').then(function() {
  console.log('Service Worker Registered'); });
  navigator.serviceWorker.addEventListener('message', function(e) {
  if (e.data && e.data.type == 'rmf-cache-require') {
  console.log('Message "rmf-cache-require" received');
  e.source.postMessage({type:'rmf-cache-prefetch', payload: rmfMetadataJSONP});
  }});
  navigator.serviceWorker.startMessages();
  }</script>  

4. Nginx配置

在Nginx配置文件中添加如下两个Location:

    location = /rmf-pwa.webmanifest {
        expires 7d;
        try_files $uri $uri/ =404;
    }

    location = /service-worker.js {
        expires 1d;
        try_files $uri $uri/ =404;
    }

5. 重启服务

sudo service nginx restart
sudo service react-micro-frontend-daemon restart

6. 结果验证

使用Chrome浏览Demo网站,可以在地址栏的后面看到一个加号(+),点击它就可以安装PWA到本地。然后,在Chrome的应用中与Windows桌面中都可以看得React Micro Frontends Demo快捷方式。

PWA在地址栏

PWA在Chrome应用

双击React Micro Frontends Demo,打开全屏的PWA程序。

PWA全屏显示