<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          gulp、webpack、rollup、vite實(shí)現(xiàn)原理

          共 31721字,需瀏覽 64分鐘

           ·

          2023-03-11 10:49

          大廠技術(shù)  高級(jí)前端  Node進(jìn)階

          點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          序言

          現(xiàn)在前端項(xiàng)目的開發(fā)過程離不開構(gòu)建工具幫助,面對(duì)琳瑯滿目的構(gòu)建工具我們?cè)撊绾芜x擇最合適自己場(chǎng)景的構(gòu)建工具是一個(gè)問題。在研究各種配置之余,我們?nèi)パ芯恳幌聵?gòu)建工具發(fā)展過程、底層原理,面對(duì)一些問題的時(shí)候往往事半功倍。

          通過本文你可以了解到:

          1. 前端構(gòu)建工具的進(jìn)化歷程
          2. 前端構(gòu)建工具技術(shù)方案對(duì)比
          3. 常用構(gòu)建工具核心實(shí)現(xiàn)原理

          什么是構(gòu)建?

          構(gòu)建簡(jiǎn)單的說就是將我們開發(fā)環(huán)境的代碼,轉(zhuǎn)化成生產(chǎn)環(huán)境可用來部署的代碼。

          市面上存在很多構(gòu)建工具,但是最終的目標(biāo)都是轉(zhuǎn)化開發(fā)環(huán)境的代碼為生產(chǎn)環(huán)境中可用的代碼。在不同的前端項(xiàng)目中使用的技術(shù)棧是不一樣的,比如:不同的框架、不同的樣式處理方案等,為了生產(chǎn)出生產(chǎn)環(huán)境可用的 JS、CSS,構(gòu)建工具實(shí)現(xiàn)了諸如:代碼轉(zhuǎn)換、代碼壓縮、tree shaking、code spliting 等。

          前端構(gòu)建工具可以做什么?

          前端構(gòu)建工具進(jìn)化史

          無模塊化時(shí)代

          YUI Tool + Ant

          YUI Tool 是 07 年左右出現(xiàn)的一個(gè)構(gòu)建工具,可以實(shí)現(xiàn)壓縮混淆前端代碼,依賴于 javaant 使用。

          在早期 web 應(yīng)用開發(fā)主要采用 JSP,處于混合開發(fā)的狀態(tài),不像是現(xiàn)在的前后端分離開發(fā)。通常由 java 開發(fā)人員來編寫 js、css 代碼。此時(shí)出現(xiàn)的構(gòu)建工具依賴于別的語(yǔ)言實(shí)現(xiàn)。

          JS 內(nèi)聯(lián)外聯(lián)

          前端代碼是否必須通過構(gòu)建才可以在瀏覽器中運(yùn)行呢?當(dāng)然不是。如下:

          <html>
            <head>
              <title>Hello World</title>
            </head>
            <body>
              <div id="root"/>
              <script type="text/javascript">
                document.getElementById('root').innerText = 'Hello World'
              
          </script>
            </body>
          </html>

          上述代碼,我們只需要按格式寫幾個(gè) HTML 標(biāo)簽,插入簡(jiǎn)單的 JS 腳本,打開瀏覽器,一個(gè) Hello World 的前端頁(yè)面就呈現(xiàn)在我們面前了。但是當(dāng)項(xiàng)目進(jìn)入真正的實(shí)戰(zhàn)開發(fā),代碼規(guī)模開始急速擴(kuò)張后,大量邏輯混雜在一個(gè)文件之中就變得難以維護(hù)起來。早期的前端項(xiàng)目一般如下組織:

          <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ū)模塊化時(shí)代

          AMD/CMD - 異步模塊加載

          為了解決瀏覽器端 JS 模塊化的問題,出現(xiàn)了通過引入相關(guān)工具庫(kù)的方式來解決這一問題。出現(xiàn)了兩種應(yīng)用比較廣的規(guī)范及其相關(guān)庫(kù):AMD(RequireJs) 和 CMD(Sea.js)。AMD 推崇依賴前置、提前執(zhí)行,CMD 推崇依賴就近、延遲執(zhí)行。下面領(lǐng)略下相關(guān)寫法:

          Require.js

          // 加載完jquery后,將執(zhí)行結(jié)果 $ 作為參數(shù)傳入了回調(diào)函數(shù)
          define(["jquery"], function (${
              $(document).ready(function(){
                  $('#root')[0].innerText = 'Hello World';
              })
              return $
          })

          Sea.js

          // 預(yù)加載jquery
          define(function(require, exports, module{
              // 執(zhí)行jquery模塊,并得到結(jié)果賦值給 $
              var $ = require('jquery');
              // 調(diào)用jquery.js模塊提供的方法
              $('#header').hide();
          });

          兩種模塊化規(guī)范實(shí)現(xiàn)的原理基本上是一致的,只不過各自堅(jiān)持的理念不同。兩者都是以異步的方式獲取當(dāng)前模塊所需的模塊,不同的地方在于 AMD 在獲取到相關(guān)模塊后立即執(zhí)行,CMD 則是在用到相關(guān)模塊的位置再執(zhí)行的。

          AMD/CMD 解決問題:

          1. 手動(dòng)維護(hù)代碼引用順序。從此不再需要手動(dòng)調(diào)整 HTML 文件中的腳本順序,依賴數(shù)組會(huì)自動(dòng)偵測(cè)模塊間的依賴關(guān)系,并自動(dòng)化的插入頁(yè)面。
          2. 全局變量污染問題。將模塊內(nèi)容在函數(shù)內(nèi)實(shí)現(xiàn),利用閉包導(dǎo)出的變量通信,不會(huì)存在全局變量污染的問題。

          Grunt/Gulp

          在 Google Chrome 推出 V8 引擎后,基于其高性能和平臺(tái)獨(dú)立的特性,Nodejs 這個(gè) JS 運(yùn)行時(shí)也現(xiàn)世了。至此,JS 打破了瀏覽器的限制,擁有了文件讀寫的能力。Nodejs 不僅在服務(wù)器領(lǐng)域占據(jù)一席之地,也將前端工程化帶進(jìn)了正軌。

          在這個(gè)背景下,第一批基于 Node.js 的構(gòu)建工具出現(xiàn)了。

          Grunt

          Grunt[1] 主要能夠幫助我們自動(dòng)化的處理一些反復(fù)重復(fù)的任務(wù),例如壓縮、編譯、單元測(cè)試、linting 等。

           // Gruntfile.js
          module.exports = function(grunt{
             // 功能配置
            grunt.initConfig({
              // 定義任務(wù)
              jshint: {
                files: ['Gruntfile.js''src/**/*.js''test/**/*.js'],
                options: {
                  globals: {
                    jQuerytrue
                  }
                }
              },
              // 時(shí)時(shí)偵聽文件變化所執(zhí)行的任務(wù)
              watch: {
                files: ['<%= jshint.files %>'],
                tasks: ['jshint']
              }
            });
            // 加載任務(wù)所需要的插件
            grunt.loadNpmTasks('grunt-contrib-jshint');
            grunt.loadNpmTasks('grunt-contrib-watch');
            // 默認(rèn)執(zhí)行的任務(wù)
            grunt.registerTask('default', ['jshint']);

          };

          Gulp

          Grunt 的 I/O 操作比較“呆板”,每個(gè)任務(wù)執(zhí)行結(jié)束后都會(huì)將文件寫入磁盤,下個(gè)任務(wù)執(zhí)行時(shí)再將文件從磁盤中讀出,這樣的操作會(huì)產(chǎn)生一些問題:

          1. 運(yùn)行速度較慢
          2. 硬件壓力大

          Gulp[2] 最大特點(diǎn)是引入了流的概念,同時(shí)提供了一系列常用的插件去處理流,流可以在插件之間傳遞。同時(shí) Gulp 設(shè)計(jì)簡(jiǎn)單,既可以單獨(dú)使用,也可以結(jié)合別的工具一起使用。

          // gulpfile.js
          const { src, dest } = require('gulp');
          // gulp提供的一系列api
          // src 讀取文件
          // dest 寫入文件
          const babel = require('gulp-babel');

          exports.default = function({
            // 將src文件夾下的所有js文件取出,經(jīng)過Babel轉(zhuǎn)換后放入output文件夾之中
            return src('src/*.js')
              .pipe(babel())
              .pipe(dest('output/'));
          }

          Browserify

          隨著 Node.js 的興起,CommonJS 模塊化規(guī)范成為了當(dāng)時(shí)的主流規(guī)范。但是我們知道 CommonJS 所使用的 require 語(yǔ)法是同步的,當(dāng)代碼執(zhí)行到 require 方法的時(shí)候,必須要等這個(gè)模塊加載完后,才會(huì)執(zhí)行后面的代碼。這種方式在服務(wù)端是可行的,這是因?yàn)榉?wù)器只需要從本地磁盤中讀取文件,速度還是很快的,但是在瀏覽器端,我們通過網(wǎng)絡(luò)請(qǐng)求獲取文件,網(wǎng)絡(luò)環(huán)境以及文件大小都可能使頁(yè)面無響應(yīng)。

          browserify[3] 致力于打包產(chǎn)出在瀏覽器端可以運(yùn)行的 CommonJS 規(guī)范的 JS 代碼。

          var browserify = require('browserify')
          var b = browserify()
          var fs = require('fs')

          // 添加入口文件
          b.add('./src/browserifyIndex.js')
          // 打包所有模塊至一個(gè)文件之中并輸出bundle
          b.bundle().pipe(fs.createWriteStream('./output/bundle.js'))

          browserify 怎么實(shí)現(xiàn)的呢?

          browserify 在運(yùn)行時(shí)會(huì)通過進(jìn)行 AST 語(yǔ)法樹分析,確定各個(gè)模塊之間的依賴關(guān)系,生成一個(gè)依賴字典。之后包裝每個(gè)模塊,傳入依賴字典以及自己實(shí)現(xiàn)的 export 和 require 函數(shù),最終生成一個(gè)可以在瀏覽器環(huán)境中執(zhí)行的 JS 文件。

          browserify 專注于 JS 打包,功能單一,一般配合 Gulp 一起使用。

          ESM 規(guī)范出現(xiàn)

          在 2015 年 JavaScript 官方的模塊化終于出現(xiàn)了,但是官方只闡述如何實(shí)現(xiàn)該規(guī)范,瀏覽器少有支持。

          Webpack

          其實(shí)在 ESM 標(biāo)準(zhǔn)出現(xiàn)之前, webpack[4]已經(jīng)誕生了,只是沒有火起來。webpack 的理念更偏向于工程化,伴隨著 MVC 框架以及 ESM 的出現(xiàn)與興起,webpack2 順勢(shì)發(fā)布,宣布支持 AMD\CommonJS\ESM、css/less/sass/stylus、babel、TypeScript、JSX、Angular 2 組件和 vue 組件。從來沒有一個(gè)如此大而全的工具支持如此多的功能,幾乎能夠解決目前所有構(gòu)建相關(guān)的問題。至此 webpack 真正成為了前端工程化的核心。

          webpack 是基于配置。

          module.exports = {
              // SPA入口文件
              entry'src/js/index.js',
              // 出口
              output: {
                filename'bundle.js'
              }
              // 模塊匹配和處理 大部分都是做編譯處理
              module: {
                  rules: [
                              // babel轉(zhuǎn)換語(yǔ)法
                      { test/.js$/use'babel-loader' },
                      //...
                  ]
              },
              // 插件
              plugins: [
                  // 根據(jù)模版創(chuàng)建html文件
                  new HtmlWebpackPlugin({ template'./src/index.html' }),
              ],
          }

          webpack 要兼顧各種方案的支持,也暴露出其缺點(diǎn):

          • 配置往往非常繁瑣,開發(fā)人員心智負(fù)擔(dān)大。
          • webpack 為了支持 cjs 和 esm,自己做了 polyfill,導(dǎo)致產(chǎn)物代碼很“丑”。

          在 webpack 出現(xiàn)兩年后,rollup 誕生了~

          Rollup

          rollup[5] 是一款面向未來的構(gòu)建工具,完全基于 ESM 模塊規(guī)范進(jìn)行打包,率先提出了 Tree-Shaking 的概念。并且配置簡(jiǎn)單,易于上手,成為了目前最流行的 JS 庫(kù)打包工具。

          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: [
              // 轉(zhuǎn)換commonjs模塊為ESM
              resolve(),
              // babel轉(zhuǎn)換語(yǔ)法
              babel({
                exclude'node_modules/**'
              })
            ]
          }

          rollup 基于 esm,實(shí)現(xiàn)了強(qiáng)大的 Tree-Shaking 功能,使得構(gòu)建產(chǎn)物足夠的簡(jiǎn)潔、體積足夠的小。但是要考慮瀏覽器的兼容性問題的話,往往需要配合額外的 polyfill 庫(kù),或者結(jié)合 webpack 使用。

          ESM 規(guī)范原生支持

          Esbuild

          在實(shí)際開發(fā)過程中,隨著項(xiàng)目規(guī)模逐漸龐大,前端工程的啟動(dòng)和打包的時(shí)間也不斷上升,一些工程動(dòng)輒幾分鐘甚至十幾分鐘,漫長(zhǎng)的等待,真的讓人絕望。這使得打包工具的性能被越來越多的人關(guān)注。

          esbuild[6]是一個(gè)非常新的模塊打包工具,它提供了類似 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'],
              bundletrue,
              outfile'out.js',
           }).catch(() => process.exit(1))

          根據(jù)官方提供的性能對(duì)比,我們可以看到性能足有百倍的提升,為什么會(huì)這么快?

          語(yǔ)言優(yōu)勢(shì)

          • esBuild 是選擇 Go 語(yǔ)言編寫的,而在 esBuild 之前,前端構(gòu)建工具都是基于 Node,使用 JS 進(jìn)行編寫。JavaScript 是一門解釋性腳本語(yǔ)言,即使 V8 引擎做了大量?jī)?yōu)化(JWT 及時(shí)編譯),本質(zhì)上還是無法打破性能的瓶頸。而 Go 是一種編譯型語(yǔ)言,在編譯階段就已經(jīng)將源碼轉(zhuǎn)譯為機(jī)器碼,啟動(dòng)時(shí)只需要直接執(zhí)行這些機(jī)器碼即可。
          • Go 天生具有多線程運(yùn)行能力,而 JavaScript 本質(zhì)上是一門單線程語(yǔ)言。esBuild 經(jīng)過精心的設(shè)計(jì),將代碼 parse、代碼生成等過程實(shí)現(xiàn)完全并行處理。

          性能至上原則

          • esBuild 只提供現(xiàn)代 Web 應(yīng)用最小的功能集合,所以其架構(gòu)復(fù)雜度相對(duì)較小,更容易將性能做到極致
          • 在 webpack、rollup 這類工具中, 我們習(xí)慣于使用多種第三方工作來增強(qiáng)工程能力。比如:babel、eslint、less 等。在代碼經(jīng)過多個(gè)工具流轉(zhuǎn)的過程中,存在著很多性能上的浪費(fèi),比如:多次進(jìn)行代碼 -> AST、AST -> 代碼的轉(zhuǎn)換。esBuild 對(duì)此類工具完全進(jìn)行了定制化重寫,舍棄部分可維護(hù)性,追求極致的編譯性能。

          雖然 esBuild 性能非常高,但是其提供的功能很基礎(chǔ),不適合直接用到生產(chǎn)環(huán)境,更適合作為底層的模塊構(gòu)建工具,在它基礎(chǔ)上進(jìn)行二次封裝。

          Vite

          vite[7] 是下一代前端開發(fā)與構(gòu)建工具,提供 noBundle 的開發(fā)服務(wù),并內(nèi)置豐富的功能,無需復(fù)雜配置。

          vite 在開發(fā)環(huán)境和生產(chǎn)環(huán)境分別做了不同的處理,在開發(fā)環(huán)境中底層基于 esBuild 進(jìn)行提速,在生產(chǎn)環(huán)境中使用 rollup 進(jìn)行打包。

          為什么 vite 開發(fā)服務(wù)這么快?

          傳統(tǒng) bundle based 服務(wù):

          • 無論是 webpack 還是 rollup 提供給開發(fā)者使用的服務(wù),都是基于構(gòu)建結(jié)果的。
          • 基于構(gòu)建結(jié)果提供服務(wù),意味著提供服務(wù)前一定要構(gòu)建結(jié)束,隨著項(xiàng)目膨脹,等待時(shí)間也會(huì)逐漸變長(zhǎng)。

          noBundle 服務(wù):

          • 對(duì)于 vite、snowpack 這類工具,提供的都是 noBundle 服務(wù),無需等待構(gòu)建,直接提供服務(wù)。
          • 對(duì)于項(xiàng)目中的第三方依賴,僅在初次啟動(dòng)和依賴變化時(shí)重構(gòu)建,會(huì)執(zhí)行一個(gè)依賴預(yù)構(gòu)建的過程。由于是基于 esBuild 做的構(gòu)建,所以非???。
          • 對(duì)于項(xiàng)目代碼,則會(huì)依賴于瀏覽器的 ESM 的支持,直接按需訪問,不必全量構(gòu)建。

          為什么在生產(chǎn)環(huán)境中構(gòu)建使用 rollup?

          • 由于瀏覽器的兼容性問題以及實(shí)際網(wǎng)絡(luò)中使用 ESM 可能會(huì)造成 RTT 時(shí)間過長(zhǎng),所以仍然需要打包構(gòu)建。
          • esbuild 雖然快,但是它還沒有發(fā)布 1.0 穩(wěn)定版本,另外 esbuild 對(duì)代碼分割和 css 處理等支持較弱,所以生產(chǎn)環(huán)境仍然使用 rollup。

          目前 vite 發(fā)布了 3.0 版本,相對(duì)于 2.0,修復(fù)了 400+issue,已經(jīng)比較穩(wěn)定,可以用于生產(chǎn)了。Vite 決定每年發(fā)布一個(gè)新的版本。

          vite.config.js:

          import { defineConfig } from 'vite'
          import vue from '@vitejs/plugin-vue'
          import {resolve} from 'path';
          // defineConfig 這個(gè)方法沒有什么實(shí)際的含義, 主要是可以提供語(yǔ)法提示
          export default defineConfig({
            resolve:{
              alias:{
               '@':resolve('src')
              }
            },
            plugins: [vue()]
          })

          技術(shù)方案對(duì)比

          前端構(gòu)建工具實(shí)在是琳瑯滿目,以工程化的視角對(duì)社區(qū)仍然比較流行的構(gòu)建工具進(jìn)行簡(jiǎn)單對(duì)比,一些功能專一、特定場(chǎng)景下的工具不在考慮范圍內(nèi)~。

          2021 前端構(gòu)建工具排行[8]

          一些值得思考的問題

          為什么 webpack 構(gòu)建產(chǎn)物看著很丑?

          我們?cè)谑褂?webpack 構(gòu)建項(xiàng)目后,會(huì)發(fā)現(xiàn)打包出來的代碼非常的“丑”,這是為什么?原因就是:webpack 支持多種模塊規(guī)范,但是最后都會(huì)變成commonJS規(guī)范(webpack5 對(duì)純 esm 做了一定的優(yōu)化),但是瀏覽器不支持commonJS規(guī)范,于是 webpack 自己實(shí)現(xiàn)了requiremodule.exports,所以會(huì)有很多 polyfill 代碼的注入。

          針對(duì)·common.js 加載 common.js 這種情況分析一下構(gòu)建產(chǎn)物。

          源代碼:

          // src/index.js
          let title = require('./title.js')
          console.log(title);

          // src/title.js
          module.exports = 'bu';

          產(chǎn)物代碼:

           (() => {
              //把所有模塊定義全部存放到modules對(duì)象里
              //屬性名是模塊的ID,也就是相對(duì)于根目錄的相對(duì)路徑,包括文件擴(kuò)展名
              //值是此模塊的定義函數(shù),函數(shù)體就是原來的模塊內(nèi)的代碼
             var modules = ({
               "./src/title.js"((module) => {
                 module.exports = 'bu';
               })
             });
             // 緩存對(duì)象
             var cache = {};

             // webpack 打包后的代碼能夠運(yùn)行, 是因?yàn)閣ebpack根據(jù)commonJS規(guī)范實(shí)現(xiàn)了一個(gè)require方法
             function require(moduleId{
               var cachedModule = cache[moduleId];
               if (cachedModule !== undefined) {
                 return cachedModule.exports;
               }
               // 緩存和創(chuàng)建模塊對(duì)象
               var module = cache[moduleId] = {
                 exports: {}
               };
               // 運(yùn)行模塊代碼
               modules[moduleId](modulemodule.exports, require "moduleId");
               return module.exports;
             }
             var exports = {};
             (() => {
               // 入口相關(guān)的代碼
               let title = require("./src/title.js")
               console.log(title);
             })();
           })();

          webpack 按需加載的模塊怎么在瀏覽器中運(yùn)行?

          在實(shí)際項(xiàng)目開發(fā)中,隨著代碼越寫越多,構(gòu)建后的 bundle 文件也會(huì)越來越大,我們往往按照種種策略對(duì)代碼進(jìn)行按需加載,將某部分代碼在用戶事件觸發(fā)后再進(jìn)行加載,那么 webpack 在運(yùn)行時(shí)是怎么實(shí)現(xiàn)的呢?

          其實(shí)原理很簡(jiǎn)單,就是以 JSONP 的方式加載按需的腳本,但是如何將這些異步模塊使用起來就比較有意思了~

          對(duì)一個(gè)簡(jiǎn)單的 case 進(jìn)行分析。

          源代碼:

          // index.js
          import("./hello").then((result) => {
              console.log(result.default);
          });

          // hello.js
          export default 'hello';

          產(chǎn)物代碼:

          main.js

           // PS: 對(duì)代碼做了部分簡(jiǎn)化及優(yōu)化, 否則太難讀了~~~
           // 定一個(gè)模塊對(duì)象
          var modules = ({});
          // webpack在瀏覽器里實(shí)現(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 對(duì)象中去
            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, { enumerabletrueget: definition[key] });
            }
          };
          require.r = (exports) => {
            Object.defineProperty(exports, Symbol.toStringTag, { value'Module' });
            Object.defineProperty(exports, '__esModule', { valuetrue });
          };

          // 給require方法定義一個(gè)m屬性, 指向模塊定義對(duì)象
          require.m = modules;

          require.f = {};

          // 利用JSONP加載一個(gè)按需引入的模塊
          require.l = function (url{
            let script = document.createElement("script");
            script.src = url;
            document.head.appendChild(script);
          }

          // 用于通過JSONP異步加載一個(gè)chunkId對(duì)應(yīng)的代碼塊文件, 其實(shí)就是hello.main.js
          require.f.j = function(chunkId, promises){
            let installedChunkData;
            // 當(dāng)前代碼塊的數(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'] = [];
          // 由于按需加載的模塊, 會(huì)在加載成功后調(diào)用此模塊,所以這是JSONP的成功后的回掉
          chunkLoadingGlobal.push = webpackJsonpCallback;

          /**
           * require.e異步加載hello代碼塊文件 hello.main.js
           * promise成功后會(huì)把 hello.main.js里面的代碼定義合并到require.m對(duì)象上,也就是modules上
           * 調(diào)用require方法加載./src/hello.js模塊,獲取 模塊的導(dǎo)出對(duì)象,進(jìn)行打印
           */

          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)物代碼中聲明了一個(gè)全局變量 webpack 并賦值為一個(gè)數(shù)組,然后改寫了這個(gè)數(shù)組的 push 方法。在異步代碼加載完成后執(zhí)行時(shí),會(huì)調(diào)用這個(gè) push 方法,在重寫的方法內(nèi)會(huì)將異步模塊放到全局模塊中然后等待使用。

          白話版 webpack 構(gòu)建流程

          時(shí)至今日,webpack 的功能集已經(jīng)非常龐大了,代碼量更是非常驚人,源碼的學(xué)習(xí)成本非常高,但是了解 webpack 構(gòu)建流程又十分有必要,可以幫我們了解構(gòu)建產(chǎn)物是怎么產(chǎn)生的,以及遇到實(shí)際問題時(shí)如何下手解決問題。

          思路實(shí)現(xiàn)

          簡(jiǎn)單模擬下 webpack 實(shí)現(xiàn)思路:

          class Compilation {
              constructor(options) {
                  this.options = options;
                  // 本次編譯所有生成出來的模塊
                  this.modules = [];
                  // 本次編譯產(chǎn)出的所有代碼塊, 入口模塊和依賴模塊打包在一起成為代碼塊
                  this.chunks = [];
                  // 本次編譯產(chǎn)出的資源文件
                  this.assets = {};
              }
              build(callback) {
                  //5.根據(jù)配置文件中的`entry`配置項(xiàng)找到所有的入口
                  let entry = {xxx'xxx'};

                  //6.從入口文件出發(fā),調(diào)用所有配置的loader規(guī)則,比如說loader對(duì)模塊進(jìn)行編譯
                  for(let entryName in entry){
                      // 6. 從入口文件出發(fā),調(diào)用所有配置的Loader對(duì)模塊進(jìn)行編譯
                      const entryModule = this.buildModule(entryName, entryFilePath);
                      this.modules.push(entryModule);

                      //8.等把所有的模塊編譯完成后,根據(jù)模塊之間的依賴關(guān)系,組裝成一個(gè)個(gè)包含多個(gè)模塊的chunk
                      let chunk = {
                          name: entryName, // 代碼塊的名稱就是入口的名稱
                          entryModule, // 此代碼塊對(duì)應(yīng)的入口模塊
                          modulesthis.modules.filter((module) => module.names.includes(entryName)) // 此代碼塊包含的依賴模塊
                      };
                      this.chunks.push(chunk);
                  }

                  //9.再把各個(gè)代碼塊chunk轉(zhuǎn)換成一個(gè)一個(gè)的文件(asset)加入到輸出列表
                  this.chunks.forEach((chunk) => {
                      const filename = this.options.output.filename.replace('[name]', chunk.name); // 獲取輸出文件名稱
                      this.assets[filename] = getSource(chunk);
                  });
                  // 調(diào)用編譯結(jié)束的回掉
                  callback(null, {
                      modulesthis.modules,
                      chunksthis.chunks,
                      assetsthis.assets
                  }, this.fileDependencies);

              }

              //當(dāng)你編譯 模塊的時(shí)候,需要傳遞你這個(gè)模塊是屬于哪個(gè)代碼塊的,傳入代碼塊的名稱
              buildModule(name, modulePath) {
                   // 6. 從入口文件出發(fā),調(diào)用所有配置的Loader對(duì)模塊進(jìn)行編譯, loader 只會(huì)在編譯過程中使用, plugin則會(huì)貫穿整個(gè)流程
                   // 讀取模塊內(nèi)容
                   let sourceCode = fs.readFileSync(modulePath, 'utf8');
                   //創(chuàng)建一個(gè)模塊對(duì)象
                   let module = {
                       id: moduleId, // 模塊ID =》 相對(duì)于工作目錄的相對(duì)路徑
                       names: [name], // 表示當(dāng)前的模塊屬于哪個(gè)代碼塊(chunk)
                       dependencies: [], // 表示當(dāng)前模塊依賴的模塊
                   }

                   // 查找所有匹配的loader,自右向左讀取loader, 進(jìn)行轉(zhuǎn)譯, 通過loader翻譯后的內(nèi)容一定是JS內(nèi)容
                   sourceCode = loaders.reduceRight((sourceCode, loader) => {
                       return require(loader)(sourceCode);
                   }, sourceCode);

                   // 7. 再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理
                   // 創(chuàng)建語(yǔ)法樹, 遍歷語(yǔ)法樹,在此過程進(jìn)行依賴收集, 繪制依賴圖
                   let ast = parser.parse(sourceCode, { sourceType'module' });
                   traverse(ast, {});
                   let { code } = generator(ast);

                   // 把轉(zhuǎn)譯后的源代碼放到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 = {
                      runnew SyncHook(),  //會(huì)在編譯剛開始的時(shí)候觸發(fā)此run鉤子
                      donenew SyncHook(), //會(huì)在編譯 結(jié)束的時(shí)候觸發(fā)此done鉤子
                  }
              }

              //4.執(zhí)行`Compiler`對(duì)象的`run`方法開始執(zhí)行編譯
              run() {
                  // 在編譯前觸發(fā)run鉤子執(zhí)行, 表示開始啟動(dòng)編譯了
                  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' );
                      }
                      //當(dāng)編譯成功后會(huì)觸發(fā)done這個(gè)鉤子執(zhí)行
                      this.hooks.done.call();
                  }
                  //開始編譯,編譯 成功之后調(diào)用onCompiled方法
                  this.compile(onCompiled);
              }

              compile(callback) {
                  // webpack雖然只有一個(gè)Compiler, 但是每次編譯都會(huì)產(chǎn)出一個(gè)新的Compilation, 用來存放本次編譯產(chǎn)出的 文件、chunk、和模塊
                  // 比如:監(jiān)聽模式會(huì)觸發(fā)多次編譯
                  let compilation = new Compilation(this.options);
                  //執(zhí)行compilation的build方法進(jìn)行編譯 ,編譯 成功之后執(zhí)行回調(diào)
                  compilation.build(callback);
              }

          }

          function webpack(options{
              //1.初始化參數(shù),從配置文件和shell語(yǔ)句中讀取并合并參數(shù),并得到最終的配置對(duì)象
              let finalOptions = {...options, ...shellOptions};

              // 2.用上一步的配置對(duì)象初始化Compiler對(duì)象, 整個(gè)編譯流程只有一個(gè)complier對(duì)象
              const compiler = new Compiler(finalOptions);

              // 3.加載所有在配置文件中配置的插件
              const { plugins } = finalOptions;
              for(let plugin of plugins){
                  plugin.apply(compiler);
              }

              return compiler;
          }

          // webpackOptions webpack的配置項(xiàng)
          const compiler = webpack(webpackOptions);
          //4.執(zhí)行對(duì)象的run方法開始執(zhí)行編譯
          compiler.run();

          為什么 Rollup 構(gòu)建產(chǎn)物很干凈?

          • rollup 只對(duì) ESM 模塊進(jìn)行打包,對(duì)于 cjs 模塊也會(huì)通過插件將其轉(zhuǎn)化為 ESM 模塊進(jìn)行打包。所以不會(huì)像 webpack 有很多的代碼注入。
          • rollup 對(duì)打包結(jié)果也支持多種 format 的輸出,比如:esm、cjs、am 等等,但是 rollup 并不保證代碼可靠運(yùn)行,需要運(yùn)行環(huán)境可靠支持。比如我們輸出 esm 規(guī)范代碼,代碼運(yùn)行時(shí)完全依賴高版本瀏覽器原生去支持 esm,rollup 不會(huì)像 webpack 一樣注入一系列兼容代碼。
          • rollup 實(shí)現(xiàn)了強(qiáng)大的 tree-shaking 能力。

          為什么 Vite 可以讓代碼直接運(yùn)行在瀏覽器上?

          前文我們提到,在開發(fā)環(huán)境時(shí),我們使用 vite 開發(fā),是無需打包的,直接利用瀏覽器對(duì) ESM 的支持,就可以訪問我們寫的組件代碼,但是一些組件代碼文件往往不是 JS 文件,而是 .ts、.tsx、.vue 等類型的文件。這些文件瀏覽器肯定直接是識(shí)別不了的,vite 在這個(gè)過程中做了些什么?

          我們以一個(gè)簡(jiǎn)單的 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>

          在瀏覽器中打開頁(yè)面后,會(huì)發(fā)現(xiàn)瀏覽器對(duì)入口文件發(fā)起了請(qǐng)求:

          我們可以觀察到 vue 這個(gè)第三方包的訪問路徑改變了,變成了node_modules/.vite下的一個(gè) vue 文件,這里真正訪問的文件就是前面我們提到的,vite 會(huì)對(duì)第三方依賴進(jìn)行依賴預(yù)構(gòu)建所生成的緩存文件。

          另外瀏覽器也對(duì) App.vue 發(fā)起了訪問,相應(yīng)內(nèi)容是 JS:

          返回內(nèi)容(做了部分簡(jiǎn)化,移除了一些熱更新的代碼):

           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;

          總結(jié):當(dāng)用戶訪問 vite 提供的開發(fā)服務(wù)器時(shí),對(duì)于瀏覽器不能直接識(shí)別的文件,服務(wù)器的一些中間件會(huì)將此類文件轉(zhuǎn)換成瀏覽器認(rèn)識(shí)的文件,從而保證正常訪問。

          Node 社群


          我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。

             “分享、點(diǎn)贊在看” 支持一波??


          參考資料

          [1]

          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 前端構(gòu)建工具排行: https://risingstars.js.org/2021/zh#section-build

          [9]

          前端構(gòu)建工具簡(jiǎn)史: https://juejin.cn/post/7085613927249215525

          [10]

          ESM 實(shí)現(xiàn)原理: https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/

          [11]

          Webpack 核心原理:https://mp.weixin.qq.com/s/SbJNbSVzSPSKBe2YStn2Zw


          瀏覽 66
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美操逼片 | 亚洲免费播放视频 | 视频一区综合网 | A片免费直接看 | 免费观看a∨视频 |