gulp、webpack、rollup、vite實現(xiàn)原理
大廠技術 高級前端 Node進階
點擊上方 程序員成長指北,關注公眾號
回復1,加入高級Node交流群
序言
現(xiàn)在前端項目的開發(fā)過程離不開構建工具幫助,面對琳瑯滿目的構建工具我們該如何選擇最合適自己場景的構建工具是一個問題。在研究各種配置之余,我們?nèi)パ芯恳幌聵嫿üぞ甙l(fā)展過程、底層原理,面對一些問題的時候往往事半功倍。
通過本文你可以了解到:
前端構建工具的進化歷程 前端構建工具技術方案對比 常用構建工具核心實現(xiàn)原理
什么是構建?

構建簡單的說就是將我們開發(fā)環(huán)境的代碼,轉化成生產(chǎn)環(huán)境可用來部署的代碼。
市面上存在很多構建工具,但是最終的目標都是轉化開發(fā)環(huán)境的代碼為生產(chǎn)環(huán)境中可用的代碼。在不同的前端項目中使用的技術棧是不一樣的,比如:不同的框架、不同的樣式處理方案等,為了生產(chǎn)出生產(chǎn)環(huán)境可用的 JS、CSS,構建工具實現(xiàn)了諸如:代碼轉換、代碼壓縮、tree shaking、code spliting 等。
前端構建工具可以做什么?

前端構建工具進化史

