天天看點

2023.03.23 - 2023.03.26 更新前端面試問題總結(26道題)

2023.03.23 - 2023.03.26 更新前端面試問題總結(26道題)

擷取更多面試問題可以通路

github 位址: https://github.com/pro-collection/interview-question/issues

gitee 位址: https://gitee.com/yanleweb/interview-question/issues

目錄:

  • 初級開發者相關問題【共計 3 道題】
    • 157.CSS 文檔流 是什麼概念?【CSS】
    • 158.CSS 中 position 常見屬性有哪些,大概講一下?【CSS】
    • 171.[Vue] 響應式資料流驅動頁面 和 傳統的事件綁定指令式驅動頁面, 有何優劣?【web架構】
  • 中級開發者相關問題【共計 10 道題】
    • 147.HTTP 與 HTTPS 的差別?【網絡】
    • 148.HTTPS 解決了什麼問題?【網絡】
    • 149.HTTPS 中的 SSL/TLS 是什麼?【網絡】
    • 154.常見的前端記憶體洩露場景有哪些?【JavaScript、浏覽器】
    • 156.實作 table header 吸頂, 有哪些實作方式?【CSS】
    • 159.[Vue] 父子元件通信方式有哪些?【web架構】
    • 160.什麼是洋蔥模型?【web架構】
    • 164.[koa] 中間件的異常處理是怎麼做的?【web架構】
    • 173.為什麼小程式裡拿不到dom相關的api【web架構】
    • 178.[React] useRef、ref、forwardsRef 的差別是什麼?【web架構】
  • 進階開發者相關問題【共計 11 道題】
    • 152.頁面崩潰如何監控?【網絡】
    • 153.如何監控前端頁面記憶體持續增長情況?【網絡】
    • 155.常見的前端檢測記憶體洩露的方法有哪些?【JavaScript、浏覽器】
    • 161.如何實作洋蔥模式?【web架構】
    • 168.[koa] 在沒有async await 的時候, koa是怎麼實作的洋蔥模型?【web架構】
    • 169.[koa] body-parser 中間件實作原理?【web架構】
    • 170.檔案上傳和上傳檔案解析的原理是啥?【網絡、浏覽器】
    • 172.es6 class 裝飾器是如何實作的?【JavaScript】
    • 174.Promise then 第二個參數和 Promise.catch 的差別是什麼?【JavaScript】
    • 175.Promise finally 怎麼實作的?【JavaScript】
    • 179.[React] useEffect的第二個參數,如何判斷依賴是否發生變化?【web架構】
  • 資深開發者相關問題【共計 2 道題】
    • 151.HTTPS 加密算法和加解密過程是啥?【網絡】
    • 176.WebWorker、SharedWorker 和 ServiceWorker 有哪些差別?【JavaScript】

初級開發者相關問題【共計 3 道題】

157.CSS 文檔流 是什麼概念?【CSS】

CSS 的文檔流(Document Flow)是指文檔中元素按照其在 HTML 中出現的順序自上而下布局的方式,也稱為正常流(Normal Flow)或預設流。文檔流定義了元素的布局順序和定位方式,包括元素的位置、大小、間距等屬性。

在文檔流中,每個元素都會占據一定的空間并盡可能充滿其包含塊的寬度。每個元素的位置都會受到前面元素的影響,如果前面的元素發生位置變化,那麼後面的元素的位置也會發生相應的變化。

文檔流中的元素按照下面的規則排列:

  1. 塊級元素:塊級元素會獨占一行,并在前面自動添加一個垂直間距。例如:<p>、<div>、<h1> 等。
  2. 行内元素:行内元素會在一行中排列,并且寬度根據内容自适應。例如:<a>、<span>、<img> 等。
  3. 行内塊級元素:行内塊級元素與行内元素類似,但是它可以設定寬度、高度等塊級元素的屬性。例如:<input>、<button>、<textarea> 等。

文檔流是 CSS 中最基本、最重要的概念之一,它決定了網頁的整體布局和排版方式,也是實作網頁布局的基礎。在實際開發中,我們需要了解文檔流的特性和工作原理,以便更好地掌握網頁布局和樣式的設計。

158.CSS 中 position 常見屬性有哪些,大概講一下?【CSS】

CSS 中 position 屬性用于指定元素的定位方式,它有以下常見的屬性值:

  1. static:預設值,元素在文檔流中正常排列。
  2. relative:元素在文檔流中正常排列,但是可以通過設定 top、right、bottom、left 屬性相對于其正常位置進行偏移,不會影響其它元素的位置。
  3. absolute:元素脫離文檔流,相對于最近的非 static 定位的祖先元素進行定位,如果沒有則相對于 <html> 元素進行定位。通過設定 top、right、bottom、left 屬性進行偏移,如果祖先元素發生位置變化,則元素的位置也會發生相應的變化。
  4. fixed:元素脫離文檔流,相對于浏覽器視窗進行定位,始終保持在視窗的固定位置,不會随頁面滾動而滾動。通過設定 top、right、bottom、left 屬性進行偏移。
  5. sticky:元素在文檔流中正常排列,當元素滾動到指定的位置時,停止滾動并固定在該位置,直到其祖先元素發生滾動時才會取消固定。通過設定 top、right、bottom、left 屬性和 z-index 屬性進行設定。

以上是 position 屬性的常見屬性值和簡單說明,不同的值會對元素進行不同的定位方式,開發人員可以根據需要選擇合适的值來實作頁面布局。

171.[Vue] 響應式資料流驅動頁面 和 傳統的事件綁定指令式驅動頁面, 有何優劣?【web架構】

Vue 響應式資料流驅動頁面和傳統的事件綁定指令式驅動頁面是兩種不同的前端開發方式,它們的優劣勢主要展現在代碼編寫方式、頁面效果、開發效率和維護難度上。

  • 響應式資料流驅動頁面:Vue 使用響應式的資料流來驅動頁面的渲染和更新。Vue 的響應式系統會自動偵測資料的變化,并且重新渲染頁面,開發者隻需要專注于資料和頁面的關系,而不用手動操作 DOM 元素。相比傳統的指令式開發方式,響應式資料流驅動頁面的代碼更簡潔、易于維護,開發效率更高。同時,Vue 的元件化開發模式也可以讓開發者輕松地實作元件複用和代碼複用。
  • 傳統的事件綁定指令式驅動頁面:傳統的事件綁定指令式驅動頁面是通過手動綁定事件和操作 DOM 元素來實作頁面互動效果。這種開發方式需要編寫大量的事件處理函數和 DOM 操作代碼,容易出現邏輯混亂和代碼備援的問題。同時,由于每個事件都需要手動綁定和處理,開發效率也會受到一定的影響。

綜上所述,響應式資料流驅動頁面和傳統的事件綁定指令式驅動頁面都有其優缺點,選擇何種開發方式需要根據具體的需求和實際情況來決定。一般來說,響應式資料流驅動頁面更适合用于建構資料驅動的、元件化的頁面,而傳統的事件綁定指令式驅動頁面更适合用于建構互動性強、動态性高的頁面。

中級開發者相關問題【共計 10 道題】

147.HTTP 與 HTTPS 的差別?【網絡】

HTTPS

基礎

https 是 http 的“更新”版本:

HTTPS = HTTP+ SSL/TLS
           

SSL 是安全層,TLS 是傳輸層安全,是SSL 的繼承。使用SSL或TLS 可確定傳輸資料的安全性。

使用 HTTP 可能看到傳輸資料是: “這是明文資訊”

使用 HTTPS 可能看到: “283hd9saj9cdsncihquhs99ndso”

HTTPS 傳輸的不再是文本,而是二進制流,使得傳輸更高效,且加密處理更加安全。

HTTPS 的工作流程

1、用戶端請求 HTTPS 請求并連接配接到伺服器的 443 端口,此過程和請求 HTTP 請求一樣,進行三次握手;

2、服務端向用戶端發送數字證書,其中包含公鑰、證書頒發者、到期日期

現比較流行的加解密碼對,即公鑰和私鑰。公鑰用于加密,私鑰用于解密。是以服務端會保留私鑰,然後發送公鑰給用戶端。

3、用戶端收到證書,會驗證證書的有效性。驗證通過後會生成一個随機的 pre-master key。再将密鑰通過接收到的公鑰加密然後發送給服務端

4、服務端接收後使用私鑰進行解密得到 pre-master key

5、獲得 pre-master key 後,伺服器和用戶端可以使用主密鑰進行通信。

HTTP 與 HTTPS 差別

是以在回答 HTTP 與 HTTPS 的差別的問題,可以從下面幾個方面進行回答:

  • 加密: HTTPS 是 HTTP 協定的更加安全的版本,通過使用SSL/TLS進行加密傳輸的資料;
  • 連接配接方式: HTTP(三次握手)和 HTTPS (三次握手+數字證書)連接配接方式不一樣;
  • 端口: HTTP 預設的端口是 80和 HTTPS 預設端口是 443

HTTP2 是什麼?

HTTP/2 超文本傳輸協定第2版,是 HTTP/1.x 的擴充。是以 HTTP/2沒有改動HTTP的應用語義,仍然使用HTTP的請求方法、狀态碼和頭字段等規則。

它主要修改了HTTP的封包傳輸格式,通過引入二進制分幀層實作性能的提升。

現有很多主流浏覽器的 HTTPS/2 的實作都是基于SSL/TLS的,是以基于 SSL/TLS 的 HTTP/2 連接配接建立過程和 HTTPS 差不多。在建立連接配接過程中會攜帶辨別期望使用 HTTP/2 協定,服務端同樣方式回應。

參考文檔

  • https://juejin.cn/post/7144400185731317768

148.HTTPS 解決了什麼問題?【網絡】

HTTPS 解決了什麼問題

