【angular5】浅谈angular5与serviceWorker——(2)

2 篇文章 0 订阅
2 篇文章 0 订阅

  上一篇文章介绍了serviceWorker是什么以及如何在项目中使用serviceWorker,这一篇着重分析ngsw-worker.js的结构,具体的缓存策略是如何实现的。

 一切要从一个中介者开始。ngsw-worker定义了一个Driver,负责worker的初始化,版本更新管理,事件的监听和任务调度。

 源头是一个driver

class Driver{
    constructor(scope, adapter, db) {
        this.scope = scope; // 作用域
        this.adapter = adapter; //网络事件适配器
        this.db = db;   // 缓存db
        //如果drive在初始化的时候,或者其他时候发生了错误,比如说获取不到需要的资源,则会进入safe_mode状态,此时不对数据缓存做多余处理。初始为normal状态
        this.state = DriverReadyState.NORMAL;
        this.stateMessage = '(nominal)';

        //尚未初始化
        this.initialized = null;  
        //版本管理,如果有新的config,或者静态资源数据有改变,则会生成一个新的version。之后的缓存和数据则以新version为准
        this.clientVersionMap = new Map();
        this.versions = new Map();
        
        this.latestHash = null;
        this.lastUpdateCheck = null;
        
        this.scheduledNavUpdateCheck = false;
        // 开启一个任务调度器,在空闲的时候调度各种任务。
        this.idle = new IdleScheduler(this.adapter, IDLE_THRESHOLD, this.debugger);
        
    }
}

 接下来driver定义了事件监听,主要对应service-worker的生命周期,以及关键的网络请求事件和push事件管理。

this.scope.addEventListener('install', (event) => {
            // 由于sw的更新和app本身的更新没有太大的关联,所以在install阶段直接越过app自身的检查更新,直接去更新sw自己的version
            event.waitUntil(this.scope.skipWaiting());
        });
        // 当新版本的sw第一次激活的时候,触发该事件
        this.scope.addEventListener('activate', (event) => {
            // 由于有新版本了,所以先越过旧版本。
            event.waitUntil(this.scope.clients.claim());
            if (this.scope.registration.active !== null) {
                this.scope.registration.active.postMessage({ action: 'INITIALIZE' });
            }
        });
        // 处理fetch,message和push事件
        this.scope.addEventListener('fetch', (event) => this.onFetch(event));
        this.scope.addEventListener('message', (event) => this.onMessage(event));
        this.scope.addEventListener('push', (event) => this.onPush(event));

 这里值得注意的是,sw在激活的时候,通过一个postmessage进行schedule的的初始化。选取这个时机,而不是在第一次fetch data的时候进行初始化是因为。第一次fetch事件可能发生在下一次application load的时候。因此在active的这个时机去触发是合适的。但是如果在这个时候进行初始化,那么watiUntil可能会block掉fetch事件,因此这里巧妙的给自己发送一个postmessage事件,在下一个task里面再执行初始化。

一切都要等initialize之后才生效

initialize做的事情很简单。首先读indexDB,找最近的config获取config数据——》从网络拉取新的config.json,对比需不需要更新数据。如果第一步读取数据库失败了,则表示是第一次访问网站,或者db被wipe了。那么sw回去获取新的manifestconfig,生成一个hash,将其保存在db里面。在获取到需要的manifestconfig之后,sw自己会将数据存储在一个AppVersion对象中,以供后续使用。如下所示:

 try {
            // 读取数据库
            [manifests, assignments, latest] = await Promise.all([
                table.read('manifests'),
                table.read('assignments'),
                table.read('latest'),
            ]);
            // 如果读取到了,那么给idle发送一个任务,请求checkupdate
            this.idle.schedule('init post-load (update, cleanup)', async () => {
                await this.checkForUpdate();
                try {
                    await this.cleanupCaches();
                }
                catch (err) {
                    // Nothing to do - cleanup failed. Just log it.
                    this.debugger.log(err, 'cleanupCaches @ init post-load');
                }
            });
        }
        catch (_) {
            // 如果糟了,那么从新创建一个manifest版本,创建新的hash,存在数据库里面。
            const manifest = await this.fetchLatestManifest();
            const hash = hashManifest(manifest);
            manifests = {};
            manifests[hash] = manifest;
            assignments = {};
            latest = { latest: hash };
            // Save the initial state to the DB.
            await Promise.all([
                table.write('manifests', manifests),
                table.write('assignments', assignments),
                table.write('latest', latest),
            ]);
        }

  无论从那里得到数据,在这里已经得到最新的数据了,然后将其按照hash,存到appVersion map里面。由于config里面保存的新的静态资源,会在schedule中插入一个任务,通知assetGroups进行update。

