ServiceWorker for website access speed optimization

ServiceWorkerActing as the middleman between the server and the browser, ServiceWorkerif it can intercept all requests of the current network and take corresponding processing actions.

ServiceWorkerThe content is a JavaScriptscript , and the corresponding processing logic can be written inside, such as caching the request, and if the cache can be used directly, the cache will be returned directly and not transferred to the server, thus greatly improving the browsing experience. Some open source toolkits have multiple CDNsites , using ServiceWorkercan automatically find the fastest access site, and if a site fails, it can automatically switch, FreeCDNwhich is achieved.

1. Concept

ServiceWorkerThe main features are:

  1. ServiceWorkerIt is an independent thread independent of JavaScriptthe main thread, and performing operations that consume a lot of resources in it will not block the main thread;
  2. It has the ability of offline caching and can be accessed cache, allowing developers to control and manage cached content and versions by themselves;
  3. Support message push.

Due to these special features ServiceWorkerof , it has relatively high security control.

  1. ServiceWorkerThe path of the script is the same as the current site and cannot be cross-domain;
  2. ServiceWorkerThe scope of must be less than or equal to the path where ServiceWorkerthe script is located. If the script is js/under the path, the installed can only take effect under this path at ServiceWorkermost ;
  3. If the site is Httpsa site , then ServiceWorkerthe script must also be Https, and the certificate must be valid for the current domain name;
  4. Maybe there are other restrictions...

ServiceWorkerUsing the event-triggered mechanism, there are mainly three events that have more contact:

  1. installEvent, executed when the script is loaded;
  2. activeThe event is executed when the script is activated. Generally, the control right of the page self.clients.claim( )is obtained , and some cache update operations can also be performed.
  3. fetchThe event is executed when the request is intercepted. ServiceWorkerThe most important part determines whether to use the cache or initiate a new request.

2. Script implementation of two ServiceWorkers

The script comes from the Internet, and this article is excerpted and recorded.

2.1 Script 1

The main function of this ServiceWorkerscript is to monitor GitHub, access to combine, and npmopen source libraries, identify whether a request belongs to an open source library, and if so, simultaneously initiate requests to multiple CDN sites of the library, and then select the site with the fastest response .

At the same time, it also has script caching function, but it is not very perfect.

const cacheStorageKey = "check-dream-2.0"
const origin = [
    "https://blog.nineya.com/",
];

const cdn = {
    
    
    gh: {
    
    
        jsdelivr: "https://cdn.jsdelivr.net/gh",
        pigax_jsd: "https://u.pigax.cn/gh",
        pigax_chenyfan_jsd: "https://cdn-jsd.pigax.cn/gh",
        tianli: "https://cdn1.tianli0.top/gh",
    },
    combine: {
    
    
        jsdelivr: "https://cdn.jsdelivr.net/combine",
        pigax_jsd: "https://u.pigax.cn/combine",
        pigax_chenyfan_jsd: "https://cdn-jsd.pigax.cn/combine",
        tianli: "https://cdn1.tianli0.top/combine",
    },
    npm: {
    
    
        eleme: "https://npm.elemecdn.com",
        jsdelivr: "https://cdn.jsdelivr.net/npm",
        zhimg: "https://unpkg.zhimg.com",
        unpkg: "https://unpkg.com",
        pigax_jsd: "https://u.pigax.cn/npm",
        pigax_unpkg: "https://unpkg.pigax.cn/",
        pigax_chenyfan_jsd: "https://cdn-jsd.pigax.cn/npm",
        tianli: "https://cdn1.tianli0.top/npm",
    },
};

// 脚本加载完毕执行时
self.addEventListener("install", (event) => {
    
    
    event.waitUntil(
        caches.open(cacheStorageKey)
            .then(function () {
    
    
                return self.skipWaiting();
            })
    )
});