一個簡單的回答可能會是 HTTP 它不安全。由于 HTTP 天生明文傳輸的特性,在 HTTP 的傳輸過程中,任何人都有可能從中截獲、修改或者僞造請求發送,是以可以認為 HTTP 是不安全的;在 HTTP 的傳輸過程中不會驗證通信方的身份,是以 HTTP 資訊交換的雙方可能會遭到僞裝,也就是沒有使用者驗證;在 HTTP 的傳輸過程中,接收方和發送方并不會驗證封包的完整性,綜上,為了結局上述問題,HTTPS 應用而生。

什麼是 HTTPS

你還記得 HTTP 是怎麼定義的嗎?HTTP 是一種 超文本傳輸協定(Hypertext Transfer Protocol) 協定,它 是一個在計算機世界裡專門在兩點之間傳輸文字、圖檔、音頻、視訊等超文本資料的約定和規範,那麼我們看一下 HTTPS 是如何定義的

HTTPS 的全稱是 Hypertext Transfer Protocol Secure,它用來在計算機網絡上的兩個端系統之間進行安全的交換資訊(secure communication),它相當于在 HTTP 的基礎上加了一個 Secure 安全的詞眼,那麼我們可以給出一個 HTTPS 的定義:HTTPS 是一個在計算機世界裡專門在兩點之間安全的傳輸文字、圖檔、音頻、視訊等超文本資料的約定和規範。 HTTPS 是 HTTP 協定的一種擴充,它本身并不保傳輸的證安全性,那麼誰來保證安全性呢?在 HTTPS 中,使用傳輸層安全性(TLS)或安全套接字層(SSL)對通信協定進行加密。也就是 HTTP + SSL(TLS) = HTTPS。

HTTPS 做了什麼

HTTPS 協定提供了三個關鍵的名額

  • 加密(Encryption), HTTPS 通過對資料加密來使其免受竊聽者對資料的監聽,這就意味着當使用者在浏覽網站時,沒有人能夠監聽他和網站之間的資訊交換,或者跟蹤使用者的活動,通路記錄等,進而竊取使用者資訊。
  • 資料一緻性(Data integrity),資料在傳輸的過程中不會被竊聽者所修改,使用者發送的資料會完整的傳輸到服務端,保證使用者發的是什麼,伺服器接收的就是什麼。
  • 身份認證(Authentication),是指确認對方的真實身份,也就是證明你是你(可以比作人臉識别),它可以防止中間人攻擊并建立使用者信任。

有了上面三個關鍵名額的保證,使用者就可以和伺服器進行安全的交換資訊了。那麼,既然你說了 HTTPS 的種種好處,那麼我怎麼知道網站是用 HTTPS 的還是 HTTP 的呢?給你兩幅圖應該就可以解釋了。

HTTPS 協定其實非常簡單,RFC 文檔很小,隻有短短的 7 頁,裡面規定了新的協定名,預設端口号443,至于其他的應答模式、封包結構、請求方法、URI、頭字段、連接配接管理等等都完全沿用 HTTP,沒有任何新的東西。

也就是說,除了協定名稱和預設端口号外(HTTP 預設端口 80),HTTPS 協定在文法、語義上和 HTTP 一樣,HTTP 有的,HTTPS 也照單全收。那麼,HTTPS 如何做到 HTTP 所不能做到的安全性呢?關鍵在于這個 S 也就是 SSL/TLS 。

149.HTTPS 中的 SSL/TLS 是什麼?【網絡】

什麼是 SSL/TLS

認識 SSL/TLS

TLS(Transport Layer Security) 是 SSL(Secure Socket Layer) 的後續版本,它們是用于在網際網路兩台計算機之間用于身份驗證和加密的一種協定。

注意:在網際網路中,很多名稱都可以進行互換。

我們都知道一些線上業務(比如線上支付)最重要的一個步驟是建立一個值得信賴的交易環境,能夠讓客戶安心的進行交易,SSL/TLS 就保證了這一點,SSL/TLS 通過将稱為 X.509 證書的數字文檔将網站和公司的實體資訊綁定到加密密鑰來進行工作。每一個密鑰對(key pairs) 都有一個 私有密鑰(private key) 和 公有密鑰(public key),私有密鑰是獨有的,一般位于伺服器上,用于解密由公共密鑰加密過的資訊;公有密鑰是公有的,與伺服器進行互動的每個人都可以持有公有密鑰,用公鑰加密的資訊隻能由私有密鑰來解密。

什麼是 X.509:X.509 是公開密鑰證書的标準格式,這個文檔将加密密鑰與(個人或組織)進行安全的關聯。

X.509 主要應用如下

SSL/TLS 和 HTTPS 用于經過身份驗證和加密的 Web 浏覽通過 S/MIME 協定簽名和加密的電子郵件代碼簽名:它指的是使用數字證書對軟體應用程式進行簽名以安全分發和安裝的過程。

通過使用由知名公共證書頒發機構(例如SSL.com)頒發的證書對軟體進行數字簽名,開發人員可以向最終使用者保證他們希望安裝的軟體是由已知且受信任的開發人員釋出;并且簽名後未被篡改或損害。

還可用于文檔簽名還可用于用戶端認證

