进入我的博客

这里有您在其他地方看不到的web前端方面的技术、知识及资源

这里有您在其他地方看不到的web前端方面的技术、知识及资源

张鑫旭-鑫空间-鑫生活

it's my whole life!

备份内容浏览

« 查看全部推荐内容

将网站改进为增强型网页应用-PWA

译者 | 京东金融-移动研发部-前端工程师 郑莉



最近有一些关于增强型网页应用(PWA)的议论,很多人都质疑PWA是否代表了未来的(移动)Web。我并无意参与到整个的原生app与PWA的争论中,但是有一件事是确定的– 在提高移动端和改善用户体验方面还有很长的路要走。到2018,移动端web访问量注定会超过其他设备的总和,你能忽视这一趋势吗?


好消息是制作一个PWA并不困难。实际上,很多可能可以将一个现有的网站转换成PWA。这也是本文将要介绍的内容– 看完本文,你将拥有一个行为类似于原生web app,能够离线运行,拥有自己的桌面图标。


什么是PWA?


增强型网页应用(Progressive Web Apps,PWAs),是web技术里一个令人激动的革新。PWAs集合了一系列技术,可以使web app有和原生移动app相同的功能。这有利于开发者和用户实现那些仅限于web和native的解决方案:


  1. 你只需要一个由开源的、标准的W3C网页技术开发的应用。无需单独开发一个原生代码库。

  2. 用户可以发现并在下载前尝试你的应用。

  3. 没有必要使用需要支付费用的应用商店,应用程序会自动更新,而用户无感知。

  4. 下载前有提示,下载后会在主屏幕上添加图标。

  5. 启动时,PWA会展示一个聚睛的启动画面。

  6. chrome浏览器选项可以设置全屏显示。

  7. 基本文件会被缓存到本地,因此PWAs比标准的网页app响应地更快(甚至比原生app还要快)。

  8. 安装包非常轻量– 可能只有几百KB的缓存数据。

  9. 所有的数据交换只能通过安全的HTTPS连接。

  10. PWAs的功能是离线的,并能在连接响应后同步数据。


现在还是早期,但对于案例的研究还是积极的。Flipkart,印度最大的电商网站,在弃用原生app改用PWA后,销售转换率增长了70%,用户的停留时间变成了三倍。阿里巴巴,世界最大的商业交易平台,转换率也差不多增长了76%。


Firefox、Chrome和其他基于Blink的浏览器都是支持PWA技术的。微软正致力于在Edge中实现。Apple保持了沉默,尽管在WebKit的五年计划中有一些有希望的评论。幸运地是,这与浏览器端的支持性几乎不相干。


PWA是渐进增强的


你的app仍然可以在不支持PWA的浏览器中的运行,只是用户无法享受到离线功能,但一切都会如常运行。在给出效益成本比回报的情况下,没有理由不往你的系统里添加PWA技术。


不仅仅是App


谷歌在推广PWA时给的大部分教程都是描述如何从头建立一个基于Chrome而似native的移动app。然而,我们不需要多加一个单页面app或非得去跟从实际的界面设计指南。大多数的网站可以在几个小时内改造成PWA,包括WordPress可静态网站。Smashing Magazine宣布他们也在使用PWA。


演示代码


演示代码可参考https://github.com/sitepoint-editors/pwa-retrofit,这是一个简单的4页网站,有一些图片、一个样式表和一个主js文件。这个网站兼容所有现在浏览器(IE10+)。如果浏览器支持PWA,那么用户可以在离线环境下阅读曾经浏览过的网页。

运行代码时,请确保已经安装了Node.js,之后可以在终端运行下面的代码启动web server:

node./server.js [port]


[port]是配的,默认值8888。打开Chrome或其他基于Blink的浏览器(比如Opera或Vivaldi),然后打开 http://localhost:8888/(端口可改成你所指定的值)。你也可以打开开发者工具(F12或Cmd/Ctrl + Shift + I)查看控制台信息。



浏览一下主页,和其他页面,然后通过以下两种方式,切换成离线状态。

  1. 使用Cmd/Ctrl + C停止web server,

  2. 在控制台的Network或Application– Service Workers 选项卡,选中离线选项。


重新访问之前浏览过的页面,发现它们仍能加载。如果访问一个从未浏览过的页面,将会展示一个‘you’re offline’页面,并包含一个可浏览页面的列表:



连接设备


你也可以在Android机上浏览演示代码,手机需要通过USB连接到PCMac上。打开开发者工具左上角三点处的More tools菜单里的Remote devices面板。


选择左侧的Settings,点击Add Rule将端口8888映射到localhost:8888。现在你可以打开手机上的浏览器并访问http://localhost:8888/