無模塊化時代
YUI Tool + Ant
YUI Tool 是 07 年左右出現(xiàn)的一個構建工具,可以實現(xiàn)壓縮混淆前端代碼,依賴于 java 的 ant 使用。
在早期 web 應用開發(fā)主要采用 JSP,處于混合開發(fā)的狀態(tài),不像是現(xiàn)在的前后端分離開發(fā)。通常由 java 開發(fā)人員來編寫 js、css 代碼。此時出現(xiàn)的構建工具依賴于別的語言實現(xiàn)。
JS 內(nèi)聯(lián)外聯(lián)
前端代碼是否必須通過構建才可以在瀏覽器中運行呢?當然不是。如下:
<html>
<head>
<title>Hello World</title>
</head>
<body>
<div id="root"/>
<script type="text/javascript">
document.getElementById('root').innerText = 'Hello World'
</script>
</body>
</html>
上述代碼,我們只需要按格式寫幾個 HTML 標簽,插入簡單的 JS 腳本,打開瀏覽器,一個 Hello World 的前端頁面就呈現(xiàn)在我們面前了。但是當項目進入真正的實戰(zhàn)開發(fā),代碼規(guī)模開始急速擴張后,大量邏輯混雜在一個文件之中就變得難以維護起來。早期的前端項目一般如下組織:
<html>
<head>
<title>JQuery</title>
</head>
<body>
<div id="root"/>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$('#root')[0].innerText = 'Hello World'
})
</script>
</body>
</html>
通過 JS 的內(nèi)聯(lián)外聯(lián)組織代碼,將不同的代碼放在不同的文件中,但是這也僅僅解決了代碼組織混亂的問題,還存在很多問題:
大量的全局變量,代碼之間的依賴是不透明的,任何代碼都可能悄悄的改變了全局變量。 腳本的引入需要依賴特定的順序。
后續(xù)出現(xiàn)過一些 IIFE、命名空間等解決方案,但是從本質(zhì)上都沒有解決依賴全局變量通信的問題。在這樣的背景下,前端工程化成為解決此類問題的正軌。
社區(qū)模塊化時代
AMD/CMD - 異步模塊加載
為了解決瀏覽器端 JS 模塊化的問題,出現(xiàn)了通過引入相關工具庫的方式來解決這一問題。出現(xiàn)了兩種應用比較廣的規(guī)范及其相關庫:AMD(RequireJs) 和 CMD(Sea.js)。AMD 推崇依賴前置、提前執(zhí)行,CMD 推崇依賴就近、延遲執(zhí)行。下面領略下相關寫法:
Require.js
// 加載完jquery后,將執(zhí)行結果 $ 作為參數(shù)傳入了回調(diào)函數(shù)
define(["jquery"], function ($) {
$(document).ready(function(){
$('#root')[0].innerText = 'Hello World';
})
return $
})
Sea.js
// 預加載jquery
define(function(require, exports, module) {
// 執(zhí)行jquery模塊,并得到結果賦值給 $
var $ = require('jquery');
// 調(diào)用jquery.js模塊提供的方法
$('#header').hide();
});
兩種模塊化規(guī)范實現(xiàn)的原理基本上是一致的,只不過各自堅持的理念不同。兩者都是以異步的方式獲取當前模塊所需的模塊,不同的地方在于 AMD 在獲取到相關模塊后立即執(zhí)行,CMD 則是在用到相關模塊的位置再執(zhí)行的。
AMD/CMD 解決問題:
手動維護代碼引用順序。從此不再需要手動調(diào)整 HTML 文件中的腳本順序,依賴數(shù)組會自動偵測模塊間的依賴關系,并自動化的插入頁面。 全局變量污染問題。將模塊內(nèi)容在函數(shù)內(nèi)實現(xiàn),利用閉包導出的變量通信,不會存在全局變量污染的問題。
Grunt/Gulp
在 Google Chrome 推出 V8 引擎后,基于其高性能和平臺獨立的特性,Nodejs 這個 JS 運行時也現(xiàn)世了。至此,JS 打破了瀏覽器的限制,擁有了文件讀寫的能力。Nodejs 不僅在服務器領域占據(jù)一席之地,也將前端工程化帶進了正軌。
在這個背景下,第一批基于 Node.js 的構建工具出現(xiàn)了。
Grunt
Grunt[1] 主要能夠幫助我們自動化的處理一些反復重復的任務,例如壓縮、編譯、單元測試、linting 等。
// Gruntfile.js
module.exports = function(grunt) {
// 功能配置
grunt.initConfig({
// 定義任務
jshint: {
files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
options: {
globals: {
jQuery: true
}
}
},
// 時時偵聽文件變化所執(zhí)行的任務
watch: {
files: ['<%= jshint.files %>'],
tasks: ['jshint']
}
});
// 加載任務所需要的插件
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
// 默認執(zhí)行的任務
grunt.registerTask('default', ['jshint']);
};
Gulp
Grunt 的 I/O 操作比較“呆板”,每個任務執(zhí)行結束后都會將文件寫入磁盤,下個任務執(zhí)行時再將文件從磁盤中讀出,這樣的操作會產(chǎn)生一些問題:
運行速度較慢 硬件壓力大
Gulp[2] 最大特點是引入了流的概念,同時提供了一系列常用的插件去處理流,流可以在插件之間傳遞。同時 Gulp 設計簡單,既可以單獨使用,也可以結合別的工具一起使用。
// gulpfile.js
const { src, dest } = require('gulp');
// gulp提供的一系列api
// src 讀取文件
// dest 寫入文件
const babel = require('gulp-babel');
exports.default = function() {
// 將src文件夾下的所有js文件取出,經(jīng)過Babel轉換后放入output文件夾之中
return src('src/*.js')
.pipe(babel())
.pipe(dest('output/'));
}
Browserify
隨著 Node.js 的興起,CommonJS 模塊化規(guī)范成為了當時的主流規(guī)范。但是我們知道 CommonJS 所使用的 require 語法是同步的,當代碼執(zhí)行到 require 方法的時候,必須要等這個模塊加載完后,才會執(zhí)行后面的代碼。這種方式在服務端是可行的,這是因為服務器只需要從本地磁盤中讀取文件,速度還是很快的,但是在瀏覽器端,我們通過網(wǎng)絡請求獲取文件,網(wǎng)絡環(huán)境以及文件大小都可能使頁面無響應。
browserify[3] 致力于打包產(chǎn)出在瀏覽器端可以運行的 CommonJS 規(guī)范的 JS 代碼。
var browserify = require('browserify')
var b = browserify()
var fs = require('fs')
// 添加入口文件
b.add('./src/browserifyIndex.js')
// 打包所有模塊至一個文件之中并輸出bundle
b.bundle().pipe(fs.createWriteStream('./output/bundle.js'))
browserify 怎么實現(xiàn)的呢?
browserify 在運行時會通過進行 AST 語法樹分析,確定各個模塊之間的依賴關系,生成一個依賴字典。之后包裝每個模塊,傳入依賴字典以及自己實現(xiàn)的 export 和 require 函數(shù),最終生成一個可以在瀏覽器環(huán)境中執(zhí)行的 JS 文件。
browserify 專注于 JS 打包,功能單一,一般配合 Gulp 一起使用。
ESM 規(guī)范出現(xiàn)
在 2015 年 JavaScript 官方的模塊化終于出現(xiàn)了,但是官方只闡述如何實現(xiàn)該規(guī)范,瀏覽器少有支持。
Webpack
其實在 ESM 標準出現(xiàn)之前, webpack[4]已經(jīng)誕生了,只是沒有火起來。webpack 的理念更偏向于工程化,伴隨著 MVC 框架以及 ESM 的出現(xiàn)與興起,webpack2 順勢發(fā)布,宣布支持 AMD\CommonJS\ESM、css/less/sass/stylus、babel、TypeScript、JSX、Angular 2 組件和 vue 組件。從來沒有一個如此大而全的工具支持如此多的功能,幾乎能夠解決目前所有構建相關的問題。至此 webpack 真正成為了前端工程化的核心。
webpack 是基于配置。
module.exports = {
// SPA入口文件
entry: 'src/js/index.js',
// 出口
output: {
filename: 'bundle.js'
}
// 模塊匹配和處理 大部分都是做編譯處理
module: {
rules: [
// babel轉換語法
{ test: /.js$/, use: 'babel-loader' },
//...
]
},
// 插件
plugins: [
// 根據(jù)模版創(chuàng)建html文件
new HtmlWebpackPlugin({ template: './src/index.html' }),
],
}
webpack 要兼顧各種方案的支持,也暴露出其缺點:
配置往往非常繁瑣,開發(fā)人員心智負擔大。 webpack 為了支持 cjs 和 esm,自己做了 polyfill,導致產(chǎn)物代碼很“丑”。
在 webpack 出現(xiàn)兩年后,rollup 誕生了~
Rollup
rollup[5] 是一款面向未來的構建工具,完全基于 ESM 模塊規(guī)范進行打包,率先提出了 Tree-Shaking 的概念。并且配置簡單,易于上手,成為了目前最流行的 JS 庫打包工具。
import resolve from 'rollup-plugin-node-resolve';
import babel from 'rollup-plugin-babel';
export default {
// 入口文件
input: 'src/main.js',
output: {
file: 'bundle.js',
// 輸出模塊規(guī)范
format: 'esm'
},
plugins: [
// 轉換commonjs模塊為ESM
resolve(),
// babel轉換語法
babel({
exclude: 'node_modules/**'
})
]
}
rollup 基于 esm,實現(xiàn)了強大的 Tree-Shaking 功能,使得構建產(chǎn)物足夠的簡潔、體積足夠的小。但是要考慮瀏覽器的兼容性問題的話,往往需要配合額外的 polyfill 庫,或者結合 webpack 使用。
ESM 規(guī)范原生支持
Esbuild
在實際開發(fā)過程中,隨著項目規(guī)模逐漸龐大,前端工程的啟動和打包的時間也不斷上升,一些工程動輒幾分鐘甚至十幾分鐘,漫長的等待,真的讓人絕望。這使得打包工具的性能被越來越多的人關注。
esbuild[6]是一個非常新的模塊打包工具,它提供了類似 webpack 資源打包的能力,但是擁有著超高的性能。
esbuild 支持 ES6/CommonJS 規(guī)范、Tree Shaking、TypeScript、JSX 等功能特性。提供了 JS API/Go API/CLI 多種調(diào)用方式。
// JS API調(diào)用
require('esbuild').build({
entryPoints: ['app.jsx'],
bundle: true,
outfile: 'out.js',
}).catch(() => process.exit(1))