政府簽發的電子身份證(詳見 www.ssl.com/article/pki…

我們後面還會讨論。

HTTPS 的核心是 HTTP

HTTPS 并不是一項新的應用層協定,隻是 HTTP 通信接口部分由 SSL 和 TLS 替代而已。通常情況下,HTTP 會先直接和 TCP 進行通信。在使用 SSL 的 HTTPS 後,則會先演變為和 SSL 進行通信,然後再由 SSL 和 TCP 進行通信。也就是說,HTTPS 就是身披了一層 SSL 的 HTTP。(我都喜歡把騷粉留在最後。。。)

SSL 是一個獨立的協定,不隻有 HTTP 可以使用,其他應用層協定也可以使用,比如 SMTP(電子郵件協定)、Telnet(遠端登入協定) 等都可以使用。

154.常見的前端記憶體洩露場景有哪些?【JavaScript、浏覽器】

大多數情況下,垃圾回收器會幫我們及時釋放記憶體,一般不會發生記憶體洩漏。但是有些場景是記憶體洩漏的高發區,我們在使用的時候一定要注意:

  • 我們在開發的時候經常會使用console在控制台列印資訊,但這也會帶來一個問題:被console使用的對象是不能被垃圾回收的,這就可能會導緻記憶體洩漏。是以在生産環境中不建議使用console.log()的理由就又可以加上一條避免記憶體洩漏了。
  • 被全局變量、全局函數引用的對象,在Vue元件銷毀時未清除,可能會導緻記憶體洩漏
  • // Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); onMounted(() => { window.arr = arr; // 被全局變量引用 window.arrFunc = () => { console.log(arr); // 被全局函數引用 } }) // 正确的方式 onBeforeUnmount(() => { window.arr = null; window.arrFunc = null; }) </script>
  • 定時器未及時在Vue元件銷毀時清除,可能會導緻記憶體洩漏
  • // Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); const timer = reactive(null); onMounted(() => { setInterval(() => { console.log(arr); // arr被定時器占用,無法被垃圾回收 }, 200); // 正确的方式 timer = setInterval(() => { console.log(arr); }, 200); }) // 正确的方式 onBeforeUnmount(() => { if (timer) { clearInterval(timer); timer = null; } }) </script>
  • setTimeout和setInterval兩個定時器在使用時都應該注意是否需要清理定時器,特别是setInterval,一定要注意清除。
  • 綁定的事件未及時在Vue元件銷毀時清除,可能會導緻記憶體洩漏
  • 綁定事件在實際開發中經常遇到,我們一般使用addEventListener來建立。
  • // Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); const printArr = () => { console.log(arr) } onMounted(() => { // 監聽事件綁定的函數為匿名函數,将無法被清除 window.addEventListener('click', () => { console.log(arr); // 全局綁定的click事件,arr被引用,将無法被垃圾回收 }) // 正确的方式 window.addEventListener('click', printArr); }) // 正确的方式 onBeforeUnmount(() => { // 注意清除綁定事件需要前後是同一個函數,如果函數不同将不會清除 window.removeEventListener('click', printArr); }) </script>
  • 被自定義事件引用,在Vue元件銷毀時未清除,可能會導緻記憶體洩漏
  • 自定義事件通過emit/on來發起和監聽,清除自定義事件和綁定事件差不多,不同的是需要調用off方法
  • // Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' import event from './event.js'; // 自定義事件 const arr = reactive([1,2,3]); const printArr = () => { console.log(arr) } onMounted(() => { // 使用匿名函數,會導緻自定義事件無法被清除 event.on('printArr', () => { console.log(arr) }) // 正确的方式 event.on('printArr', printArr) }) // 正确的方式 onBeforeUnmount(() => { // 注意清除自定義事件需要前後是同一個函數,如果函數不同将不會清除 event.off('printArr', printArr) }) </script>

除了及時清除監聽器、事件等,對于全局變量的引用,我們可以選擇WeakMap、WeakSet等弱引用資料類型。這樣的話,即使我們引用的對象資料要被垃圾回收,弱引用的全局變量并不會阻止GC。

156.實作 table header 吸頂, 有哪些實作方式?【CSS】

實作 table header 吸頂的方法有多種,以下是一些基于 CSS 的實作方式:

  1. 使用 position: sticky 屬性:在表格頭部的 CSS 中,使用 position: sticky 屬性可以使表格頭部保持在視窗的頂部或底部,而不會随着滾動而消失。例如:
  2. cssCopy codeth { position: sticky; top: 0; background-color: #fff; }
  3. 使用 CSS transform 屬性:在表格頭部的 CSS 中,使用 CSS transform 屬性可以使表格頭部保持固定位置,而不會随着滾動而消失。例如:
  4. cssCopy codeth { position: relative; z-index: 1; } thead { position: fixed; top: 0; visibility: hidden; z-index: 2; transform: translateY(0); } tbody { margin-top: 50px; }
  5. 使用 JavaScript 和 CSS:使用 JavaScript 和 CSS 可以使表格頭部保持在視窗的頂部或底部,而不會随着滾動而消失。例如:
  6. htmlCopy code<div class="table-wrapper"> <table> <thead> <tr> <th>Column 1</th> <th>Column 2</th> <th>Column 3</th> </tr> </thead> <tbody> <tr> <td>Row 1, Column 1</td> <td>Row 1, Column 2</td> <td>Row 1, Column 3</td> </tr> <tr> <td>Row 2, Column 1</td> <td>Row 2, Column 2</td> <td>Row 2, Column 3</td> </tr> ... </tbody> </table> </div> <script> window.onscroll = function() { var header = document.querySelector(".table-wrapper thead"); if (window.pageYOffset > 150) { header.classList.add("sticky"); } else { header.classList.remove("sticky"); } }; </script> <style> .table-wrapper { position: relative; } .table-wrapper thead { position: fixed; top: 0; z-index: 1; background-color: #fff; } .table-wrapper th { height: 50px; } .table-wrapper.sticky thead { position: absolute; top: 50px; } </style>

通過以上方法的一些組合使用,可以實作 table header 吸頂,提升表格的使用者體驗和易用性。

159.[Vue] 父子元件通信方式有哪些?【web架構】

Vue 父子元件通信

  • Prop(常用)
  • $emit (元件封裝用的較多)
  • .sync文法糖 (較少)
  • $attrs & $listeners (元件封裝用的較多)
  • provide & inject (高階元件/元件庫用的較多)
  • slot-scope & v-slot ([email protected]+)新增
  • scopedSlots 屬性
  • 其他方式通信

具體使用場景參考連結:https://juejin.cn/post/6844903700243316749

160.什麼是洋蔥模型?【web架構】

說到洋蔥模型,就必須聊一聊中間件,中間件這個概念,我們并不陌生,比如平時我們用的 redux、express 、koa 這些庫裡,都離不開中間件。

那 koa 裡面的中間件是什麼樣的呢?其本質上是一個函數,這個函數有着特定,單一的功能,koa将一個個中間件注冊進來,通過組合實作強大的功能。

先看 demo :

// index.js
const Koa = require("koa")
const app = new Koa();

// 中間件1
app.use(async (ctx, next) => {
    console.log("1")
    await next()
    console.log("2")
});
// 中間件2
app.use(async (ctx, next) => {
    console.log("3")
    await next()
    console.log("4")
});
// 中間件3
app.use(async (ctx, next) => {
    console.log("5")
    await next()
    console.log("6")
});
app.listen(8002);
           

先後注冊了三個中間件,運作一下index.js ,可以看到輸出結果為:

1
3
5
6
4
2           

沒接觸過洋蔥模型的人第一眼可能會疑惑,為什麼調用了一個 next 之後,直接從1 跳到了 3 ,而不是先輸出1 ,再輸出2呢。 其實這就是洋蔥模型特點,下圖是它的執行過程:

2023.03.23 - 2023.03.26 更新前端面試問題總結(26道題)

一開始我們先後注冊了三個中間件,分别是中間件1,中間件2,中間件3,調用listen方法,打開對應端口的頁面,觸發了中間件的執行。

首先會先執行第一個中間件的 next 的前置語句,相當于 demo 裡面的 console.log('1') ,當調用 next() 之後,會直接進入第二個中間件,繼續重複上述邏輯,直至最後一個中間件,就會執行 next 的後置語句,然後繼續上一個中間件的後置語句,繼續重複上述邏輯,直至執行第一個中間件的後置語句,最後輸出。

2023.03.23 - 2023.03.26 更新前端面試問題總結(26道題)

正是因為它這種執行機制,才被稱為洋蔥模型。

164.[koa] 中間件的異常處理是怎麼做的?【web架構】

在 Koa 中,中間件函數的異常處理可以通過兩種方式來實作:

  1. 使用 try...catch 捕獲異常:在中間件函數中使用 try...catch 語句來捕獲異常,然後通過 ctx.throw() 方法抛出異常資訊,例如:
vbnetCopy codeasync function myMiddleware(ctx, next) {
  try {
    await next();
  } catch (err) {
    ctx.throw(500, 'Internal Server Error');
  }
}           

在這個例子中,await next() 表示調用下一個中間件函數,如果這個函數抛出異常,就會被捕獲到,然後通過 ctx.throw() 方法抛出一個包含錯誤狀态碼和錯誤資訊的異常。

  1. 使用 Koa 的錯誤進行中間件:Koa 提供了一個錯誤進行中間件 koa-json-error,可以通過在應用程式中使用該中間件來處理異常。這個中間件會自動捕獲應用程式中未被處理的異常,并将錯誤資訊以 JSON 格式傳回給用戶端。例如:
const Koa = require('koa');
const jsonError = require('koa-json-error');

const app = new Koa();

// 注冊錯誤進行中間件
app.use(jsonError());

// 中間件函數
async function myMiddleware(ctx, next) {
  await next();
  throw new Error('Internal Server Error');
}

// 應用中間件
app.use(myMiddleware);

// 啟動伺服器
app.listen(3000);           

在這個例子中,koa-json-error 中間件會自動捕獲應用程式中未被處理的異常,并将錯誤資訊以 JSON 格式傳回給用戶端。開發人員可以通過自定義錯誤處理函數來處理異常,例如:

const Koa = require('koa');
const jsonError = require('koa-json-error');

const app = new Koa();

// 自定義錯誤處理函數
function errorHandler(err, ctx) {
  ctx.status = err.status || 500;
  ctx.body = {
    message: err.message,
    status: ctx.status
  };
}

// 注冊錯誤進行中間件
app.use(jsonError(errorHandler));

// 中間件函數
async function myMiddleware(ctx, next) {
  await next();
  throw new Error('Internal Server Error');
}

// 應用中間件
app.use(myMiddleware);

// 啟動伺服器
app.listen(3000);           

在這個例子中,我們自定義了一個錯誤處理函數 errorHandler,将錯誤資訊格式化為 JSON 格式,并設定響應狀态碼。然後将這個函數作為參數傳遞給 koa-json-error 中間件,用于處理異常。

173.為什麼小程式裡拿不到dom相關的api【web架構】

ES6 中的裝飾器是一種特殊的文法,用于動态修改類的行為。在 JavaScript 中,裝飾器本質上是一個函數,它可以接受一個類作為參數,并傳回一個新的類,實作了類的增強或修改。裝飾器可以被用于類、方法、屬性等各種地方,可以友善地實作類似 AOP、元程式設計等功能。

裝飾器是 ES7 中的一個提案,目前還沒有正式納入标準。在 ES6 中使用裝飾器需要借助第三方庫,如 babel-plugin-transform-decorators-legacy。

裝飾器實作的基本原理是,在裝飾器函數和被裝飾對象之間建立一個代理層,通過代理層來實作裝飾器的邏輯。在類的裝飾器中,裝飾器函數的第一個參數是被裝飾的類本身,裝飾器函數内部可以通路、修改該類的屬性和方法。在方法和屬性的裝飾器中,裝飾器函數的第一個參數分别是被裝飾的方法或屬性所在的類的原型對象,裝飾器函數内部可以通路、修改該方法或屬性的屬性描述符等資訊。

以下是一個簡單的裝飾器示例,用于給類的方法添加一個計時器:

function timer(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args) {
    console.time(name);
    const result = originalMethod.apply(this, args);
    console.timeEnd(name);
    return result;
  };
  return descriptor;
}

class MyClass {
  @timer
  myMethod() {
    // do something
  }
}           

在上面的示例中,timer 函數就是一個裝飾器函數,它接受三個參數,分别是被裝飾的方法所在類的原型對象、被裝飾的方法的名稱、被裝飾的方法的屬性描述符。在 timer 函數内部,将被裝飾的方法替換為一個新的方法,新方法先執行 console.time() 方法,再執行原始方法,最後執行 console.timeEnd() 方法。最後将新的屬性描述符傳回,完成方法的裝飾。

通過類似這種方式,我們可以友善地實作各種類型的裝飾器,以增強或修改類的行為。

178.[React] useRef、ref、forwardsRef 的差別是什麼?【web架構】

在 React 中,ref 是一種用于通路 DOM 元素或元件執行個體的方法,useRef 和 forwardRef 是 ref 的兩個相關 Hook 和高階元件。

  1. ref:ref 是 React 中用于通路 DOM 元素或元件執行個體的方法。在函數元件中,可以使用 useRef Hook 來建立一個 ref 對象,然後将其傳遞給需要引用的元素或元件。在類元件中,可以直接在類中定義 ref 屬性,并将其設定為元素或元件的執行個體。
  2. useRef:useRef 是 React 中的 Hook,用于建立一個 ref 對象,并在元件生命周期内保持其不變。useRef 可以用于通路 DOM 元素或元件執行個體,并且在每次渲染時都會傳回同一個 ref 對象。通常情況下,useRef 更适合用于存儲不需要觸發重新渲染的值,例如定時器的 ID 或者其他副作用。
  3. forwardRef:forwardRef 是一個高階元件,用于将 ref 屬性轉發給其子元件。通常情況下,如果一個元件本身并不需要使用 ref 屬性,但是其子元件需要使用 ref 屬性,那麼可以使用 forwardRef 來傳遞 ref 屬性。forwardRef 接受一個函數作為參數,并将 ref 對象作為第二個參數傳遞給該函數,然後傳回一個新的元件,該元件接受 ref 屬性并将其傳遞給子元件。

簡而言之,ref 是 React 中通路 DOM 元素或元件執行個體的方法,useRef 是一個 Hook,用于建立并保持一個不變的 ref 對象,forwardRef 是一個高階元件,用于傳遞 ref 屬性給子元件。

進階開發者相關問題【共計 11 道題】

152.頁面崩潰如何監控?【網絡】

頁面崩潰如何監控?

對于 web 頁面線上監控,如果頁面崩潰了,通常會出現 500 或 404 狀态碼,或者頁面停止響應或顯示白屏等情況。

以下是一些監控崩潰的方法:

  1. 使用網站性能監測工具:這些工具可以檢測頁面的狀态碼和響應時間,如果頁面崩潰了,就會發出警報。一些流行的性能監測工具包括 New Relic, Pingdom, 和 UptimeRobot 等。
  2. 設定異常檢測:異常檢測可以監測頁面異常的行為,例如頁面響應時間超過特定時間限制,或者頁面元素加載失敗等。通過設定這些異常檢測,可以在頁面崩潰時自動觸發警報。
  3. 實時使用者行為監測:實時監測使用者行為可以幫助識别使用者在頁面上的行為,例如頁面停留時間,點選按鈕的位置等,以便檢測頁面異常行為。這些監測可以使用 Google Analytics, Mixpanel 等網站分析工具實作。
  4. 前端代碼錯誤監測:使用前端監測工具,例如 Sentry, Raygun, 和 Bugsnag 等,可以監測前端代碼錯誤,包括 JavaScript 和 CSS 錯誤,以便快速識别和解決問題。

通過以上方法的一些組合使用,可以幫助您監控 web 頁面的崩潰,及時發現和解決問題,提升使用者體驗和網站可靠性。

如果是頁面運作時頁面崩潰, 如何監控?

如果在運作時發生頁面崩潰,可以使用以下方法進行監控:

  1. 實時監控日志:可以設定日志監控,将日志實時發送到日志收集工具,例如 ELK Stack、Splunk 等。這些工具可以分析和提取有關頁面崩潰的資訊,例如錯誤消息、堆棧跟蹤等,以便快速識别和解決問題。
  2. 頁面截圖:當頁面崩潰時,可以使用截圖工具進行截屏,以捕獲頁面的目前狀态。這些截圖可以用于快速檢查頁面崩潰的根本原因。
  3. 人工檢測:可以雇用專業的品質測試人員或專業服務公司進行頁面品質測試,以便在頁面崩潰時進行手動檢測和識别。
  4. 實時異常檢測:實時監測頁面異常的行為,例如頁面響應時間超過特定時間限制,或者頁面元素加載失敗等。通過設定這些異常檢測,可以在頁面崩潰時自動觸發警報。

通過以上方法的一些組合使用,可以幫助您在運作時監控 web 頁面的崩潰,及時發現和解決問題,提升使用者體驗和網站可靠性。

153.如何監控前端頁面記憶體持續增長情況?【網絡】

監控前端頁面記憶體持續增長可以幫助我們及時發現記憶體洩漏和其他記憶體問題,進而優化前端頁面的性能和穩定性。以下是一些監控前端頁面記憶體持續增長的方法:

  1. 使用浏覽器開發工具:現代浏覽器的開發工具提供了記憶體監控功能。您可以使用 Chrome 開發者工具、Firefox 開發者工具等浏覽器工具來監控記憶體的使用情況,并在記憶體使用超過門檻值時進行警報。
  2. 手動檢查頁面代碼:您可以手動檢查頁面的代碼,特别是 JavaScript 代碼和其他 DOM 操作,以查找可能導緻記憶體洩漏的問題。例如,可能存在未清理的定時器、事件監聽器、未釋放的 DOM 元素等。
  3. 使用性能監測工具:性能監測工具,例如 New Relic、AppDynamics 等,可以監測前端頁面的性能,并提供關于記憶體使用的警報和報告。
  4. 使用記憶體檢測工具:記憶體檢測工具,例如 memoryjs、heapdump.js 等,可以幫助檢測記憶體洩漏和記憶體問題。這些工具可以生成記憶體快照,分析記憶體使用情況,以及識别潛在的記憶體洩漏問題。

通過以上方法的一些組合使用,可以幫助您監控前端頁面記憶體持續增長的情況,及時發現和解決記憶體問題,提升使用者體驗和網站可靠性。

155.常見的前端檢測記憶體洩露的方法有哪些?【JavaScript、浏覽器】

怎麼檢測記憶體洩漏

記憶體洩漏主要是指的是記憶體持續升高,但是如果是正常的記憶體增長的話,不應該被當作記憶體洩漏來排查。排查記憶體洩漏,我們可以借助Chrome DevTools的Performance和Memory選項。舉個栗子:

我們建立一個memory.html的檔案,完整代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      text-align: center;
    }
  </style>
</head>
<body>
  <p>檢測記憶體變化</p>
  <button id="btn">開始</button>
  <script>
    const arr = [];
    // 數組中添加100萬個資料
    for (let i = 0; i < 100 * 10000; i++) {
      arr.push(i)
    }
    function bind() {
      const obj = {
        str: JSON.stringify(arr) // 淺拷貝的方式建立一個比較大的字元串
      }
      // 每次調用bind函數,都在全局綁定一個onclick監聽事件,不一定非要執行
      // 使用綁定事件,主要是為了保持obj被全局标記
      window.addEventListener('click', () => {
        // 引用對象obj
        console.log(obj);
      })
    }
    let n = 0;
    function start() {
      setTimeout(() => {
        bind(); // 調用bind函數
        n++; // 循環次數增加
        if (n < 50) {
          start(); // 循環執行50次,注意這裡并沒有使用setInterval定時器
        } else {
          alert('done');
        }
      }, 200);
    }
    document.getElementById('btn').addEventListener('click', () => {
      start();
    })
  </script>
</body>
</html>           

頁面上有一個按鈕用來開始函數調用,友善我們控制。點選按鈕,每個200毫秒執行一次bind函數,即在全局監聽click事件,循環次數為50次。

在無法确定是否發生記憶體洩漏時,我們可以先使用Performance來錄制一段頁面加載的性能變化,先判斷是否有記憶體洩漏發生。

Performance

本次案例僅以Chrome浏覽器展開描述,其他浏覽器可能會有些許差異。首先我們滑鼠右鍵選擇檢查或者直接F12進入DevTools頁面,面闆上選擇Performance,選擇後應該是如下頁面:

在開始之前,我們先點選一下Collect garbage和clear來保證記憶體幹淨,沒有其他遺留記憶體的幹擾。然後我們點選Record來開始錄制,并且同時我們也要點選頁面上的開始按鈕,讓我們的代碼跑起來。等到代碼結束後,我們再點選Record按鈕以停止錄制,錄制的時間跟代碼執行的時間相比會有出入,隻要保證代碼是完全執行完畢的即可。停止錄制後,我們會得到如下的結果:

Performance的内容很多,我們隻需要關注記憶體的變化,由此圖可見,記憶體這塊區域的曲線是在一直升高的并且到達頂點後并沒有回落,這就有可能發生了記憶體洩漏。因為正常的記憶體變化曲線應該是類似于“鋸齒”,也就是有上有下,正常增長後會有一定的回落,但不一定回落到和初始值一樣。而且我們還可以隐約看到程式運作結束後,記憶體從初始的6.2MB增加到了差不多351MB,這個數量級的增加還是挺明顯的。我們隻是執行了50次循環,如果執行的次數更多,将會耗盡浏覽器的記憶體空間,導緻頁面卡死。

雖然是有記憶體洩漏,但是如果我們想進一步看記憶體洩漏發生的地方,那麼Performance就不夠用了,這個時候我們就需要使用Memory面闆。

Memory

DevTools的Memory選項主要是用來錄制堆記憶體的快照,為的是進一步分析記憶體洩漏的詳細資訊。有人可能會說,為啥不一開始就直接使用Memory呢,反而是先使用Performance。因為我們剛開始就說了,記憶體增長不表示就一定出現了記憶體洩漏,有可能是正常的增長,直接使用Memory來分析可能得不到正确的結果。

我們先來看一下怎麼使用Memory:

首先選擇Memory選項,然後清除緩存,在配置選項中選擇堆記憶體快照。記憶體快照每次點選錄制按鈕都會記錄目前的記憶體使用情況,我們可以在程式開始前點選一下記錄初始的記憶體使用,代碼結束後再點一下記錄最終的記憶體使用,中間可以點選也可以不點選。最後在快照清單中至少可以得到兩個記憶體記錄:

初始記憶體我們暫時不深究,我們選擇清單的最後一條記錄,然後在篩選下拉框選擇最後一個,即第一個快照和第二個快照的差異。

這裡我們重點說一下Shallow Size和Retained Size的差別:

  • Shallow Size:對象自身占用的記憶體大小,一般來說字元串、數組的Shallow Size都會比較大
  • Retained Size:這個是對象自身占用的記憶體加上無法被GC釋放的記憶體的大小,如果Retained Size和Shallow Size相差不大,基本上可以判定沒有發生記憶體洩漏,但是如果相差很大,例如上圖的Object,這就表明發生了記憶體洩漏。

我們再來細看一下Object,任意展開一個對象,可以在樹結構中發現每一個對象都有一個全局事件綁定,并且占用了較大的記憶體空間。解決本案例涉及的記憶體洩漏也比較簡單,就是及時釋放綁定的全局事件。

關于Performance和Memory的詳細使用可以參考:手把手教你排查Javascript記憶體洩漏

161.如何實作洋蔥模式?【web架構】

思路

  • 首先調用 use 方法收集中間件,調用 listen 方法執行中間件。
  • 每一個中間件都有一個next參數(暫時不考慮ctx參數),next參數可以控制進入下一個中間件的時機。

需要解決的問題

  • 最後一個中間件調用next如何處理
  • 如何解決同一個中間件多次調用next

完整代碼

其中最精華的部分就是compose函數,細數一下,隻有11行代碼,1比1還原了koa的compose函數(去除了不影響主邏輯判斷)。

koa是利用koa-compose這個庫進行組合中間件的,在koa-compose裡面,next傳回的都是一個promise函數。
function Koa () {
  this.middleares = [];
}
Koa.prototype.use = function (middleare) {
  this.middleares.push(middleare);
  return this;
}
Koa.prototype.listen = function () {
  const fn = compose(this.middleares);
}
function compose(middleares) {
  let index = -1;
  const dispatch = (i) => {
    if(i <= index) throw new Error('next() 不能調用多次');
    index = i;
    if(i >= middleares.length) return;
    const middleare = middleares[i];
    return middleare('ctx', dispatch.bind(null, i + 1));
  }
  return dispatch(0);
}

const app = new Koa();
app.use(async (ctx, next) => {
  console.log('1');
  next();
  console.log('2');
});
app.use(async (ctx, next) => {
  console.log('3');
  next();
  console.log('4');
});
app.use(async (ctx, next) => {
  console.log('5');
  next();
  console.log('6');
});

app.listen();           

使用

const Koa = require('koa');
const app = new Koa();

// 中間件過多,可以建立一個middleares檔案夾,将cors函數放到middleares/cors.js檔案裡面
const cors = () => {
  return async (ctx, next) => {
    ctx.set('Access-Control-Allow-Headers', 'X-Requested-With')
    ctx.set('Access-Control-Allow-Origin', '*')
    ctx.set('Access-Control-Allow-Methods', 'GET,HEAD,PUT,POST,DELETE,PATCH')
    await next();
  }
};

app.use(cors());
app.use(async (ctx, next) => {
  console.log('第一個中間件', ctx.request.method,ctx.request.url);
  await next();
  ctx.body = 'hello world'
});           

koa的中間件都是有固定模闆的,首先是一個函數,并且傳回一個async函數(閉包的應用),這個async函數有兩個參數,一個是koa的context,一個是next函數。

168.[koa] 在沒有async await 的時候, koa是怎麼實作的洋蔥模型?【web架構】

在沒有 async/await 的時候,Koa 通過使用 ES6 的生成器函數來實作洋蔥模型。具體來說,Koa 中間件函數是一個帶有 next 參數的生成器函數,當中間件函數調用 next 方法時,它會挂起目前的執行,轉而執行下一個中間件函數,直到執行完最後一個中間件函數,然後将執行權傳回到前一個中間件函數,繼續執行下面的代碼。這個過程就像一層一層剝開洋蔥一樣,是以被稱為洋蔥模型。

下面是一個使用生成器函數實作的簡單的 Koa 中間件函數:

function* myMiddleware(next) {
  // 中間件函數的代碼
  console.log('Start');
  yield next;
  console.log('End');
}           

在這個中間件函數中,yield next 表示挂起目前的執行,執行下一個中間件函數。假設我們有兩個中間件函數 middleware1 和 middleware2,它們的代碼如下:

function* middleware1(next) {
  console.log('middleware1 Start');
  yield next;
  console.log('middleware1 End');
}

function* middleware2(next) {
  console.log('middleware2 Start');
  yield next;
  console.log('middleware2 End');
}           

我們可以使用 compose 函數将它們組合成一個洋蔥模型:

scssCopy codeconst compose = require('koa-compose');

const app = compose([middleware1, middleware2]);

app();           

在這個例子中,compose 函數将 middleware1 和 middleware2 組合成一個函數 app,然後調用這個函數即可執行整個中間件鍊。執行的結果如下:

sqlCopy codemiddleware1 Start
middleware2 Start
middleware2 End
middleware1 End           

可以看到,這個結果與洋蔥模型的特點相符。

169.[koa] body-parser 中間件實作原理?【web架構】

Koa 中間件 koa-bodyparser 的原理是将 HTTP 請求中的 request body 解析成 JavaScript 對象,并将其挂載到 ctx.request.body 屬性上,友善後續的處理。

具體來說,koa-bodyparser 中間件會監聽 HTTP 請求的 data 事件和 end 事件,然後将請求中的資料流解析成一個 JavaScript 對象,并将其作為參數傳遞給 ctx.request.body 屬性,最後調用 await next(),将控制權交給下一個中間件。

在實作過程中,koa-bodyparser 中間件會根據請求頭中的 Content-Type 字段來判斷請求體的類型,支援解析的請求體類型有 application/json、application/x-www-form-urlencoded 和 multipart/form-data。對于其他類型的請求體,koa-bodyparser 會将其解析成一個空對象 {}。

下面是一個簡單的 koa-bodyparser 中間件的實作示例:

function bodyParser() {
  return async (ctx, next) => {
    if (ctx.request.method === 'POST' || ctx.request.method === 'PUT') {
      let data = '';
      ctx.req.on('data', (chunk) => {
        data += chunk;
      });
      ctx.req.on('end', () => {
        if (ctx.request.headers['content-type'] === 'application/json') {
          ctx.request.body = JSON.parse(data);
        } else if (ctx.request.headers['content-type'] === 'application/x-www-form-urlencoded') {
          ctx.request.body = querystring.parse(data);
        } else if (ctx.request.headers['content-type'].startsWith('multipart/form-data')) {
          // 解析 multipart/form-data 請求體
          // ...
        } else {
          ctx.request.body = {};
        }
        return next();
      });
    } else {
      return next();
    }
  };
}           

在這個實作中,如果請求方法為 POST 或者 PUT,則開始監聽 data 事件和 end 事件,将請求體資料解析成一個 JavaScript 對象并挂載到 ctx.request.body 上,最後調用 next() 将控制權交給下一個中間件。對于其他請求方法,則直接調用 next() 交給下一個中間件處理。注意,這個實作隻支援解析 application/json 和 application/x-www-form-urlencoded 類型的請求體,對于其他類型的請求體需要進行特殊處理。

170.檔案上傳和上傳檔案解析的原理是啥?【網絡、浏覽器】

前端如何上傳圖檔等檔案

在前端上傳圖檔或其他檔案字段,一般使用 HTML 中的 <input> 元素,并設定 type="file",這樣就可以讓使用者選擇本地的檔案進行上傳。

例如:

htmlCopy code<form enctype="multipart/form-data">
  <input type="file" name="image" accept="image/*">
  <input type="submit" value="上傳">
</form>           

在這個例子中,<form> 元素的 enctype 屬性設定為 multipart/form-data,這樣就可以将檔案作為二進制資料進行上傳。

當使用者選擇了本地的檔案後,送出表單時就會将檔案作為 FormData 對象的一部分進行發送。在 JavaScript 中,可以通過 FormData 構造函數來建立一個 FormData 對象,并使用 append() 方法向其中添加上傳的檔案資料。例如:

const formData = new FormData();
const fileInput = document.querySelector('input[type="file"]');
formData.append('image', fileInput.files[0]);           

這樣就可以将檔案資料添加到 formData 對象中,然後使用 fetch() 方法或其他方式将其發送到後端進行處理。

需要注意的是,由于安全原因,浏覽器限制了使用者上傳檔案的大小和類型。可以使用 accept 屬性來限制檔案的類型,例如 accept="image/*" 表示隻允許上傳圖檔類型的檔案。可以使用 multiple 屬性來允許使用者選擇多個檔案進行上傳。同時,還需要在後端對上傳的檔案進行處理和驗證,以確定安全性和正确性。

後端如何解析?koa 為例

在 Koa 中解析上傳的檔案需要使用一個叫做 koa-body 的中間件,它可以自動将 multipart/form-data 格式的請求體解析成 JavaScript 對象,進而擷取到上傳的檔案和其他表單資料。

以下是一個使用 koa-body 中間件解析上傳檔案的例子:

const Koa = require('koa');
const koaBody = require('koa-body');

const app = new Koa();

// 注冊 koa-body 中間件
app.use(koaBody({
  multipart: true, // 支援上傳檔案
}));

// 處理上傳檔案的請求
app.use(async (ctx) => {
  const { files, fields } = ctx.request.body; // 擷取上傳的檔案和其他表單資料
  const file = files && files.image; // 擷取上傳的名為 image 的檔案

  if (file) {
    console.log(`Received file: ${file.name}, type: ${file.type}, size: ${file.size}`);
    // 處理上傳的檔案
  } else {
    console.log('No file received');
  }

  // 傳回響應
  ctx.body = 'Upload success';
});

app.listen(3000);           

在上述代碼中,使用 koa-body 中間件注冊了一個解析請求體的函數,并在請求處理函數中擷取到了上傳的檔案和其他表單資料。其中,files 對象包含了所有上傳的檔案,fields 對象包含了所有非檔案類型的表單資料。

可以根據實際需要從 files 對象中擷取到需要處理的檔案,例如上面的例子中使用了 files.image 來擷取名為 image 的上傳檔案。可以使用上傳檔案的屬性,如 name、type 和 size 來擷取檔案的資訊,并進行處理。最後傳回響應,表示上傳成功。

需要注意的是,koa-body 中間件需要設定 multipart: true 才能支援上傳檔案。另外,在處理上傳檔案時需要注意安全性和正确性,可以使用第三方的檔案上傳處理庫來進行處理。

解析上傳檔案的原理是啥?

在 HTTP 協定中,上傳檔案的請求通常使用 multipart/form-data 格式的請求體。這種格式的請求體由多個部分組成,每個部分以一個 boundary 字元串作為分隔符,每個部分都代表一個字段或一個檔案。

對于一個上傳檔案的請求,浏覽器會将請求體按照 multipart/form-data 格式構造,其中每個部分都有一些描述資訊和内容,例如檔案名、檔案類型、檔案大小、内容等。

伺服器端需要對這些部分進行解析,提取出所需要的資訊。常見的解析方式有兩種:

  1. 手動解析:根據 multipart/form-data 格式的規範,按照 boundary 字元串将請求體切分為多個部分,然後解析每個部分的頭部和内容,提取出檔案名、檔案類型、檔案大小等資訊。這種方式比較麻煩,需要手動處理較多的細節,容易出錯。
  2. 使用第三方庫:可以使用第三方的解析庫,如 multer、formidable、busboy 等,來友善地解析 multipart/form-data 格式的請求體。這些庫通常會将解析出的資訊存儲到一個對象中,友善進一步處理。

在 Node.js 中,使用 http 子產品自己實作 multipart/form-data 的解析比較麻煩,常見的做法是使用第三方庫來解析上傳檔案,例如在 Koa 中使用 koa-body 中間件就可以友善地處理上傳檔案。

172.es6 class 裝飾器是如何實作的?【JavaScript】

ES6 中的裝飾器是一種特殊的文法,用于動态修改類的行為。在 JavaScript 中,裝飾器本質上是一個函數,它可以接受一個類作為參數,并傳回一個新的類,實作了類的增強或修改。裝飾器可以被用于類、方法、屬性等各種地方,可以友善地實作類似 AOP、元程式設計等功能。

裝飾器是 ES7 中的一個提案,目前還沒有正式納入标準。在 ES6 中使用裝飾器需要借助第三方庫,如 babel-plugin-transform-decorators-legacy。

裝飾器實作的基本原理是,在裝飾器函數和被裝飾對象之間建立一個代理層,通過代理層來實作裝飾器的邏輯。在類的裝飾器中,裝飾器函數的第一個參數是被裝飾的類本身,裝飾器函數内部可以通路、修改該類的屬性和方法。在方法和屬性的裝飾器中,裝飾器函數的第一個參數分别是被裝飾的方法或屬性所在的類的原型對象,裝飾器函數内部可以通路、修改該方法或屬性的屬性描述符等資訊。

以下是一個簡單的裝飾器示例,用于給類的方法添加一個計時器:

function timer(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args) {
    console.time(name);
    const result = originalMethod.apply(this, args);
    console.timeEnd(name);
    return result;
  };
  return descriptor;
}

