PWA 实践经验 建立

发布于 2022-02-28 14:09:43 字数 5889 浏览 1193 评论 0

0x00 开源选型

由于项目是基于 webpack 打包,打包之后的项目本地资源路径和线上路径有一个映射关系,如 /img/logo.gif 映射成 //cdn/img/logo.abcd.gif
所以我需要一个 webpack 插件去抹平这一部分差异。
当时比较了 sw-precache-webpack-plugin(后来进化成 workbox)和 offline-plugin ,觉得前者功能太简单,后者可自定义化程度高,所以选了后者

0x01 实践与尝试

大概过了一遍文档后,就开始撸码了,实践的过程很难在这篇幅里面展开,而且涉及到一些工作业务内容不便透露。只能说,实践最好从简单的示例、简单的配置开始入手,再一步步演化成复杂的配置。目前我 offline-plugin 的配置如下

{
  caches: {
      main: [],
      additional:[
          ':rest:'
      ]
  },
  rewrites:function(asset){
      if(/tpl\.htm$/.test(asset)){
          return null;
      };
      if(/inlineImg|manifest/.test(asset)){
          return null;
      }
      if(/sw.js$/.test(asset)){
          return null;
      }
      return  asset.replace(/_tmp\//,'');
  },
  ServiceWorker:{
      events:true,
      output: path.join('pages/',ver_file_name, pageChannel, '/sw.js'),
      publicPath:'/sw.js',
      cacheName:`xxxxxx`,//省略
      entry:__dirname+'/src/global/js/utils/sw.js',
      prefetchRequest:{
          credentials: "include",
          mode: "no-cors" //跨域请求
      },
      // minify:false
  },
  AppCache:false,
}

但其实一开始的时候,rewrites,ServiceWorker,AppCache 的配置都不是必要的,caches 也可以先写一条明确的资源,先验证了自己的想法。
实践中由于本地和线上url域名差别(主要是cdn等问题)每一次修改都必须要打包上到测试环境(测试环境域名与生产相同)才能验证,这就要求自己在改动的时候尽量明确,精确。不过有时候由于文档理解的差异就只能自己坑了才知道。

0x02 留有后路

一开始实践 PWA 的时候,公司叫上了 google 的人开了个远程会议,g 说 PWA 在国内实践得比较好的就是饿了么。确实是这样,比如 这篇 文章 ,就说到,要有一个开关,当PWA有什么问题的时候,能远程关掉,于是我仿效也建了个开头,当然这个比较坑的就是,它的逻辑是先把 PWA 建立起来,然后再让它自杀。

0x03 合成 sw.js

背景

在实践过程中有很多细节要处理啊,比如本地打包同时用了gulp 和 webpack,资源映射路径要有点调整,比如项目前后端分离不彻底,要相应处理。其中印象最深刻的是测试环境cookie问题和合成sw.js问题,在此说一下后者。
由于项目历史遗留问题,我的项目实际上是多个webpack项目合成在一起的,也就是当你执行npm run build-all的时候,实质上是执行了node,node再调用了多个webpack进程,多个webpack再依次打包对应文件夹下面的文件(下称“频道”)。先不讨论合理性问题,在这个场景下,要部署sw进程,可以有怎么样的方案?我想过有三个方案。

  1. 只让其中最重要的频道上 sw
  2. 让 sw.js 分不同 scope,各管各
  3. 把不同频道的内容合成在一起

综合考虑之后,我决定要用第三套方案

可行性

我们来看一下offline-plugin 生成的sw.js的结构

"use strict";
var __wpo = {
    //yyyyyyy
};
!function(e) {
    //xxxxxxxxx
}

可以观察出这里由两部分组成,一部分是__wpo表示配置数据,另外一部分是sw的安装更新逻辑,查看源代码也可以确认这事
https://github.com/NekR/offline-plugin/blob/a2eee00260840e8ea3dc14e16d40b3151ce30b9c/lib/service-worker.js#L229
另外源代码中也保留了私有标志进行测试用例的编写
https://github.com/NekR/offline-plugin/blob/05cbe95caa07204997d80b051919345e953304d3/lib/default-options.js#L77
针对以上事实,我觉得方案3可行,而且效果最好

执行

具体的执行方案如下

  1. 每个频道生成一个 sw.js 和一个 sw.meta.js,后者只有 __wpo 数据
  2. 用 rewire 把 各个频道的sw.meta.js里面的__wpo读出来(因为没有export出来,我只想到用这个)
  3. 把所有 meta 合成一个
  4. 再用 babel ,以其中一个 sw.js为蓝本,把其中的__wpo替换成 步骤3合成 的 meta

部分代码如下

var mainSwPath = path.join(config.output.path, 'pages/', ver_file_name, 'main/sw.js');

if(!fs.existsSync(mainSwPath) || pageChannel !== 'main') return;
console.log(`合成 sw.js 开始`)

const channels = fs.readdirSync(path.join(config.output.path, 'pages/', ver_file_name));
const allmeta = channels.filter(/* 一些过滤条件,具体就不说了 */).reduce(function(accumulator,currentValue,currentIndex,array){
    const swmeta = rewire(path.join( config.output.path, 'pages/',ver_file_name, currentValue,'/sw.meta.js' )).__get__(wpo);
    if(accumulator){
        accumulator.assets.main = accumulator.assets.main.concat(swmeta.assets.main);
        accumulator.assets.additional = accumulator.assets.additional.concat(swmeta.assets.additional);
        accumulator.assets.optional = accumulator.assets.optional.concat(swmeta.assets.optional);
        accumulator.externals = accumulator.externals.concat(swmeta.externals);
        accumulator.hashesMap = Object.assign(accumulator.hashesMap, swmeta.hashesMap);
        return accumulator;
    }else{
        return swmeta;
    }
},null);

if(allmeta){
    allmeta.assets.main = uniqueArray(allmeta.assets.main).sort();
    allmeta.assets.additional = uniqueArray(allmeta.assets.additional).sort();
    allmeta.assets.optional = uniqueArray(allmeta.assets.optional).sort();
    allmeta.externals = uniqueArray(allmeta.externals).sort();

    var babelConfig = {
        sourceType:'script',
        babelrc:false,
        presets:[[
          "env", {
            "targets": {
              "browsers": ["chrome >= 40"]
            }
          }
        ]],
      }

    var babel = require("babel-core");
    var traverse = require('babel-traverse');
    var generate = require('babel-generator');
    var allMetaAST = babel.transform(`var ${wpo}=${JSON.stringify(allmeta)};`, babelConfig)
    var oneSwAST = babel.transformFileSync(mainSwPath, babelConfig);
    traverse.default(oneSwAST.ast,{
        VariableDeclaration(path){
            var Identifier = path.node.declarations[0].id.name;
            if(Identifier === wpo){
                path.node.declarations[0].init = allMetaAST.ast.program.body["0"].declarations["0"].init;
            }
        }
    });
    fs.writeFileSync(path.join(config.output.path,'sw.js'), generate.default(oneSwAST.ast,{
        minified:true
    }).code, { encoding:'utf8' });   
    //xxxxxx
}
                
                
function uniqueArray(arr){
    var s = new Set(arr);
    var r = [];
    s.forEach(item=>r.push(item));
    return r;
}

0x04 迭代执行

当然实际执行的时候,我是先上了其中一个频道的 sw,加上了开关,就一个迭代了,后面的是慢慢加上去的。互联网产品刚开始不要想着太完善,需要慢慢迭代出来的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84959 人气
更多

推荐作者

lioqio

文章 0 评论 0

Single

文章 0 评论 0

禾厶谷欠

文章 0 评论 0

alipaysp_2zg8elfGgC

文章 0 评论 0

qq_N6d4X7

文章 0 评论 0

放低过去

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文