根據(jù)官方提供的性能對比,我們可以看到性能足有百倍的提升,為什么會這么快?
語言優(yōu)勢
esBuild 是選擇 Go 語言編寫的,而在 esBuild 之前,前端構建工具都是基于 Node,使用 JS 進行編寫。JavaScript 是一門解釋性腳本語言,即使 V8 引擎做了大量優(yōu)化(JWT 及時編譯),本質(zhì)上還是無法打破性能的瓶頸。而 Go 是一種編譯型語言,在編譯階段就已經(jīng)將源碼轉譯為機器碼,啟動時只需要直接執(zhí)行這些機器碼即可。 Go 天生具有多線程運行能力,而 JavaScript 本質(zhì)上是一門單線程語言。esBuild 經(jīng)過精心的設計,將代碼 parse、代碼生成等過程實現(xiàn)完全并行處理。
性能至上原則
esBuild 只提供現(xiàn)代 Web 應用最小的功能集合,所以其架構復雜度相對較小,更容易將性能做到極致 在 webpack、rollup 這類工具中, 我們習慣于使用多種第三方工作來增強工程能力。比如:babel、eslint、less 等。在代碼經(jīng)過多個工具流轉的過程中,存在著很多性能上的浪費,比如:多次進行代碼 -> AST、AST -> 代碼的轉換。esBuild 對此類工具完全進行了定制化重寫,舍棄部分可維護性,追求極致的編譯性能。
雖然 esBuild 性能非常高,但是其提供的功能很基礎,不適合直接用到生產(chǎn)環(huán)境,更適合作為底層的模塊構建工具,在它基礎上進行二次封裝。
Vite
vite[7] 是下一代前端開發(fā)與構建工具,提供 noBundle 的開發(fā)服務,并內(nèi)置豐富的功能,無需復雜配置。
vite 在開發(fā)環(huán)境和生產(chǎn)環(huán)境分別做了不同的處理,在開發(fā)環(huán)境中底層基于 esBuild 進行提速,在生產(chǎn)環(huán)境中使用 rollup 進行打包。
為什么 vite 開發(fā)服務這么快?