class MyClass {
  @timer
  myMethod() {
    // do something
  }
}           

在上面的示例中,timer 函數就是一個裝飾器函數,它接受三個參數,分别是被裝飾的方法所在類的原型對象、被裝飾的方法的名稱、被裝飾的方法的屬性描述符。在 timer 函數内部,将被裝飾的方法替換為一個新的方法,新方法先執行 console.time() 方法,再執行原始方法,最後執行 console.timeEnd() 方法。最後将新的屬性描述符傳回,完成方法的裝飾。

通過類似這種方式,我們可以友善地實作各種類型的裝飾器,以增強或修改類的行為。

174.Promise then 第二個參數和 Promise.catch 的差別是什麼?【JavaScript】

Promise.then() 方法可以接受兩個參數,第一個參數是 onFulfilled 回調函數,第二個參數是 onRejected 回調函數。當 Promise 狀态變為 fulfilled 時,将會調用 onFulfilled 回調函數;當 Promise 狀态變為 rejected 時,将會調用 onRejected 回調函數。其中,第二個參數 onRejected 是可選的。

Promise.catch() 方法是一個特殊的 Promise.then() 方法,它隻接受一個參數,即 onRejected 回調函數。如果 Promise 狀态變為 rejected,則會調用 onRejected 回調函數;如果狀态變為 fulfilled,則不會調用任何回調函數。是以,Promise.catch() 方法可以用來捕獲 Promise 中的錯誤,相當于使用 Promise.then(undefined, onRejected)。

