前端開發遇到的問題及解決方法(前端開發中遇到什麼技術難題)

專案/技術

一、跨域

1、同源策略

瀏覽器同源策略限制請求

同源是指"協議 域名 埠"三者相同,即便兩個不同的域名指向同一個ip地址,也非同源。

限制以下行為

  1. Cookie、LocalStorage 和 IndexDB 無法讀取
  2. DOM 和 Js物件無法獲得
  3. AJAX 請求不能傳送

有三個標籤是允許跨域載入資源

  • vue實現

    this.$http.jsonp(   'http://www.domain2.com:8080/login',     {          params: {},         jsonp: 'onBack'    }   ).then(res => { console.log(res); } )

    2)document.domain iframe跨域

    僅限主域相同,子域不同的跨域應用場景

    實現原理:兩個頁面都通過js強制設定document.domain為基礎主域,就實現了同域

    document.domain = 'domain.com';       var user = 'admin';document.domain = 'domain.com';       // 獲取父視窗中變數   alert('get js data from parent ---> '  window.parent.user);

    3)location.hash iframe

    實現原理: a欲與b跨域相互通訊,通過中間頁c來實現。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通訊。

    具體實現: A域:a.html -> B域:b.html -> A域:c.html,

    a與b不同域只能通過hash值單向通訊,b與c也不同域也只能單向通訊,但c與a同域,所以c可通過parent.parent訪問a頁面所有物件。

    4)window.name iframe跨域

    window.name屬性的獨特之處:name值在不同的頁面(甚至不同域名)載入後依舊存在,並且可以支援非常長的 name 值(2MB)。

    通過iframe的src屬性由外域轉向本地域,跨域資料即由iframe的window.name從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。

    5) postMessage跨域

    postMessage一般用於解決以下問題

    a.) 頁面和其開啟的新視窗的資料傳遞

    b.) 多視窗之間訊息傳遞

    c.) 頁面與巢狀的iframe訊息傳遞

    d.) 上面三個場景的跨域資料傳遞

    var iframe = document.getElementById('iframe');   iframe.onload = function() {             var data = {                  name: 'aym'     };             // 向domain2傳送跨域資料     iframe.contentWindow.postMessage     (JSON.stringify(data),      'http://www.domain2.com');   };       // 接受domain2返回資料   window.addEventListener   ('message', function(e) {     alert('data from domain2 ---> '   e.data);   },     false);// 接收domain1的資料   window.addEventListener   ('message', function(e) {     alert('data from domain1 ---> '   e.data);             var data = JSON.parse(e.data);             if (data) {       data.number = 16;                   // 處理後再發回domain1       window.parent.postMessage(JSON.stringify(data),                                 'http://www.domain1.com');     }   }, false);

    6)跨域資源共享(CORS):主流的跨域解決方案

    服務端設定
       Access-Control-Allow-Origin即可

    若要帶cookie請求:前後端都需要設定。

    前端: : 檢查前端設定是否帶cookie:xhr.withCredentials = true;

    通過這種方式解決跨域問題的話,會在傳送請求時出現兩種情況,分別為簡單請求複雜請求

    簡單請求:

    使用下列方法之一:

    • GET
    • HEAD
    • POST

    Content-Type 的值僅限於下列三者之一:

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

    不符合以上條件的請求就肯定是複雜請求了。 複雜請求的CORS請求,會在正式通訊之前,增加一次HTTP查詢請求,稱為"預檢"請求, 該請求是 option 方法的,通過該請求來知道服務端是否允許跨域請求。

    OPTIONS預檢請求

    請求頭:

    • Origin:當前請求源,和響應頭裡的Access-Control-Allow-Origin 對標, 是否允許當前源訪問,Origin是不可修改的
    • Access-Control-Request-Headers:本次真實請求的額外請求頭,和響應頭裡的Access-Control-Allow-Headers對標,是否允許真實請求的請求頭
    • Access-Control-Request-Method:本次真實請求的額外方法,和響應頭裡的Access-Control-Allow-Methods對標,是否允許真實請求使用的請求方法

    響應頭

    • Access-Control-Allow-Credentials:
    • 這裡的Credentials(憑證)其意包括:Cookie ,授權標頭或 TLS 客戶端證書,預設CORS請求是不帶Cookies的,這與JSONP不同,JSONP每次請求都攜帶Cookies的,當然跨域允許帶Cookies會導致CSRF漏洞。如果非要跨域傳遞Cookies,web端需要給ajax設定withCredentials為true,同時,伺服器也必須使用Access-Control-Allow-Credentials頭響應。此響應頭true意味著伺服器允許cookies(或其他使用者憑據)包含在跨域請求中。另外,簡單的GET請求是不預檢的,即使請求的時候設定widthCrenditials為true,如果響應頭不帶Access-Control-Allow-Credentials,則會導致整個響應資源被瀏覽器忽略。
    • Access-Control-Allow-Headers
    • Access-Control-Allow-Methods
    • Access-Control-Allow-Origin
    • Access-Control-Expose-Headers:
    • 在CORS中,預設的,只允許客戶端讀取下面六個響應頭(在axios響應物件的headers裡能看到):
      • Cache-Control
      • Content-Language
      • Content-Type
      • Expires
      • Last-Modified
      • Pragma

    如果這六個以外的響應頭要是想讓客戶端讀取到,就需要設定
       Access-Control-Expose-Headers這個為響應頭名了,比如
       Access-Control-Expose-Headers: Token

    • Access-Control-Max-Age:設定預檢請求的有效時長,就是伺服器允許的請求方法和請求頭做個快取。

    7)nginx代理跨域

    nginx配置解決iconfont跨域

    瀏覽器跨域訪問js、css、img等常規靜態資源被同源策略許可,但iconfont字型檔案(eot|otf|ttf|woff|svg)例外,此時可在nginx的靜態資源伺服器中加入以下配置。

    location / {   add_header    Access-Control-Allow-Origin *; }

    反向代理

    跨域原理: 同源策略是瀏覽器的安全策略,不是HTTP協議的一部分。伺服器端呼叫HTTP介面只是使用HTTP協議,不會執行JS指令碼,不需要同源策略,也就不存在跨越問題。

    通過nginx配置一個代理伺服器(域名與domain1相同,埠不同)做跳板機,反向代理訪問domain2介面,並且可以順便修改cookie中domain資訊,方便當前域cookie寫入,實現跨域登入。

    #proxy伺服器 server {      listen         81;       server_name  www.domain1.com;       location / {             proxy_pass       http://www.domain2.com:8080;       #反向代理     proxy_cookie_domain      www.domain2.com www.domain1.com;     # 修改cookie裡域名     index  index.html index.htm;            # 用webpack-dev-server等中介軟體代理介面訪問nignx時,     # 此時無瀏覽器參與,故沒有同源限制,面的跨域配置可不啟用     add_header Access-Control-Allow-Origin      http://www.domain1.com;       # 當前端只跨域不帶cookie時,可為*     add_header Access-Control-Allow-Credentials true;   } }

    8)nodejs中介軟體代理跨域

    node中介軟體實現跨域代理,原理大致與nginx相同,都是通過啟一個代理伺服器,實現資料的轉發,也可以通過設定cookieDomainRewrite引數修改響應頭中cookie中域名,實現當前域的cookie寫入,方便介面登入認證。

    9)WebSocket協議跨域

    WebSocket protocol是HTML5一種新的協議。它實現了瀏覽器與伺服器全雙工通訊,同時允許跨域通訊,是server push技術的一種很好的實現。

    原生WebSocket API使用起來不太方便,我們使用Socket.io,它很好地封裝了webSocket介面,提供了更簡單、靈活的介面,也對不支援webSocket的瀏覽器提供了向下相容。

    user input:var socket = io('                         http://www.domain2.com:8080');                         // 連線成功處理                         socket.on('connect', function() {         // 監聽服務端訊息     socket.on('message', function(msg) {               console.log('data from server: ---> '   msg);      });        // 監聽服務端關閉     socket.on('disconnect', function() {        console.log('Server socket has closed.');      });   });   document.getElementsByTagName('input')   [0].onblur = function() {     socket.send(this.value);   };

    二、 輪播圖實現

    1、藉助元件或者框架:Swiper、BootStrap

    實現原理:參考程式碼

    2、css3動畫實現的輪播圖

    實現原理:

    1. 設定大的div
    2. a) 設定絕對定位,定位位置,
    3. b) 設定圖片展示出來的高度和寬度(height和width);
    4. c) 設定overflow:hidden;設定超出部分隱藏;使得圖片只能在這個框中顯示;
    5. 設定小的div,將所有圖片都包起來;寬度是所有圖片的寬度;設定position:relative / position:absolute 來讓它可以實現輪播的功能;必不可少。(自己的理解,設定大div和小div 的position,來讓div浮起來,脫離文件流,就像雲一樣,可以飄了~)
    6. 給裡面的圖片設定float:left;向左浮動,可以讓所有圖片都在同一行;如果沒有float:left;會導致圖片輪播的時候出現空白;
    7. 加入動畫;每次都向左偏移一個圖片的寬度,即可實現圖片輪播;
    8. 將第一張圖片與最後一張圖片設定成一樣的,是為了實現視覺上的無縫連線;

    3、JS方法

    輪播圖原理就是圖片的移動。所有輪播圖片橫向排列,在一個視窗中顯示一張圖片,視窗外的圖片隱藏,每一次一排圖片就是移動一張圖片的距離,切換到下一張圖片,即可實現圖片輪播。

    圖片的移動有兩種方式:

    • translate 實現的圖片移動
    • position定位實現圖片的偏移

    圖片的自動播放,使用間隔定時器 setInterval

    通過定位的方式,改變left或top的值,形成輪播圖的效果

    1、自動輪播:

    用setInterval(func,time);

    被呼叫的函式不斷地自加,也就是不斷地往後迴圈,當圖片到最後一張時,讓其跳轉到第一張。

    先將所有圖片,下方指示點的樣式設定為一樣的,再對當前索引對應的圖片,設定特別的樣式。

    2. 滑鼠移入,移出事件

    注意:

    1)如果你想要通過點選事件來改變圖片的移動時,就必須讓滑鼠移動到上面時設定清除計時器;因為如果不設定的話,當你通過點選事件改變它時,它自身也會自己改變,會出現混亂。

    2)當清除完後,滑鼠移出後需要重新啟動計時器,這時候不能再給它設定var jishi;因為如果再加上var 的話,相當於重新又定義了一個變數,會有好幾個計時器同時進行,會越來越快。

    3. 手動輪播 底下指示點的按鈕控制

    判斷點選的是哪個點,然後將它的索引值賦值給index,再通過呼叫change功能,實現它的改變。

    4. 左右按鈕的控制

    讓它實現自增或自減,然後呼叫change功能來改變樣式。 其實這裡的知識點和自動輪播裡的知識點差不多。(從最後一張圖片跳轉到第一張圖片 ,從第一張跳轉到最後一張。)

    無論是自動輪播,還是點選控制,都要加入change功能以及index 來實現對樣式的控制,從而實現輪播的效果。

    三、圖片懶載入

    原理

    優先載入可視區域的內容,其他部分等進入了可視區域再載入,從而提高效能

    原理:

    一張圖片就是一個標籤,瀏覽器是否發起請求圖片是根據的src屬性,所以實現懶載入的關鍵就是,在圖片沒有進入可視區域時,先不給的src賦值,這樣瀏覽器就不會傳送請求了,等到圖片進入可視區域再給src賦值。

    實現思路:

    1. 載入loading圖片
    2. 判斷哪些圖片要載入【重點】
    3. 當圖片距離頂部的距離top-height等於可視區域h和滾動區域高度s之和時說明圖片馬上就要進入可視區了
    4. 隱形載入圖片
    5. 建立一個臨時圖片,new Image() ,不會載入到頁面上去,實現隱形載入
    6. 替換真圖片
    7. 替換src屬性

    頁面佈局位置基礎知識

    網頁可見區域寬: document.body.clientWidth; 網頁可見區域高:
           document.body.clientHeight; 網頁可見區域寬: document.body.offsetWidth (包括邊線的寬); 網頁可見區域高:
           document.body.offsetHeight (包括邊線的高);

    網頁正文全文寬: document.body.scrollWidth; 網頁正文全文高:
           document.body.scrollHeight; 網頁被捲去的高: document.body.scrollTop; 網頁被捲去的左: document.body.scrollLeft;

    網頁正文部分上: window.screenTop; 網頁正文部分左: window.screenLeft; 螢幕解析度的高: window.screen.height; 螢幕解析度的寬: window.screen.width; 螢幕可用工作區高度: window.screen.availHeight;

    • 對塊級元素來說,offsetTop、offsetLeft、offsetWidth 及 offsetHeight 描述了元素相對於 offsetParent 的邊界框
    • HTMLElement.offsetParent 是一個只讀屬性,返回一個指向最近的(指包含層級上的最近)包含該元素的定位元素或者最近的 table,td,th,body元素。當元素的 style.display 設定為 "none" 時,offsetParent 返回 null。offsetParent 很有用,因為 offsetTop 和 offsetLeft 都是相對於其內邊距邊界的。
    • HTMLElement.offsetTop 為只讀屬性,它返回當前元素相對於其 offsetParent 元素的頂部的距離。
    • window.innerHeight:瀏覽器視窗的視口(viewport)高度(以畫素為單位);如果有水平滾動條,也包括滾動條高度。

    程式碼實現

    可以給img標籤統一自定義屬性data-src='default.png',當檢測到圖片出現在視窗之後再補充src屬性,此時才會進行圖片資源載入。

    function lazyload() {   const imgs = document.getElementsByTagName('img');   const len = imgs.length;   // 視口的高度   const viewHeight = document.documentElement.clientHeight;   // 滾動條高度   const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;   for (let i = 0; i < len; i  ) {     const offsetHeight = imgs[i].offsetTop;     if (offsetHeight < viewHeight   scrollHeight) {       const src = imgs[i].dataset.src;       imgs[i].src = src;     }   } } // 可以使用節流優化一下 window.addEventListener('scroll', lazyload);

    四、單點登入(SSO)實現

    1. 說明

    單點登入(Single Sign On),簡稱為 SSO,是目前比較流行的企業業務整合的解決方案之一

    SSO的定義是在多個應用系統中,使用者只需要登入一次就可以訪問所有相互信任的應用系統

    SSO 一般都需要一個獨立的認證中心(passport),子系統的登入均得通過passport,子系統本身將不參與登入操作

    當一個系統成功登入以後,passport將會頒發一個令牌給各個子系統,子系統可以拿著令牌會獲取各自的受保護資源,為了減少頻繁認證,各個子系統在被passport授權以後,會建立一個區域性會話,在一定時間內可以無需再次向passport發起認證

    2. 同域名下的單點登入

    cookie的domain屬性設定為當前域的父域,並且父域的cookie會被子域所共享。path屬性預設為web應用的上下文路徑

    利用 Cookie 的這個特點,我們只需要將Cookie的domain屬性設定為父域的域名(主域名),同時將 Cookie的path屬性設定為根路徑,將 Session ID(或 Token)儲存到父域中。這樣所有的子域應用就都可以訪問到這個Cookie

    不過這要求應用系統的域名需建立在一個共同的主域名之下,如tieba.baidu.com 和 map.baidu.com,它們都建立在 baidu.com這個主域名之下,那麼它們就可以通過這種方式來實現單點登入

    3. 不同域名下的單點登入

    這裡只介紹使用過的:CASCentral Authentication Service) 官網流程圖:

    app1登入

    1. 使用者訪問app1系統,app1系統是需要登入的,但使用者現在沒有登入。重定向到sso認證中心,並將自己的地址作為引數。
    2. www.sso.com?service=www.java3y.com (www.java3y.com視為app系統的地址)
    3. 跳轉到CAS server,即SSO登入系統,以後圖中的CAS Server我們統一叫做SSO系統。 SSO系統也沒有登入,彈出使用者登入頁。
    4. 使用者填寫使用者名稱、密碼,SSO系統進行認證後,將登入狀態寫入SSO的session,瀏覽器(Browser)中寫入SSO域下的Cookie
    5. 即:使用者與認證中心建立全域性會話(生成一份Token,寫到Cookie中,儲存在瀏覽器上)
    6. 認證中心重定向回app系統,並把Token攜帶過去給app1,重定向的地址如:
    7. www.java3y.com?token=xxxxxxx
    8. SSO系統登入完成後會生成一個ST(Service Ticket) ,然後跳轉到app1系統,同時將ST作為引數傳遞給app1系統。
    9. app1系統拿到ST後,從後臺向SSO傳送請求,驗證ST是否有效
    10. 驗證通過後,app1系統將登入狀態寫入session並設定app域下的Cookie。注意,此處的 cookie 和 session 儲存的是使用者在 app1 系統的登入狀態,和 CAS 無關。

    app2登入時:

    1. 使用者訪問app2系統,app2系統沒有登入,跳轉到SSO。
    2. 由於SSO已經登入了,不需要重新登入認證。
    3. SSO生成ST,瀏覽器跳轉到app2系統,並將ST作為引數傳遞給app2。
    4. app2拿到ST,後臺訪問SSO,驗證ST是否有效。
    5. 驗證成功後,app2將登入狀態寫入session,並在app2域下寫入Cookie。

    五、前端水印

    1. 顯性水印 DOM元素直接遮蓋: 將水印文字直接通過一層DOM元素,覆蓋到需要新增水印的圖片上
    2. 顯性水印 Canvas
    3. 演算法和顯性水印 DOM元素直接遮蓋一樣,但其效能優於方案一,安全性略高於方案一 直接通過Canvas繪畫,避免了在水印密度較大的情況下大量DOM元素的建立與新增 並且Canvas在部分環境與瀏覽器下擁用GPU加速的功能,故而效能提升較大
    4. 保護程式 DOM元素直接遮蓋
    5. 上述方案中,將資源繪製在Canvas雖是一種可行方案,但對於普通的DOM元素(非圖片) 雖然有可行方案例如html2canva來將DOM轉化為·Canvas,但是實現過於繁雜 並且DOM將失去其事件處理響應功能,故而並不推薦這麼使用,除非需要保護的資源沒有任何互動 使用瀏覽器新增的MutationObserver特性(主流瀏覽器都已支援,參考資料中有具體文件連結) 用來監視需要保護的DOM元素及其子代的更改(包括監視DOM及其子代的刪減、Style的變化,標籤屬性變化等等),一旦回撥函式通知出現了任何更改 我們可以做出提示,提醒使用者操作違法,並且刪除掉水印,並且重新生成水印DOM 或者在使用者更改了水印DOM的時候,將需要顯示的保護資源DOM一併刪除
    6. Base64傳輸 將資原始檔通過Base64編碼並且通過request請求返回(或是直接後端儲存Base64) 而對於Img來說,Base64只需要一些小小的的處理就可以在Web中使用(Base64字串可以直接作為img的url,但建議使用Js Image物件,這樣避免了暴露原始URL到HTML中
    7. 加料的Base64

    六、大檔案斷點續傳

    上傳大檔案時,以下幾個變數會影響我們的使用者體驗

    • 伺服器處理資料的能力
    • 請求超時
    • 網路波動

    分片上傳:

    分片上傳,就是將所要上傳的檔案,按照一定的大小,將整個檔案分隔成多個資料塊(Part)來進行分片上傳,上傳完之後再由服務端對所有上傳的檔案進行彙總整合成原始的檔案

    大致流程如下:

    1. 將需要上傳的檔案按照一定的分割規則,分割成相同大小的資料塊;
    2. 初始化一個分片上傳任務,返回本次分片上傳唯一標識;
    3. 按照一定的策略(序列或並行)傳送各個分片資料塊;
    4. 傳送完成後,服務端根據判斷資料上傳是否完整,如果完整,則進行資料塊合成得到原始檔案

    斷點續傳:

    斷點續傳指的是在下載或上傳時,將下載或上傳任務人為的劃分為幾個部分

    每一個部分採用一個執行緒進行上傳或下載,如果碰到網路故障,可以從已經上傳或下載的部分開始繼續上傳下載未完成的部分,而沒有必要從頭開始上傳下載。使用者可以節省時間,提高速度

    一般實現方式有兩種:

    • 伺服器端返回,告知從哪開始
    • 瀏覽器端自行處理

    上傳過程中將檔案在伺服器寫為臨時檔案,等全部寫完了(檔案上傳完),將此臨時檔案重新命名為正式檔案即可

    如果中途上傳中斷過,下次上傳的時候根據當前臨時檔案大小,作為在客戶端讀取檔案的偏移量,從此位置繼續讀取檔案資料塊,上傳到伺服器從此偏移量繼續寫入檔案即可

    實現思路:

    整體思路比較簡單,拿到檔案,儲存檔案唯一性標識,切割檔案,分段上傳,每次上傳一段,根據唯一性標識判斷檔案上傳進度,直到檔案的全部片段上傳完畢

    七、掃描二維碼登入的原理

    1)移動端基於 token 的認證機制

    基於 token 的認證機制,只有在第一次使用需要輸入賬號密碼,後續使用將不在輸入賬號密碼。 其實在登陸的時候不僅傳入賬號、密碼,還傳入了手機的裝置資訊

    在服務端驗證賬號、密碼正確後,服務端會做兩件事。

    1. 將賬號與裝置關聯起來,在某種意義上,裝置資訊就代表著賬號。
    2. 生成一個 token 令牌,並且在 token 與賬號、裝置關聯,類似於key/value,token作為 key,賬號、裝置資訊作為value,持久化在磁碟上。

    將 token 返回給移動端,移動端將 token 存入在本地,往後移動端都通過 token 訪問服務端 API ,當然除了 token 之外,還需要攜帶裝置資訊,因為 token 可能會被劫持。帶上裝置資訊之後,就算 token 被劫持也沒有關係,因為裝置資訊是唯一的

    總結:裝置資訊加token唯一確定使用者,完成登入認證


    2)二維碼掃碼登入的原理

    1. 待掃描階段
    2. 待掃描階段也就是流程圖中 1~5 階段,即生成二維碼階段,這個階段跟移動端沒有關係,是 PC 端跟服務端的互動過程。
    3. 首先 PC 端攜帶裝置資訊向服務端發起生成二維碼請求,服務端會生成唯一的二維碼 ID,你可以理解為 UUID,並且將 二維碼 ID 跟 PC 裝置資訊關聯起來,這跟移動端登入有點相似。
    4. PC 端接受到二維碼 ID 之後,將二維碼 ID 以二維碼的形式展示,等待移動端掃碼。此時在 PC 端會啟動一個定時器,輪詢查詢二維碼的狀態。如果移動端未掃描的話,那麼一段時間後二維碼將會失效。

    總結:PC端裝置資訊==>服務端生成二維碼==> PC端顯示==>定時器輪詢二維碼狀態

    1. 已掃描待確認階段
    2. 流程圖中第 6 ~ 10 階段,我們在 PC 端登入微信時,手機掃碼後,PC 端的二維碼會變成已掃碼,請在手機端確認。這個階段是移動端跟服務端互動的過程。
    3. 首先移動端掃描二維碼,獲取二維碼 ID,然後將手機端登入的資訊憑證(token)和 二維碼 ID 作為引數傳送給服務端,此時的手機一定是登入的,不存在沒登入的情況。
    4. 服務端接受請求後,會將 token 與二維碼 ID 關聯,為什麼需要關聯呢?你想想,我們使用微信時,移動端退出, PC 端是不是也需要退出,這個關聯就有點把子作用了。然後會生成一個一次性 token,這個 token 會返回給移動端,一次性 token 用作確認時候的憑證。
    5. PC 端的定時器,會輪詢到二維碼的狀態已經發生變化,會將 PC 端的二維碼更新為已掃描,請確認。

    總結:手機掃碼==>手機端token 二維碼ID傳送到服務端==>伺服器關聯token和二維碼ID ==> 生成token返回移動端 ==> PC端二維碼狀態更新

    1. 已確認
    2. 流程圖中的 第 11 ~ 15 步驟,這是掃碼登入的最後階段,移動端攜帶上一步驟中獲取的臨時 token ,確認登入,服務端校對完成後,會更新二維碼狀態,並且給 PC 端生成一個正式的 token ,後續 PC 端就是持有這個 token 訪問服務端。
    3. PC 端的定時器,輪詢到了二維碼狀態為登入狀態,並且會獲取到了生成的 token ,完成登入,後續訪問都基於 token 完成。
    4. 在伺服器端會跟手機端一樣,維護著 token 跟二維碼、PC 裝置資訊、賬號等資訊。

    總結:PC端獲取生成的token==> 完成登入==>正常訪問(基於此token)

    八、前端檔案下載

    1、實現方法

    1)form表單提交

    為一個下載按鈕新增click事件,點選時動態生成一個表單,利用表單提交的功能來實現檔案的下載

    /**  * 下載檔案  * @param {String} path - 請求的地址  * @param {String} fileName - 檔名  */ function downloadFile (downloadUrl, fileName) {     // 建立表單     const formObj = document.createElement('form');     formObj.action = downloadUrl;     formObj.method = 'get';     formObj.style.display = 'none';     // 建立input,主要是起傳參作用     const formItem = document.createElement('input');     formItem.value = fileName; // 傳參的值     formItem.name = 'fileName'; // 傳參的欄位名     // 插入到網頁中     formObj.appendChild(formItem);     document.body.appendChild(formObj);     formObj.submit(); // 傳送請求     document.body.removeChild(formObj); // 傳送完清除掉 }

    2)a標籤的download

    點選下載點選下載 // 指定檔名 // 檢測瀏覽器是否支援download屬性 const isSupport = 'download' in document.createElement('a');

    3)open或location.href

    本質上和a標籤訪問下載連結一樣

    window.open('downloadFile.zip'); location.href = 'downloadFile.zip';

    4)Blob物件

    呼叫api,將檔案流轉為Blob二進位制物件,

    注:IE10以下不支援。

    思路: 發請求獲取二進位制資料,轉化為Blob物件,利用URL.createObjectUrl生成url地址,賦值在a標籤的href屬性上,結合download進行下載。

    /**  * 下載檔案  * @param {String} path - 下載地址/下載請求地址。  * @param {String} name - 下載檔案的名字/重新命名(考慮到相容性問題,最好加上字尾名)  */ downloadFile (path, name) {     const xhr = new XMLHttpRequest();     xhr.open('get', path);     xhr.responseType = 'blob';     xhr.send();     xhr.onload = function () {         if (this.status === 200 || this.status === 304) {             // 如果是IE10及以上,不支援download屬性,採用msSaveOrOpenBlob方法,但是IE10以下也不支援msSaveOrOpenBlob             if ('msSaveOrOpenBlob' in navigator) {                 navigator.msSaveOrOpenBlob(this.response, name);                 return;             }             /*                如果傳送請求時不設定xhr.responseType = 'blob',               預設ajax請求會返回DOMString型別的資料,即字串。               此時需要使用兩處註釋的程式碼,對返回的文字轉化為Blob物件,然後建立blob url,               此時需要註釋掉原本的const url = URL.createObjectURL(target.response)。             */             /*              const blob = new Blob([this.response], { type: xhr.getResponseHeader('Content-Type') });             const url = URL.createObjectURL(blob);             */             const url = URL.createObjectURL(this.response); // 使用上面則註釋此行             const a = document.createElement('a');             a.style.display = 'none';             a.href = url;             a.download = name;             document.body.appendChild(a);             a.click();             document.body.removeChild(a);             URL.revokeObjectURL(url);         }     }; }

    // 上面方法本地測試有時會有跨域問題,下面使用axios重寫一下 // 已經配置好proxy downloadFile (path, name) {     axios.get({       url: path,       method: 'get'     }).then(res => {       const blob = new Blob([res.data], { type: res.headers['content-type'] });       const url = URL.createObjectURL(blob);       const a = document.createElement('a');       a.style.display = 'none';       a.href = url;       a.download = name;       document.body.appendChild(a);       a.click();       document.body.removeChild(a);       URL.revokeObjectURL(url);     }) }

    該方法不能缺少a標籤的download屬性的設定。

    因為發請求時已設定返回資料型別為Blob型別(xhr.responseType = 'blob'),所以target.response就是一個Blob物件,列印出來會看到兩個屬性size和type。雖然type屬性已指定了檔案的型別,但是為了穩妥起見,還是在download屬性值裡指定字尾名,如Firefox不指定下載下來的檔案就會不識別型別。

    5)利用Base64

    用法跟上面用Blob大同小異,基本上思路一樣

    不同點: 上面是利用Blob物件生成Blob URL, 這裡則是生成Data URL,即base64編碼後的url形式。

    /**  * 下載檔案  * @param {String} path - 下載地址/下載請求地址。  * @param {String} name - 下載檔案的名字(考慮到相容性問題,最好加上字尾名)  */ downloadFile (path, name) {     const xhr = new XMLHttpRequest();     xhr.open('get', path);     xhr.responseType = 'blob';     xhr.send();     xhr.onload = function () {         if (this.status === 200 || this.status === 304) {             const fileReader = new FileReader();             fileReader.readAsDataURL(this.response);             fileReader.onload = function () {                 const a = document.createElement('a');                 a.style.display = 'none';                 a.href = this.result;                 a.download = name;                 document.body.appendChild(a);                 a.click();                 document.body.removeChild(a);             };         }     }; }

    2、如何獲取檔名

    返回檔案流的時候,在瀏覽器上觀察介面返回的資訊,會看到有這麼一個header:Content-Disposition

    其中包含了檔名:filename=和filename*=可以擷取這段字串中的這兩個欄位值了

    // xhr是XMLHttpRequest物件 const content = xhr.getResponseHeader('content-disposition'); // 注意是全小寫,自定義的header也是全小寫 if (content) {     let name1 = content.match(/filename=(.*);/)[1]; // 獲取filename的值     let name2 = content.match(/filename*=(.*)/)[1]; // 獲取filename*的值     name1 = decodeURIComponent(name1);     name2 = decodeURIComponent(name2.substring(6)); // 這個下標6就是UTF-8'' }

    九、滾動載入

    監聽頁面滾動事件,分析clientHeightscrollTopscrollHeight三者的屬性關係。

    window.addEventListener('scroll', function() {   const clientHeight = document.documentElement.clientHeight;   const scrollTop = document.documentElement.scrollTop;   const scrollHeight = document.documentElement.scrollHeight;   if (clientHeight   scrollTop >= scrollHeight) {     // 檢測到滾動至頁面底部,進行後續操作     // ...   } }, false);

    十、渲染大資料

    渲染大資料時,合理使用createDocumentFragment和requestAnimationFrame,將操作切分為一小段一小段執行。

    setTimeout(() => {   // 插入十萬條資料   const total = 100000;   // 一次插入的資料   const once = 20;   // 插入資料需要的次數   const loopCount = Math.ceil(total / once);   let countOfRender = 0;   const ul = document.querySelector('ul');   // 新增資料的方法   function add() {     const fragment = document.createDocumentFragment();     for(let i = 0; i < once; i  ) {       const li = document.createElement('li');       li.innerText = Math.floor(Math.random() * total);       fragment.appendChild(li);     }     ul.appendChild(fragment);     countOfRender  = 1;     loop();   }   function loop() {     if(countOfRender < loopCount) {       window.requestAnimationFrame(add);     }   }   loop(); }, 0)

    十一、VDOM轉真實DOM基本原理

    // vnode結構: // { //   tag, //   attrs, //   children, // } //Virtual DOM => DOM function render(vnode, container) {   container.appendChild(_render(vnode)); } function _render(vnode) {   // 如果是數字型別轉化為字串   if (typeof vnode === 'number') {     vnode = String(vnode);   }   // 字串型別直接就是文字節點   if (typeof vnode === 'string') {     return document.createTextNode(vnode);   }   // 普通DOM   const dom = document.createElement(vnode.tag);   if (vnode.attrs) {     // 遍歷屬性     Object.keys(vnode.attrs).forEach(key => {       const value = vnode.attrs[key];       dom.setAttribute(key, value);     })   }   // 子陣列進行遞迴操作   vnode.children.forEach(child => render(child, dom));   return dom; }