前端構建這十年

寫在前面

前端模塊化/構建工具從最開始的基於瀏覽器運行時加載的 RequireJs/Sea.js 到將全部資源組裝依賴打包 webpack/rollup/parcelbundle類模塊化構建工具,再到如今的bundleless基於瀏覽器原生 ES 模塊的 snowpack/vite,前端的模塊化/構建工具發展到如今已經快 10 年了。javascript

本文主要回顧 10 年間,前端模塊化/構建工具的發展歷程及其實現原理。css

(因涉及一些歷史、趨勢,本文觀點僅表明我的主觀見解)html

基於瀏覽器的模塊化

CommonJS

一切的開始要從CommonJS規範提及。前端

CommonJS 原本叫ServerJs,其目標原本是爲瀏覽器以外javascript代碼制定規範,在那時NodeJs尚未出生,有一些零散的應用於服務端的JavaScript代碼,可是沒有完整的生態。vue

以後就是 NodeJsCommonJS 社區的規範中吸收經驗建立了自己的模塊系統。java

RequireJs 和 AMD

CommonJs 是一套同步模塊導入規範,可是在瀏覽器上還無法實現同步加載,這一套規範在瀏覽器上明顯行不通,因此基於瀏覽器的異步模塊 AMD(Asynchronous Module Definition)規範誕生。node

define(id?, dependencies?, factory);react

define("alpha", ["require", "exports", "beta"], function (
  require,
  exports,
  beta
) {
  exports.verb = function () {
    return beta.verb();
    //Or:
    return require("beta").verb();
  };
});

AMD規範採用依賴前置,先把須要用到的依賴提早寫在 dependencies 數組裏,在全部依賴下載完成後再調用factory回調,經過傳參來獲取模塊,同時也支持require("beta")的方式來獲取模塊,但實際上這個require只是語法糖,模塊並不是在require的時候導入,而是跟前面說的同樣在調用factory回調以前就被執行,關於依賴前置和執行時機這點在當時有很大的爭議,被 CommonJs社區所不容。webpack

在當時瀏覽器上應用CommonJs還有另一個流派 module/2.0, 其中有BravoJS的 Modules/2.0-draft 規範和 FlyScriptModules/Wrappings規範。git

代碼實現大體以下:

module.declare(function (require, exports, module) {
  var a = require("a");
  exports.foo = a.name;
});

奈何RequireJs如日中天,根本爭不過。

關於這段的內容能夠看玉伯的 前端模塊化開發那點歷史

Sea.js 和 CMD

在不斷給 RequireJs 提建議,但不斷不被採納後,玉伯結合RequireJsmodule/2.0規範寫出了基於 CMD(Common Module Definition)規範的Sea.js

define(factory);

define(function (require, exports, module) {
  var add = require("math").add;
  exports.increment = function (val) {
    return add(val, 1);
  };
});

在 CMD 規範中,一個模塊就是一個文件。模塊只有在被require纔會被執行。
相比於 AMD 規範,CMD 更加簡潔,並且也更加易於兼容 CommonJSNode.jsModules 規範。

總結

RequireJsSea.js都是利用動態建立script來異步加載 js 模塊的。

在做者仍是前端小白使用這兩個庫的時候就很好奇它是怎麼在函數調用以前就獲取到其中的依賴的,後來看了源碼後恍然大悟,沒想到就是簡單的函數 toString 方法

經過對factory回調toString拿到函數的代碼字符串,而後經過正則匹配獲取require函數裏面的字符串依賴

這也是爲何兩者都不容許require更換名稱或者變量賦值,也不容許依賴字符串使用變量,只能使用字符串字面量的緣由

規範之爭在當時仍是至關混亂的,先有CommonJs社區,而後有了 AMD/CMD 規範和 NodeJsmodule 規範,可是當那些CommonJs的實現庫逐漸沒落,並隨着NodeJs愈來愈火,咱們口中所說的CommonJs 好像就只有 NodeJs所表明的modules了。

bundle 類的構建工具

Grunt

隨着NodeJs的逐漸流行,基於NodeJs的自動化構建工具Grunt誕生

Grunt能夠幫咱們自動化處理須要反覆重複的任務,例如壓縮(minification)、編譯、單元測試、linting 等,還有強大的插件生態。

Grunt採用配置化的思想:

module.exports = function (grunt) {
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON("package.json"),
    uglify: {
      options: {
        banner:
          '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
      },
      build: {
        src: "src/<%= pkg.name %>.js",
        dest: "build/<%= pkg.name %>.min.js",
      },
    },
  });

  // 加載包含 "uglify" 任務的插件。
  grunt.loadNpmTasks("grunt-contrib-uglify");

  // 默認被執行的任務列表。
  grunt.registerTask("default", ["uglify"]);
};