差別主要在于使用的方式不同。Promise.then(onFulfilled, onRejected) 可以同時傳遞兩個回調函數,用來處理 Promise 狀态變為 fulfilled 或者 rejected 的情況;而 Promise.catch(onRejected) 則隻能用來處理 Promise 狀态變為 rejected 的情況,并且使用更加簡潔明了。

175.Promise finally 怎麼實作的?【JavaScript】

Promise.finally() 方法是在 ES2018 中引入的,用于指定不管 Promise 狀态如何都要執行的回調函數。與 Promise.then() 和 Promise.catch() 不同的是,Promise.finally() 方法不管 Promise 是成功還是失敗都會執行回調函數,而且不會改變 Promise 的狀态。如果傳回的值是一個 Promise,那麼 Promise.finally() 方法會等待該 Promise 執行完畢後再繼續執行。

Promise.finally() 方法的實作思路如下:

  1. Promise.finally() 方法接收一個回調函數作為參數,傳回一個新的 Promise 執行個體。
  2. 在新的 Promise 執行個體的 then() 方法中,首先調用原 Promise 的 then() 方法,将原 Promise 的結果傳遞給下一個 then() 方法。
  3. 在新的 Promise 執行個體的 then() 方法中,調用回調函數并将原 Promise 的結果傳遞給回調函數。
  4. 如果回調函數傳回一個 Promise,則需要在新的 Promise 執行個體的 then() 方法中等待該 Promise 執行完畢,再将結果傳遞給下一個 then() 方法。
  5. 在新的 Promise 執行個體的 finally() 方法中,傳回一個新的 Promise 執行個體。

