<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【秋招求職之路】騰訊-QQ音樂一面復(fù)盤總結(jié)

          共 23203字,需瀏覽 47分鐘

           ·

          2021-06-11 02:34

          往期精彩文章推薦



          騰訊 QQ音樂 面經(jīng)

          一面

          自我介紹

          了解ajax跨域嘛?

          什么是跨域

          回顧一下 URI 的組成:

          瀏覽器遵循「同源政策」(scheme(協(xié)議)host(主機(jī))port(端口) 都相同則為同源)。非同源站點有這樣一些限制:

          • 不能讀取和修改對方的 DOM
          • 不讀訪問對方的 Cookie、IndexDB 和 LocalStorage
          • 限制 XMLHttpRequest 請求。(后面的話題著重圍繞這個)

          當(dāng)瀏覽器向目標(biāo) URI 發(fā) Ajax 請求時,只要當(dāng)前 URL 和目標(biāo) URL 不同源,則產(chǎn)生跨域,被稱為 跨域請求。

          跨域請求的響應(yīng)一般會被瀏覽器所攔截,注意,是被「瀏覽器」攔截,響應(yīng)其實是成功到達(dá)客戶端了。那這個攔截是如何發(fā)生呢?

          首先要知道的是,瀏覽器是多進(jìn)程的,以 Chrome 為例,進(jìn)程組成如下:

          「WebKit 渲染引擎」「V8 引擎」都在渲染進(jìn)程當(dāng)中。

          當(dāng) xhr.send 被調(diào)用,即 Ajax 請求準(zhǔn)備發(fā)送的時候,其實還只是在「渲染進(jìn)程」的處理。為了防止黑客通過腳本觸碰到系統(tǒng)資源,瀏覽器將每一個渲染進(jìn)程裝進(jìn)了沙箱,并且為了防止 CPU 芯片一直存在的Spectre 和 Meltdown漏洞,采取了站點隔離的手段,給每一個不同的站點(一級域名不同)分配了沙箱,互不干擾。具體見YouTube上Chromium安全團(tuán)隊的演講視頻。

          在沙箱當(dāng)中的渲染進(jìn)程是沒有辦法發(fā)送網(wǎng)絡(luò)請求的,那怎么辦?只能通過網(wǎng)絡(luò)進(jìn)程來發(fā)送。那這樣就涉及到「進(jìn)程間通信」(IPC,Inter Process Communication)了。

          總的來說就是利用Unix Domain Socket套接字,配合事件驅(qū)動的高性能網(wǎng)絡(luò)并發(fā)庫libevent完成進(jìn)程的 IPC 過程。

          好,現(xiàn)在數(shù)據(jù)傳遞給了「瀏覽器主進(jìn)程」,主進(jìn)程接收到后,才真正地發(fā)出相應(yīng)的網(wǎng)絡(luò)請求。在服務(wù)端處理完數(shù)據(jù)后,將響應(yīng)返回,主進(jìn)程檢查到跨域,且沒有cors(后面會詳細(xì)說)響應(yīng)頭,將響應(yīng)體全部丟掉,并不會發(fā)送給渲染進(jìn)程。這就達(dá)到了攔截數(shù)據(jù)的目的。

          cors跨域怎么做?

          CORS 其實是 W3C 的一個標(biāo)準(zhǔn),全稱是 跨域資源共享 。它需要瀏覽器和服務(wù)器的共同支持,具體來說,非 IE 和 IE10 以上支持CORS,服務(wù)器需要附加特定的響應(yīng)頭,后面具體拆解。不過在弄清楚 CORS 的原理之前,我們需要清楚兩個概念: 「簡單請求」「非簡單請求」

          瀏覽器根據(jù)請求方法和請求頭的特定字段,將請求做了一下分類,具體來說規(guī)則是這樣,凡是滿足下面條件的屬于「簡單請求」:

          • 請求方法為 GET、POST 或者 HEAD
          • 請求頭的取值范圍: Accept、Accept-Language、Content-Language、Content-Type(只限于三個值application/x-www-form-urlencodedmultipart/form-datatext/plain)

          瀏覽器畫了這樣一個圈,在這個圈里面的就是「簡單請求」, 圈外面的就是「非簡單請求」,然后針對這兩種不同的請求進(jìn)行不同的處理。

          簡單請求

          請求發(fā)出去之前,瀏覽器做了什么?

          它會自動在請求頭當(dāng)中,添加一個Origin字段,用來說明請求來自哪個。服務(wù)器拿到請求之后,在回應(yīng)時對應(yīng)地添加Access-Control-Allow-Origin字段,如果Origin不在這個字段的范圍中,那么瀏覽器就會將響應(yīng)攔截。

          因此,Access-Control-Allow-Origin字段是服務(wù)器用來決定瀏覽器是否攔截這個響應(yīng),這是必需的字段。與此同時,其它一些可選的功能性的字段,用來描述如果不會攔截,這些字段將會發(fā)揮各自的作用。

          「Access-Control-Allow-Credentials」。這個字段是一個布爾值,表示是否允許發(fā)送 Cookie,對于跨域請求,瀏覽器對這個字段「默認(rèn)值」設(shè)為 false,而如果需要拿到瀏覽器的 Cookie,需要添加這個響應(yīng)頭并設(shè)為true, 并且在前端也需要設(shè)置withCredentials屬性:

          let xhr = new XMLHttpRequest();
          xhr.withCredentials = true;

          「Access-Control-Expose-Headers」。這個字段是給 XMLHttpRequest 對象賦能,讓它不僅可以拿到基本的 6 個響應(yīng)頭字段(包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified 和 Pragma), 還能拿到這個字段聲明的響應(yīng)頭字段。比如這樣設(shè)置:

          Access-Control-Expose-Headers: aaa

          那么在前端可以通過 XMLHttpRequest.getResponseHeader('aaa')拿到 aaa 這個字段的值。

          非簡單請求

          非簡單請求相對而言會有些不同,體現(xiàn)在兩個方面: 「預(yù)檢請求」「響應(yīng)字段」

          我們以 PUT 方法為例。

          var url = 'http://xxx.com';
          var xhr = new XMLHttpRequest();
          xhr.open('PUT', url, true);
          xhr.setRequestHeader('X-Custom-Header''xxx');
          xhr.send();

          當(dāng)這段代碼執(zhí)行后,首先會發(fā)送「預(yù)檢請求」。這個預(yù)檢請求的請求行和請求體是下面這個格式:

          OPTIONS / HTTP/1.1
          Origin: 當(dāng)前地址
          Host: xxx.com
          Access-Control-Request-Method: PUT
          Access-Control-Request-Headers: X-Custom-Header

          預(yù)檢請求的方法是 OPTIONS,同時會加上Origin源地址和Host目標(biāo)地址,這很簡單。同時也會加上兩個關(guān)鍵的字段:

          • Access-Control-Request-Method, 列出 CORS 請求用到哪個HTTP方法
          • Access-Control-Request-Headers,指定 CORS 請求將要加上什么請求頭

          這是預(yù)檢請求。接下來是「響應(yīng)字段」,響應(yīng)字段也分為兩部分,一部分是對于「預(yù)檢請求」的響應(yīng),一部分是對于 「CORS 請求」的響應(yīng)。

          「預(yù)檢請求的響應(yīng)」。如下面的格式:

          HTTP/1.1 200 OK
          Access-Control-Allow-Origin: *
          Access-Control-Allow-Methods: GET, POST, PUT
          Access-Control-Allow-Headers: X-Custom-Header
          Access-Control-Allow-Credentials: true
          Access-Control-Max-Age: 1728000
          Content-Type: text/html; charset=utf-8
          Content-Encoding: gzip
          Content-Length: 0

          其中有這樣幾個關(guān)鍵的「響應(yīng)頭字段」:

          • Access-Control-Allow-Origin: 表示可以允許請求的源,可以填具體的源名,也可以填*表示允許任意源請求。
          • Access-Control-Allow-Methods: 表示允許的請求方法列表。
          • Access-Control-Allow-Credentials: 簡單請求中已經(jīng)介紹。
          • Access-Control-Allow-Headers: 表示允許發(fā)送的請求頭字段
          • Access-Control-Max-Age: 預(yù)檢請求的有效期,在此期間,不用發(fā)出另外一條預(yù)檢請求。

          在預(yù)檢請求的響應(yīng)返回后,如果請求不滿足響應(yīng)頭的條件,則觸發(fā)XMLHttpRequestonerror方法,當(dāng)然后面真正的「CORS」請求也不會發(fā)出去了。

          「CORS 請求的響應(yīng)」。繞了這么一大轉(zhuǎn),到了真正的 CORS 請求就容易多了,現(xiàn)在它和「簡單請求」的情況是一樣的。瀏覽器自動加上Origin字段,服務(wù)端響應(yīng)頭返回「Access-Control-Allow-Origin」。可以參考以上簡單請求部分的內(nèi)容。

          說說jsonp原理

          雖然XMLHttpRequest對象遵循同源政策,但是script標(biāo)簽不一樣,它可以通過 src 填上目標(biāo)地址從而發(fā)出 GET 請求,實現(xiàn)跨域請求并拿到響應(yīng)。這也就是 JSONP 的原理,接下來我們就來封裝一個 JSONP:

          /* 自定義封裝jsonp */
          let jsonp = ({ url, params, callbackName }) => {
            let generateUrl = () => {
              let dataStr = ''
              for (let key in params) {
                dataStr += `${key}=${params[key]}`
              }
              dataStr += `callback=${callbackName}`
              return `${url}?${dataStr}`
            }
            return new Promise((resolve, reject) => {
             // 創(chuàng)建 script 元素并加入到當(dāng)前文檔中
              let scriptEle = document.createElemet('script')
              script.src = generateUrl
              document.body.appendChild(scriptEle)
              try {
                resolve(data)
              } catch (e) {
                reject(e)
              } finally {
              // script 執(zhí)行完了,成為無用元素,需要清除
                document.body.removeChild(scriptEle)
              }
            })
          }

          當(dāng)然在服務(wù)端也會有響應(yīng)的操作, 以 「express」 為例:

          /* 服務(wù)端相關(guān)操作,以express為例 */
          let express = require('express')
          let app = express()
          const port = '3000'
          app.get('/',function(req,res){
            let {a,b,callback} = req.query
            console.log(a)
            console.log(b)
            // 注意,返回給script標(biāo)簽,瀏覽器直接把這部分字符串執(zhí)行
            res.send(`${callback}('數(shù)據(jù)包')`)
          })
          app;listen(port)

          前端這樣簡單地調(diào)用一下就好了:

          /* 前端調(diào)用 */
          jsonp({
            url'http://localhost:3000',
            params: {
              a1,
              b2
            }
          }).then(data=>{
            console.log(data) //該數(shù)據(jù)包
          })

          CORS相比,JSONP 最大的優(yōu)勢在于兼容性好,IE 低版本不能使用 CORS 但可以使用 JSONP,缺點也很明顯,請求方法單一,只支持 GET 請求。

          拓展:Nginx

          Nginx 是一種高性能的 反向代理 服務(wù)器,可以用來輕松解決跨域問題。

          正向代理幫助客戶端「訪問」客戶端自己訪問不到的服務(wù)器,然后將結(jié)果返回給客戶端。

          反向代理拿到客戶端的請求,將請求轉(zhuǎn)發(fā)給其他的服務(wù)器,主要的場景是維持服務(wù)器集群的「負(fù)載均衡」,換句話說,反向代理幫「其它的服務(wù)器」拿到請求,然后「選擇一個合適」的服務(wù)器,將請求轉(zhuǎn)交給它。

          因此,兩者的區(qū)別就很明顯了,正向代理服務(wù)器是幫「客戶端」做事情,而反向代理服務(wù)器是幫其它的「服務(wù)器」做事情。

          好了,那 Nginx 是如何來解決跨域的呢?

          比如說現(xiàn)在客戶端的域名為 client.com,服務(wù)器的域名為 server.com,客戶端向服務(wù)器發(fā)送 Ajax 請求,當(dāng)然會跨域了,那這個時候讓 Nginx 登場了,通過下面這個配置:

          server {
            listen  80;
            server_name  client.com;
            location /api {
              proxy_pass server.com;
            }
          }

          Nginx 相當(dāng)于起了一個跳板機(jī),這個跳板機(jī)的域名也是 client.com,讓客戶端首先訪問 client.com/api,這當(dāng)然沒有跨域,然后 Nginx 服務(wù)器作為反向代理,將請求轉(zhuǎn)發(fā)給 server.com,當(dāng)響應(yīng)返回時又將響應(yīng)給到客戶端,這就完成整個跨域請求的過程。

          還有一些不太常用的方式,了解即可,比如postMessage,當(dāng)然WebSocket也是一種方式,但是已經(jīng)不屬于 HTTP 的范疇。

          如果是用node來做跨域的話,你會怎么做?

          (1) 瀏覽器會自動進(jìn)行 CORS 通信,實現(xiàn) CORS 通信的關(guān)鍵是后端。只要后端實現(xiàn)了 CORS,就實現(xiàn)了跨域。(2) 服務(wù)端設(shè)置 Access-Control-Allow-Origin 就可以開啟 CORS。該屬性表示哪些域名可以訪 問資源,如果設(shè)置通配符(*)則表示所有網(wǎng)站都可以訪問資源。(3)設(shè)置 CORS 和前端沒什么關(guān)系,但是通過這種方式解決跨域問題的話,會在發(fā)送請求時出現(xiàn)兩種情況,分 別為簡單請求和復(fù)雜請求。(上文已經(jīng)提到)

          先以 express 為例

          //allow custom header and CORS
          app.all('*',function (req, res, next{
            res.header('Access-Control-Allow-Origin''*');
            res.header('Access-Control-Allow-Headers''Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
            res.header('Access-Control-Allow-Methods''PUT, POST, GET, DELETE, OPTIONS');

            if (req.method == 'OPTIONS') {
              res.send(200); /讓options請求快速返回/
            }
            else {
              next();
            }
          });

          介紹一個 cors 模塊,引入就可以解決了。代碼如下:

          /* express引入cors模塊 */
          let express = require('express')
          let cors = require('cors')
          let app = express()

          app.use(cors())

          app.get('/products/:id'function (req, res, next{
            res.json({ msg'This is CORS-enabled for all origins!' })
          })

          app.listen(80function ({
            console.log('CORS-enabled web server listening on port 80')
          })

          然后,我們再以 koa 為例

          「服務(wù)端: 3001端口」

          var Koa = require('koa');
          var Router = require('koa-router');
          const bodyParser = require('koa-bodyparser')
          var app = new Koa();
          var router = new Router();

          app.use(bodyParser()); // 解析body數(shù)據(jù)
          router.options('/test',async (ctx, next) => {
            ctx.set("Access-Control-Allow-Origin""*");
            ctx.set("Access-Control-Allow-Headers""Content-Type");
            ctx.set("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
            ctx.set('Access-Control-Allow-Credentials'true);
            ctx.set("Content-Type""application/json;charset=utf-8");
            ctx.status = 204;
          });
          router.post('/test',async (ctx, next) => {
            // ctx.router available
            ctx.set("Access-Control-Allow-Origin""*");
            ctx.set("Access-Control-Allow-Headers""Content-Type");
            ctx.set("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
            ctx.set('Access-Control-Allow-Credentials'true);
            ctx.set("Content-Type""application/json;charset=utf-8");
            ctx.body = {
              status'success',
              result: ctx.request.body
            };
          });

          app
            .use(router.routes())
            .use(router.allowedMethods());

          app.listen(3001);

          「客戶端:」

          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <title>測試</title>
          </head>
          <body>
            <script>
              fetch('http://localhost:3001/test', {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json;charset=utf-8'
                },
                body: JSON.stringify({
                  data: 'Test'
                })
              }).then(data => {
                console.log(data);
              })
            </script>
          </body>
          </html>

          koa 也有個 cors 模塊.代碼如下:

          /* koa引入cors模塊 */
          var koa = require('koa')
          var route = require('koa-route')
          var cors = require('koa-cors')
          var app = koa()

          app.use(cors())

          app.use(
            route.get('/'function ({
              this.body = { msg'Hello World!' }
            })
          )
          app.listen(3000)

          怎樣給一個新增的dom節(jié)點綁定事件?(詢問事件代理的作用)

          事件委托(事件代理)的作用?

          • 支持為同一個DOM元素注冊多個同類型事件
          • 可將事件分成事件捕獲和事件冒泡機(jī)制

          「注冊多個事件」

          用以往注冊事件的方法,如果存在多個事件,后注冊的事件會覆蓋先注冊的事件

          //index.html
          <div id="div1"></div>

          window.onload = function(){
              let div1 = document.getElementById('div1');
              div1.onclick = function(){
                  console.log('打印第一次')
              }
              div1.onclick = function(){
                  console.log('打印第二次')
              }
          }

          可以看到第二個點擊注冊事件覆蓋了第一個注冊事件,只執(zhí)行了console.log('打印第二次');

          addEventListener(type,listener,useCapture)實現(xiàn)

          • type: 必須,String類型,事件類型
          • listener: 必須,函數(shù)體或者JS方法
          • useCapture: 可選,boolean類型。指定事件是否發(fā)生在捕獲階段。「默認(rèn)」為false,事件發(fā)生在「冒泡」階段
          <div id="div1"></div>

          window.onload = function(){
              let div1 = document.getElementById('div1');
              div1.addEventListener('click',function(){
                  console.log('打印第一次')
              })
              div1.addEventListener('click',function(){
                  console.log('打印第二次')
              })
          }

          可以看到兩個注冊事件都成功觸發(fā)了。useCapture是事件委托的關(guān)鍵,我們后面詳解

          「事件捕獲和事件冒泡機(jī)制」

          事件捕獲

          當(dāng)一個事件觸發(fā)后,從Window對象觸發(fā),不斷「經(jīng)過下級節(jié)點,直到目標(biāo)節(jié)點」。在事件到達(dá)目標(biāo)節(jié)點之前的過程就是捕獲階段。所有經(jīng)過的節(jié)點,都會觸發(fā)對應(yīng)的事件

          事件冒泡

          當(dāng)事件到達(dá)目標(biāo)節(jié)點后,會「沿著捕獲階段的路線原路返回」。同樣,所有經(jīng)過的節(jié)點,都會觸發(fā)對應(yīng)的事件

          通過例子理解兩個事件機(jī)制: 例子:假設(shè)有body和body節(jié)點下的div1均有綁定了一個注冊事件. 效果:

          • 當(dāng)為事件捕獲(useCapture:true)時,先執(zhí)行body的事件,再執(zhí)行div的事件
          • 當(dāng)為事件冒泡(useCapture:false)時,先執(zhí)行div的事件,再執(zhí)行body的事件
          //當(dāng)useCapture為默認(rèn)false時,為事件冒泡
          <body>
              <div id="div1"></div>
          </body>

          window.onload = function(){
              let body = document.querySelector('body');
              let div1 = document.getElementById('div1');
              body.addEventListener('click',function(){
                  console.log('打印body')
              })
              div1.addEventListener('click',function(){
                  console.log('打印div1')
              })
          }
          /
          /結(jié)果:打印div1  打印body
          //當(dāng)useCapture為true時,為事件捕獲
          <body>
              <div id="div1"></div>
          </body>

          window.onload = function(){
              let body = document.querySelector('body');
              let div1 = document.getElementById('div1');
              body.addEventListener('click',function(){
                  console.log('打印body')
              },true)
              div1.addEventListener('click',function(){
                  console.log('打印div1')
              })
          }

          /
          /結(jié)果:打印body   打印div1

          事件委托和新增節(jié)點綁定事件的關(guān)系?

          事件委托的優(yōu)點:

          • 「提高性能」: 每一個函數(shù)都會占用內(nèi)存空間,只需添加一個事件處理程序代理所有事件,所占用的內(nèi)存空間更少。
          • 「動態(tài)監(jiān)聽」: 使用事件委托可以「自動綁定動態(tài)添加的元素」,即新增的節(jié)點不需要主動添加也可以一樣具有和其他元素一樣的事件。

          例子解析:

          <script>
              window.onload = function(){
                  let div = document.getElementById('div');
                  
                  div.addEventListener('click',function(e){
                      console.log(e.target)
                  })
                  
                  let div3 = document.createElement('div');
                  div3.setAttribute('class','div3')
                  div3.innerHTML = 'div3';
                  div.appendChild(div3)
              }
          </script>


          <body>
              <div id="div">
                  <div class="div1">div1</
          div>
                  <div class="div2">div2</div>
              </div>
          </
          body>

          雖然沒有給div1和div2添加點擊事件,但是無論是點擊div1還是div2,都會打印當(dāng)前節(jié)點。因為其父級綁定了點擊事件,點擊div1后冒泡上去的時候,執(zhí)行父級的事件。

          這樣無論后代新增了多少個節(jié)點,一樣具有這個點擊事件的功能。

          了解瀏覽器緩存嗎?(強(qiáng)緩存、協(xié)商緩存)你怎樣更新強(qiáng)緩存呢?

          1.強(qiáng)緩存

          強(qiáng)緩存是指直接通過本地緩存獲取資源,不用經(jīng)過服務(wù)器

          常用字段:

          • expires 值為一個絕對時間的 GMT 格式的時間字符串,如果發(fā)送請求的時間在 expires 之前,那么本地緩存有效,否則就會發(fā)送請求到服務(wù)器來獲取資源。

          缺點:無法保證客戶端按照標(biāo)準(zhǔn)時間設(shè)定

          • Cache-Control(常用值如下):

          max-age:允許的最大緩存秒數(shù) no-store:不允許使用緩存,「每次都要向服務(wù)器獲取」no-cache:不允許使用本地緩存,「每次都要向服務(wù)器進(jìn)行協(xié)商緩存」public:允許被「所有」中間代理和終端瀏覽器緩存 private:只允許被終端瀏覽器緩存 Cache-Control 比 expires 優(yōu)先級高

          2.協(xié)商緩存

          協(xié)商緩存是指客戶端「向服務(wù)端確認(rèn)資源是否用」

          常用字段:

          • Last-Modified / If-Modified-Since:

          值是 GMT 格式的時間字符串,具體流程如下:

          瀏覽器第一次請求資源,服務(wù)端返回 Last-Modified,表示資源在服務(wù)端的最后修改時間。瀏覽器第二次請求的時候會在請求頭上攜帶If-Modified-Since,值為上次返回的 Last-Modified服務(wù)端收到請求后,比較保存的 Last-Modified 和 請求報文中的 If-Modified-Since,如果一致就返回 304 狀態(tài)碼,不一致就返回新資源,同時更新 Last-Modified

          • ETag / If-None-Match

          值是服務(wù)器生成的資源標(biāo)識符,當(dāng)資源修改后這個值會被改變,

          具體流程與 Last-Modified、If-Modified-Since 相似,但與 Last-Modified 不一樣的是,當(dāng)服務(wù)器返回304的響應(yīng)時,由于 ETag 重新生成過,response header中還會把這個 ETag 返回,即使這個 ETag 跟之前的沒有變化。

          既生 Last-Modified 何生 Etag

          • 一些文件也許會周期性的更改,但是他的內(nèi)容并不改變(僅僅改變的修改時間),這個時候我們并不希望客戶端認(rèn)為這個文件被修改了,我們就可以使用 Etag 來做

          • 某些文件修改非常頻繁,比如在「秒以下的時間內(nèi)」進(jìn)行修改,(比方說1s內(nèi)修改了N次),If-Modified-Since 能檢查到的粒度是s級的,這種修改無法判斷

          最佳實踐

          緩存的意義就在于減少請求,更多地使用本地的資源,給用戶更好的體驗的同時,也減輕服務(wù)器壓力。所以,最佳實踐,就應(yīng)該是盡可能命中強(qiáng)緩存,同時,能在更新版本的時候讓客戶端的緩存失效。

          在更新版本之后,如何讓用戶第一時間使用最新的資源文件呢?機(jī)智的前端們想出了一個方法,在更新版本的時候,順便「把靜態(tài)資源的路徑」改了,這樣,就相當(dāng)于第一次訪問這些資源,就不會存在緩存的問題了。

          偉大的「webpack」可以讓我們在打包的時候,在文件的命名上帶上 hash

          entry:{
              main: path.join(__dirname,'./main.js'),
              vendor: ['react''antd']
          },
          output:{
              path:path.join(__dirname,'./dist'),
              publicPath'/dist/',
              filname'bundle.[chunkhash].js'
          }

          綜上所述,我們可以得出一個較為合理的緩存方案:

          • HTML:使用協(xié)商緩存。
          • CSS&JS&圖片:使用強(qiáng)緩存,文件命名帶上hash值。

          哈希值計算方式

          webpack給我們提供了三種哈希值計算方式,分別是 hashchunkhashcontenthash。那么這三者有什么區(qū)別呢?

          • hash:跟整個項目的構(gòu)建相關(guān),構(gòu)建生成的文件hash值都是一樣的,只要項目里有文件更改,整個項目構(gòu)建的hash值都會更改。
          • chunkhash:根據(jù)不同的入口文件(Entry)進(jìn)行依賴文件解析、構(gòu)建對應(yīng)的chunk,生成對應(yīng)的hash值。
          • contenthash:由文件內(nèi)容產(chǎn)生的hash值,內(nèi)容不同產(chǎn)生的contenthash值也不一樣。

          顯然,我們是不會使用第一種的。改了一個文件,打包之后,其他文件的hash都變了,緩存自然都失效了。這不是我們想要的。

          chunkhashcontenthash 的主要應(yīng)用場景是什么呢?在實際在項目中,我們一般會把項目中的css都抽離出對應(yīng)的「css文件」來加以引用。如果我們使用chunkhash,當(dāng)我們改了css代碼之后,會發(fā)現(xiàn)css文件hash值改變的同時,js文件的hash值也會改變。這時候,contenthash就派上用場了。

          補(bǔ)充:后端需要怎么設(shè)置

          上文主要說的是前端如何進(jìn)行打包,那后端怎么做呢?我們知道,瀏覽器是根據(jù)響應(yīng)頭的相關(guān)字段來決定緩存的方案的。所以,后端的關(guān)鍵就在于,根據(jù)不同的請求返回對應(yīng)的緩存字段。以nodejs為例,如果「需要瀏覽器強(qiáng)緩存」,我們可以這樣設(shè)置:

          res.setHeader('Cache-Control''public, max-age=xxx');

          如果需要協(xié)商緩存,則可以這樣設(shè)置:

          res.setHeader('Cache-Control''public, max-age=0');
          res.setHeader('Last-Modified', xxx);
          res.setHeader('ETag', xxx);

          在做前端緩存時,我們盡可能設(shè)置長時間的強(qiáng)緩存,通過文件名加hash的方式來做版本更新。在代碼分包的時候,應(yīng)該將一些不常變的公共庫獨立打包出來,使其能夠更持久的緩存。

          HTTP緩存

          緩存配置

          方案如下:

          • 方案1:cache-control: no-store:不緩存,每次訪問都從服務(wù)下載所有資源。
          • 方案2:cache-control: no-cache或cache-control: max-age=0:對比緩存,緩存當(dāng)前資源,但每次訪問都需要跟服務(wù)器對比,檢查資源是否被修改。(等同于expires = 過去的時間或無效時間,緩存但立即過期)
          • 方案3:cache-control: max-age=seconds //seconds > 0:強(qiáng)緩存,緩存當(dāng)前資源,在一定時期內(nèi),再次請求資源直接讀取本地緩存。

          注:強(qiáng)緩存下資源也并非不可更新,例如chrome的ctrl + f5等同于直接觸發(fā)方案1,f5或者webview的刷新鍵會直接觸發(fā)方案2,但都是基于客戶端操作,不建議納入實際項目考慮。

          實際項目中,方案1的應(yīng)用基本上看不到,對比方案2和方案3,方案1沒有任何優(yōu)勢。在方案2和方案3的選擇中,我們會對資源作區(qū)分。

          • 對于img,css,js,fonts等非html資源,我們可以直接考慮方案3,并且max-age配置的時間可以盡可能久,類似于緩存規(guī)則案例中,cache-control: max-age=31535000 配置365天的緩存,需要注意的是,這樣配置并不代表這些資源就一定一年不變,其根本原因在于目前前端構(gòu)建工具在靜態(tài)資源中都會加入戳的概念(例如,webpack中的[hash],gulp中的gulp-rev),「每次修改均會改變文件名或增加query參數(shù),本質(zhì)上改變了請求的地址」,也就不存在緩存更新的問題。

          • 對于html資源,我們建議根據(jù)項目的更新頻度來確定采用哪套方案。html作為前端資源的入口文件,一旦被強(qiáng)緩存,那么相關(guān)的js,css,img等均無法更新。對于「高頻維護(hù)的業(yè)務(wù)類」項目,建議采用方案2,或是方案3但max-age設(shè)置一個較小值,例如3600,一小時過期。對于一些活動項目,上線后「不會進(jìn)行較大改動,建議采用方案3」,不過max-age也不要設(shè)置過大,否則一旦出現(xiàn)bug或是未知問題,用戶無法及時更新。

          除了以上考慮,有時候其他因素也會影響緩存的配置,例如QQ紅包除夕活動,高并發(fā)大流量很容易給服務(wù)器帶來極大挑戰(zhàn),這時我們作為前端開發(fā),就可以采用方案3來避免用戶多次進(jìn)入帶來的流量壓力。

          對于http緩存的配置,我們始終要做到兩點,一是清楚明白http緩存的原理與規(guī)則,二是明確緩存的配置不是一次性的,根據(jù)不同的情況配置不同的規(guī)則,才能夠更好的發(fā)揮http緩存的價值。

          cdn緩存

          cdn緩存是一種服務(wù)端緩存,CDN服務(wù)商將源站的資源緩存到遍布全國的高性能加速節(jié)點上,當(dāng)用戶訪問相應(yīng)的業(yè)務(wù)資源時,用戶會被調(diào)度至最接近的節(jié)點最近的節(jié)點ip返回給用戶,在web性能優(yōu)化中,它主要起到了,緩解源站壓力,優(yōu)化不同用戶的訪問速度與體驗的作用。

          緩存規(guī)則

          與http緩存規(guī)則不同的是,這個規(guī)則并不是規(guī)范性的,而是由cdn服務(wù)商來制定,我們以騰訊云舉例,打開cdn加速服務(wù)配置,面板如下。

          可以看到,提供給我們的配置項只有文件類型(或文件目錄)和刷新時間,意義也很簡單,針對不同文件類型,在cdn節(jié)點上緩存對應(yīng)的時間。

          cdn運作流程

          由圖我們可以看出,cdn緩存的配置主要作用在緩存處理階段,雖然配置項只有文件類型和緩存時間,但流程卻并不簡單,我們先來明確一個概念——「回源」,回源的意思就是「返回源站,何為源站,就是我們自己的服務(wù)器」,很多人誤解接入cdn就是把資源放在了cdn上,其實不然,如圖中所示,接入cdn后,我們的服務(wù)器就是源站,源站一般情況下只會在cdn節(jié)點沒有資源或cdn資源失效時接收到cdn節(jié)點的請求,其他時間,源站并不會接收請求(當(dāng)然,如果我們知道源站的地址,我們可以直接訪問源站)。明確了回源的概念后,cdn的流程就顯得不那么復(fù)雜了,簡單的理解就是, 「沒有資源就去源站讀取,有資源就直接發(fā)送給用戶。」 與http緩存不同的是,cdn中沒有no-cache(max-age=0)的情況,當(dāng)我們設(shè)置緩存時間為0的時候,該類型文件就被認(rèn)定為不緩存文件,就是所有請求直接轉(zhuǎn)發(fā)源站,「只有當(dāng)緩存時間大于0且緩存過期的時候,才會與源站對比緩存是否被修改。」

          緩存配置

          各個cdn服務(wù)商并不完全一致,以騰訊云為例,在緩存配置的文檔中特別有以下說明。

          這會對我們有什么影響呢?

          • 如果我們http緩存設(shè)置cache-control: max-age=600,即緩存10分鐘,但cdn緩存配置中設(shè)置文件緩存時間為1小時,那么就會出現(xiàn)如下情況,文件被訪問后第12分鐘修改并上傳到服務(wù)器,用戶重新訪問資源,響應(yīng)碼會是304,對比緩存未修改,資源依然是舊的,一個小時后再次訪問才能更新為最新資源

          • 如果不設(shè)置cache-control呢,在http緩存中我們說過,如果不設(shè)置cache-control,那么會有默認(rèn)的緩存時間,但在這里,cdn服務(wù)商明確會在沒有cache-control字段時主動幫我們添加cache-control: max-age=600。

          注:針對問題1,也并非沒有辦法,當(dāng)我們必須要在緩存期內(nèi)修改文件,并且不想影響用戶體驗,那么我們可以「使用cdn服務(wù)商提供的強(qiáng)制更新緩存功能」,主要注意的是,這里的強(qiáng)制更新是更新服務(wù)端緩存,「http緩存」依然按照http頭部規(guī)則進(jìn)行自己的緩存處理,并「不會受到影響」

          http緩存與cdn緩存的結(jié)合

          當(dāng)用戶訪問我們的業(yè)務(wù)服務(wù)器時,首先進(jìn)行的就是http緩存處理,如果http緩存通過校驗,則直接響應(yīng)給用戶,如果「未通過校驗,則繼續(xù)進(jìn)行cdn緩存」的處理,cdn緩存處理完成后返回給客戶端,由客戶端進(jìn)行http緩存規(guī)則存儲并響應(yīng)給用戶。

          總結(jié)

          「CDN的原理」

          首先,瀏覽器會先請求CDN域名,CDN域名服務(wù)器會給瀏覽器返回指定域名的CNAME,瀏覽器在對這些CNAME進(jìn)行解析,得到CDN緩存服務(wù)器的IP地址,瀏覽器在去請求緩存服務(wù)器,CDN緩存服務(wù)器根據(jù)內(nèi)部專用的DNS解析得到實際IP,然后緩存服務(wù)器會向?qū)嶋HIP地址請求資源,緩存服務(wù)器得到資源后進(jìn)行本地保存和返回給瀏覽器客戶端。

          如何檢測JS錯誤,如何保證你的產(chǎn)品質(zhì)量?(錯誤監(jiān)控)(僅僅答了window.onerror)跨域的js運行錯誤可以捕獲嗎,錯誤提示什么,應(yīng)該怎么處理?

          script error 由來

          我們的頁面往往將靜態(tài)資源( js、css、image )存放到第三方 CDN,或者依賴于外部的靜態(tài)資源。當(dāng)從「第三方加載的 javascript 執(zhí)行出錯」時,由于同源策略,為了保證用戶信息不被泄露,不會返回詳細(xì)的錯誤信息,取之返回 script error。

          webkit 源碼:

          bool ScriptExecutionContext::sanitizeScriptError(String& errorMessage, int& lineNumber, String& sourceURL) {
            KURL targetURL = completeURL(sourceURL);

            if (securityOrigin()->canRequest(targetURL)) return false;
            // 非同源,將相關(guān)的錯誤信息設(shè)置成默認(rèn),錯誤信息置為 Script error,行號置成0
            errorMessage = "Script error.";
            sourceURL = String();
            ineNumber = 0;
            return true;
          }

          bool ScriptExecutionContext::dispatchErrorEvent(const String& errorMessage, int lineNumber, const String& sourceURL) {
            EventTarget* target = errorEventTarget();
            if (!target) return false;
            String message = errorMessage;
            int line = lineNumber;
            String sourceName = sourceURL;
            sanitizeScriptError(message, line, sourceName);
            ASSERT(!m_inDispatchErrorEvent);
            m_inDispatchErrorEvent = true;
            RefPtr<ErrorEvent> errorEvent = ErrorEvent::create(message, sourceName, line);
            target->dispatchEvent(errorEvent);
            m_inDispatchErrorEvent = false;
            return errorEvent->defaultPrevented();
          }

          常見的解決方案

          • 開啟 CORS 跨域資源共享

          a) 添加 crossorigin="anonymous" 屬性:

          <script src="http://domain/path/*.js" crossorigin="anonymous"></script>

          當(dāng)有 crossorigin="anonymous",瀏覽器以匿名的方式獲取目標(biāo)腳本,請求腳本時不會向服務(wù)器發(fā)送用戶信息( cookie、http 證書等)。

          b) 此時靜態(tài)服務(wù)器需要添加跨域協(xié)議頭:

          Access-Control-Allow-Origin: *

          完成這兩步后 window.onerror 就能夠捕獲對應(yīng)跨域腳本發(fā)生錯誤時的詳細(xì)錯誤信息了。

          • try catch

          crossorigin="anonymous" 確實可以完美解決 badjs 上報 script error 問題,但是需要服務(wù)端進(jìn)行跨域頭支持,而往往在大型企業(yè),域名多的令人發(fā)指,導(dǎo)致跨域規(guī)則配置非常復(fù)雜,所以很難全部都配置上,而且依賴的一些外部資源也不能確保支持,所以我們在調(diào)用外部資源方法以及一些不確認(rèn)是否配置跨域頭的資源方法時采用 try catch 包裝,并在 catch 到問題時上報對應(yīng)的錯誤

          function invoke(obj, method, args{
            try {
              return obj[method].apply(this, args);
            } catch (e) {
              reportBadjs(e); // report the error
            }
          }

          參考:前端 JavaScript 錯誤分析實踐

          結(jié)果

          一面涼經(jīng),繼續(xù)努力。

          小獅子有話說

          我是小獅子團(tuán)隊的【一百個Chocolate】,全網(wǎng)同名,周更的前端博主,分享一些前端技術(shù)干貨與程序員生活日常,歡迎各位小伙伴的持續(xù)關(guān)注,一起變優(yōu)秀~


          學(xué)如逆水行舟,不進(jìn)則退

          點擊【在看】可能會有紅包福利出現(xiàn)~

          瀏覽 61
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩三级久久久 | 亚洲一区在线免费观看 | 无码一区二区三区免费 | 免费国产黄色电影 | 亚洲无码123 |