基於 nodejs 的一系列自動化工具的出現,也標誌着前端進入了新的時代。

browserify

browserify致力於在瀏覽器端使用CommonJs,他使用跟 NodeJs 同樣的模塊化語法,而後將全部依賴文件編譯到一個bundle文件,在瀏覽器經過<script>標籤使用的,而且支持 npm 庫。

var foo = require("./foo.js");
var gamma = require("gamma");

var elem = document.getElementById("result");
var x = foo(100);
elem.textContent = gamma(x);
$ browserify main.js > bundle.js

當時RequireJs(r.js)雖然也有了 node 端的 api 能夠編譯AMD語法輸出到單個文件,但主流的仍是使用瀏覽器端的RequireJs

AMD / RequireJS:

require(["./thing1", "./thing2", "./thing3"], function (
  thing1,
  thing2,
  thing3
) {
  // 告訴模塊返回/導出什麼
  return function () {
    console.log(thing1, thing2, thing3);
  };
});

CommonJS:

var thing1 = require("./thing1");
var thing2 = require("./thing2");
var thing3 = require("./thing3");

// 告訴模塊返回/導出什麼
module.exports = function () {
  console.log(thing1, thing2, thing3);
};

相比於 AMD 規範爲瀏覽器作出的妥協,在服務端的預編譯方面CommonJs的語法更加友好。

經常使用的搭配就是 browserify + Grunt,使用Gruntbrowserify插件來構建模塊化代碼,並對代碼進行壓縮轉換等處理。

UMD

如今有了RequireJs,也有了browserify可是這兩個用的是不一樣的模塊化規範,因此有了 UMD - 通用模塊規範,UMD 規範就是爲了兼容AMDCommonJS規範。就是如下這坨東西:

(function (global, factory) {
  typeof exports === "object" && typeof module !== "undefined"
    ? (module.exports = factory())
    : typeof define === "function" && define.amd
    ? define(factory)
    : (global.libName = factory());
})(this, function () {
  "use strict";
});

Gulp

上面說到Grunt是基於配置的,配置化的上手難度較高,須要瞭解每一個配置的參數,當配置複雜度上升的時候,代碼看起來比較混亂。
gulp 基於代碼配置和對 Node.js 流的應用使得構建更簡單、更直觀。能夠配置更加複雜的任務。

var browserify = require("browserify");
var source = require("vinyl-source-stream");
var buffer = require("vinyl-buffer");
var uglify = require("gulp-uglify");
var size = require("gulp-size");
var gulp = require("gulp");

gulp.task("build", function () {
  var bundler = browserify("./index.js");

  return bundler
    .bundle()
    .pipe(source("index.js"))
    .pipe(buffer())
    .pipe(uglify())
    .pipe(size())
    .pipe(gulp.dest("dist/"));
});

以上是一個配置browserify的例子,能夠看出來很是簡潔直觀。

webpack

在說webpack以前,先放一下阮一峯老師的吐槽

webpack1支持CommonJsAMD模塊化系統,優化依賴關係,支持分包,支持多種類型 script、image、file、css/less/sass/stylus、mocha/eslint/jshint 的打包,豐富的插件體系。

以上的 3 個庫 Grunt/Gulp/browserify 都是偏向於工具,而 webpack將以上功能都集成到一塊兒,相比於工具它的功能大而全。

webpack的概念更偏向於工程化,可是在當時並無立刻火起來,由於當時的前端開發並無太複雜,有一些 mvc 框架但都是曇花一現,前端的技術棧在 requireJs/sea.js、grunt/gulp、browserify、webpack 這幾個工具之間抉擇。

webpack真正的火起來是在2015/2016,隨着ES2015ES6)發佈,不止帶來了新語法,也帶來了屬於前端的模塊規範ES modulevue/react/Angular三大框架打得火熱,webpack2 發佈:支持ES modulebabeltypescript,jsx,Angular 2 組件和 vue 組件,webpack搭配react/vue/Angular成爲最佳選擇,至此前端開發離不開webpackwebpack真正成爲前端工程化的核心。

webpack的其餘功能就不在這裏贅述。

原理

webpack主要的三個模塊就是,後兩個也是咱們常常配置的:

  • 核心流程
  • loader
  • plugins

webpack依賴於Tapable作事件分發,內部有大量的hooks鉤子,在Compilercompilation 核心流程中經過鉤子分發事件,在plugins中註冊鉤子,實際代碼全都由不一樣的內置 plugins 來執行,而 loader 在中間負責轉換代碼接受一個源碼處理後返回處理結果content string -> result string