下面是一個簡單的實作示例:

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
}           

這個實作方法中,使用了 Promise.resolve() 來傳回一個新的 Promise 執行個體,是以可以避免了 Promise 鍊的狀态改變。另外,由于 finally() 方法隻是在 Promise 鍊的最後執行回調函數,是以不需要使用異步函數。

179.[React] useEffect的第二個參數,如何判斷依賴是否發生變化?【web架構】

useEffect的第二個參數是一個依賴數組,用于判斷副作用函數的依賴是否發生變化。React使用JavaScript的Object.is方法來判斷依賴項是否發生變化。在比較依賴項時,React首先檢查依賴項的值是否相等。如果依賴項的值是引用類型,React會比較它們的引用位址,而不是比較它們的屬性值。是以,在比較引用類型時,即使對象具有相同的屬性值,但它們的引用位址不同,React仍然認為它們是不同的。

需要注意的是,如果依賴項是一個數組或對象,由于它們是引用類型,是以即使數組或對象中的元素或屬性沒有發生變化,但數組或對象本身的引用位址發生變化,也會導緻React重新執行副作用函數。在這種情況下,我們可以使用useCallback和useMemo來緩存回調函數和計算結果,以便避免在依賴數組發生變化時重新計算和建立。

資深開發者相關問題【共計 2 道題】

151.HTTPS 加密算法和加解密過程是啥?【網絡】

探究 HTTPS

我說,你起這麼牛逼的名字幹嘛,還想吹牛批?你 HTTPS 不就抱上了 TLS/SSL 的大腿麼,咋這麼牛批哄哄的,還想探究 HTTPS,瞎胡鬧,趕緊改成 TLS 是我主,贊美我主。

SSL 即安全套接字層,它在 OSI 七層網絡模型中處于第五層,SSL 在 1999 年被 IETF(網際網路工程組)更名為 TLS ,即傳輸安全層,直到現在,TLS 一共出現過三個版本,1.1、1.2 和 1.3 ,目前最廣泛使用的是 1.2,是以接下來的探讨都是基于 TLS 1.2 的版本上的。

TLS 用于兩個通信應用程式之間提供保密性和資料完整性。TLS 由記錄協定、握手協定、警告協定、變更密碼規範協定、擴充協定等幾個子協定組成,綜合使用了對稱加密、非對稱加密、身份認證等許多密碼學前沿技術(如果你覺得一項技術很簡單,那你隻是沒有學到位,任何技術都是有美感的,牛逼的人隻是欣賞,并不是貶低)。

