当前位置:网站首页>Use service worker to preferentially request resources - continuous update

Use service worker to preferentially request resources - continuous update

2022-06-13 04:37:00 Let let

Preface

See my personal blog for long-term updates , Because I can't synchronize updates on so many platforms https://blog.imlete.cn/article/Service-Worker-Preferred-Request-Resource.html

When your site or blog has multiple deployment sites , The access speed deployed on a platform is relatively fast , So you resolve your domain name to this platform , But sometimes it gets very slow , At this time, other sites may become a little faster than what you currently use , Is there any way to resolve domain names back and forth ? It's too troublesome

Is there any way to directly return to the fastest website resources ?

  1. Use the domain name management platform , Some platforms can parse sites in different networks or regions
    for example Tencent cloud It can distinguish and analyze the three major domestic operators 、 Territory 、 overseas 、 And other parsing options ( Not very easy to use , You also need to test yourself , Is it difficult to ask friends who use mobile phones from other operators to help you test your speed ~)
  2. Use js Block all requests from the website , And tamper with sending requests to all your sites , Among these sites, if which site returns the fastest , Then use the fastest returned information , At the same time, cut off all other requests

Text

This article details how to Use Service Worker Preferred request resource Make your website faster than before , A more stable

Service Worker In the following content, they are uniformly called sw

Service Worker

For more details, please see https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API

Service workers Act essentially as Web Applications 、 Browser and Web ( When available ) Proxy server between

Service worker Running on the worker Context , So it can't access DOM. Relative to the main driver of the application JavaScript Threads , It runs in other threads , So it won't block .

For safety reasons ,Service workers Only by HTTPS bearing , After all, the ability to modify network requests can be very dangerous when exposed to man in the middle attacks .

register

register sw It's simple , Just one line of code , If the registration is successful , be sw Will be downloaded to the client and installed and activated , This step is just registration , The whole thing is : download —> install —> Activate

Be careful : sw The registration log of is recorded in Chrome In the browser, you can access chrome://serviceworker-internals see

navigator.serviceWorker.register('/Service-Worker.js')

among /Service-Worker.js Must be under the current domain js file , He cannot be a member of another domain , Even if js The contents of the file are exactly the same , That won 't do either

If you only want to write in a certain path, use sw Words , You can use scope Options , Of course /Service-Worker.js The location of can also be customized ( As long as it is homologous and https The agreement will do ), As follows, only in /article/ Article page sw To launch , Other paths are written sw No processing

navigator.serviceWorker.register('/sw-test/Service-Worker.js', {
     scope: '/article/' })

And must be https agreement , If it's local 127.0.0.1 or localhost It's allowed

This is a complete registration code

Place the installation code in <head> after

<script> ;(function () {
       if ('serviceWorker' in navigator) {
       navigator.serviceWorker .register('/sw.js') .then((result) => {
       //  Determine if... Is installed sw if (!localStorage.getItem('installSW')) {
       localStorage.setItem('installSW', true) //  There is no need to clean up here setInterval 了 , Because there will be no after the page is refreshed  setInterval(() => {
       //  Judge sw After installation , Is it active , Refresh the page after activation  if (result && result.active && result.active.state === 'activated') {
       location.reload() } }, 100) } }) .catch((err) => {
       console.log(err) }) } })() </script>

Life cycle

installing state

When the registration is successful, it will trigger install event , Then the trigger activate event , At this point, if you refresh the page again , Neither of them will be triggered