// 监听所有请求
self.addEventListener("activate", (event) => {
    
    
    event.waitUntil(
        //获取所有cache名称
        caches.keys().then(function (cacheNames) {
    
    
            return Promise.all(
                //移除不是该版本的所有资源
                cacheNames.filter(function (cacheName) {
    
    
                    return cacheName !== cacheStorageKey
                }).map(function (cacheName) {
    
    
                    return caches.delete(cacheName)
                })
            )

        }).then(function () {
    
    
            //在新安装的 SW 中通过调用 self.clients.claim( ) 取得页面的控制权,这样之后打开页面都会使用版本更新的缓存。
            return self.clients.claim()
        })
    )
});

self.addEventListener("fetch", (event) => {
    
    
    event.respondWith(caches.match(event.request).then(response => {
    
    
        if (response != null) {
    
    
            return response;
        }
        handleRequest(event.request)
            .then((result) => {
    
    
                caches
                    .open(cacheStorageKey)
                    .then(cache => {
    
    
                        cache.put(event.request, result)
                    });
                return result;
            })
            .catch(() => 0);
    }))
});

// 返回响应
async function progress(res) {
    
    
    return new Response(await res.arrayBuffer(), {
    
    
        status: res.status,
        headers: res.headers,
    });
}

function handleRequest(req) {
    
    
    const urls = [];
    const urlStr = req.url;
    let urlObj = new URL(urlStr);
    // 为了获取 cdn 类型
    // 例如获取gh (https://cdn.jsdelivr.net/gh)
    const path = urlObj.pathname.split("/")[1];

    // 匹配 cdn
    for (const type in cdn) {
    
    
        if (type === path) {
    
    
            for (const key in cdn[type]) {
    
    
                const url = cdn[type][key] + urlObj.pathname.replace("/" + path, "");
                urls.push(url);
            }
        }
    }

    // 如果上方 cdn 遍历 匹配到 cdn 则直接统一发送请求
    if (urls.length) return fetchAny(urls);

    // 将用户访问的当前网站与所有源站合并
    let origins = [location.origin, ...origin];

    // 遍历判断当前请求是否是源站主机
    const is = origins.find((i) => {
    
    
        const {
    
    hostname} = new URL(i);
        const reg = new RegExp(hostname);
        return urlStr.match(reg);
    });

    // 不是源站则直接请求返回结果
    if (!is) return fetch(urlStr).then(progress);

    // 如果以上都不是,则将当前访问的url参数追加到所有源站后,统一请求。
    // 谁先返回则使用谁的返回结果
    origins = origins.map((i) => i + urlObj.pathname + urlObj.search);
    return fetchAny(origins);
}

// Promise.any 的 polyfill
function createPromiseAny() {
    
    
    Promise.any = function (promises) {
    
    
        return new Promise((resolve, reject) => {
    
    
            promises = Array.isArray(promises) ? promises : [];
            let len = promises.length;
            let errs = [];
            if (len === 0)
                return reject(new AggregateError("All promises were rejected"));
            promises.forEach((p) => {
    
    
                if (!p instanceof Promise) return reject(p);
                p.then(
                    (res) => resolve(res),
                    (err) => {
    
    
                        len--;
                        errs.push(err);
                        if (len === 0) reject(new AggregateError(errs));
                    }
                );
            });
        });
    };
}

// 发送所有请求
function fetchAny(urls) {
    
    
    // 中断一个或多个请求
    const controller = new AbortController();
    const signal = controller.signal;

    // 遍历将所有的请求地址转换为promise
    const PromiseAll = urls.map((url) => {
    
    
        return new Promise(async (resolve, reject) => {
    
    
            fetch(url, {
    
    signal})
                .then(progress)
                .then((res) => {
    
    
                    if (res.status !== 200) reject(null);
                    controller.abort(); // 中断
                    resolve(res);
                })
                .catch(() => reject(null));
        });
    });

    // 判断浏览器是否支持 Promise.any
    if (!Promise.any) createPromiseAny();

    // 谁先返回"成功状态"则返回谁的内容,如果都返回"失败状态"则返回null
    return Promise.any(PromiseAll)
        .then(res => res)
        .catch(() => null);
}