//Driver
 await Promise.all(Object.keys(manifests).map(async (hash) => {
            try {
                //尝试初始化最新的这个version。如果失败,则整个initialize失败
                await this.scheduleInitialization(this.versions.get(hash), this.latestHash === hash);
            }
            catch (err) {
                this.debugger.log(err, `initialize: schedule init of ${hash}`);
                return false;
            }
        })); 
// AppVersion

async initializeFully(updateFrom) {
        try {
            //依次排排坐等着每一个group update
            await this.assetGroups.reduce(async (previous, group) => {
                // 同步执行,线性关系,如果前面有失败的,则整个流程失败。
                await previous;
                // Initialize this group.
                return group.initializeFully(updateFrom);
            }, Promise.resolve());
        }
        catch (err) {
            this._okay = false;
            throw err;
        }
    }

等待assetsGroup更新完毕之后,可以认为初始化工作已完成。

当有网络请求的时候会做什么?

ngsw-worker.js hook住了所有的网络请求,当监听到有请求发生时,在请求发出去之前,调用onFetch进行相应的操作。可以看到onFetch先做一步粗略的过滤,将一些显性会失败的请求过滤掉,然后剩下的丢给handleFetch进行细分处理。

// onFetch

        const req = event.request;
        if (this.adapter.parseUrl(req.url, this.scope.registration.scope).path === '/ngsw/state') {
            // debugger 可以handle一切网络请求,但是不会对sw的状态有任何影响
            event.respondWith(this.debugger.handleFetch(req));
            return;
        }
        // 如果当前sw处在unsafe的状态,那么直接将请求降级到网络上。在这里直接跳过,而不是使用responseWith,是因为后者会表明该请求被sw处理过。实际上并没有处理。
        if (this.state === DriverReadyState.SAFE_MODE) {
            // 通知idle schedule进行更新检查等操作。
            event.waitUntil(this.idle.trigger());
            return;
        }
        // 如果cache头为only-if-cached,而request mode不是same-origin,那么该请求一定会失败,所以这里记录以下错误原因,然后直接返回。
       
        if (req.cache === 'only-if-cached' && req.mode !== 'same-origin') {
            // Log the incident only the first time it happens, to avoid spamming the logs.
            if (!this.loggedInvalidOnlyIfCachedRequest) {
                this.loggedInvalidOnlyIfCachedRequest = true;
                this.debugger.log(`Ignoring invalid request: 'only-if-cached' can be set only with 'same-origin' mode`, `Driver.fetch(${req.url}, cache: ${req.cache}, mode: ${req.mode})`);
            }
            return;
        }
        //  经过handleFetch处理请求。
        event.respondWith(this.handleFetch(event));

  在handleFetch里面,如果在还没有初始化完成的时候,就受到了网络请求,handleFetch会先手动加初始化。然后获取当前appversion,用对应的appVersion中的handle策略来处理该请求。如果获取appversion失败,则降级到safeFetch。最后完成之后,通知idle执行后台操作。具体操作如下所示:

function handleFetch      
  // 如果还没有initialize,则先进行初始化
        if (this.initialized === null) {
         
            this.initialized = this.initialize();
        }
        
        try {
            // Wait for initialization.
            await this.initialized;
        }
        catch (e) {
            // 初始化失败,则进入safe_mode,所有的请求不做任何缓存,直接走网络。
           
            this.state = DriverReadyState.SAFE_MODE;
            this.stateMessage = `Initialization failed due to error: ${errorToString(e)}`;
            // 通知后台进行后台操作。
            event.waitUntil(this.idle.trigger());
            
            return this.safeFetch(event.request);
        }
        // 如果是navigation请求,需要先检查更新
        if (event.request.mode === 'navigate' && !this.scheduledNavUpdateCheck) {
            this.scheduledNavUpdateCheck = true;
            this.idle.schedule('check-updates-on-navigation', async () => {
                this.scheduledNavUpdateCheck = false;
                await this.checkForUpdate();
            });
        }
        // 获取对应的appversion
        const appVersion = await this.assignVersion(event);
        
        if (appVersion === null) {
            event.waitUntil(this.idle.trigger());
            return this.safeFetch(event.request);
        }
        let res = null;
        try {
            // 先根据请求的类型,执行对应的缓存操作等,如果这个工作失败了,那么再降级到网络上
            res = await appVersion.handleFetch(event.request, event);
        }
        catch (err) {
            if (err.isCritical) {
                // Something went wrong with the activation of this version.
                await this.versionFailed(appVersion, err, this.latestHash === appVersion.manifestHash);
                event.waitUntil(this.idle.trigger());
                return this.safeFetch(event.request);
            }
            throw err;
        }
         // 如果失败了,则执行后台操作,并且通过网络执行请求。
        if (res === null) {
            event.waitUntil(this.idle.trigger());
            return this.safeFetch(event.request);
        }
        // 执行请求,把结果返回到上层。到这里,相应的缓存已经完成了
        event.waitUntil(this.idle.trigger());
        // The AppVersion returned a usable response, so return it.
        return res;