until /sw.js It has changed , It will trigger once install ( It's not just code changes , Even if it is one more space or one less space , Or writing a comment will be triggered ), But only implemented install event , It's not implemented activate event

activing state

Why? activate The event is not triggered ? Because there is already one sw 了 , It is in a waiting state , As for when it will be triggered , That's what happened before we waited sw It will not trigger until it stops activate event

Is there any way not to let it wait ? The answer is : Yes

Use skipWaiting() Skip waiting , It returns a promise object ( Asynchronous ), Prevention is still being implemented skipWaiting() When I get there, I just jump to activate event , We need to use async/await, You can also use event.waitUntil(skipWaiting()) Method to skipWaiting() Put it in there , and async/await The effect is the same

// sw.js

//  stay sw Can be used in this or self Express oneself 
self.addEventListener('install', async (event) => {
    
  // event.waitUntil(self.skipWaiting())
  await self.skipWaiting()
})

Trigger activate After the event , This time the web page will not be sw Managed , The next page refresh is required sw management , So how to make it manage pages immediately ?

For more details, please see :
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting
https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim

self.addEventListener('activate', async (event) => {
    
  // event.waitUntil(self.clients.claim())
  await self.clients.claim() //  Manage pages now 
})

install

The above life cycle has been described in detail

// sw.js
self.addEventListener('install', async (event) => {
    
  // event.waitUntil(self.skipWaiting())
  await self.skipWaiting()
})

Capture request

// sw.js
self.addEventListener('fetch', async (event) => {
    
  //  All requests have to go to  handleRequest  In function processing 
  handleRequest(event.request)
    //  If  handleRequest  If the request is successful, the data will be responded to the web page 
    .then((result) => event.respondWith(result))
    //  If  handleRequest  request was aborted , Don't do anything? ( This is the actual request of the web page )
    .catch(() => 0)
})

//  Processing requests 
function handleRequest(req) {
    
  return fetch(req.url)
}

event.respondWith(): Give the browser a response , Because we have used Fetch API Sent a request for the browser , And get the result and return , Then it is natural to return to the browser

Fetch API You can talk to XMLHttpRequest equally , You can send any request

It is worth mentioning that Fetch API There will be cross domain problems when sending requests , Once it is intercepted across domains , Then there is no return from the above , The page will not display the requested content ( For example, the image is blocked across domains ), and img、script Label that they do not have cross domain request problems , So the above catch Catch an exception 0 and null almost
Since it is not used event.respondWith Of course, there is no data returned to the browser , Then the browser requests itself ( This problem is well avoided )

Tamper request

We can all use Fetch API Sent a request for the browser , Can it be tampered with ?

//  Processing requests 
function handleRequest(req) {
    
  //  Just an example , More wonderful uses are waiting for you to explore 
  const str = 'https://cdn.jsdelivr.net/npm/xhr-ajax/dist/'
  const url = req.url.replace(str + 'ajax.js', str + 'ajax.min.js')
  return fetch(url)
}

Code above , We can ajax Third party Library of requests js File requests become compressed requests , And return to browser ( Tampering succeeded )

//  Batch request 
function handleRequest(req) {
    
  //  It can be more than one 
  const urls = [
    "https://cdn.jsdelivr.net/npm/xhr-ajax/dist/ajax.min.js",
    "https://unpkg.com/xhr-ajax/dist/ajax.min.js",
  ];
  //  Interrupt one or more requests 
  const controller = new AbortController();
  //  The request can be interrupted 
  // https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController#%E5%B1%9E%E6%80%A7
  const signal = controller.signal;

  //  Traversal converts all request addresses to promise
  const PromiseAll = urls.map((url) => {
    
    return new Promise(async (resolve, reject) => {
    
      fetch(url, {
     signal })
        .then(
          (res) =>
            new Response(await res.arrayBuffer(), {
    
              status: res.status,
              headers: res.headers,
            })
        )
        .then((res) => {
    
          if (res.status !== 200) reject(null);
          //  As long as one request returns successfully, disconnect all requests 
          controller.abort(); //  interrupt 
          resolve(res);
        })
        .catch(() => reject(null));
    });
  });
  //  Use  Promise.any  Send bulk requests , It receives an iteratable object , For example, an array is an iteratable object 
  return Promise.any(PromiseAll)
    .then((res) => res)
    .catch(() => null);
}

Promise.any Specific please see : https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
AbortController Specific please see : https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController

As long as any one of the iteration objects passed in promise Return to success (resolve) state , Then it returns the success status , If all of them promises All failed , Then all the failures will be returned

So as long as Promise.any There is a successful status data return , Then we will respond this data to the browser , Others All requests are cut off , In this way, you can efficiently provide users with the fastest response resources in different regions ~

This is also the problem we need to solve before the text begins

complete sw.js file

This is a summary I wrote sw.js file , You just need to put the following origin Just change the array to your blog address , Others can be left untouched , If you want to add something new , It's up to you ~ Ha ha ha

const origin = ['https://blog.imlete.cn', 'https://lete114.github.io']

const cdn = {
    
  gh: {
    
    jsdelivr: 'https://cdn.jsdelivr.net/gh',
    fastly: 'https://fastly.jsdelivr.net/gh',
    gcore: 'https://gcore.jsdelivr.net/gh',
    testingcf: 'https://testingcf.jsdelivr.net/gh',
    test1: 'https://test1.jsdelivr.net/gh',
    tianli: 'https://cdn1.tianli0.top/gh'
  },
  combine: {
    
    jsdelivr: 'https://cdn.jsdelivr.net/combine',
    fastly: 'https://fastly.jsdelivr.net/combine',
    gcore: 'https://gcore.jsdelivr.net/combine',
    testingcf: 'https://testingcf.jsdelivr.net/combine',
    test1: 'https://test1.jsdelivr.net/combine',
    tianli: 'https://cdn1.tianli0.top/combine'
  },
  npm: {
    
    jsdelivr: 'https://cdn.jsdelivr.net/npm',
    fastly: 'https://fastly.jsdelivr.net/npm',
    gcore: 'https://gcore.jsdelivr.net/npm',
    testingcf: 'https://testingcf.jsdelivr.net/npm',
    test1: 'https://test1.jsdelivr.net/npm',
    eleme: 'https://npm.elemecdn.com',
    unpkg: 'https://unpkg.com',
    tianli: 'https://cdn1.tianli0.top/npm'
  }
}

self.addEventListener('install', async () => {
    
  await self.skipWaiting()
})

self.addEventListener('activate', async () => {
    
  await self.clients.claim()
})

self.addEventListener('fetch', async (event) => {
    
  try {
    
    event.respondWith(handleRequest(event.request))
  } catch (e) {
    }
})

//  Return response 
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)
  //  In order to obtain  cdn  type 
  //  For example, get gh (https://cdn.jsdelivr.net/gh)
  const path = urlObj.pathname.split('/')[1]

  //  matching  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)
      }
    }
  }

  //  If it's above  cdn  Traverse   Match to  cdn  Send the request directly and uniformly ( It won't go down )
  if (urls.length) return fetchAny(urls)

  //  Merge the current web site visited by the user with all source sites 
  let origins = [location.origin, ...origin]

  //  Traverse to determine whether the current request is the origin host 
  const is = origins.find((i) => {
    
    const {
     hostname } = new URL(i)
    const reg = new RegExp(hostname)
    return urlStr.match(reg)
  })

  //  If it is the origin , Then race to get ( It won't go down )
  if (is) {
    
    origins = origins.map((i) => i + urlObj.pathname + urlObj.search)
    return fetchAny(origins)
  }
  //  The exception is thrown to make sw Don't intercept requests 
  throw new Error(' Not the origin ')
}

// Promise.any  Of  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))
          }
        )
      })
    })
  }
}

//  Send all requests 
function fetchAny(urls) {
    
  //  Interrupt one or more requests 
  const controller = new AbortController()
  const signal = controller.signal

  //  Traversal converts all request addresses to promise
  const PromiseAll = urls.map((url) => {
    
    return new Promise((resolve, reject) => {
    
      fetch(url, {
     signal })
        .then(progress)
        .then((res) => {
    
          const r = res.clone()
          if (r.status !== 200) reject(null)
          controller.abort() //  interrupt 
          resolve(r)
        })
        .catch(() => reject(null))
    })
  })

  //  Determine whether the browser supports  Promise.any
  if (!Promise.any) createPromiseAny()

  //  Who will go back first " Success status " Whose content is returned , If they all return " Failure status " Then return to null
  return Promise.any(PromiseAll)
    .then((res) => res)
    .catch(() => null)
}
原网站

版权声明
本文为[Let let]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/164/202206130423394775.html