天天看點

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

Vite 是什麼

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

Vite(法語單詞,“快” 的意思)由 vue 作者尤雨溪開發的 web 開發工具,是一種新型的前端建構工具。

Vite,一個基于浏覽器原生 ES imports 的開發伺服器,利用浏覽器去解析 imports,在伺服器端按需編譯傳回,完全跳過了打包這個概念,伺服器随啟随用,它是下一代前端開發與建構工具。

Vite 優缺點

優點:

  • 💡 極速的服務啟動:使用原生 ESM 檔案,無需打包
  • ⚡️ 輕量快速的熱重載:無論應用程式大小如何,都始終極快的子產品熱重載(HMR)
  • 🛠️ 豐富的功能:對 TypeScript、JSX、CSS 等支援開箱即用
  • 📦 優化的建構:可選 “多頁應用” 或 “庫” 模式的預配置 Rollup 建構
  • 🔩 通用的插件:在開發和建構之間共享 Rollup-superset 插件接口
  • 🔑 完全類型化的API:靈活的 API 和完整的 TypeScript 類型

缺點:

  1. 生态不如 webpack 豐富
  2. 生産環境的建構,目前用的 Rollup:原因在于 ESBuild 對于代碼分割和 CSS 處理方面不是很友好

為什麼選 Vite

當代的前端建構工具有很多,比較受歡迎的有 Webpack、Rollup、Parcel等,絕大多數腳手架工具都是使用 Webpack 作為建構工具,如 Vue-CLI。

在利用 Webpack 作為建構工具時,開發過程中,每次修改代碼,都會導緻重新編譯,當我們開始建構越來越大型的應用時,需要處理的 JavaScript 代碼量也呈指數級增長。包含數千個子產品的大型項目相當普遍。這就會導緻熱更新的速度也随之變慢,甚至要幾秒鐘才能看到視圖的更新。生産環境下,它将各個子產品之間通過編碼的方式聯系在一起,最終生成一個龐大的 bundle 檔案。

導緻這些問題出現的原因,有以下幾點:

  1. HTTP 1.1 時代,各個浏覽器資源請求并發是有上限的
如谷歌浏覽器為 6 個,這導緻你必須要減少資源請求數
  1. 浏覽器并不支援 CommonJS 子產品化系統
它不能直接運作在浏覽器環境下,它是 Node 提出的子產品化規範,是以需要經過 Webpack 的打包,編譯成浏覽器可識别的 JS 腳本)
  1. 子產品與子產品之間的依賴順序和管理問題
檔案依賴層級越多,靜态資源也就變得越多,如果一個資源有 100 個依賴關系,可能需要加載 100 個網絡請求,這對生産環境可能是災難,是以在生産環境最終會打包成一個 bundle 腳本,會提前進行資源按需加載的配置。

不打包的建構趨勢原因

  1. 工程越來越龐大,熱更新變得緩慢,影響開發體驗。
  2. 各大浏覽器已經開始慢慢的支援原生 ES Module (谷歌、火狐、Safari、Edge 的最新版本,都已支援。)
  3. HTTP 2.0 采用的多路複用。不用太擔心請求并發量的問題。
  4. 越來越多的 npm 包開始采用了原生 ESM 的開發形式。

bundle 和 bundleless 的差別

Bundle(Webpack) Bundleless(Vite)
開發環境啟動 需完成打包建構,存入記憶體之後才能啟動 隻需啟動開發伺服器,按需加載
項目建構時間 随項目體積線性增長 建構時間複雜度O(1)
檔案加載 加載打包後的 bundle 通過請求,映射到本地
檔案更新 重新打包建構 不重新打包
開發調試 依賴 Source Map 可單檔案直接調試
周邊生态 loader和plugin非常豐富 生态不夠webpack成熟