傳統(tǒng) bundle based 服務:
無論是 webpack 還是 rollup 提供給開發(fā)者使用的服務,都是基于構建結果的。 基于構建結果提供服務,意味著提供服務前一定要構建結束,隨著項目膨脹,等待時間也會逐漸變長。
noBundle 服務:
對于 vite、snowpack 這類工具,提供的都是 noBundle 服務,無需等待構建,直接提供服務。 對于項目中的第三方依賴,僅在初次啟動和依賴變化時重構建,會執(zhí)行一個 依賴預構建的過程。由于是基于 esBuild 做的構建,所以非???。對于項目代碼,則會依賴于瀏覽器的 ESM 的支持,直接按需訪問,不必全量構建。
為什么在生產(chǎn)環(huán)境中構建使用 rollup?
由于瀏覽器的兼容性問題以及實際網(wǎng)絡中使用 ESM 可能會造成 RTT 時間過長,所以仍然需要打包構建。 esbuild 雖然快,但是它還沒有發(fā)布 1.0 穩(wěn)定版本,另外 esbuild 對代碼分割和 css 處理等支持較弱,所以生產(chǎn)環(huán)境仍然使用 rollup。
目前 vite 發(fā)布了 3.0 版本,相對于 2.0,修復了 400+issue,已經(jīng)比較穩(wěn)定,可以用于生產(chǎn)了。Vite 決定每年發(fā)布一個新的版本。
vite.config.js:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {resolve} from 'path';
// defineConfig 這個方法沒有什么實際的含義, 主要是可以提供語法提示
export default defineConfig({
resolve:{
alias:{
'@':resolve('src')
}
},
plugins: [vue()]
})
技術方案對比
前端構建工具實在是琳瑯滿目,以工程化的視角對社區(qū)仍然比較流行的構建工具進行簡單對比,一些功能專一、特定場景下的工具不在考慮范圍內(nèi)~。
2021 前端構建工具排行[8]

