8

pwa在前端web应用中的详细实现

 3 years ago
source link: https://my.oschina.net/u/3480701/blog/4989801
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
pwa在前端web应用中的详细实现 - ZoaR的个人空间 - OSCHINA - 中文开源技术交流社区

这次要说的是我认为的在前端以后发展的大趋势(最看好的):pwa 。渐进式web应用,可以类比成安卓或ios的原生应用,和普通的网站思维上其实是有很大差别的;它提供了你可以离线访问网站的能力,整个过程就和使用安卓原生应用一样:下载 --》安装 --》使用;还提供了远程推送(当然这部分中国不能用);当然也有现阶段存在的问题,比如首次加载肯定比纯网页慢等(后面会详细说),但对于它的创新来说,可以说是革命性的。题外话:对于新技术,你可能跟我一样领导会按老思想各种为难你,但我是认为向前是不至于错的,就像阿里腾讯这些对于新技术是很快去尝试的,小公司也是,反而是一些不大不小的公司。飞信的创始人在演讲上说【当时离微信上线还有一年的时候他们就已经有了和微信一模一样的项目了,但当时领导就是不通过,他说站在领导的角度,当时短信光春晚一天就能有40亿的收入,真的通过的话,动了收入这个蛋糕,所以领导也没错】,但我认为这个前提是除非你要别人技术也停滞不前不创新,你不动,自然别人也会来动你的蛋糕,领导才没有错。说多了,接下去进行详细的讲解。

先了解下网站访问逻辑

如上图,整个过程大致如此,普通的网站是不缓存html文件的(如果缓存这个网站就更新不了,或者等到缓存到期),这就注定了网站必须是要联网的状态,pwa这边多了一个代理,截断所有请求,包括你的post接口数据,是的除了缓存资源,还能缓存接口数据。当你开始访问时,代理是直接从缓存里返回了你所需要所有资源,这样你可以直接看到网站,然后在后台请求,如果有新版本下载缓存。所以你有时会看到如下图这个位置会有两条,我这里没有新版本所以只有一个。之后你关掉重新访问时代理就把新版本直接返回。

具体代码实现

我这边用的是vue,主要的实现关键就是ServiceWorker,这个就是主要就是来定义代理行为的,根据自己需要定义缓存规则。

手动实现,主要实现有几个文件

  • registerServiceWorker.js,用来定义安装ServiceWorker的生命周期
import {
  register
} from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
//注册引用service-worker.js
  register(`${process.env.BASE_URL}service-worker.js`, {
    ready() {
      console.log('Service worker ready.')
    },
    registered() {
      console.log('Service worker registered.')
    },
    cached() {
      console.log('Service worker cached.')
    },
    updatefound() {
	//有更新时触发
      console.log('Service worker updatefound.')
      localStorage.setItem("updatefound", "true");
    }
    updated() {
      console.log('Service worker updated.')
      // window.location.reload();
    },
    offline() {
      console.log('Service worker offline.')
    },
    error(error) {
      console.error('Service worker error.', error)
    }
  })
}
  • service-worker.js,定义整个代理的缓存规则
//缓存名称
const CACHE_NAME = 'people-cache';
//配置需要缓存的资源,/表示所有
const CACHE_LIST = [
	'/'
];

self.addEventListener('install', (event) => {
	console.log('install', event);
	event.waitUntil(
		caches.open(CACHE_NAME).then(cache => cache.addAll(CACHE_LIST))
	)
});

/**
 * 激活事件被触发的时候,service-worker.js 文件更新
 */
self.addEventListener('activate', (event) => {
	console.log('activate');
	// self.skipWaiting();//跳过等待
	event.waitUntil(
		caches.keys()
		.then(function (keys) {
			//删除所有旧的缓存
			return Promise.all(keys
				.filter(function (key) {
					return key !== CACHE_NAME;
				})
				.map(function (key) {
					return caches.delete(key);
				})
			);
		})
	)
});