safeFetch是一个包装器。如果result.code 不为200,或者请求发生了错误,则构造一个假的504的请求,返回给上层。

实现文件缓存由AssetsGroup类完成。

AssetsGroup的核心内容为根据请求的内容,查看是否由缓存,如果已有缓存并且缓存可用,则返回给上层,否则通过网络获取数据,然后将结果保存在缓存里。核心方法是handleFetch方法,中心思想如下图所示:

  const url = this.getConfigUrl(req.url);
        //首先判断请求的url是不是config里面指定的url,或者符合pattern里面约定的需要缓存的正则规则
        if (this.config.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url))) {
            // 打开缓存,检查该请求是否已经被缓存过,还是需要通过网络获取。
            const cache = await this.cache;
        
            const cachedResponse = await cache.match(req);
            if (cachedResponse !== undefined) {
                // 先判断下该请求是不是有hash,如果有,说明缓存是可以用的,直接返回。否则需要看看这个请求是多久之前发生的,缓存是不是还可以用。
                if (this.hashes.has(url)) {
                    return cachedResponse;
                }
                else {
                    if (await this.needToRevalidate(req, cachedResponse)) {
                        this.idle.schedule(`revalidate(${this.prefix}, ${this.config.name}): ${req.url}`, async () => { await this.fetchAndCacheOnce(req); });
                    }
                    // In either case (revalidation or not), the cached response must be good.
                    return cachedResponse;
                }
            }
            // 没有可用的缓存,通过网络获取,并将结果保存在缓存里面。
            const res = await this.fetchAndCacheOnce(this.adapter.newRequest(req.url));
            // 需要将结果clone一下,因为结果可能要供多个请求使用。
            return res.clone();
        }
        else {
            return null;
        }

这里需要注意的是有缓存,但是没有hash的情况。说明这个缓存是由http cache header决定的。需要通过needToRevalidate来判断该请求/相应是否需要更新。有三种策略需要考虑

  1. 如果请求包括cache-control头部,则需要check请求的age,
  2. 如果请求有expires 头,则需要查看timestamp
  3. 如果没有任何跟缓存相关的头,那么是不可用的,直接返回true。

如果缓存不可用,都通过fetchAndCacheOnce来处理。

// inFlightRequest 里面存的是需要经过网络获取,已经通过网络请求,但是结果还没有回来的请求们。首先需要check现在要发出的请求是不是这一类,如果是的话,不应该再重复发送请求,直接等待上一个的返回结果。
        if (this.inFlightRequests.has(req.url)) {
            
            return this.inFlightRequests.get(req.url);
        }
        // 没有针对该请求的缓存操作正在执行,从这里开始出来,从网络获取并处理。
        const fetchOp = this.fetchFromNetwork(req);
        // 要先把请求放到inFlightRequest里面,表示该url的请求由此处理。
        this.inFlightRequests.set(req.url, fetchOp);
        try {
            // 等待网络结果
            const res = await fetchOp;
            // 需要确保只有正确返回的结果才会被缓存起来,否则不对的结果会block掉应用
            if (!res.ok) {
                throw new Error(`Response not Ok (fetchAndCacheOnce): request for ${req.url} returned response ${res.status} ${res.statusText}`);
            }
            // 正确的结果,放到缓存里面
            const cache = await this.scope.caches.open(`${this.prefix}:${this.config.name}:cache`);
            await cache.put(req, res.clone());
            // 如果该请求还没有hash,则更新它的hash值
            if (!this.hashes.has(req.url)) {
                // Metadata is tracked for requests that are unhashed.
                const meta = { ts: this.adapter.time, used };
                const metaTable = await this.metadata;
                await metaTable.write(req.url, meta);
            }
            return res;
        }
        finally {
            // 无论结果是否被正确返回,都将该请求从inFlightRequest里面删掉。表示这一次网络获取结束。
            this.inFlightRequests.delete(req.url);
        }

DataGroup呢?

DataGroup的思想跟AssetGroup差不多,区别在于Datagroup是针对的是网络上的请求,所以缓存的数据会很多。如果都无脑的放在cache里面,cache可能会哭出来。所以针对这种请求,DataGroup采用LRU策略进行处理。具体情况懒得写了。。下次再见。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值