說了這麼半天,我們還沒有看到 TLS 的命名規範呢,下面舉一個 TLS 例子來看一下 TLS 的結構(可以參考 www.iana.org/assignments…

ECDHE-ECDSA-AES256-GCM-SHA384

           

這是啥意思呢?我剛開始看也有點懵啊,但其實是有套路的,因為 TLS 的密碼套件比較規範,基本格式就是 密鑰交換算法 - 簽名算法 - 對稱加密算法 - 摘要算法 組成的一個密碼串,有時候還有分組模式,我們先來看一下剛剛是什麼意思

使用 ECDHE 進行密鑰交換,使用 ECDSA 進行簽名和認證,然後使用 AES 作為對稱加密算法,密鑰的長度是 256 位,使用 GCM 作為分組模式,最後使用 SHA384 作為摘要算法。

TLS 在根本上使用對稱加密和 非對稱加密 兩種形式。

對稱加密

在了解對稱加密前,我們先來了解一下密碼學的東西,在密碼學中,有幾個概念:明文、密文、加密、解密

  • 明文(Plaintext),一般認為明文是有意義的字元或者比特集,或者是通過某種公開編碼就能獲得的消息。明文通常用 m 或 p 表示
  • 密文(Ciphertext),對明文進行某種加密後就變成了密文
  • 加密(Encrypt),把原始的資訊(明文)轉換為密文的資訊變換過程
  • 解密(Decrypt),把已經加密的資訊恢複成明文的過程。

對稱加密(Symmetrical Encryption)顧名思義就是指加密和解密時使用的密鑰都是同樣的密鑰。隻要保證了密鑰的安全性,那麼整個通信過程也就是具有了機密性。

TLS 裡面有比較多的加密算法可供使用,比如 DES、3DES、AES、ChaCha20、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK 等。目前最常用的是 AES-128, AES-192、AES-256 和 ChaCha20。

DES 的全稱是 Data Encryption Standard(資料加密标準) ,它是用于數字資料加密的對稱密鑰算法。盡管其 56 位的短密鑰長度使它對于現代應用程式來說太不安全了,但它在加密技術的發展中具有很大的影響力。

3DES 是從原始資料加密标準(DES)衍生過來的加密算法,它在 90 年代後變得很重要,但是後面由于更加進階的算法出現,3DES 變得不再重要。

AES-128, AES-192 和 AES-256 都是屬于 AES ,AES 的全稱是Advanced Encryption Standard(進階加密标準),它是 DES 算法的替代者,安全強度很高,性能也很好,是應用最廣泛的對稱加密算法。

ChaCha20 是 Google 設計的另一種加密算法,密鑰長度固定為 256 位,純軟體運作性能要超過 AES,曾經在移動用戶端上比較流行,但 ARMv8 之後也加入了 AES 硬體優化,是以現在不再具有明顯的優勢,但仍然算得上是一個不錯算法。

(其他可自行搜尋)

加密分組

對稱加密算法還有一個分組模式 的概念,對于 GCM 分組模式,隻有和 AES,CAMELLIA 和 ARIA 搭配使用,而 AES 顯然是最受歡迎和部署最廣泛的選擇,它可以讓算法用固定長度的密鑰加密任意長度的明文。

最早有 ECB、CBC、CFB、OFB 等幾種分組模式,但都陸續被發現有安全漏洞,是以現在基本都不怎麼用了。最新的分組模式被稱為 AEAD(Authenticated Encryption with Associated Data),在加密的同時增加了認證的功能,常用的是 GCM、CCM 和 Poly1305。

比如 ECDHE_ECDSA_AES128_GCM_SHA256 ,表示的是具有 128 位密鑰, AES256 将表示 256 位密鑰。GCM 表示具有 128 位塊的分組密碼的現代認證的關聯資料加密(AEAD)操作模式。

我們上面談到了對稱加密,對稱加密的加密方和解密方都使用同一個密鑰,也就是說,加密方必須對原始資料進行加密,然後再把密鑰交給解密方進行解密,然後才能解密資料,這就會造成什麼問題?這就好比《小兵張嘎》去送信(信已經被加密過),但是嘎子還拿着解密的密碼,那嘎子要是在途中被鬼子發現了,那這信可就是被完全的暴露了。是以,對稱加密存在風險。

非對稱加密

非對稱加密(Asymmetrical Encryption) 也被稱為公鑰加密,相對于對稱加密來說,非對稱加密是一種新的改良加密方式。密鑰通過網絡傳輸交換,它能夠確定及時密鑰被攔截,也不會暴露資料資訊。非對稱加密中有兩個密鑰,一個是公鑰,一個是私鑰,公鑰進行加密,私鑰進行解密。公開密鑰可供任何人使用,私鑰隻有你自己能夠知道。

使用公鑰加密的文本隻能使用私鑰解密,同時,使用私鑰加密的文本也可以使用公鑰解密。公鑰不需要具有安全性,因為公鑰需要在網絡間進行傳輸,非對稱加密可以解決密鑰交換的問題。網站保管私鑰,在網上任意分發公鑰,你想要登入網站隻要用公鑰加密就行了,密文隻能由私鑰持有者才能解密。而黑客因為沒有私鑰,是以就無法破解密文。

非對稱加密算法的設計要比對稱算法難得多(我們不會探讨具體的加密方式),常見的比如 DH、DSA、RSA、ECC 等。

其中 RSA 加密算法是最重要的、最出名的一個了。例如 DHE_RSA_CAMELLIA128_GCM_SHA256。它的安全性基于 整數分解,使用兩個超大素數的乘積作為生成密鑰的材料,想要從公鑰推算出私鑰是非常困難的。

ECC(Elliptic Curve Cryptography)也是非對稱加密算法的一種,它基于橢圓曲線離散對數的數學難題,使用特定的曲線方程和基點生成公鑰和私鑰, ECDHE 用于密鑰交換,ECDSA 用于數字簽名。

TLS 是使用對稱加密和非對稱加密 的混合加密方式來實作機密性。

混合加密

RSA 的運算速度非常慢,而 AES 的加密速度比較快,而 TLS 正是使用了這種混合加密方式。在通信剛開始的時候使用非對稱算法,比如 RSA、ECDHE ,首先解決密鑰交換的問題。然後用随機數産生對稱算法使用的會話密鑰(session key),再用公鑰加密。對方拿到密文後用私鑰解密,取出會話密鑰。這樣,雙方就實作了對稱密鑰的安全交換。

現在我們使用混合加密的方式實作了機密性,是不是就能夠安全的傳輸資料了呢?還不夠,在機密性的基礎上還要加上完整性、身份認證的特性,才能實作真正的安全。而實作完整性的主要手段是 摘要算法(Digest Algorithm)

摘要算法

如何實作完整性呢?在 TLS 中,實作完整性的手段主要是 摘要算法(Digest Algorithm)。摘要算法你不清楚的話,MD5 你應該清楚,MD5 的全稱是 Message Digest Algorithm 5,它是屬于密碼雜湊演算法(cryptographic hash algorithm)的一種,MD5 可用于從任意長度的字元串建立 128 位字元串值。盡管 MD5 存在不安全因素,但是仍然沿用至今。MD5 最常用于驗證檔案的完整性。但是,它還用于其他安全協定和應用程式中,例如 SSH、SSL 和 IPSec。一些應用程式通過向明文加鹽值或多次應用哈希函數來增強 MD5 算法。

什麼是加鹽?在密碼學中,鹽就是一項随機資料,用作哈希資料,密碼或密碼的單向函數的附加輸入。鹽用于保護存儲中的密碼。例如 什麼是單向?就是在說這種算法沒有密鑰可以進行解密,隻能進行單向加密,加密後的資料無法解密,不能逆推出原文。

我們再回到摘要算法的讨論上來,其實你可以把摘要算法了解成一種特殊的壓縮算法,它能夠把任意長度的資料壓縮成一種固定長度的字元串,這就好像是給資料加了一把鎖。

除了常用的 MD5 是加密算法外,SHA-1(Secure Hash Algorithm 1) 也是一種常用的加密算法,不過 SHA-1 也是不安全的加密算法,在 TLS 裡面被禁止使用。目前 TLS 推薦使用的是 SHA-1 的後繼者:SHA-2。

SHA-2 的全稱是Secure Hash Algorithm 2 ,它在 2001 年被推出,它在 SHA-1 的基礎上做了重大的修改,SHA-2 系列包含六個哈希函數,其摘要(哈希值)分别為 224、256、384 或 512 位:SHA-224, SHA-256, SHA-384, SHA-512。分别能夠生成 28 位元組、32 位元組、48 位元組、64 位元組的摘要。

有了 SHA-2 的保護,就能夠實作資料的完整性,哪怕你在檔案中改變一個标點符号,增加一個空格,生成的檔案摘要也會完全不同,不過 SHA-2 是基于明文的加密方式,還是不夠安全,那應該用什麼呢?

安全性更高的加密方式是使用 HMAC,在了解什麼是 HMAC 前,你需要先知道一下什麼是 MAC。

MAC 的全稱是message authentication code,它通過 MAC 算法從消息和密鑰生成,MAC 值允許驗證者(也擁有秘密密鑰)檢測到消息内容的任何更改,進而保護了消息的資料完整性。

HMAC 是 MAC 更進一步的拓展,它是使用 MAC 值 + Hash 值的組合方式,HMAC 的計算中可以使用任何加密哈希函數,例如 SHA-256 等。

現在我們又解決了完整性的問題,那麼就隻剩下一個問題了,那就是認證,認證怎麼做的呢?我們再向伺服器發送資料的過程中,黑客(攻擊者)有可能僞裝成任何一方來竊取資訊。它可以僞裝成你,來向伺服器發送資訊,也可以僞裝稱為伺服器,接受你發送的資訊。那麼怎麼解決這個問題呢?

認證

如何确定你自己的唯一性呢?我們在上面的叙述過程中出現過公鑰加密,私鑰解密的這個概念。提到的私鑰隻有你一個人所有,能夠辨識唯一性,是以我們可以把順序調換一下,變成私鑰加密,公鑰解密。使用私鑰再加上摘要算法,就能夠實作數字簽名,進而實作認證。

到現在,綜合使用對稱加密、非對稱加密和摘要算法,我們已經實作了加密、資料認證、認證,那麼是不是就安全了呢?非也,這裡還存在一個數字簽名的認證問題。因為私鑰是是自己的,公鑰是誰都可以釋出,是以必須釋出經過認證的公鑰,才能解決公鑰的信任問題。

是以引入了 CA,CA 的全稱是 Certificate Authority,證書認證機構,你必須讓 CA 頒布具有認證過的公鑰,才能解決公鑰的信任問題。

全世界具有認證的 CA 就幾家,分别頒布了 DV、OV、EV 三種,差別在于可信程度。DV 是最低的,隻是域名級别的可信,EV 是最高的,經過了法律和審計的嚴格核查,可以證明網站擁有者的身份(在浏覽器位址欄會顯示出公司的名字,例如 Apple、GitHub 的網站)。不同的信任等級的機構一起形成了層級關系。

通常情況下,數字證書的申請人将生成由私鑰和公鑰以及證書簽名請求(CSR)組成的密鑰對。CSR是一個編碼的文本檔案,其中包含公鑰和其他将包含在證書中的資訊(例如域名,組織,電子郵件位址等)。密鑰對和 CSR生成通常在将要安裝證書的伺服器上完成,并且 CSR 中包含的資訊類型取決于證書的驗證級别。與公鑰不同,申請人的私鑰是安全的,永遠不要向 CA(或其他任何人)展示。

生成 CSR 後,申請人将其發送給 CA,CA 會驗證其包含的資訊是否正确,如果正确,則使用頒發的私鑰對證書進行數字簽名,然後将其發送給申請人。

總結

本篇文章我們主要講述了 HTTPS 為什麼會出現 ,HTTPS 解決了 HTTP 的什麼問題,HTTPS 和 HTTP 的關系是什麼,TLS 和 SSL 是什麼,TLS 和 SSL 解決了什麼問題?如何實作一個真正安全的資料傳輸?

文章參考

  • https://juejin.cn/post/6844904089495535624

www.ssl.com/faqs/what-i…

www.ibm.com/support/kno…

en.wikipedia.org/wiki/Messag…

en.wikipedia.org/wiki/HMAC

www.quora.com/What-does-i…

hpbn.co/transport-l…

www.ssl2buy.com/wiki/symmet…

crypto.stackexchange.com/questions/2…

en.wikipedia.org/wiki/Advanc…

www.comparitech.com/blog/inform…

《極客時間-透析 HTTP 協定》

www.tutorialsteacher.com/https/how-s…

baike.baidu.com/item/密碼系統/5…

baike.baidu.com/item/對稱加密/2…

www.ssl.com/faqs/faq-wh…

en.wikipedia.org/wiki/HTTPS

support.google.com/webmasters/…

www.cloudflare.com/learning/ss…

www.cisco.com/c/en/us/pro…

www.freecodecamp.org/news/web-se…

176.WebWorker、SharedWorker 和 ServiceWorker 有哪些差別?【JavaScript】

前言

衆所周知,JavaScript 是單線程的語言。當我們面臨需要大量計算的場景時(比如視訊解碼等),UI 線程就會被阻塞,甚至浏覽器直接卡死。現在前端遇到大量計算的場景越來越多,為了有更好的體驗,HTML5 中提出了 Web Worker 的概念。Web Worker 可以使腳本運作在新的線程中,它們獨立于主線程,可以進行大量的計算活動,而不會影響主線程的 UI 渲染。當計算結束之後,它們可以把結果發送給主線程,進而形成了高效、良好的使用者體驗。Web Worker 是一個統稱,具體可以細分為普通的 Worker、SharedWorker 和 ServiceWorker 等,接下來我們一一介紹其使用方法和适合的場景。

普通 Worker

  1. 建立 Worker 通過 new 的方式來生成一個執行個體,參數為 url 位址,該位址必須和其建立者是同源的。
const worker = new Worker('./worker.js'); // 參數是url,這個url必須與建立者同源            
  1. Worker 的方法
  • onmessage 主線程中可以在 Worker 上添加 onmessage 方法,用于監聽 Worker 的資訊。
  • onmessageerror 主線程中可以在 Worker 上添加 onmessageerror 方法,用于監聽 Worker 的錯誤資訊。
  • postMessage() 主線程通過此方法給 Worker 發送消息,發送參數的格式不限(可以是數組、對象、字元串等),可以根據自己的業務選擇。
  • terminate() 主線程通過此方法終止 Worker 的運作。
  1. 通信

Worker 的作用域跟主線程中的 Window 是互相獨立的,并且 Worker 中是擷取不到 DOM 元素的。是以在 Worker 中你無法使用 Window 變量。取而代之的是可以用 self 來表示全局對象。self 上有哪些方法和屬性,感興趣的小夥伴可以自行輸出檢視。比較常用的方法是 onmessage、postMessage,主要用來跟主線程進行通信。

  1. Worker 中引用其他腳本的方式

跟常用的 JavaScript 一樣,Worker 中也是可以引入其他的子產品的。但是方式不太一樣,是通過 importScripts 來引入。這邊我為了示範,建立了一個 constant.js。在 constant.js 定義了一些變量和函數。

示例:

// Worker.js
   importScripts('constant.js');
   // 下面就可以擷取到 constant.js 中的所有變量了

   // constant.js
   // 可以在 Worker 中使用
   const a = 111;

   // 不可以在 Worker 中使用,原因未知
   const b = function () {
     console.log('test');
   };

   // 可以在 Worker 中使用
   function c() {
     console.log('test');
   }           
  1. 調試方法

寫代碼難免要進行調試。Worker 的調試在浏覽器控制台中有專門展示的地方, 以 chrome 浏覽器為例: dev tools --> source --> worker.js

  1. 常見使用場景一般的視訊網站 以優酷為例,當我們開始播放優酷視訊的時候,就能看到它會調用 Worker,解碼的代碼應該寫在 Worker 裡面。需要大量計算的網站 比如 imgcook 這個網站,它能在前端解析 sketch 檔案,這部分解析的邏輯就寫在 Worker 裡。

SharedWorker

SharedWorker 是一種特定的 Worker。從它的命名就能知道,它是一種共享資料的 Worker。它可以同時被多個浏覽器環境通路。這些浏覽器環境可以是多個 window, iframes 或者甚至是多個 Worker,隻要這些 Workers 處于同一主域。為跨浏覽器 tab 共享資料提供了一種解決方案。

  1. 建立 SharedWorker
  2. 建立的方法跟上面普通 Worker 完全一模一樣。
const worker = new SharedWorker("./shareWorker.js"); // 參數是url,這個url必須與建立者同源            
  1. SharedWorker 的方法
  2. SharedWorker 的方法都在 port 上,這是它與普通 Worker 不同的地方。
  • port.onmessage
  • 主線程中可以在 worker 上添加 onmessage 方法,用于監聽 SharedWorker 的資訊
  • port.postMessage()
  • 主線程通過此方法給 SharedWorker 發送消息,發送參數的格式不限
  • port.start()
  • 主線程通過此方法開啟 SharedWorker 之間的通信
  • port.close()
  • 主線程通過此方法關閉 SharedWorker
  1. 通信
  2. SharedWorker 跟普通的 Worker 一樣,可以用 self 來表示全局對象。不同之處是,它需要等 port 連接配接成功之後,利用 port 的onmessage、postMessage,來跟主線程進行通信。當你打開多個視窗的時候,SharedWorker 的作用域是公用的,這也是其特點。
  3. Worker 中引用其他腳本
  4. 這個與普通的 Worker 方法一樣,使用 importScripts
  5. 調試方法
  6. 在浏覽器中檢視和調試 SharedWorker 的代碼,需要輸入 chrome://inspect/

ServiceWorker

ServiceWorker 一般作為 Web 應用程式、浏覽器和網絡之間的代理服務。他們旨在建立有效的離線體驗,攔截網絡請求,以及根據網絡是否可用采取合适的行動,更新駐留在伺服器上的資源。他們還将允許通路推送通知和背景同步 API。

  1. 建立 ServiceWorker
// index.js
   if ('serviceWorker' in navigator) {
     window.addEventListener('load', function () {
       navigator.serviceWorker
         .register('./serviceWorker.js', { scope: '/page/' })
         .then(
           function (registration) {
             console.log(
               'ServiceWorker registration successful with scope: ',
               registration.scope
             );
           },
           function (err) {
             console.log('ServiceWorker registration failed: ', err);
           }
         );
     });
   }           

隻要建立了 ServiceWorker,不管這個建立 ServiceWorker 的 html 是否打開,這個 ServiceWorker 是一直存在的。它會代理範圍是根據 scope 決定的,如果沒有這個參數,則其代理範圍是建立目錄同級别以及子目錄下所有頁面的網絡請求。代理的範圍可以通過registration.scope 檢視。

  1. 安裝 ServiceWorker
// serviceWorker.js
   const CACHE_NAME = 'cache-v1';
   // 需要緩存的檔案
   const urlsToCache = [
     '/style/main.css',
     '/constant.js',
     '/serviceWorker.html',
     '/page/index.html',
     '/serviceWorker.js',
     '/image/131.png',
   ];
   self.oninstall = (event) => {
     event.waitUntil(
       caches
         .open(CACHE_NAME) // 這傳回的是promise
         .then(function (cache) {
           return cache.addAll(urlsToCache); // 這傳回的是promise
         })
     );
   };           

在上述代碼中,我們可以看到,在 install 事件的回調中,我們打開了名字為 cache-v1 的緩存,它傳回的是一個 promise。在打開緩存之後,我們需要把要緩存的檔案 add 進去,基本上所有類型的資源都可以進行緩存,例子中緩存了 css、js、html、png。如果所有緩存資料都成功,就表示 ServiceWorker 安裝成功;如果控制台提示 Uncaught (in promise) TypeError: Failed to execute 'Cache' on 'addAll': Request failed,則表示安裝失敗。

  1. 緩存和傳回請求
self.onfetch = (event) => {
     event.respondWith(
       caches
         .match(event.request) // 此方法從服務工作線程所建立的任何緩存中查找緩存的結果
         .then(function (response) {
           // response為比對到的緩存資源,如果沒有比對到則傳回undefined,需要fetch資源
           if (response) {
             return response;
           }
           return fetch(event.request);
         })
     );
   };           

在 fetch 事件的回調中,我們去比對 cache 中的資源。如果比對到,則使用緩存資源;沒有比對到則用 fetch 請求。正因為 ServiceWorker 可以代理網絡請求,是以為了安全起見,規範中規定它隻能在 https 和 localhost 下才能開啟。

  1. 調試方法
  2. 在浏覽器中檢視和調試 ServiceWorker 的代碼,需要輸入 chrome://inspect/#service-workers
  3. 常見使用場景
  4. 緩存資源檔案,加快渲染速度
  5. 這個我們以語雀為例。我們在打開語雀網站的時候,可以看到它使用 ServiceWorker 緩存了很多 css、js 檔案,進而達到優化的效果。

總結

類型 Worker SharedWorker ServiceWorker
通信方式 postMessage port.postMessage 單向通信,通過addEventListener 監聽serviceWorker 的狀态
使用場景 适合大量計算的場景 适合跨 tab、iframes之間共享資料 緩存資源、網絡優化
相容性 >= IE 10>= Chrome 4 不支援 IE、Safari、Android、iOS>= Chrome 4 不支援 IE>= Chrome 40

本文介紹了 3 種 Worker,他們分别适合不同的場景,總結如上面表格。普通的 Worker 可以在需要大量計算的時候使用,建立新的線程可以降低主線程的計算壓力,不會導緻 UI 卡頓。SharedWorker 主要是為不同的 window、iframes 之間共享資料提供了另外一個解決方案。ServiceWorker 可以緩存資源,提供離線服務或者是網絡優化,加快 Web 應用的開啟速度,更多是優化體驗方面的。

示例代碼:github.com/Pulset/Web-…

參考文獻

  • https://juejin.cn/post/7091068088975622175
  • 在網絡應用中添加服務工作線程和離線功能
  • Service worker overview
  • Workers
  • SharedWorker

繼續閱讀