PWA 实践经验 建立

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,加上了开关,就一个迭代了,后面的是慢慢加上去的。互联网产品刚开始不要想着太完善,需要慢慢迭代出来的。

以上是 PWA 实践经验 建立 的全部内容, 来源链接: utcz.com/z/264646.html

回到顶部