Vite
在去年就已經出來了,但我真正的去了解它倒是在最近Vue Conf
上李奎關於Vite: 下一代web工具
的分享。其中他提到的幾點吸引到了我。分享的開始,他簡要說明了本次分享的關鍵點:html
其中的ESM
和esbuild
會在下文詳細說明
接下來他提到了Bundle-Based Dev Server
。也就是咱們一直在用的webpack
的處理方式:前端
這裏引用官網的一段話:vue
當咱們開始構建愈來愈大型的應用時,須要處理的JavaScript
代碼量也呈指數級增加。大型項目包含數千個模塊的狀況並很多見。咱們開始遇到性能瓶頸 —— 使用JavaScript
開發的工具一般須要很長時間(甚至是幾分鐘!)才能啓動開發服務器,即便使用HMR
,文件修改後的效果也須要幾秒鐘才能在瀏覽器中反映出來。如此循環往復,遲鈍的反饋會極大地影響開發者的開發效率和幸福感。
簡單總結下就是,若是應用比較複雜,使用Webpack
的開發過程相對沒有那麼絲滑:node
Webpack Dev Server
冷啓動時間會比較長Webpack HMR
熱更新的反應速度比較慢這就是Vite
出現的緣由,你能夠把它簡單理解爲:No-Bundler
構建方案。其實正是利用了瀏覽器原生ESM
的能力。webpack
但首次提出利用瀏覽器原生ESM
能力的工具並不是是Vite
,而是一個叫作Snowpack
的工具。固然本文不會展開去對比Vite
與它的區別,想了解的可戳 Vite 與 X 的區別是?
到這裏,我不由開始去想一個問題:爲何Vite
這個工具能夠出現,他又是基於哪些前提條件呢?git
帶着這個問題,結合分享和Vite
的源碼以及社區的一些文章,我發現了以下幾個與Vite
能夠出現密不可分的模塊:github
ES Modules
HTTP2
ESBuild
這幾塊其實本身都聽過,可是具體的細節也都沒有深刻去了解。今天正好去深刻剖析一下。golang
ES Modules
在現代前端工程體系中,咱們其實一直在使用ES Modules
:web
import a from 'xxx' import b from 'xxx' import c from 'xxx'
多是過於日常化,你們早已習覺得常。但若是沒有很深刻的瞭解ES Modules
,那麼可能對於咱們理解現有的一些輪子(好比本文的Vite
),會有一些阻礙。算法
ES Modules
是瀏覽器原生支持的模塊系統。而在以前,經常使用的是CommonJS
和基於 AMD
的其餘模塊系統 如 RequireJS
。
來看下目前瀏覽器對其的支持:
主流的瀏覽器(IE11 除外)均已經支持,其最大的特色是在瀏覽器端使用 export
、 import
的方式導入和導出模塊,在 script
標籤裏設置 type="module"
,而後使用模塊內容。
上面說了這麼多,畢竟也只是ES Modules
的自我介紹
。一直以來,他就像黑盒
同樣,咱們並不清楚內部的執行機制。下面就讓咱們來一窺究竟。
咱們先來看一下模塊系統的做用:傳統script
標籤的代碼加載容易致使全局做用域污染,並且要維繫一系列script
的書寫順序,項目一大,維護起來愈來愈困難。模塊系統經過聲明式的暴露和引用模塊使得各個模塊之間的依賴變得明顯。
當你在使用模塊進行開發時,實際上是在構建一張依賴關係圖
。不一樣模塊之間的連線就表明了代碼中的導入語句。
正是這些導入語句告訴瀏覽器或者Node
該去加載哪些代碼。
咱們要作的是爲依賴關係圖指定一個入口文件。從這個入口文件開始,瀏覽器或者Node
就會順着導入語句找出所依賴的其餘代碼文件。
對於 ES
模塊來講,這主要有三個步驟:
構造
。查找、下載並解析全部文件到模塊記錄中。實例化
。在內存中尋找一塊區域來存儲全部導出的變量(但尚未填充值)。而後讓 export 和 import 都指向這些內存塊。這個過程叫作連接(linking)。求值
。運行代碼,在內存塊中填入變量的實際值。在構造階段,每一個模塊都會經歷三件事情:
Find
:找出從哪裏下載包含該模塊的文件(也稱爲模塊解析)Download
:獲取文件(從 URL 下載或從文件系統加載)Parse
:將文件解析爲模塊記錄一般咱們會有一個主文件main.js
做爲一切的開始:
<script src="main.js" type="module"></script>
而後經過import
語句去引入其餘模塊所導出的內容:
import
語句中的一部分稱爲 Module Specifier
。它告訴 Loader
在哪裏能夠找到引入的模塊。
關於模塊標識符有一點須要注意:它們有時須要在瀏覽器和Node
之間進行不一樣的處理。每一個宿主都有本身的解釋模塊標識符字符串的方式。
目前在瀏覽器中只能使用 URL
做爲 Module Specifier
,也就是使用 URL
去加載模塊。
而有個問題也隨之而來,瀏覽器在解析文件前並不知道文件依賴哪些模塊,固然獲取文件以前更沒法解析文件。
這將致使整個解析依賴關係的流程是阻塞的。
像這樣阻塞主線程會讓採用了模塊的應用程序速度太慢而沒法使用。這是 ES
模塊規範將算法分爲多個階段的緣由之一。將構造過程單獨分離出來,使得瀏覽器在執行同步的初始化過程前能夠自行下載文件並創建本身對於模塊圖的理解。
對於 ES
模塊,在進行任何求值以前,你須要事先構建整個模塊圖。這意味着你的模塊標識符中不能有變量,由於這些變量尚未值。
但有時候在模塊路徑使用變量確實很是有用。例如,你可能須要根據代碼的運行狀況或運行環境來切換加載某個模塊。
爲了讓 ES
模塊支持這個,有一個名爲 動態導入
的提案。有了它,你能夠像 import(${path} /foo.js
這樣使用 import
語句。
它的原理是,任何經過 import()
加載的的文件都會被做爲一個獨立的依賴圖的入口。動態導入的模塊開啓一個新的依賴圖,並單獨處理。
實際上解析文件是有助於瀏覽器瞭解模塊內的構成,而咱們把它解析出來的模塊構成表 稱爲 Module Record
模塊記錄。
模塊記錄包含了當前模塊的 AST
,引用了哪些模塊的變量,之前一些特定屬性和方法。
一旦模塊記錄被建立,它會被記錄在模塊映射Module Ma
中。被記錄後,若是再有對相同 URL
的請求,Loader
將直接採用 Module Map
中 URL
對應的Module Record
。
解析中有一個細節可能看起來微不足道,但實際上有很大的影響。全部的模塊都被看成在頂部使用了 "use strict"
來解析。還有一些其餘細微差異。例如,關鍵字 await
保留在模塊的頂層代碼中,this
的值是 undefined
。
這種不一樣的解析方式被稱爲解析目標
。若是你使用不一樣的目標解析相同的文件,你會獲得不一樣的結果。因此在開始解析前你要知道正在解析的文件的類型:它是不是一個模塊?
在瀏覽器中這很容易。你只需在 script
標記中設置 type="module"
。這告訴瀏覽器此文件應該被解析爲一個模塊。
但在 Node
中,是沒有 HTML
標籤的,因此須要其餘的方式來辨別,社區目前的主流解決方式是修改文件的後綴爲 .mjs
,來告訴 Node
這將是一個模塊。不過尚未標準化,並且還存在不少兼容問題。
到這裏,在加載過程結束時,從普通的主入口文件變成了一堆模塊記錄Module Record
。
下一步是實例化此模塊並將全部實例連接在一塊兒。
爲了實例化 Module Record
,引擎將採用 Depth First Post-order Traversal
(深度優前後序)進行遍歷,JS
引擎將會爲每個 Module Record
建立一個 Module Environment Record
模塊環境記錄,它將管理 Module Record
對應的變量,併爲全部 export
分配內存空間。
ES Modules
的這種鏈接方式被稱爲 Live Bindings
(動態綁定)。
之因此 ES Modules
採用 Live Bindings
,是由於這將有助於作靜態分析以及規避一些問題,如循環依賴。
而 CommonJS
導出的是 copy
後的 export
對象,這意味着若是導出模塊稍後更改該值,則導入模塊並不會看到該更改。
這也就是一般所見到的結論:CommonJS 模塊導出是值的拷貝,而 ES Modules 是值的引用
。
最後一步是在內存中填值。還記得咱們經過內存鏈接好了全部 export
和 import
嗎,但內存還還沒有有值。
JS
引擎經過執行頂層代碼(函數以外的代碼),來向這些內存區添值。
至此ES Modules
的黑盒我就大體分析完了。
固然這部分我是參考了 es-modules-a-cartoon-deep-dive,而後結合本身的理解得出的分析,想更深刻了解其背後實現,可狠狠戳上面的連接。
ES Modules
在Vite
中的體現咱們能夠打開一個運行中的Vite
項目:
從上圖能夠看到:
import { createApp } from "/node_modules/.vite/vue.js?v=2122042e";
與以往的import { createApp } from "vue"
不一樣,這裏對引入的模塊路徑進行了重寫:
Vite
利用現代瀏覽器原生支持ESM
特性,省略了對模塊的打包。(這也是開發環境下項目啓動和熱更新比較快的很重要的緣由)
HTTP2
來看HTTP2
前,咱們先來了解一下HTTP
的發展史。
咱們知道 HTTP
是瀏覽器中最重要且使用最多的協議,是瀏覽器和服務器之間的通訊語言。隨着瀏覽器的發展,HTTP
爲了能適應新的形式也在持續進化。
最開始出現的HTTP/0.9
實現相對較爲簡單:採用了基於請求響應的模式,從客戶端發出請求,服務器返回數據。
從圖中能夠看出其只有一個請求行且服務器也沒有返回頭信息。
萬維網的高速發展帶來了不少新的需求,而 HTTP/0.9
已經不能適用新興網絡的發展,因此這時就須要一個新的協議來支撐新興網絡,這就是 HTTP/1.0
誕生的緣由。
而且在瀏覽器中展現的不單是 HTML
文件了,還包括了 JavaScript
、CSS
、圖片、音頻、視頻等不一樣類型的文件。所以支持多種類型的文件下載是 HTTP/1.0
的一個核心訴求。
爲了讓客戶端和服務器能更深刻地交流,HTTP/1.0
引入了請求頭和響應頭,它們都是以 Key-Value
形式保存的,在 HTTP
發送請求時,會帶上請求頭信息,服務器返回數據時,會先返回響應頭信息。HTTP/1.0
具體的請求流程能夠參考下圖:
HTTP/1.0
每進行一次 HTTP
通訊,都須要經歷創建 TCP
鏈接、傳輸 HTTP
數據和斷開 TCP
鏈接三個階段。在當時,因爲通訊的文件比較小,並且每一個頁面的引用也很少,因此這種傳輸形式沒什麼大問題。可是隨着瀏覽器普及,單個頁面中的圖片文件愈來愈多,有時候一個頁面可能包含了幾百個外部引用的資源文件,若是在下載每一個文件的時候,都須要經歷創建 TCP鏈接
、傳輸數據
和斷開鏈接
這樣的步驟,無疑會增長大量無謂的開銷。
爲了解決這個問題,HTTP/1.1
中增長了持久鏈接
的方法,它的特色是在一個 TCP
鏈接上能夠傳輸多個 HTTP
請求,只要瀏覽器或者服務器沒有明確斷開鏈接,那麼該 TCP
鏈接會一直保持。而且瀏覽器中對於同一個域名,默認容許同時創建 6
個 TCP 持久鏈接
。
經過這些方式在某種程度上大幅度提升了頁面的下載速度。
以前咱們使用Webpack
打包應用代碼,使之成爲一個bundle.js
,有一個很重要的緣由是:零散的模塊文件會產生大量的HTTP
請求。而大量的HTTP
請求在瀏覽器端就會產生併發請求資源的問題:
如上圖所示,紅色圈起來的部分的請求就是併發請求,可是後面的請求就由於域名鏈接數已超過限制,而被掛起等待了一段時間。
在HTTP1.1
的標準下,每次請求都須要單獨創建TCP
鏈接,通過完整的通信過程,很是耗時。之因此會出現這個問題,主要是由如下三個緣由致使的:
TCP
的慢啓動TCP
鏈接之間相互競爭帶寬前兩個問題是因爲TCP
自己的機制致使的,而隊頭阻塞是因爲HTTP/1.1
的機制致使的。
爲了解決這些已知問題,HTTP/2
的思路就是一個域名只使用一個 TCP 長鏈接來傳輸數據
,這樣整個頁面資源的下載過程只須要一次慢啓動,同時也避免了多個 TCP
鏈接競爭帶寬所帶來的問題。
也就是常說的多路複用
,它能實現資源的並行傳輸。
上文中也提到了Vite
使用 ESM
在瀏覽器裏使用模塊,就是使用 HTTP
請求拿到模塊。這樣就會產生大量的HTTP
請求,但因爲HTTP/2
的多路複用
機制的出現,很好的解決了傳輸耗時久的問題。
esbuild
官方的介紹:它是一個JavaScript Bundler
打包和壓縮工具,它能夠將JavaScript
和TypeScript
代碼打包分發在網頁上運行。esbuild
底層使用的golang
進行編寫的,在對比傳統web
構建工具的打包速度上,具備明顯的優點。編譯Typescript
的速度遠超官方的tsc
。
對於JSX
、或者TS
等須要編譯的文件,Vite
是用esbuild
來進行編譯的,不一樣於Webpack
的總體編譯,Vite
是在瀏覽器請求時,纔對文件進行編譯,而後提供給瀏覽器。由於esbuild
編譯夠快,這種每次頁面加載都進行編譯的實際上是不會影響加速速度的。
結合上面的分析和源碼,能夠用一句話來簡述Vite
的原理:Static Server + Compile + HMR
:
攔截部分文件請求
import node_modules
中的模塊vue
單文件組件(SFC
)的編譯WebSocket
實現HMR
固然關於相似手寫Vite實現
的文章社區已經有不少了,這裏就不贅述了,大體原理都是同樣的。
本文寫完帶給我更多的是一些思考。從一次分享去發掘其背後龐大的生態體系以及那些咱們一直在用卻並未深刻了解的技術黑盒
。
更多的是,感嘆大佬們的想法,站在技術的制高點,擁有較高的深度和廣度,開發一些對於提升生產力極其有用的輪子。
因此,文章寫完了,學習的步伐任在前進~