有两种方式向桌面添加图标,你可以使用浏览器菜单中的“Add to Home screen”,也可以浏览一些页面后,浏览器会提示你下载。浏览完页面后,关闭Chrome并断开设备连接。然后再次启动PWA Website应用你会看到一个启动画面,也能够在不连接服务器的情况下,浏览之前阅读过的页面。

下面介绍如何三步将你的网站改造成一个PWA

Step 1:开启HTTPS

显然,PWAs需要HTTPS连接。虽然成本会增加,但这些成本和代价是值得的,毕竟谷歌搜索会将安全的网站排名更高。

HTTPS对于上边的演示代码是非必需的,因为Chrome允许使用localhost或任何127.x.x.x地址进行测试。如果想在HTTP网站上测试PWA技术,在启动Chrome时可以使用下面的命令行标记:

·--user-data-dir

·--unsafety-treat-insecure-origin-as-secure

Step 2:创建Web App Manifest

Webapp manifest包括应用程序信息,比如名称、描述和操作系统桌面图标、启动画面和视口的图片。事实上,manifest用一个单独文件替代了页面中已存在的大量的特定图标和主题meta标签。

ManifestJSON文件,位于app根目录下。它的Content-Type需要设置为application/manifest+json 或 application/json。这个文件的名称可随意更改,但在该演示代码中被命名为/manifest.json

{

  "name"              :"PWAWebsite",

  "short_name"        :"PWA",

  "description"       :"Anexample PWA website",

  "start_url"         :"/",

  "display"           :"standalone",

  "orientation"       :"any",

  "background_color"  :"#ACE",

  "theme_color"       :"#ACE",

  "icons":[

    {

      "src"           :"/images/logo/logo072.png",

      "sizes"         :"72x72",

      "type"          :"image/png"

    },

    {

      "src"           :"/images/logo/logo152.png",

      "sizes"         :"152x152",

      "type"          :"image/png"

    },

    {

      "src"           :"/images/logo/logo192.png",

      "sizes"         :"192x192",

      "type"          :"image/png"

    },

    {

      "src"           :"/images/logo/logo256.png",

      "sizes"         :"256x256",

      "type"          :"image/png"

    },

    {

      "src"           :"/images/logo/logo512.png",

      "sizes"         :"512x512",

      "type"          :"image/png"

    }

  ]

}

需要在所有页面中添加如下链接:

<link rel="manifest"href="/manifest.json">

Manifest中的主要属性有:

  1. Ÿ  name – 展示给用户的应用程序的全称

  2. Ÿ  short_name –全称无法完全显示时使用的短名称

  3. Ÿ  description– 对于应用的详细描述

  4. Ÿ  start_url –启动程序对应的URL(一般是‘/’)

  5. Ÿ  scope – 导航范围。例如‘/app/’指限制应用只访问该文件夹

  6. Ÿ  background-color– 启动界面和chrome浏览器(如果需要的话)的背景颜色

  7. Ÿ  theme_color– 应用程序的颜色,通常与背景色相同,它将影响app的显示效果

  8. Ÿ  orientation– 设置优先的显示方向:any, natural, landscape, landscape-primary, landscape-secondary,portrait, portrait-primary,和 portrait-secondary

  9. Ÿ  display – 设置优先的显示模式:fullscreen (非chrome), standalone (看上去像一个原生app), minimal-ui (包含一小部分UI控件) and browser (一个常规的浏览器标签页)

  10. Ÿ  icons – 一个图片对象列表,定义了src(地址)、sizes(宽高)和type(类型)。有一系统的图标应该被定义。