一些值得思考的問題
為什么 webpack 構建產(chǎn)物看著很丑?
我們在使用 webpack 構建項目后,會發(fā)現(xiàn)打包出來的代碼非常的“丑”,這是為什么?原因就是:webpack 支持多種模塊規(guī)范,但是最后都會變成commonJS規(guī)范(webpack5 對純 esm 做了一定的優(yōu)化),但是瀏覽器不支持commonJS規(guī)范,于是 webpack 自己實現(xiàn)了require和module.exports,所以會有很多 polyfill 代碼的注入。
針對·common.js 加載 common.js 這種情況分析一下構建產(chǎn)物。
源代碼:
// src/index.js
let title = require('./title.js')
console.log(title);
// src/title.js
module.exports = 'bu';
產(chǎn)物代碼:
(() => {
//把所有模塊定義全部存放到modules對象里
//屬性名是模塊的ID,也就是相對于根目錄的相對路徑,包括文件擴展名
//值是此模塊的定義函數(shù),函數(shù)體就是原來的模塊內(nèi)的代碼
var modules = ({
"./src/title.js": ((module) => {
module.exports = 'bu';
})
});
// 緩存對象
var cache = {};
// webpack 打包后的代碼能夠運行, 是因為webpack根據(jù)commonJS規(guī)范實現(xiàn)了一個require方法
function require(moduleId) {
var cachedModule = cache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 緩存和創(chuàng)建模塊對象
var module = cache[moduleId] = {
exports: {}
};
// 運行模塊代碼
modules[moduleId](module, module.exports, require "moduleId");
return module.exports;
}
var exports = {};
(() => {
// 入口相關的代碼
let title = require("./src/title.js")
console.log(title);
})();
})();
webpack 按需加載的模塊怎么在瀏覽器中運行?
在實際項目開發(fā)中,隨著代碼越寫越多,構建后的 bundle 文件也會越來越大,我們往往按照種種策略對代碼進行按需加載,將某部分代碼在用戶事件觸發(fā)后再進行加載,那么 webpack 在運行時是怎么實現(xiàn)的呢?
其實原理很簡單,就是以 JSONP 的方式加載按需的腳本,但是如何將這些異步模塊使用起來就比較有意思了~
對一個簡單的 case 進行分析。
源代碼:
// index.js
import("./hello").then((result) => {
console.log(result.default);
});
// hello.js
export default 'hello';
產(chǎn)物代碼:
main.js
// PS: 對代碼做了部分簡化及優(yōu)化, 否則太難讀了~~~
// 定一個模塊對象
var modules = ({});
// webpack在瀏覽器里實現(xiàn)require方法
function require(moduleId) {xxx}
/**
* chunkIds 代碼塊的ID數(shù)組
* moreModules 代碼塊的模塊定義
*/
function webpackJsonpCallback([chunkIds, moreModules]) {
const result = [];
for(let i = 0 ; i < chunkIds.length ; i++){
const chunkId = chunkIds[i];
result.push(installedChunks[chunkId][0]);
installedChunks[chunkId] = 0; // 表示此代碼塊已經(jīng)下載完畢
}
// 將代碼塊合并到 modules 對象中去
for(const moduleId in moreModules){
modules[moduleId] = moreModules[moduleId];
}
//依次將require.e方法中的promise變?yōu)槌晒B(tài)
while(result.length){
result.shift()();
}
}
// 用來存放代碼塊的加載狀態(tài), key是代碼塊的名字
// 每次打包至少產(chǎn)生main的代碼塊
// 0 表示已經(jīng)加載就緒
var installedChunks = {
"main": 0
}
require.d = (exports, definition) => {
for (var key in definition) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
};
require.r = (exports) => {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
Object.defineProperty(exports, '__esModule', { value: true });
};
// 給require方法定義一個m屬性, 指向模塊定義對象
require.m = modules;
require.f = {};
// 利用JSONP加載一個按需引入的模塊
require.l = function (url) {
let script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
}
// 用于通過JSONP異步加載一個chunkId對應的代碼塊文件, 其實就是hello.main.js
require.f.j = function(chunkId, promises){
let installedChunkData;
// 當前代碼塊的數(shù)據(jù)
const promise = new Promise((resolve, reject) => {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 獲取模塊的訪問路徑
const url = chunkId + '.main.js';
require.l(url);
}
require.e = function(chunkId) {
let promises = [];
require.f.j(chunkId, promises);
console.log(promises);
return Promise.all(promises);
}
var chunkLoadingGlobal = window['webpack'] = [];
// 由于按需加載的模塊, 會在加載成功后調(diào)用此模塊,所以這是JSONP的成功后的回掉
chunkLoadingGlobal.push = webpackJsonpCallback;
/**
* require.e異步加載hello代碼塊文件 hello.main.js
* promise成功后會把 hello.main.js里面的代碼定義合并到require.m對象上,也就是modules上
* 調(diào)用require方法加載./src/hello.js模塊,獲取 模塊的導出對象,進行打印
*/
require.e('hello').then(require.bind(require, './src/hello.js')).then(result => console.log(result));
hello.main.js
"use strict";
(self["webpack"] = self["webpack"] || []).push([
["hello"], {
"./src/hello.js": ((module, exports, require) => {
require.r(exports);
require.d(exports, {
"default": () => (_DEFAULT_EXPORT__)
});
const _DEFAULT_EXPORT__ = ("hello");
})
}
]);
webpack 在產(chǎn)物代碼中聲明了一個全局變量 webpack 并賦值為一個數(shù)組,然后改寫了這個數(shù)組的 push 方法。在異步代碼加載完成后執(zhí)行時,會調(diào)用這個 push 方法,在重寫的方法內(nèi)會將異步模塊放到全局模塊中然后等待使用。
白話版 webpack 構建流程
時至今日,webpack 的功能集已經(jīng)非常龐大了,代碼量更是非常驚人,源碼的學習成本非常高,但是了解 webpack 構建流程又十分有必要,可以幫我們了解構建產(chǎn)物是怎么產(chǎn)生的,以及遇到實際問題時如何下手解決問題。

思路實現(xiàn)
簡單模擬下 webpack 實現(xiàn)思路:
class Compilation {
constructor(options) {
this.options = options;
// 本次編譯所有生成出來的模塊
this.modules = [];
// 本次編譯產(chǎn)出的所有代碼塊, 入口模塊和依賴模塊打包在一起成為代碼塊
this.chunks = [];
// 本次編譯產(chǎn)出的資源文件
this.assets = {};
}
build(callback) {
//5.根據(jù)配置文件中的`entry`配置項找到所有的入口
let entry = {xxx: 'xxx'};
//6.從入口文件出發(fā),調(diào)用所有配置的loader規(guī)則,比如說loader對模塊進行編譯
for(let entryName in entry){
// 6. 從入口文件出發(fā),調(diào)用所有配置的Loader對模塊進行編譯
const entryModule = this.buildModule(entryName, entryFilePath);
this.modules.push(entryModule);
//8.等把所有的模塊編譯完成后,根據(jù)模塊之間的依賴關系,組裝成一個個包含多個模塊的chunk
let chunk = {
name: entryName, // 代碼塊的名稱就是入口的名稱
entryModule, // 此代碼塊對應的入口模塊
modules: this.modules.filter((module) => module.names.includes(entryName)) // 此代碼塊包含的依賴模塊
};
this.chunks.push(chunk);
}
//9.再把各個代碼塊chunk轉換成一個一個的文件(asset)加入到輸出列表
this.chunks.forEach((chunk) => {
const filename = this.options.output.filename.replace('[name]', chunk.name); // 獲取輸出文件名稱
this.assets[filename] = getSource(chunk);
});
// 調(diào)用編譯結束的回掉
callback(null, {
modules: this.modules,
chunks: this.chunks,
assets: this.assets
}, this.fileDependencies);
}
//當你編譯 模塊的時候,需要傳遞你這個模塊是屬于哪個代碼塊的,傳入代碼塊的名稱
buildModule(name, modulePath) {
// 6. 從入口文件出發(fā),調(diào)用所有配置的Loader對模塊進行編譯, loader 只會在編譯過程中使用, plugin則會貫穿整個流程
// 讀取模塊內(nèi)容
let sourceCode = fs.readFileSync(modulePath, 'utf8');
//創(chuàng)建一個模塊對象
let module = {
id: moduleId, // 模塊ID =》 相對于工作目錄的相對路徑
names: [name], // 表示當前的模塊屬于哪個代碼塊(chunk)
dependencies: [], // 表示當前模塊依賴的模塊
}
// 查找所有匹配的loader,自右向左讀取loader, 進行轉譯, 通過loader翻譯后的內(nèi)容一定是JS內(nèi)容
sourceCode = loaders.reduceRight((sourceCode, loader) => {
return require(loader)(sourceCode);
}, sourceCode);
// 7. 再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
// 創(chuàng)建語法樹, 遍歷語法樹,在此過程進行依賴收集, 繪制依賴圖
let ast = parser.parse(sourceCode, { sourceType: 'module' });
traverse(ast, {});
let { code } = generator(ast);
// 把轉譯后的源代碼放到module._source上
module._source = code;
// 再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
module.dependencies.forEach(({ depModuleId, depModulePath }) => {
const depModule = this.buildModule(name, depModulePath);
this.modules.push(depModule)
});
return module;
}
}
function getSource(chunk) {
return `
(() => {
var modules = {
${chunk.modules.map(
(module) => `
"${module.id}": (module) => {
${module._source}
}
`
)}
};
var cache = {};
function require(moduleId) {
var cachedModule = cache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports, require "moduleId");
return module.exports;
}
var exports ={};
${chunk.entryModule._source}
})();
`;
}
class Compiler {
constructor(options) {
this.options = options;
this.hooks = {
run: new SyncHook(), //會在編譯剛開始的時候觸發(fā)此run鉤子
done: new SyncHook(), //會在編譯 結束的時候觸發(fā)此done鉤子
}
}
//4.執(zhí)行`Compiler`對象的`run`方法開始執(zhí)行編譯
run() {
// 在編譯前觸發(fā)run鉤子執(zhí)行, 表示開始啟動編譯了
this.hooks.run.call();
// 編譯成功之后的回掉
const onCompiled = (err, stats, fileDependencies) => {
// 10. 在確定好輸出內(nèi)容后,根據(jù)配置確定輸出的路徑和文件名,把文件內(nèi)容寫入到文件系統(tǒng)
for(let filename in stats.assets) {
fs.writeFileSync(filePath,stats.assets[filename], 'utf8' );
}
//當編譯成功后會觸發(fā)done這個鉤子執(zhí)行
this.hooks.done.call();
}
//開始編譯,編譯 成功之后調(diào)用onCompiled方法
this.compile(onCompiled);
}
compile(callback) {
// webpack雖然只有一個Compiler, 但是每次編譯都會產(chǎn)出一個新的Compilation, 用來存放本次編譯產(chǎn)出的 文件、chunk、和模塊
// 比如:監(jiān)聽模式會觸發(fā)多次編譯
let compilation = new Compilation(this.options);
//執(zhí)行compilation的build方法進行編譯 ,編譯 成功之后執(zhí)行回調(diào)
compilation.build(callback);
}
}
function webpack(options) {
//1.初始化參數(shù),從配置文件和shell語句中讀取并合并參數(shù),并得到最終的配置對象
let finalOptions = {...options, ...shellOptions};
// 2.用上一步的配置對象初始化Compiler對象, 整個編譯流程只有一個complier對象
const compiler = new Compiler(finalOptions);
// 3.加載所有在配置文件中配置的插件
const { plugins } = finalOptions;
for(let plugin of plugins){
plugin.apply(compiler);
}
return compiler;
}
// webpackOptions webpack的配置項
const compiler = webpack(webpackOptions);
//4.執(zhí)行對象的run方法開始執(zhí)行編譯
compiler.run();
為什么 Rollup 構建產(chǎn)物很干凈?
rollup 只對 ESM 模塊進行打包,對于 cjs 模塊也會通過插件將其轉化為 ESM 模塊進行打包。所以不會像 webpack 有很多的代碼注入。 rollup 對打包結果也支持多種 format 的輸出,比如:esm、cjs、am 等等,但是 rollup 并不保證代碼可靠運行,需要運行環(huán)境可靠支持。比如我們輸出 esm 規(guī)范代碼,代碼運行時完全依賴高版本瀏覽器原生去支持 esm,rollup 不會像 webpack 一樣注入一系列兼容代碼。 rollup 實現(xiàn)了強大的 tree-shaking 能力。
為什么 Vite 可以讓代碼直接運行在瀏覽器上?
前文我們提到,在開發(fā)環(huán)境時,我們使用 vite 開發(fā),是無需打包的,直接利用瀏覽器對 ESM 的支持,就可以訪問我們寫的組件代碼,但是一些組件代碼文件往往不是 JS 文件,而是 .ts、.tsx、.vue 等類型的文件。這些文件瀏覽器肯定直接是識別不了的,vite 在這個過程中做了些什么?
我們以一個簡單的 vue 組件訪問分析一下:
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
// /src/main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app');
// src/App.vue
<template>
<h1>Hello</h1>
</template>
在瀏覽器中打開頁面后,會發(fā)現(xiàn)瀏覽器對入口文件發(fā)起了請求:

我們可以觀察到 vue 這個第三方包的訪問路徑改變了,變成了node_modules/.vite下的一個 vue 文件,這里真正訪問的文件就是前面我們提到的,vite 會對第三方依賴進行依賴預構建所生成的緩存文件。
另外瀏覽器也對 App.vue 發(fā)起了訪問,相應內(nèi)容是 JS:

返回內(nèi)容(做了部分簡化,移除了一些熱更新的代碼):
const _sfc_main = {
name: 'App'
}
// vue 提供的一些API,用于生成block、虛擬DOM
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/vue.js?v=b618a526"
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("h1", null, "App"))
}
// 組件的render方法
_sfc_main.render = _sfc_render;
export default _sfc_main;
總結:當用戶訪問 vite 提供的開發(fā)服務器時,對于瀏覽器不能直接識別的文件,服務器的一些中間件會將此類文件轉換成瀏覽器認識的文件,從而保證正常訪問。
Node 社群 我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一波??
參考資料
Grunt: https://www.gruntjs.net/
[2]Gulp: https://www.gulpjs.com.cn/
[3]browserify: https://browserify.org/
[4]webpack: https://webpack.docschina.org/
[5]rollup: https://rollupjs.org/guide/zh/
[6]esbuild: https://esbuild.github.io/
[7]vite: https://cn.vitejs.dev/guide/
[8]2021 前端構建工具排行: https://risingstars.js.org/2021/zh#section-build
[9]前端構建工具簡史: https://juejin.cn/post/7085613927249215525
[10]ESM 實現(xiàn)原理: https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
[11]Webpack 核心原理:https://mp.weixin.qq.com/s/SbJNbSVzSPSKBe2YStn2Zw
