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进程,可以有怎么样的方案?我想过有三个方案。
- 只让其中最重要的频道上 sw
- 让 sw.js 分不同 scope,各管各
- 把不同频道的内容合成在一起
综合考虑之后,我决定要用第三套方案
可行性
我们来看一下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可行,而且效果最好
执行
具体的执行方案如下
- 每个频道生成一个 sw.js 和一个 sw.meta.js,后者只有 __wpo 数据
- 用 rewire 把 各个频道的sw.meta.js里面的__wpo读出来(因为没有export出来,我只想到用这个)
- 把所有 meta 合成一个
- 再用 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