MDN提供了完整的属性列表(https://developer.mozilla.org/en-US/docs/Web/Manifest

Chrome开发者工具Application面板中的Manifest选项,显示了应用的manifest JSON,还提供了“Add to homescreen”链接,可以在桌面设备上运行:

Step 3:创建Service Worker

SeviceWorker 是可编程的代理,能够拦截并响应网络请求。它是位于应用根目录下的一个JavaScript文件。

页面js(在该演示代码中是/js/main.js)中可以检查是否支持serviceworker并注册该文件:

if('serviceWorker'in navigator){

 

  //register service worker

  navigator.serviceWorker.register('/service-worker.js');

 

}

如果你不需要离线功能,那么只需要创建一个空白的/service-worker.js文件用户将被提示安装当前应用。

serviceworker会有些难懂,但你应该去改写演示代码以满足你的需要。这是标准的web worker脚本,浏览器会下载(如果支持的话)该脚本并运行在独立的线程中。它不能访问DOM和其他页面的API,但会拦截各种网络请求,包括由页面改变、资源下载和Ajax调用所触发的。

这是网站需要使用HTTPS的首要原因。试想一下,如果一个第三方脚本从另外的域名注入了其自己的sevice worker,将会造成怎样的状况。它将可以检测并修改客户端和服务器之间的所有数据交换!

serviceworker会对3种主要事件作出反应:installactivatefetch

Install事件

该事件发生在应用安装时。通常是使用Cache API来缓存基本文件。

首先,我们将定义一些配置项:

  1. 缓存名称(CACHE)和版本号(version)。应用可以有多个缓存区,但我们只需要一个。版本号可以保证,当我们大幅修改应用后,本地能够使用新的缓存并忽视原有的缓存文件。

  2. 一个离线页面URL(offlineURL)。当离线用户尝试加载一个从未访问过的页面时,将显示该离线页面。

  3. 一个必要文件数组,包含了网站离线运行必要的文件(installFilesEssential)。数组里还应包括资源文件,如CSS和JavaScript;我还会加上主页(/)和logo。如果某些URL可以通过多种路径定位到,例如‘/’和‘/index.html’,那么也应全部添加进去。注意:offlineURL也要加到这个数组中。

  4. 可选项,一个描述文件数组(installFilesDesirable)。这些文件也会被下载,但如果下载失败不会导致安装中止。



// configuration

const

 version ='1.0.0',

  CACHE= version +'::PWAsite',

 offlineURL ='/offline/',

 installFilesEssential =[

    '/',

    '/manifest.json',

    '/css/styles.css',

    '/js/main.js',

    '/js/offlinepage.js',

    '/images/logo/logo152.png'

  ].concat(offlineURL),

 installFilesDesirable =[

    '/favicon.ico',

    '/images/logo/logo016.png',

    '/images/hero/power-pv.jpg',

    '/images/hero/power-lo.jpg',

    '/images/hero/power-hi.jpg'

  ];

installStaticFiles()函数使用基于promiseCache API将文件添加到缓存中。只有当缓存了必要文件时才会返回数值:

// install static assets
functioninstallStaticFiles(){
  return caches.open(CACHE)
    .then(cache =>{
      // cache desirable files
      cache.addAll(installFilesDesirable);
      // cache essential files
      return cache.addAll(installFilesEssential);
    });
}

最后,我们添加一个install事件监听器。waitUntil方法确保serviceworker在所有封闭代码都已运行后才被安装,方法中运行installStaticFiles() 后运行 self.skipWaiting() ,激活service worker

// application installation
self.addEventListener('install', event =>{
  console.log('service worker: install');
  // cache core files
  event.waitUntil(
    installStaticFiles()
    .then(()=> self.skipWaiting())
  );
});

Activate事件

该事件发生在serviceworker被激活时,不论是安装后还是返回时。你可以用不到这一句柄,但演示代码中使用了该事件,用于删除现有的旧缓存:

// clear old caches
functionclearOldCaches(){
  return caches.keys()
    .then(keylist =>{
      return Promise.all(
        keylist
          .filter(key => key !== CACHE)
          .map(key => caches.delete(key))
      );
    });
}
// application activated
self.addEventListener('activate', event =>{
  console.log('service worker: activate');
    // delete old caches
  event.waitUntil(
    clearOldCaches()
    .then(()=> self.clients.claim())
    );
});

Fetch事件

该事件发生在网络请求产生时。它会调用respondWith()方法来劫持GET请求并:

  1. 从缓存中获取资源,

  2. 如果#1失败,使用Fetch API(与fetch事件无关)从网络上加载资源,并将该资源加进缓存,

  3. 如果#1和#2都失败了,返回一个适当的结果。

// application fetch network data
self.addEventListener('fetch', event =>{
  // abandon non-GET requests
  if(event.request.method !=='GET')return;
 let url = event.request.url; 
  event.respondWith(
    caches.open(CACHE)
      .then(cache =>{ 
        return cache.match(event.request)
          .then(response =>{ 
            if(response){
              // return cached file
              console.log('cache fetch: '+ url);
              return response;
            } 
            // make network request
            returnfetch(event.request)
              .then(newreq =>{
                console.log('network fetch: '+ url);
                if(newreq.ok) cache.put(event.request, newreq.clone());
                return newreq; 
              })
              // app is offline
              .catch(()=>offlineAsset(url)); 
          });
      })
  ); 
});

最后调用的offlineAsset(url)方法返回了一个适当的结果,提供了一些帮助功能:

// is image URL?
let iExt =['png','jpg','jpeg','gif','webp','bmp'].map(f =>'.'+ f);
functionisImage(url){
  return iExt.reduce((ret, ext)=> ret || url.endsWith(ext),false);
}
// return offline asset
functionofflineAsset(url){ 
  if(isImage(url)){ 
    // return image
    returnnewResponse(
      '<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
      { headers:{
        'Content-Type':'image/svg+xml',
        'Cache-Control':'no-store'
      }}
    ); 
  }
  else{ 
    // return page
    return caches.match(offlineURL);
  } 
}

offlineAsset(url)函数会检查该request是否请求的是一张图片,是的话返回一个包含“offline”文字的SVG,不是的话,返回offlineURL页面。

Chrome开发者工具Application面板中的ServiceWorker选项,提供了有关worker的信息,

在开发者工具的 Cache Storage 选项列出了所有当前域内的缓存和所包含的静态文件。当缓存更新的时候,你可以点击左下角的刷新按钮来更新缓存:

不出意料, Clear storage 选项可以删除你的 serviceworker 和缓存:

再来一步 – Step 4:创建一个可用的离线页面

离线页面可以是一个静态页面,来说明当前用户请求不可用。然而,我们也可以在这个页面上列出可以访问的页面链接。

main.js中我们可以使用 Cache API 。然而API 使用promises,在不支持的浏览器中会引起所有javascript运行阻塞。为了避免这种情况,我们在加载另一个 /js/offlinepage.js 文件之前必须检查离线文件列表和是否支持 Cache API

// load script to populate offline page list
if (document.getElementById('cachedpagelist') && 'caches'inwindow) {
  var scr = document.createElement('script');
  scr.src = '/js/offlinepage.js';
  scr.async = 1;
  document.head.appendChild(scr);
}

/js/offlinepage.js locates the most recent cache by version name, 取到所有 URLkey的列表,移除所有无用 URL,排序所有的列表并且把他们加到 ID cachedpagelist DOM 节点中:

ent('li'),
          // cache name
const
  CACHE = '::PWAsite',
  offlineURL = '/offline/',
  list = document.getElementById('cachedpagelist');
// fetch all caches
window.caches.keys()
  .then(cacheList => {
    // find caches by and order by most recent
    cacheList = cacheList
      .filter(cName => cName.includes(CACHE))
      .sort((a, b) => a - b);
    // open first cache
    caches.open(cacheList[0])
      .then(cache => {
        // fetch cached pages
        cache.keys()
          .then(reqList => {
            let frag = document.createDocumentFragment();
            reqList
              .map(req => req.url)
              .filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
              .sort()
              .forEach(req => {
                let
                  li = document.createElem        a = li.appendChild(document.createElement('a'));
                  a.setAttribute('href', req);
                  a.textContent = a.pathname;
                  frag.appendChild(li);
              });
            if (list) list.appendChild(frag);
          });
      })
  });

开发工具

如果你觉得 javascript调试困难,那么 service worker 也不会很好。Chrome的开发者工具的Application 提供了一系列调试工具。

你应该打开 隐身窗口 来测试你的 app,这样在你关闭这个窗口之后缓存文件就不会保存下来。

最后,Lighthouse extension for Chrome 提供了很多改进 PWA 的有用信息。

PWA 陷阱

有几点需要注意:

URL 隐藏

我们的示例代码隐藏了 URL栏,我不推荐这种做法,除非你有一个单 url 应用,比如一个游戏。对于多数网站,manifest 选项 display: minimal-ui 或者 display: browser是最好的选择。

缓存太多

你可以缓存你网站的所有页面和所有静态文件。这对于一个小网站是可行的,但这对于上千个页面的大型网站实际吗?没有人会对你网站的所有内容都感兴趣,而设备的内存容量将是一个限制。即使你像示例代码一样只缓存访问过的页面和文件,缓存大小也会增长的很快。

也许你需要注意:

    ·只缓存重要的页面,类似主页,和最近的文章。

    ·不要缓存图片,视频和其他大型文件

    ·经常删除旧的缓存文件

    ·提供一个缓存按钮给用户,让用户决定是否缓

缓存刷新

在示例代码中,用户在请求网络前先检查该文件是否缓存。如果缓存,就使用缓存文件。这在离线情况下很棒,但也意味着在联网情况下,用户得到的可能不是最新数据。

静态文件,类似于图片和视频等,不会经常改变的资源,做长时间缓存没有很大的问题。你可以在HTTP 头里设置 Cache-Control 来缓存文件使其缓存时间为一年(31,536,000 seconds):

    Cache-Control: max-age=31536000

页面,CSS script 文件会经常变化,所以你应该改设置一个很短的缓存时间比如 24 小时,并在联网时与服务端文件进行验证:

    Cache-Control: must-revalidate, max-age=86400


PS: 备份内容仅显示纯文字。

抱歉,服务器忙,未能成功提交,稍后重试。