2.2 Screenplay 2

The main function of this script is to cache the specified path locally and specify the version of the cache.

var assetsToCache = [];

//对request url 进行匹配的,而不是当前的页面地址匹配
const caceheList = [
    "themes/",//handsome内置js
    "upload/",// 文章中的图片
    "vditor",
    "jquery",
    "bootstrap",
    "mathjax",
    "mdui",
    "?action=get_search_cache",
    "hm.js" //百度统计js
];

const notCacheList = [
    "/admin/"
]
//添加缓存
self.addEventListener('install', function(event) {
    
    
    event.waitUntil(self.skipWaiting()) //这样会触发activate事件
});

// self.addEventListener('message', function (event) {
    
    
//     console.log("recv message" + event.data);
//     if (event.data === 'skipWaiting') {
    
    
//         self.skipWaiting();
//         console.log("skipwaiting");
//     }
// })



//可以进行版本修改,删除缓存
var version = "9.0.0";
var versionTag = "62625b61dfa93";

var CACHE_NAME = version+versionTag;

self.addEventListener('activate', function(event) {
    
    
    // console.log('activated!');
    var mainCache = [CACHE_NAME];
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
    
    
            return Promise.all(
                cacheNames.map(function(cacheName) {
    
    
                    if ( mainCache.indexOf(cacheName) === -1) {
    
    //没有找到该版本号下面的缓存
                        // When it doesn't match any condition, delete it.
                        console.info('version changed, clean the cache, SW: deleting ' + cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
    return self.clients.claim();
});


function isExitInCacheList(list,url){
    
    
    return list.some(function (value) {
    
    
        return url.indexOf(value) !== -1
    })
}

var CDN_ADD = "" //博客本地图片资源替换
var BLOG_URL = "https://www.ihewro.com" //博客本地图片资源替换


function fetchLocal(event){
    
    

    // console.log("fectch error",CDN_ADD,BLOG_URL)
    // 判断地址前缀是否是CDN_ADD,进行回退
    if (CDN_ADD!="" && BLOG_URL!= CDN_ADD && event.request.url.indexOf(CDN_ADD)!==-1){
    
    
        const new_request_url = event.request.url.replace(CDN_ADD,BLOG_URL);

        return caches.open(CACHE_NAME).then(function(cache) {
    
    
            return fetch(new_request_url).then(function (response) {
    
    
                if (response.status < 400) {
    
    //回退成功,则进行缓存,这个地方肯定是可以获取到status因为地址替换成本地的了
                    // console.log("【yes2】 put in the cache" + event.request.url);
                    console.log("fetch retry [success],old_url:%s ,new_url:%s",event.request.url,new_request_url);

                    cache.put(event.request, response.clone());
                }else{
    
    
                    console.warn("fetch retry [error:%s],old_url:%s,new_url:%s",response.status,event.request.url,new_request_url);
                }
                // console.log(response);
                return response;
            }).catch(function (error){
    
    
                console.warn("fetch retry [error2:%s],old_url:%s,new_url:%s",error,event.request.url,new_request_url);
                // throw error;
            });
        })
    }else{
    
    
        console.warn("fetch error and [not retry]",event.request.url);
        return false;
    }
}

function is_same_request(urla,urlb){
    
    
    const white_query =new Set([// 除了这这些参数,其它的查询参数必须要一致,才认为是同一个请求
        "t",
        "v",
        "version",
        "time",
        "ts",
        "timestamp"
    ]);

    const a_url = urla.split('?');
    const b_url = urlb.split('?');
    if (a_url[0] !== b_url[0] ){
    
    
        return false;
    }

    const a_params = new URLSearchParams('?' + a_url[1]);
    const b_params = new URLSearchParams('?' + b_url[1]);

    // 显示所有的键
    for (var key of a_params.keys()) {
    
    
        if (white_query.has(key)){
    
    //对于版本号的key 忽略
            continue;
        }
        if (a_params.get(key) !== b_params.get(key)){
    
    //其它key的值必须相等,比如type=POST 这种
            return false;
        }
    }

    return true;
}

function getMatchRequestResponse(cache_response_list,request_url) {
    
    
    if (cache_response_list){
    
    
        for (const cache_response of cache_response_list) {
    
    
            // console.log(cache_response.url,request_url)
            if (is_same_request(cache_response.url,request_url)){
    
    
                return cache_response;
            }
        }
    }
    return null;
}
// 拦截请求使用缓存的内容
self.addEventListener('fetch', function(event) {
    
    
    // console.log('Handling fetch event for', event.request.url);
    if(event.request.method !== "GET") {
    
    
        return false;
    }else{
    
    
        if (isExitInCacheList(caceheList, event.request.url) && !isExitInCacheList(notCacheList, event.request.url)){
    
    
            // 只捕获需要加入cache的请求
            // 劫持 HTTP Request
            // console.log(event.request.url);
            event.respondWith(
                caches.open(CACHE_NAME).then(function(cache) {
    
    
                    // var start = performance.now();
                    // return cache.match(event.request,{"ignoreSearch":true}).then(function(cache_response) {
    
    
                    return cache.matchAll(event.request,{
    
    "ignoreSearch":true}).then(function(cache_response_list) {
    
    
                        const cache_response = getMatchRequestResponse(cache_response_list,event.request.url);
                        // var end = performance.now();
                        // console.log("match all cost:",end - start,"ms",event.request.url)
                        if (cache_response && cache_response.url === event.request.url) {
    
    //地址(包含查询参数)完全一致才返回缓存
                            // 使用 Service Worker 回應
                            // console.log("【cache】use the cache " + event.request.url)
                            return cache_response;
                        } else {
    
    
                            // console.log("not use cache",event.request.url);
                            return fetch(event.request)
                                .then(function(response) {
    
    
                                    //判断查询参数里面是否存在type参数,如果存在
                                    // 由于跨域访问导致获得response是非透明响应无法获取响应码(响应码是0
                                    //https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
                                    if (response.status < 400){
    
    //对于响应码为0,暂时无法进一步判断,只能全部认为加载成功
                                        //跨域的地址 服务器端的错误目前不会回退,只能直接加到cache里面,如果服务器问题解除需要更新缓存
                                        // console.log("【yes】 put in the cache" + event.request.url);
                                        if (cache_response && is_same_request(cache_response.url,event.request.url)){
    
    //删除旧版本号的资源
                                            // console.log("存在缓存,但是可查询的字符串版本号不一致,所以需要删除缓存",cache_response.url,event.request.url)
                                            cache.delete(cache_response.url);
                                        }
                                        cache.put(event.request, response.clone());
                                    }else{
    
    
                                        console.warn("response is not ok",response.status,response.statusText,event.request.url);
                                        const new_response =  fetchLocal(event);
                                        if (new_response){
    
    
                                            return new_response;
                                        }else {
    
    //在获取response 失败的时候,优先考虑可以回退旧版本的response里面
                                            if (cache_response && is_same_request(cache_response.url,event.request.url)){
    
    
                                                return cache_response;
                                            }
                                            return response;
                                        }
                                    }
                                    return response;
                                })
                                .catch(function(error) {
    
    
                                    console.error(error)
                                    const response =  fetchLocal(event);
                                    if (response){
    
    
                                        return response;
                                    }else {
    
    
                                        // console.log('Fetching request url ,' +event.request.url+' failed:', error);
                                        // throw error;
                                    }
                                });
                        }
                    })
                })
            );
        }else{
    
    
            return false;
        }
    }
});

Guess you like

Origin blog.csdn.net/nineya_com/article/details/124531321