ImQi1
封面
简单给网站加个PWA
将Web应用转化成能够在多平台设备上使用的原生应用程序并且具有和原生应用接近的交互体验,无需下载直接安装

PWA(Progressive Web Apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。这些应用无处不在、功能丰富,使其具有与原生应用相同的用户体验优势。PWA 是可被发现、易安装、可链接、独立于网络、渐进式、可重用、响应性和安全的。

简单理解,PWA可以将Web应用转化成能够在多平台设备上使用的原生应用程序并且具有和原生应用接近的交互体验,无需下载直接安装。

条件

  • 网页为HTTPS;
  • 有图标,并且推荐分辨率在256×256以上。

步骤

下面以本站为例,简要介绍怎么构建PWA项目。

首先编写manifest项目。它用于描述网页离线时的图标、地址、名称等信息。

{
  "name": "ImQi1",
  "short_name": "ImQi1",
  "start_url": "https://imqi1.com/",
  "display": "fullscreen",
  "background_color": "#ffffff",
  "scrope": "/",
  "description": "做技术的分享者、生活的摄影师、时事的评论员。",
  "theme_color": "#f9fafb",
  "lang": "zh-CN",
  "icons": [
    {
      "src": "/static/img/imqi1-64.png",
      "sizes": "64x64",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/static/img/imqi1-144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/static/img/imqi1-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

接下来在网页的head标签中引入manifest标签:

<meta rel="manifest" href="/path/to/your/manifest.json">

接下来注册service worker。Service Worker 是一种在浏览器背后运行的脚本,可以用来支持离线体验、网络请求拦截以及资源缓存等功能。它作为一个网页与网络之间的代理服务器,可以控制页面(或整个站点)的网络请求,让开发者能够创建可靠的、快速的和离线可用的网页应用。

首先先创建一个service-worker.js,本站中这个文件用于定义缓存什么文件。这里不介绍更多知识,仅给出代码。

importScripts('/sw.js');
if (workbox) {
    console.log(`Workbox成功加载`);
} else {
    console.log(`Workbox加载失败`);
}
workbox.routing.registerRoute(new RegExp('.*\.(?:js|css)'), workbox.strategies.staleWhileRevalidate({
    cacheName: 'css&js-cache',
    plugins: [new workbox.cacheableResponse.Plugin({statuses: [0, 200]}), new workbox.expiration.Plugin({
        maxEntries: 200,
        maxAgeSeconds: 7 * 24 * 60 * 60,
    })]
}));

.*\.(?:js|css)用于匹配以js和css结尾的文件,并缓存7天。你可以根据你的站点制定更详细的缓存规则。

接下来创建sw.js,这两个JS文件都放在网页的根目录中,当然你可以根据自己的网页结构更改引用路径。

!function(){"use strict";try{self["workbox:sw:4.3.1"]&&_()}catch(t){}const t="https://storage.googleapis.com/workbox-cdn/releases/4.3.1",e={backgroundSync:"background-sync",broadcastUpdate:"broadcast-update",cacheableResponse:"cacheable-response",core:"core",expiration:"expiration",googleAnalytics:"offline-ga",navigationPreload:"navigation-preload",precaching:"precaching",rangeRequests:"range-requests",routing:"routing",strategies:"strategies",streams:"streams"};self.workbox=new class{constructor(){return this.v={},this.t={debug:"localhost"===self.location.hostname,modulePathPrefix:null,modulePathCb:null},this.s=this.t.debug?"dev":"prod",this.o=!1,new Proxy(this,{get(t,s){if(t[s])return t[s];const o=e[s];return o&&t.loadModule(`workbox-${o}`),t[s]}})}setConfig(t={}){if(this.o)throw new Error("Config must be set before accessing workbox.* modules");Object.assign(this.t,t),this.s=this.t.debug?"dev":"prod"}loadModule(t){const e=this.i(t);try{importScripts(e),this.o=!0}catch(s){throw console.error(`Unable to import module '${t}' from '${e}'.`),s}}i(e){if(this.t.modulePathCb)return this.t.modulePathCb(e,this.t.debug);let s=[t];const o=`${e}.${this.s}.js`,r=this.t.modulePathPrefix;return r&&""===(s=r.split("/"))[s.length-1]&&s.splice(s.length-1,1),s.push(o),s.join("/")}}}();
//# sourceMappingURL=workbox-sw.js.map
这个文件托管于谷歌,由于直接访问速度太慢,因此我将它放到了本地。

然后将下面的代码放在网页的script中,放在靠后的位置,用于引入service worker。

<script>
    if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js'); }); } 
</script>

这样浏览器右上角就会有个安装的按钮。

但是我们有时候让用户自己点击安装按钮然后获取到无缝于原生应用的体验,你就可以在页面中添加一个按钮,在用户点击后弹出安装提示。

假设你的按钮id为install,那么你可以在网页中添加如下代码。

const installButton = document.getElementById('installButton');
window.addEventListener('appinstalled', () => {  deferredPrompt = null; });
window.addEventListener('beforeinstallprompt', (event) => { event.preventDefault(); window.deferredPrompt = event; });
installButton.addEventListener('click', async () => { const promptEvent = window.deferredPrompt; if (!promptEvent) { return; } promptEvent.prompt(); const result = await promptEvent.userChoice; window.deferredPrompt = null;  });

当然这部分代码也要放在script中。

本站的PWA就采用这种方式实现。除此之外,你可以在页面中引入Google对于PWA优化的软件,它可以解决跨平台的兼容问题,如Safari不能完整地支持PWA,需要加一些额外的处理,使用这个库就可以省去这些处理。

由于这个JS代码也托管于Google或Github,因此我直接给出代码,你可以放在一个JS文件中然后在你的网站中引入这个JS文件。注意这个JS文件要在head中引入,而不是在页面底部。
(function(){function B(a){try{return document.head.querySelector(a)}catch(b){return null}}function v(a,b){a="__pwacompat_"+a;void 0!==b&&(w[a]=b);return w[a]}function F(){var a=(x=B('link[rel="manifest"]'))?x.href:"";if(!a)throw'can\'t find <link rel="manifest" href=".." />\'';var b=Q([a,location]),d=v("manifest");if(d)try{var g=JSON.parse(d);G(g,b)}catch(r){console.warn("PWACompat error",r)}else{var n=new XMLHttpRequest;n.open("GET",a);n.withCredentials="use-credentials"===x.getAttribute("crossorigin");
n.onload=function(){try{var r=JSON.parse(n.responseText);v("manifest",n.responseText);G(r,b)}catch(t){console.warn("PWACompat error",t)}};n.send(null)}}function Q(a){for(var b={},d=0;d<a.length;b={c:b.c},++d){b.c=a[d];try{return new URL("",b.c),function(g){return function(n){return(new URL(n||"",g.c)).toString()}}(b)}catch(g){}}return function(g){return g||""}}function C(a,b,d){if(!B(a+d)){a=document.createElement(a);for(var g in b)a.setAttribute(g,b[g]);document.head.appendChild(a);return a}}function k(a,
b){b&&(!0===b&&(b="yes"),C("meta",{name:a,content:b},'[name="'+a+'"]'))}function R(a){var b=a.sizes.split(/\s+/g).map(function(d){return"any"===d?Infinity:parseInt(d,10)||0});return{src:a.src,type:a.type,sizes:a.sizes,h:Math.max.apply(null,b),f:a.f?a.f.split(/\s+/g):["any"]}}function G(a,b){function d(f,c,h,m){var e=window.devicePixelRatio,l=D({width:f*e,height:c*e});l.scale(e,e);l.fillStyle=y;l.fillRect(0,0,f,c);l.translate(f/2,(c-20)/2);m&&(c=m.width/e,e=m.height/e,128<e&&(c/=e/128,e=128),48<=c&&
48<=e&&(l.drawImage(m,c/-2,e/-2,c,e),l.translate(0,e/2+20)));l.fillStyle=S?"white":"black";l.font="24px HelveticaNeue-CondensedBold";l.font=getComputedStyle(x).getPropertyValue("--pwacompat-splash-font");e=a.name||a.short_name||document.title;c=l.measureText(e);m=c.j||24;l.translate(0,m);if(c.width<.8*f)l.fillText(e,c.width/-2,0);else for(e=e.split(/\s+/g),c=1;c<=e.length;++c){var H=e.slice(0,c).join(" "),I=l.measureText(H).width;if(c===e.length||I>.6*f)l.fillText(H,I/-2,0),l.translate(0,1.2*m),e.splice(0,
c),c=0}return function(){var J=l.canvas.toDataURL();g(h,J);return J}}function g(f,c){var h=document.createElement("link");h.setAttribute("rel","apple-touch-startup-image");h.setAttribute("media","(orientation: "+f+")");h.setAttribute("href",c);document.head.appendChild(h)}function n(f,c){var h=window.screen,m=d(h.width,h.height,"portrait",f),e=d(h.height,h.width,"landscape",f);setTimeout(function(){u.p=m();setTimeout(function(){u.l=e();c()},10)},10)}function r(f){function c(){--h||f()}var h=z.length+
1;c();z.forEach(function(m){var e=new Image;e.crossOrigin="anonymous";e.onerror=c;e.onload=function(){e.onload=null;m.href=K(e,y,!0);u.i[e.src]=m.href;c()};e.src=m.href})}function t(){v("iOS",JSON.stringify(u))}function L(){var f=z.shift();if(f){var c=new Image;c.crossOrigin="anonymous";c.onerror=function(){return void L()};c.onload=function(){c.onload=null;n(c,function(){var h=a.background_color&&K(c,y);h?(f.href=h,u.i[c.src]=h,r(t)):t()})};c.src=f.href}else n(null,t)}var p=(a.icons||[]).map(R).sort(function(f,
c){return c.h-f.h}),q=p.filter(function(f){return-1<f.f.indexOf("any")});p=p.filter(function(f){return-1<f.f.indexOf("maskable")});var z=(0<p.length?p:q).map(function(f){var c={rel:"icon",href:b(f.src),sizes:f.sizes},h='[sizes="'+f.sizes+'"]';C("link",c,'[rel="icon"]'+h);if(A&&!(120>f.h))return c.rel="apple-touch-icon",C("link",c,'[rel="apple-touch-icon"]'+h)}).filter(Boolean);p=B('meta[name="viewport"]');var T=!!(p&&p.content||"").match(/\bviewport-fit\s*=\s*cover\b/),M=a.display;p=-1!==U.indexOf(M);
k("mobile-web-app-capable",p);V(a.theme_color||"black",T);W&&(k("application-name",a.short_name),k("msapplication-tooltip",a.description),k("msapplication-starturl",b(a.start_url||".")),k("msapplication-navbutton-color",a.theme_color),(q=q[0])&&k("msapplication-TileImage",b(q.src)),k("msapplication-TileColor",a.background_color));k("theme-color",a.theme_color);if(A){var y=a.background_color||"#f8f9fa",S=N(y);(q=X(a.related_applications))&&k("apple-itunes-app","app-id="+q);k("apple-mobile-web-app-capable",
p);k("apple-mobile-web-app-title",a.short_name||a.name);if(q=v("iOS"))try{var E=JSON.parse(q);g("portrait",E.p);g("landscape",E.l);z.forEach(function(f){var c=E.i[f.href];c&&(f.href=c)});return}catch(f){}var u={i:{}};L()}else q={por:"portrait",lan:"landscape"}[String(a.orientation||"").substr(0,3)]||"",k("x5-orientation",q),k("screen-orientation",q),"fullscreen"===M?(k("x5-fullscreen","true"),k("full-screen","yes")):p&&(k("x5-page-mode","app"),k("browsermode","application"))}function X(a){var b;(a||
[]).filter(function(d){return"itunes"===d.platform}).forEach(function(d){d.id?b=d.id:(d=d.url.match(/id(\d+)/))&&(b=d[1])});return b}function V(a,b){if(A||Y){var d=N(a);if(A)k("apple-mobile-web-app-status-bar-style",b?"black-translucent":d?"black":"default");else{a:{try{var g=Windows.UI.ViewManagement.ApplicationView.getForCurrentView().titleBar;break a}catch(n){}g=void 0}if(b=g)d=d?255:0,b.foregroundColor={r:d,g:d,b:d,a:255},a=O(a),b.backgroundColor={r:a[0],g:a[1],b:a[2],a:a[3]}}}}function O(a){var b=
D();b.fillStyle=a;b.fillRect(0,0,1,1);return b.getImageData(0,0,1,1).data||[]}function N(a){a=O(a).map(function(b){b/=255;return.03928>b?b/12.92:Math.pow((b+.055)/1.055,2.4)});return 3<Math.abs(1.05/(.2126*a[0]+.7152*a[1]+.0722*a[2]+.05))}function K(a,b,d){d=void 0===d?!1:d;var g=D(a);g.drawImage(a,0,0);if(d||255!==g.getImageData(0,0,1,1).data[3])return g.globalCompositeOperation="destination-over",g.fillStyle=b,g.fillRect(0,0,a.width,a.height),g.canvas.toDataURL()}function D(a){a=void 0===a?{width:1,
height:1}:a;var b=a.height,d=document.createElement("canvas");d.width=a.width;d.height=b;return d.getContext("2d")}if("onload"in XMLHttpRequest.prototype&&!navigator.m){var U=["standalone","fullscreen","minimal-ui"],P=navigator.userAgent||"",A=navigator.vendor&&-1!==navigator.vendor.indexOf("Apple")&&(-1!==P.indexOf("Mobile/")||"standalone"in navigator)||!1,W=!!P.match(/(MSIE |Edge\/|Trident\/)/),Y="undefined"!==typeof Windows,x;try{var w=sessionStorage}catch(a){}w=w||{};"complete"===document.readyState?
F():window.addEventListener("load",F)}})();
评论
评论即代表你已阅读并同意评论协议
    Akria
    Akria www.imets.cn 5个月前

    贵站是怎么部署的呢,纯静态的吗?

      棋
      imqi1.com 5个月前 回复 @Akria

      不是啊,typecho + tpcache

        Akria
        Akria www.imets.cn 5个月前 回复 @棋

        你评论用的是?我那用tpcache会出错

          棋
          imqi1.com 5个月前 回复 @Akria

          就是Typecho原生的评论系统呀

            Akria
            Akria www.imets.cn 5个月前 回复 @棋

            哈哈,我那是主题的,用tpache会出问题,dns服务是自建的吗

              棋
              imqi1.com 5个月前 回复 @Akria

              好的,DNS服务用的是腾讯云edgeone提供的自定义解析域名

    网安冷墨寒
    网安冷墨寒 www.ink0.cn 9个月前

    功能挺实用呢

双击可关闭搜索
内容加载中...