拓展:[​​Vue官方成員:Vite生态發展的怎麼樣了​​

聊一聊 ES Module

曆史上,JavaScript 一直沒有子產品(module)體系,無法将一個大程式拆分成互相依賴的小檔案,再用簡單的方法拼裝起來。其他語言都有這項功能,比如 Ruby 的 ​

​require​

​​、Python 的 ​

​import​

​​,甚至就連 CSS 都有 ​

​@import​

​,但是 JavaScript 任何這方面的支援都沒有,這對開發大型的、複雜的項目形成了巨大障礙。

在 ES6 之前,社群制定了一些子產品加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用于伺服器,後者用于浏覽器。ES6 在語言标準的層面上,實作了子產品功能,而且實作得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為浏覽器和伺服器通用的子產品解決方案。

ES6 子產品的設計思想是盡量的靜态化,使得編譯時就能确定子產品的依賴關系,以及輸入和輸出的變量。CommonJS 和 AMD 子產品,都隻能在運作時确定這些東西。比如,CommonJS 子產品就是對象,輸入時必須查找對象屬性。

// CommonJS子產品
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;      

上面代碼的實質是整體加載fs子產品(即加載fs的所有方法),生成一個對象(_fs),然後再從這個對象上面讀取 3 個方法。這種加載稱為“運作時加載”,因為隻有運作時才能得到這個對象,導緻完全沒辦法在編譯時做“靜态優化”。

ES6 子產品不是對象,而是通過 ​

​export​

​​ 指令顯式指定輸出的代碼,再通過 ​

​import​

​ 指令輸入。

// ES6子產品
import { stat, exists, readFile } from 'fs';      

上面代碼的實質是從fs子產品加載 3 個方法,其他方法不加載。這種加載稱為“編譯時加載”或者靜态加載,即 ES6 可以在編譯時就完成子產品加載,效率要比 CommonJS 子產品的加載方式高。

子產品功能主要由兩個指令構成:export 和 import。

  • ​​export​​ 指令用于規定子產品的對外接口
  • ​​import​​ 指令用于輸入其他子產品提供的功能

ES Module 基本特性

  1. ES6 的子產品自動采用嚴格模式,不管你有沒有在子產品頭部加上"use strict"
  2. ES6 子產品之中,頂層的​

    ​this​

    ​​ 指向​

    ​undefined​

    ​​,即不應該在頂層代碼使用​

    ​this​

  3. 一個子產品就是一個獨立的檔案,該檔案内部的所有變量,外部無法擷取。
  4. ESM 是通過 CORS 的方式請求外部 JS 子產品的
  5. ESM 的 script 标簽會延遲執行腳本(浏覽器頁面渲染後執行)

Vite 進行了什麼改進

1、緩慢的伺服器啟動

當冷啟動開發伺服器時,基于打包器的方式是在提供服務前去急切地抓取和建構你的整個應用。

那麼 Vite 是怎麼處理的?

Vite 通過在一開始将應用中的子產品區分為 依賴 和 源碼 兩類,改進了開發伺服器啟動時間。
  • 依賴大多為在開發時不會變動的純 JavaScript。一些較大的依賴(例如有上百個子產品的元件庫)處理的代價也很高。依賴也通常會存在多種子產品化格式(例如 ESM 或者 CommonJS)。
  • Vite 将會使用 esbuild 預建構依賴。Esbuild 使用 Go 編寫,并且比以 JavaScript 編寫的打包器預建構依賴快 10-100 倍。
  • 源碼通常包含一些并非直接是 JavaScript 的檔案,需要轉換(例如 JSX,CSS 或者 Vue/Svelte 元件),時常會被編輯。同時,并不是所有的源碼都需要同時被加載(例如基于路由拆分的代碼子產品)。
  • Vite 以 原生 ESM 方式提供源碼。這實際上是讓浏覽器接管了打包程式的部分工作:Vite 隻需要在浏覽器請求源碼時進行轉換并按需提供源碼。根據情景動态導入代碼,即隻在目前螢幕上實際使用時才會被處理。

Vue 腳手架工具 vue-cli 使用 webpack 進行打包,開發時可以啟動本地開發伺服器,實時預覽。因為需要對整個項目檔案進行打包,開發伺服器啟動緩慢。

webpack 打包過程:

  1. 識别入口檔案
  2. 通過逐層識别子產品依賴。(Commonjs、amd 或者 es6 的 import,webpack 都會對其進行分析。來擷取代碼的依賴)
  3. webpack 做的就是分析代碼,轉換代碼,編譯代碼,輸出代碼
  4. 最終形成打包後的代碼
  5. 【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具
  6. Vite 隻啟動一台靜态頁面的伺服器,對檔案代碼不打包,伺服器會根據用戶端的請求加載不同的子產品處理,實作真正的按需加載。

舉個例子:

當聲明一個 script 标簽類型為 module 時:

<script type="module" src="/src/main.js"></script>      

浏覽器就會像伺服器發起一個GET:​

​http://localhost:3000/src/main.js​

​​ 去請求 ​

​main.js​

​ 檔案:

// /src/main.js:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')      

浏覽器請求到了 ​

​main.js​

​​ 檔案,檢測到内部含有 ​

​import​

​​ 引入的包,又會對其内部的 ​

​import​

​ 引用發起 HTTP 請求擷取子產品的内容檔案

GET http://localhost:3000/@modules/vue.js
GET http://localhost:3000/src/App.vue      

而 Vite 的主要功能就是通過劫持浏覽器的這些請求,并在後端進行相應的處理将項目中使用的檔案通過簡單的分解與整合,然後再傳回給浏覽器,vite整個過程中沒有對檔案進行打包編譯。

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

2、緩慢的更新

基于打包器啟動時,重建整個包的效率很低。原因顯而易見:因為這樣更新速度會随着應用體積增長而直線下降。

一些打包器的開發伺服器将建構内容存入記憶體,這樣它們隻需要在檔案更改時使子產品圖的一部分失活,但它也仍需要整個重新建構并重載頁面。這樣代價很高,并且重新加載頁面會消除應用的目前狀态,是以打包器支援了動态子產品熱重載(HMR):允許一個子產品 “熱替換” 它自己,而不會影響頁面其餘部分。這大大改進了開發體驗 —— 然而,在實踐中我們發現,即使采用了 HMR 模式,其熱更新速度也會随着應用規模的增長而顯著下降。

那麼 Vite 是怎麼處理的?

  • 在 Vite 中,HMR 是在原生 ESM 上執行的。當編輯一個檔案時,Vite 隻需要精确地使已編輯的子產品與其最近的 HMR 邊界之間的鍊失活(大多數時候隻是子產品本身),使得無論應用大小如何,HMR 始終能保持快速更新。
  • Vite 同時利用 HTTP 頭來加速整個頁面的重新加載(再次讓浏覽器為我們做更多事情):源碼子產品的請求會根據​

    ​304 Not Modified​

    ​​ 進行協商緩存,而依賴子產品請求則會通過​

    ​Cache-Control: max-age=31536000,immutable​

    ​ 進行強緩存,是以一旦被緩存它們将不需要再次請求。

Vite 建構原理

Vite 的生産模式和開發模式是不同的概念。Vite 在開發模式下,有一個 ​

​依賴預建構​

​ 的概念。

什麼是依賴預建構

在 Vite 啟動開發伺服器之後,它将第三方依賴的多個靜态資源整合為一個,比如 ​

​lodash、qs、axios​

​​ 等這類資源包,存入 ​

​·node_modules/.vite​

​ 檔案下。

為什麼需要依賴預建構

如果直接采用 ES Module 的形式開發代碼,會産生一大串依賴,就好像俄羅斯套娃一樣,一層一層的嵌套,在浏覽器資源有限的情況下,同時請求大量的靜态資源,會造成浏覽器的卡頓,并且資源響應的時間也會變慢。

下面通過兩個例子來詳細分析為什麼需要依賴預建構。

實列1:手動搭建原生 ES Module 開發形式

下面我們先不通過 Vite,而是手動搭建原生 ES Module 開發形式,通過引入 ​

​lodash-es​

​ 包,實作一個數組去重的小例子,來詳細分析一下。

1、建立檔案夾 ESM

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

2、初始化前端工程

在檔案夾 ESM 裡,執行下面指令初始化一個工程

npm      

3、安裝 lodash-es

lodash 為了良好的浏覽器相容性, 它使用了舊版 es5 的子產品文法;而 ​

​lodash-es​

​ 使用了 es6 的子產品文法。

npm      
【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

4、編寫業務代碼

先建立 ​

​index.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>手動搭建原生 ES Module 開發形式</title>
</head>
<body>
  <script type="module" src="./main.js"></script>
</body>
</html>      

這裡記得寫 ​

​type="module"​

​​,不然會報錯 ​

​Uncaught SyntaxError: Cannot use import statement outside a module (at main.js:1:1)​

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

然後再建立 ​

​main.js​

​ 檔案,裡面編寫代碼:

import uniq from './node_modules/lodash-es/uniq.js'

const kaimo = [1, 2, 3, 3, 4]

console.log(uniq(kaimo))      

5、用 vscode 的 Live Server 啟動項目

沒有可以安裝 ​

​vscode​

​​,然後在拓展工具添加 ​

​Live Server​

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

添加完之後,我們啟動服務,右擊 html 打開 Live Server 或者 Go Live

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

啟動一個 Web 服務後,會浏覽器自動打開。

6、f12 檢視結果

我們發現結果正常,重複的 3 被去掉了。

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

我們在檢測一下網絡:我們發現請求竟然有 50 多個。

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

那這是為什麼?

我們先點開 ​

​main.js​

​,發現有一個 import。

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

再打開 ​

​uniq.js​

​,發現又有一個 import。

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

再打開 ​

​_baseUniq.js​

​,發現又有 6 個 import。

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

而這種俄羅斯套娃的模式,會一直引用到 ​

​uniq.js​

​ 相關的所有腳本代碼。

可想而知,如果我們太多的子產品引入,勢必浏覽器頂不住。是以這時候 Vite 便引入了 「依賴預建構」 的概念。

實列2:依賴現預建構淺析

下面我們通過 Vite 建構出一個 React 項目,去實作上述邏輯,我們在去觀察 Vite 是怎麼處理的。

1、生成項目

通過 Vite 指令生成項目

npm      
【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

2、安裝 lodash-es

我們進入 ESM2 檔案夾

npm      
【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

3、修改 main.jsx 代碼

import uniq from 'lodash-es/uniq.js'

const kaimo = [1, 2, 3, 3, 4]

console.log(uniq(kaimo))      
【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

4、啟動服務檢視 f12

執行下面指令

npm      
【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

通路​

​http://localhost:3000/​

​,我們發現結果是正确的:

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

然後我們去看網絡部分,我們發現請求資源數少了好多

【蜻蜓點水】了解一下 vite 下一代前端開發與建構工具

而 ​

​lodash-es/uniq​

​​ 已經被 Vite 提前預編譯到了 ​

​.vite​

​ 檔案夾下

參考資料

  • ​​Vite 官方中文文檔​​
  • ​​Vite 和Webpack 的核心差異​​
  • ​​初識 Vite 基礎知識 ​​
  • ​​Module 的文法​​
  • ​​Module 的加載實作​​

拓展閱讀

  • ​​強力推薦:ES modules: A cartoon deep-dive​​
  • [​​Vue官方成員:Vite生态發展的怎麼樣了​​
  • ​​MDN import​​
  • ​​MDN export​​

繼續閱讀