self.addEventListener("fetch", event => {
	// 只捕获get请求
	if (event.request.method !== 'GET') {
		return;
	}
	// 让get请求取缓存中查找资源
	event.respondWith(
		caches
		.match(event.request)
		.then(function (cached) {
			// 将缓存中的资源立即返回,并且同时去服务器下载最新的资源存到缓存中
			var networked = fetch(event.request)
				.then(fetchedFromNetwork, unableToResolve)
				.catch(unableToResolve);

			// 通过caches.match这个方法,如果缓存中有资源,直接就返回了,如果没有转向网络
			return cached || networked;

			function fetchedFromNetwork(response) {
				// 从网络中加载资源
				var cacheCopy = response.clone();
				caches
					// 存储资源
					.open(CACHE_NAME)
					.then(function add(cache) {
						cache.put(event.request, cacheCopy);
					})
					.then(() => {});
				return response;
			}

			// 既不能从网络中获取资源又不能从缓存中获取,就会调用此方法
			function unableToResolve() {
				return new Response('<h1>Service Unavailable</h1>', {
					status: 503,
					statusText: 'Service Unavailable',
					headers: new Headers({
						'Content-Type': 'text/html'
					})
				});
			}
		})
	);
});
  • 在入口文件main.js中记得引入 import './registerServiceWorker.js'
  • vue.config.js添加如下配置
pwa: {
        // configure the workbox plugin
        workboxPluginMode: 'InjectManifest', //GenerateSW,InjectManifest
        workboxOptions: {
            // swSrc is required in InjectManifest mode.
            swSrc: './public/service-worker.js',
            // ...other Workbox options...
        }
    },

两种模式:GenerateSW,InjectManifest。GenerateSW是自动生成配置,不用你指定service-worker.js;InjectManifest是手动配置,需要配置swSrc指向自己写的service-worker.js。
手动实现基本就是这些,但还有几个点注意,当然我这边是单页模式的,多页模式的会不一样点,但原理一样:

  1. 如果你配置缓存了html文件,那么你是可以离线访问,但是你没办法第一时间访问更新后的版本,例如你在访问页面的时候刚好发布了新版本,那么你下次访问时还是旧版本,然后会下载缓存新版本,然后在下一次重新打开才会变成新版,就出现了3次访问问题。我试过在updated生命方法中去reload,抱歉,刷新完后updated会继续触发,导致无限reload,原因是你不关闭整个service-worker安装未全部完成,感觉是一个bug;还有一种是利用updatefound只触发一次然后自己设置标志;但从项目结构来说这些其实都不好。
  2. 如果你不缓存html,那么一有更新就可以刷出来,最多就2次,但是没有了离线访问的能力了,毕竟html需要连网才能访问。

自动实现使用workbox-webpack-plugin(推荐,毕竟满足大部分场景,还简单)

这是google封装工具,npm i workbox-webpack-plugin,安装后应该会自动生成service-worker.js文件

importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.0.0-alpha.3/workbox-sw.js');

if (workbox) {
    console.log(`Yay! workbox is loaded `);
}
else {
    console.log(`Boo! workbox didn't load `);
}

还是要记得在入口文件main.js中记得引入import './registerServiceWorker.js'。 然后在vue.config.js只需要简单的配置即可。

configureWebpack: config => {
        if (process.env.NODE_ENV === 'production') {
            return {
                plugins: [
                    new CompressionPlugin({
                        test: /\.js$|\.html$|\.css|\.otf/,
                        threshold: 1024, //大于多少是压缩
                        deleteOriginalAssets: false
                    }),
                    new GenerateSW({
                        skipWaiting: false,
                        clientsClaim: false,
                        // include: [/.(?:html|png|jpg|jpeg|svg|js|css|json|eot|otf|woff|woff2|ttf)/], //自定义配置缓存
                        // exclude: [/.(?:html)/], //html不缓存,每次都加载新的,避免二次刷新,但无法离线访问
                        // directoryIndex: 'index.html',//配置首页,默认/和/index.html
                    })
                ]
            }
        }
    }

我用的单页应用,用的是动态组件,可以减少首页加载文件的体积,当然也并不是说分的越细越多就越好,要知道请求的时间,如果内容小那么连接握手的时间占比就会越高,所有资源的累加时间其实是变多的,所以个人经验是除了首页其他的用动态组件就好,service-worker会进行预加载后面资源。 这里我还配置了gzip的压缩,用的是compression-webpack-plugin,因为单页应用有一个问题就是主文件会比较大,项目大点就可能会达到10M,这样会导致加载时间比较长,会有长时间的空白,使用gzip可以压缩到2-3M,可以很大的提高体验,但如果要进一步,可以在index.html中加提示<div id="app"> loading中 </div>,提示本身也需要加载,所以个人觉得其实需要浏览器有个自带的加载动画等来提高体验更好。开发项目和写这篇博客的有段时间了,如果有遗漏什么的还请见谅。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK