當我們談論vite時,我們在談論什么(從零上手!)
作者:supot
原文:https://juejin.cn/post/6962902504212267021
前言
在ES6之前,JavaScript沒有一個標準的模塊方案,社區(qū)比較流行的是AMD方案和node使用的CommonJS的方案。
為了能夠在瀏覽器端使用npm上大量的CommonJS規(guī)范的包,需要我們?nèi)δK進行兼容和處理,這就是前端打包需要解決的一個問題。
Webpack
Webpack是現(xiàn)在主流的打包工具,不管是螞蟻的umi還是vue的vue-cli,底層都是基于Webpack來進行二次封裝的。
Webpack 官方將它定位為一個 module bundler,它通過定義的入口文件,進行依賴收集,構建出項目的依賴圖,最后生成并輸出一個或多個bundle。
下圖是官方給出的Webapck對于模塊的處理流程

一個能在瀏覽器中運行的模塊系統(tǒng)
我們希望瀏覽器能夠順利的運行第三方的包和業(yè)務代碼,不管是commonjs、requirejs或者是es6的模塊,所以需要提供一個新的模塊系統(tǒng),將這些第三方包和項目里的業(yè)務模塊進行統(tǒng)一處理,以便在瀏覽器中能正確的運行。
webpack 用類似于node 的commonjs的模塊方案,將最終打包的代碼運行在瀏覽器中。
(function(modules) { // webpackBootstrap
// 已經(jīng)加載的模板
var installedModules = {};
// 模塊加載函數(shù)
function __webpack_require__(moduleId) {
// ...
}
// 入口文件
return __webpack_require__(__webpack_require__.s = "./index.js");
})
({
"./index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
"./utils/test.js": (function(module, __webpack_exports__, __webpack_require__) {})
});
復制代碼
上面就是webpack打包出來的代碼,其中index.js是項目的入口文件,所以的模塊都通過 __webpack_require__ 進行加載,并且會把加載完成的模塊進行緩存。
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId, // Module ID
l: false, //
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
通過__webpack_require_的代碼可以看出,webpack實現(xiàn)了一個類似CommonJS的模塊方案。
插件系統(tǒng)
對于webpack來說,如果僅僅解決不同方案模塊的聚合和運行,是遠遠不夠的。對于越來越復雜的前端項目,需要提供更加底層的能力給開發(fā)者,去實現(xiàn)不同業(yè)務場景的不同需求。
Webpack 基于 Tapable 實現(xiàn)了一個復雜的插件系統(tǒng),在它的構建過程中,對外拋出了各個關鍵的節(jié)點的hook,開發(fā)者可以通過訂閱這些勾子,對webpack中的資源進行加工和處理,從而完成定制化的需求
通過Plugin和Loader,能夠不斷擴展Webpack的功能。

存在的問題
當然,Webpack 也不是十全十美的,在學習和試用過程中,也會存在大大小小的問題
概念和配置項太多,需要配合實際的項目場景不斷的優(yōu)化配置文件,umi、vue-cli等腳手架就是為了解決無法開箱即用的問題 隨著項目的不斷變大,devServer的啟動時間會越來越長,并且hmr的速度也會受到影響 功能缺失,在webpack5之前,沒有系統(tǒng)級別的文件緩存系統(tǒng)
契機的出現(xiàn)
契機一:http/2
由于http/2的普及,使得很多基于http/1的優(yōu)化工作都變成了反模式,其中最主要的一條就是合并代碼減少網(wǎng)絡請求。
因為在http/1的時代,瀏覽器只能并行的發(fā)起5個請求,這就造成如果文件太多,會存在請求阻塞的問題,所以在很多項目中,我們都會去對代碼進行打包生成盡量少的vendor。但是在http/2里,所有的請求都會在一個tcp鏈接里面完成,并且資源都是并行加載的,這時候單個大文件的加載時間反而會比多個小文件的時間要多。所以在http/2的網(wǎng)絡里,我們需要對項目資源進行合理的拆分,充分利用資源的并行請求來減少資源的加載時間。
契機二:ES Module
在es6中,加入了JavaScript的模塊。
只需要在script標簽上加上 type=module 的屬性,瀏覽器就會將內(nèi)聯(lián)代碼或者外部引用的腳本視為ECMAScript模塊。
相比于 Node 的 CommonJS 模塊,ES Module 有很多的不同:
ES module 拋出的是一個引用,而exports拋出的是一個值 require 可以進行動態(tài)引用,而ES module需要在作用域的頂層聲明所有的依賴,這就導致Node是可以在運行時去加載模塊的, 而ES module可以在編譯階段就完成所有的依賴分析,并為后面的優(yōu)化做準備(比如tree shaking) ...
下面的代碼可以在瀏覽器中直接運行
// test.js
export default function hello() {
console.log('hello world');
}
// index.html
<script type="module">
import hello from './test.js';
hello(); // hello world
</scirpt>
在支持ES module的瀏覽器中,當解析到ES module模塊的時候,瀏覽器就會自動發(fā)起一個請求去加載對應的模塊資源,不再需要我們?nèi)ヌ幚砟K的引入和加載。
對于ES module的支持,主流的現(xiàn)代瀏覽器已經(jīng)有這不錯的兼容性,隨著使用者的升級,這個覆蓋率會越來越高。

Vite
通過上面的介紹,我們似乎可以利用 http/2 和 瀏覽器對 ES module 的支持,來直接加載代碼,而不再需要進行代碼的打包。
Vite 和 snowpack 就是基于這種想法而誕生的前端構建工具。
Vite是什么
Vite,一個基于瀏覽器原生 ES imports 的開發(fā)服務器。利用瀏覽器去解析 imports,在服務器端按需編譯返回,完全跳過了打包這個概念,服務器隨起隨用。同時不僅有 Vue 文件支持,還搞定了熱更新,而且熱更新的速度不會隨著模塊增多而變慢。針對生產(chǎn)環(huán)境則可以把同一份代碼用 rollup 打。雖然現(xiàn)在還比較粗糙,但這個方向我覺得是有潛力的,做得好可以徹底解決改一行代碼等半天熱更新的問題。
-- 摘自尤雨溪微博

Bundleless
Vite 直接使用ES Module,利用瀏覽器去解析文件中import的依賴,對于使用到的依賴通過請求去獲得相應的資源文件, 從而不再需要像Webpack那樣,從入口文件出發(fā),收集所有的依賴然后整體打包,最后把打包的bundle文件交給瀏覽器解析運行

上圖是各個構建工具在打包生成最終的代碼所需要的時間,其中snowpack和vite都是Bundleless的方案,所以在最后生成代碼的時候不需要任何的構建,直接把ES module的代碼拋給瀏覽器即可。
極速的hmr
Vite 不會因為模塊數(shù)量的膨脹而造成熱更新的速度變慢,因為 Vite 不像 Webpack 需要構建一份全量的依賴圖,并且這份依賴圖有可能在模塊依賴特別復雜的時候會造成熱更新的不準確
devServer使用304來進行協(xié)商緩存,對于已經(jīng)加載過的模塊,會增加Cache-Control: max-age=31536000 來做緩存,減少網(wǎng)絡請求

上圖是snowpack和webpack 熱更新所需時間的對比
真正的按需編譯
Vite 通過瀏覽器的解析來進行依賴資源的加載,所以在文件沒有被使用之前,所有的依賴文件都不用進行處理,真正做到了按需編譯
Webpack 的spa模式很難進行按需編譯,因為從入口文件開始,會做完所有的依賴分析和處理,就算是進行了按需加載的模塊,也還是會被編譯。另一個可行的方案是mpa,但是就脫離了單頁應用的開發(fā)模式了,并且項目在開發(fā)和發(fā)布的時候復雜度都會增加。


上面兩張圖片可以直觀的看出Vite在編譯上的優(yōu)勢,項目只需要在路由層面進行按需加載,就能夠做到按需編譯。
這里需要注意的是,如果項目沒有對路由進行按需加載的處理,最后還是會一次性編譯加載所有的依賴
極速的編譯速度
Vite 使用 Esbuild 來進行代碼的編譯,其中jsx和tsx都是通過Esbuild解析生成AST,再生成最終的es module的代碼,commonjs到ES module也使用它來進行轉(zhuǎn)換
Esbuild是使用Go語言進行開發(fā)的,并且發(fā)布的是進行編譯過的更底層的機器碼,在執(zhí)行效率上遠遠高于JavaScript編寫的打包器,預期會快10-100百倍
在 Vite 1.0 中,使用 rollup 的 @rollup/plugin-commonjs 來實現(xiàn)commonjs轉(zhuǎn)換成ES6,在Vite 2.0 中,使用esbuild來處理模塊轉(zhuǎn)換,大大提高了效率

上圖是對10份there.js的包進行打包時,各個構建工具所需要的時間,可以看出esbuild的優(yōu)勢十分巨大
依賴預構建
在 Vite 首次運行的時候,會從入口文件出發(fā),對其中的第三方依賴進行分析和打包,并把這些chunk進行緩存,提高頁面的加載速度
很多第三方的依賴都是commonjs和umd的模塊,通過預構建,用 esbuild 將模塊轉(zhuǎn)換為ESM,這樣這些第三方的包就能在瀏覽器中直接運行了
將多入口的模塊,比如lodash,轉(zhuǎn)換成單文件的模塊,減少網(wǎng)絡請求,提高頁面的加載性能
開箱即用
除了對于Vue的第一優(yōu)先級的支持,Vite 還在內(nèi)部內(nèi)置了大量默認的處理,比如支持jsx和tsx,樣式預處理庫支持less、sass 和 CSS module
完整的腳手架工具鏈,可以快速生成Vue、React等項目的模板,提供了大量的配置項,作者說后期有可能會替換掉vue-cli
和vue、vue-cli 一樣優(yōu)秀又友好的文檔,并且有對應的中文文檔
簡單的處理流程
使用的核心依賴
Connect & Connect middleware Rollup & Rollup plugin esbuild & esbuild plugin acorn、es-module-lexer ...
Vite 1.0 的版本中使用 koa 來作為devServer,并且通過koa的插件機制,使用不同的插件來處理各種格式的文件和各種拓展的功能
Vite 2.0 中使用Connect替代了koa,并通過中間件來進行流程控制,因為Vite使用Rollup來進行最終的代碼build,所以直接擁抱了Rollup的開源社區(qū), 在內(nèi)部也繼承了Rollup的插件擴展方案,通過Rollup插件來拓展Vite的功能

不足之處
Esbuild 現(xiàn)在還不夠穩(wěn)定,無法用于生產(chǎn)環(huán)境的打包,項目打包的時候需要使用Rollup,這就造成開發(fā)環(huán)境和生產(chǎn)環(huán)境的代碼不一致,有可能一些bug會無法定位 瀏覽器的支持度還有待提升 ssr還在試驗階段
慣性思維導致的一些問題
在試用vite跑demo的時候,遇到了一些問題。
因為使用的技術棧是react + antd,所以就簡單的搭了一個demo,來看一下實際的開發(fā)體驗。在以往的開發(fā)中經(jīng)驗中,對于有antd的項目,做的第一件事可能就是引入 babel-plugin-import 實現(xiàn)模塊的按需加載,在這個demo中也試用了相同的方案實現(xiàn)按需加載。
但是在項目本地運行以后,發(fā)現(xiàn)在沒有緩存的第一性運行時,頁面加載速度有時候會非常慢,通過network的瀑布圖分析后,是因為每次頁面進來的時候,都會加載試用到的antd模塊。后來分析發(fā)現(xiàn),是 babel-plugin-import 這個插件造成的。
babel-plugin-import的作用是做antd引入語法的轉(zhuǎn)換,轉(zhuǎn)換的效果如下
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style/css');
插件將antd的bare import的語法轉(zhuǎn)換成絕對路徑的引入方式,這就導致Vite對于antd 的預編譯失效,因為預編譯只對bare import的模塊有效,并且如果當前路由依賴的antd的模塊比較多,就會造成頁面的加載速度比較慢
最近遇到的另一個babel-plugin-import造成的問題是組件庫打包的問題。因為組件依賴了antd,在打包成umd包的時候,需要通過externals將antd、react和react-dom 排除出去,但是在分析最后打包產(chǎn)物的時候,發(fā)現(xiàn)react和react-dom沒有打包進來,但是antd用到的模塊還是被打包進來了。后來經(jīng)過排查發(fā)現(xiàn),在打包的babel配置中,使用了babel-plugin-import,因為loader的編譯是在build構建之前完成的,所有的antd的bare import全被編譯成了絕對路徑的引入方式,導致webpack build的時候externals替換規(guī)則失效,因為externals也只能對bare import的模塊進行替換。
所以慣性思維會對我們造成一定程度的困擾,需要我們更透徹的理解其中的原理和構建過程,才能避免很多坑。
總結
基于 ES Module 的前端構建工具,充分利用瀏覽器自身的能力,本質(zhì)上解決本地開發(fā)項目構建時間長等問題 相比于Webpack,更加輕量,封裝的層級更高 完整的生態(tài),活躍的社區(qū),穩(wěn)定的核心開發(fā)

推薦閱讀
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「 sherlocked_93 」拉你進技術群,長期交流學習...
關注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

