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,加上了开关,就一个迭代了,后面的是慢慢加上去的。互联网产品刚开始不要想着太完善,需要慢慢迭代出来的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: PWA 实践经验 fetch
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论