由於鉤子太多了,webpack 源碼看起來十分的繞,簡單說一下大體流程:

  1. 經過命令行和 webpack.config.js 來獲取參數
  2. 建立compiler對象,初始化plugins
  3. 開始編譯階段,addEntry添加入口資源
  4. addModule 建立模塊
  5. runLoaders 執行 loader
  6. 依賴收集,js 經過acorn解析爲 AST,而後查找依賴,並重復 4 步
  7. 構建完依賴樹後,進入生成階段,調用compilation.seal
  8. 通過一系列的optimize優化依賴,生成 chunks,寫入文件

webpack的優勢就不用說了,如今說一下 2 個缺點:

  • 配置複雜
  • 大型項目構建慢

配置複雜這一塊一直是webpack被吐槽的一點,主要仍是太重的插件系統,複雜的插件配置,插件文檔也不清晰,更新過快插件沒跟上或者文檔沒跟上等問題。

好比如今 webpack 已經到 5 了網上一搜全都是 webpack3 的文章,每每是新增一個功能,按照文檔配置完後,誒有報錯,網上一頓查,這裏拷貝一段,那裏拷貝一段,又來幾個報錯,又通過一頓搞後終於能夠運行。

後來針對這個問題,衍生出了前端腳手架,react出了create-react-appvue出了vue-cli,腳手架內置了webpack開發中的經常使用配置,達到了 0 配置,開發者無需關心 webpack 的複雜配置。

rollup

2015 年,前端的ES module發佈後,rollup應聲而出。

rollup編譯ES6模塊,提出了Tree-shaking,根據ES module靜態語法特性,刪除未被實際使用的代碼,支持導出多種規範語法,而且導出的代碼很是簡潔,若是看過 vuedist 目錄代碼就知道導出的 vue 代碼徹底不影響閱讀。

rollup的插件系統支持:babelCommonJstersertypescript等功能。

相比於browserifyCommonJsrollup專一於ES module
相比於webpack大而全的前端工程化,rollup專一於純javascript,大多被用做打包tool工具或library庫。

react、vue 等庫都使用rollup打包項目,而且下面說到的vite也依賴rollup用做生產環境打包 js。

Tree-shaking

export const a = 1;
export const b = 2;
import { a } from "./num";

console.log(a);

以上代碼最終打包後 b 的聲明就會被刪除掉。

這依賴ES module的靜態語法,在編譯階段就能夠肯定模塊的導入導出有哪些變量。

CommonJs 由於是基於運行時的模塊導入,其導出的是一個總體,而且require(variable)內容能夠爲變量,因此在ast編譯階段沒辦法識別爲被使用的依賴。

webpack4中也開始支持tree-shaking,可是由於歷史緣由,有太多的基於CommonJS代碼,須要額外的配置。

parcel

上面提到過webpack的兩個缺點,而parcel的誕生就是爲了解決這兩個缺點,parcel 主打極速零配置

打包工具 時間
browserify 22.98s
webpack 20.71s
parcel 9.98s
parcel - with cache 2.64s

以上是 parcel 官方的一個數據,基於一個合理大小的應用,包含 1726 個模塊,6.5M 未壓縮大小。在一臺有 4 個物理核心 CPU 的 2016 MacBook Pro 上構建。

parcel 使用 worker 進程去啓用多核編譯,而且使用文件緩存。

parcel 支持 0 配置,內置了 html、babel、typescript、less、sass、vue等功能,無需配置,而且不一樣於webpack只能將 js 文件做爲入口,在 parcel 中萬物皆資源,因此 html 文件 css 文件均可以做爲入口來打包。

因此不須要webpack的複雜配置,只須要一個parcel index.html命令就能夠直接起一個自帶熱更新的server來開發vue/react項目。

parcel 也有它的缺點:

  • 0 配置的代價,0 配置是好,可是若是想要配置一些複雜的配置就很麻煩。
  • 生態,相比於webpack比較小衆,若是遇到錯誤查找解決方案比較麻煩。

原理

  1. commander 獲取命令
  2. 啓動 server 服務,啓動 watch監聽文件,啓動 WebSocket 服務用於 hmr,啓動多線程
  3. 若是是第一次啓動,針對入口文件開始編譯
  4. 根據擴展名生成對應asset資源,例如jsAssetcssAssetvueAsset,若是parcel識別 less 文件後項目內若是沒有 less 庫會自動安裝
  5. 讀取緩存,若是有緩存跳到第 7 步
  6. 多線程編譯文件,調用 asset 內方法parse -> ast -> 收集依賴 -> transform(轉換代碼) -> generate(生成代碼),在這個過程當中收集到依賴,編譯完結果寫入緩存
  7. 編譯依賴文件,重複第 4 步開始
  8. createBundleTree 建立依賴樹,替換 hash 等,package打包生成最終代碼
  9. watch文件發生變化,重複第 4 步,並將結果 7 經過WebSocket發送到瀏覽器,進行熱更新。

一個完整的模塊化打包工具就以上功能和流程。

基於瀏覽器 ES 模塊的構建工具

browserifywebpackrollupparcel這些工具的思想都是遞歸循環依賴,而後組裝成依賴樹,優化完依賴樹後生成代碼。
可是這樣作的缺點就是慢,須要遍歷完全部依賴,即便 parcel 利用了多核,webpack 也支持多線程,在打包大型項目的時候依然慢可能會用上幾分鐘,存在性能瓶頸。

因此基於瀏覽器原生 ESM 的運行時打包工具出現:


僅打包屏幕中用到的資源,而不用打包整個項目,開發時的體驗相比於 bundle類的工具只能用極速來形容。
(實際生產環境打包依然會構建依賴方式打包)

snowpack 和 vite

由於 snowpackvite 比較相似,都是bundleless因此一塊兒拿來講,區別能夠看一下 vite 和 snowpack 區別,這裏就不贅述了。

bundleless類運行時打包工具的啓動速度是毫秒級的,由於不須要打包任何內容,只須要起兩個server,一個用於頁面加載,另外一個用於HMRWebSocket,當瀏覽器發出原生的ES module請求,server收到請求只需編譯當前文件後返回給瀏覽器不須要管依賴。

bundleless工具在生產環境打包的時候依然bundle構建因此依賴視圖的方式,vite 是利用 rollup 打包生產環境的 js 的。

原理拿 vite 舉例:

vite在啓動服務器後,會預先以全部 html 爲入口,使用 esbuild 編譯一遍,把全部的 node_modules 下的依賴編譯並緩存起來,例如vue緩存爲單個文件。

當打開在瀏覽器中輸入連接,渲染index.html文件的時候,利用瀏覽器自帶的ES module來請求文件。

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

vite 收到一個src/main.jshttp 文件請求,使用esbuild開始編譯main.js,這裏不進行main.js裏面的依賴編譯。

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

瀏覽器獲取到並編譯main.js後,再次發出 2 個請求,一個是 vue 的請求,由於前面已經說了 vue 被預先緩存下來,直接返回緩存給瀏覽器,另外一個是App.vue文件,這個須要@vitejs/plugin-vue來編譯,編譯完成後返回結果給瀏覽器(@vitejs/plugin-vue會在腳手架建立模板的時候自動配置)。

由於是基於瀏覽器的ES module,因此編譯過程當中須要把一些 CommonJsUMD 的模塊都轉成 ESM

Vite 同時利用 HTTP 頭來加速整個頁面的從新加載(再次讓瀏覽器爲咱們作更多事情):源碼模塊的請求會根據 304 Not Modified 進行協商緩存,而依賴模塊請求則會經過 Cache-Control: max-age=31536000,immutable 進行強緩存,所以一旦被緩存它們將不須要再次請求,即便緩存失效只要服務沒有被殺死,編譯結果依然保存在程序內存中也會很快返回。

上面屢次提到了esbuildesbuild使用 go 語言編寫,因此在 i/o 和運算運行速度上比解釋性語言 NodeJs 快得多,esbuild 號稱速度是 node 寫的其餘工具的 10~100 倍。

ES module 依賴運行時編譯的概念 + esbuild + 緩存 讓 vite 的速度遠遠甩開其餘構建工具。

總結

簡單的彙總:

  • 前端運行時模塊化

    • RequireJs AMD 規範
    • sea.js CMD 規範
  • 自動化工具

    • Grunt 基於配置
    • Gulp 基於代碼和文件流
  • 模塊化

    • browserify 基於CommonJs規範只負責模塊化
    • rollup 基於ES moduletree shaking優化代碼,支持多種規範導出,可經過插件集成壓縮、編譯、commonjs 語法 等功能
  • 工程化

    • webpack 大而全的模塊化構建工具
    • parcel 極速 0 配置的模塊化構建工具
    • snowpack/vite ESM運行時模塊化構建工具

這 10 年,前端的構建工具隨着 nodejs 的逐漸成熟衍生出一系列的工具,除了文中列舉的還有一些其餘的工具,或者基於這些工具二次封裝,在nodejs出現以前前端也不是沒有構建工具雖然不多,只能說nodejs的出現讓更多人能夠參與進來,尤爲是前端可使用自己熟悉的語言參與到開發工具使用工具中,npm 上至今已經有 17 萬個包,周下載量 300 億。

在這個過程當中也有些模塊化歷史遺留問題,咱們如今還在使用着 UMD 規範庫來兼容這 AMD 規範,npm 的包大都是基於CommonJs,不得不兼容ESMCommonJs

webpack統治前端已經 5 年,人們提到開發項目只會想到 webpack,而下一個 5 年會由誰來替代?snowpack/vite嗎,當打包速度達到 0 秒後,將來有沒有可能出現新一代的構建工具?下一個 10 年前端又會有什麼變化?

相關文章
相關標籤/搜索