當面試官問Webpack的時候他想知道什麼

前言

在前端工程化日趨複雜的今天,模塊打包工具在咱們的開發中起到了愈來愈重要的做用,其中webpack就是最熱門的打包工具之一。前端

說到webpack,可能不少小夥伴會以爲既熟悉又陌生,熟悉是由於幾乎在每個項目中咱們都會用上它,又由於webpack複雜的配置和五花八門的功能感到陌生。尤爲當咱們使用諸如umi.js之類的應用框架還幫咱們把webpack配置再封裝一層的時候,webpack的本質彷佛離咱們更加遙遠和深不可測了。vue

當面試官問你是否瞭解webpack的時候,或許你能夠說出一串耳熟能詳的webpack loaderplugin的名字,甚至還能說出插件和一系列配置作按需加載和打包優化,那你是否瞭解他的運行機制以及實現原理呢,那咱們今天就一塊兒探索webpack的能力邊界,嘗試瞭解webpack的一些實現流程和原理,拒作API工程師。webpack

圖片

你知道webpack的做用是什麼嗎?

從官網上的描述咱們其實不難理解,webpack的做用其實有如下幾點:git

  • 模塊打包。能夠將不一樣模塊的文件打包整合在一塊兒,而且保證它們之間的引用正確,執行有序。利用打包咱們就能夠在開發的時候根據咱們本身的業務自由劃分文件模塊,保證項目結構的清晰和可讀性。
  • 編譯兼容。在前端的「上古時期」,手寫一堆瀏覽器兼容代碼一直是令前端工程師頭皮發麻的事情,而在今天這個問題被大大的弱化了,經過webpackLoader機制,不只僅能夠幫助咱們對代碼作polyfill,還能夠編譯轉換諸如.less, .vue, .jsx這類在瀏覽器沒法識別的格式文件,讓咱們在開發的時候可使用新特性和新語法作開發,提升開發效率。
  • 能力擴展。經過webpackPlugin機制,咱們在實現模塊化打包和編譯兼容的基礎上,能夠進一步實現諸如按需加載,代碼壓縮等一系列功能,幫助咱們進一步提升自動化程度,工程效率以及打包輸出的質量。

說一下模塊打包運行原理?

若是面試官問你Webpack是如何把這些模塊合併到一塊兒,而且保證其正常工做的,你是否瞭解呢?github

首先咱們應該簡單瞭解一下webpack的整個打包流程:web

  • 一、讀取webpack的配置參數;
  • 二、啓動webpack,建立Compiler對象並開始解析項目;
  • 三、從入口文件(entry)開始解析,而且找到其導入的依賴模塊,遞歸遍歷分析,造成依賴關係樹;
  • 四、對不一樣文件類型的依賴模塊文件使用對應的Loader進行編譯,最終轉爲Javascript文件;
  • 五、整個過程當中webpack會經過發佈訂閱模式,向外拋出一些hooks,而webpack的插件便可經過監聽這些關鍵的事件節點,執行插件任務進而達到干預輸出結果的目的。

其中文件的解析與構建是一個比較複雜的過程,在webpack源碼中主要依賴於compilercompilation兩個核心對象實現。面試

compiler對象是一個全局單例,他負責把控整個webpack打包的構建流程。compilation對象是每一次構建的上下文對象,它包含了當次構建所須要的全部信息,每次熱更新和從新構建,compiler都會從新生成一個新的compilation對象,負責這次更新的構建過程。前端工程化

而每一個模塊間的依賴關係,則依賴於AST語法樹。每一個模塊文件在經過Loader解析完成以後,會經過acorn庫生成模塊代碼的AST語法樹,經過語法樹就能夠分析這個模塊是否還有依賴的模塊,進而繼續循環執行下一個模塊的編譯解析。api

最終Webpack打包出來的bundle文件是一個IIFE的執行函數。數組

// webpack 5 打包的bundle文件內容

(() => { // webpackBootstrap
    var __webpack_modules__ = ({
        'file-A-path': ((modules) => { // ... })
        'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
    })
    
    // The module cache
    var __webpack_module_cache__ = {};
    
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
                return cachedModule.exports;
        }
        // Create a new module (and put it into the cache)
        var module = __webpack_module_cache__[moduleId] = {
                // no module.id needed
                // no module.loaded needed
                exports: {}
        };

        // Execute the module function
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");

        // Return the exports of the module
        return module.exports;
    }
    
    // startup
    // Load entry module and return exports
    // This entry module can't be inlined because the eval devtool is used.
    var __webpack_exports__ = __webpack_require__("./src/index.js");
})

webpack4相比,webpack5打包出來的bundle作了至關的精簡。在上面的打包demo中,整個當即執行函數裏邊只有三個變量和一個函數方法,__webpack_modules__存放了編譯後的各個文件模塊的JS內容,__webpack_module_cache__ 用來作模塊緩存,__webpack_require__Webpack內部實現的一套依賴引入函數。最後一句則是代碼運行的起點,從入口文件開始,啓動整個項目。

其中值得一提的是__webpack_require__模塊引入函數,咱們在模塊化開發的時候,一般會使用ES Module或者CommonJS規範導出/引入依賴模塊,webpack打包編譯的時候,會統一替換成本身的__webpack_require__來實現模塊的引入和導出,從而實現模塊緩存機制,以及抹平不一樣模塊規範之間的一些差別性。

你知道sourceMap是什麼嗎?

提到sourceMap,不少小夥伴可能會馬上想到Webpack配置裏邊的devtool參數,以及對應的evaleval-cheap-source-map等等可選值以及它們的含義。除了知道不一樣參數之間的區別以及性能上的差別外,咱們也能夠一塊兒瞭解一下sourceMap的實現方式。

sourceMap是一項將編譯、打包、壓縮後的代碼映射回源代碼的技術,因爲打包壓縮後的代碼並無閱讀性可言,一旦在開發中報錯或者遇到問題,直接在混淆代碼中debug問題會帶來很是糟糕的體驗,sourceMap能夠幫助咱們快速定位到源代碼的位置,提升咱們的開發效率。sourceMap其實並非Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap

既然是一種源碼的映射,那必然就須要有一份映射的文件,來標記混淆代碼裏對應的源碼的位置,一般這份映射文件以.map結尾,裏邊的數據結構大概長這樣:

{
  "version" : 3,                          // Source Map版本
  "file": "out.js",                       // 輸出文件(可選)
  "sourceRoot": "",                       // 源文件根目錄(可選)
  "sources": ["foo.js", "bar.js"],        // 源文件列表
  "sourcesContent": [null, null],         // 源內容列表(可選,和源文件列表順序一致)
  "names": ["src", "maps", "are", "fun"], // mappings使用的符號名稱列表
  "mappings": "A,AAAB;;ABCDE;"            // 帶有編碼映射數據的字符串
}

其中mappings數據有以下規則:

  • 生成文件中的一行的每一個組用「;」分隔;
  • 每一段用「,」分隔;
  • 每一個段由一、4或5個可變長度字段組成;

有了這份映射文件,咱們只須要在咱們的壓縮代碼的最末端加上這句註釋,便可讓sourceMap生效:

//# sourceURL=/path/to/file.js.map

有了這段註釋後,瀏覽器就會經過sourceURL去獲取這份映射文件,經過解釋器解析後,實現源碼和混淆代碼之間的映射。所以sourceMap其實也是一項須要瀏覽器支持的技術。

若是咱們仔細查看webpack打包出來的bundle文件,就能夠發如今默認的development開發模式下,每一個_webpack_modules__文件模塊的代碼最末端,都會加上//# sourceURL=webpack://file-path?,從而實現對sourceMap的支持。

sourceMap映射表的生成有一套較爲複雜的規則,有興趣的小夥伴能夠看看如下文章,幫助理解soucrMap的原理實現:

Source Map的原理探究[1]

Source Maps under the hood – VLQ, Base64 and Yoda[2]

是否寫過Loader?簡單描述一下編寫loader的思路?

從上面的打包代碼咱們其實能夠知道,Webpack最後打包出來的成果是一份Javascript代碼,實際上在Webpack內部默認也只可以處理JS模塊代碼,在打包過程當中,會默認把全部遇到的文件都看成 JavaScript代碼進行解析,所以當項目存在非JS類型文件時,咱們須要先對其進行必要的轉換,才能繼續執行打包任務,這也是Loader機制存在的意義。

Loader的配置使用咱們應該已經很是的熟悉:

// webpack.config.js
module.exports = {
  // ...other config
  module: {
    rules: [
      {
        test: /^your-regExp$/,
        use: [
          {
             loader: 'loader-name-A',
          }, 
          {
             loader: 'loader-name-B',
          }
        ]
      },
    ]
  }
}

經過配置能夠看出,針對每一個文件類型,loader是支持以數組的形式配置多個的,所以當Webpack在轉換該文件類型的時候,會按順序鏈式調用每個loader,前一個loader返回的內容會做爲下一個loader的入參。所以loader的開發須要遵循一些規範,好比返回值必須是標準的JS代碼字符串,以保證下一個loader可以正常工做,同時在開發上須要嚴格遵循「單一職責」,只關心loader的輸出以及對應的輸出。

loader函數中的this上下文由webpack提供,能夠經過this對象提供的相關屬性,獲取當前loader須要的各類信息數據,事實上,這個this指向了一個叫loaderContextloader-runner特有對象。有興趣的小夥伴能夠自行閱讀源碼。

module.exports = function(source) {
    const content = doSomeThing2JsString(source);
    
    // 若是 loader 配置了 options 對象,那麼this.query將指向 options
    const options = this.query;
    
    // 能夠用做解析其餘模塊路徑的上下文
    console.log('this.context');
    
    /*
     * this.callback 參數:
     * error:Error | null,當 loader 出錯時向外拋出一個 error
     * content:String | Buffer,通過 loader 編譯後須要導出的內容
     * sourceMap:爲方便調試生成的編譯後內容的 source map
     * ast:本次編譯生成的 AST 靜態語法樹,以後執行的 loader 能夠直接使用這個 AST,進而省去重複生成 AST 的過程
     */
    this.callback(null, content);
    // or return content;
}

更詳細的開發文檔能夠直接查看官網的 Loader API[3]。

是否寫過Plugin?簡單描述一下編寫plugin的思路?

若是說Loader負責文件轉換,那麼Plugin即是負責功能擴展。LoaderPlugin做爲Webpack的兩個重要組成部分,承擔着兩部分不一樣的職責。

上文已經說過,webpack基於發佈訂閱模式,在運行的生命週期中會廣播出許多事件,插件經過監聽這些事件,就能夠在特定的階段執行本身的插件任務,從而實現本身想要的功能。

既然基於發佈訂閱模式,那麼知道Webpack到底提供了哪些事件鉤子供插件開發者使用是很是重要的,上文提到過compilercompilationWebpack兩個很是核心的對象,其中compiler暴露了和 Webpack整個生命週期相關的鉤子(compiler-hooks[4]),而compilation則暴露了與模塊和依賴有關的粒度更小的事件鉤子(Compilation Hooks[5])。

Webpack的事件機制基於webpack本身實現的一套Tapable事件流方案(github[6])

// Tapable的簡單使用
const { SyncHook } = require("tapable");

class Car {
    constructor() {
        // 在this.hooks中定義全部的鉤子事件
        this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            brake: new SyncHook(),
            calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
        };
    }

    /* ... */
}


const myCar = new Car();
// 經過調用tap方法便可增長一個消費者,訂閱對應的鉤子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的開發和開發Loader同樣,須要遵循一些開發上的規範和原則:

  • 插件必須是一個函數或者是一個包含 apply 方法的對象,這樣才能訪問compiler實例;
  • 傳給每一個插件的 compilercompilation 對象都是同一個引用,若在一個插件中修改了它們身上的屬性,會影響後面的插件;
  • 異步的事件須要在插件處理完任務時調用回調函數通知 Webpack 進入下一個流程,否則會卡住;

瞭解了以上這些內容,想要開發一個 Webpack Plugin,其實也並不困難。

class MyPlugin {
  apply (compiler) {
    // 找到合適的事件鉤子,實現本身的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
        // compilation: 當前打包構建流程的上下文
        console.log(compilation);
        
        // do something...
    })
  }
}

更詳細的開發文檔能夠直接查看官網的 Plugin API[7]。

最後

本文也是結合一些優秀的文章和webpack自己的源碼,大概地說了幾個相對重要的概念和流程,其中的實現細節和設計思路還須要結合源碼去閱讀和慢慢理解。

Webpack做爲一款優秀的打包工具,它改變了傳統前端的開發模式,是現代化前端開發的基石。這樣一個優秀的開源項目有許多優秀的設計思想和理念能夠借鑑,咱們天然也不該該僅僅停留在API的使用層面,嘗試帶着問題閱讀源碼,理解實現的流程和原理,也能讓咱們學到更多知識,理解得更加深入,在項目中才能遊刃有餘的應用。

參考資料

[1]Source Map的原理探究: https://blog.fundebug.com/201...

[2]Source Maps under the hood – VLQ, Base64 and Yoda: *https://docs.microsoft.com/zh...

[3]Loader API: *https://www.webpackjs.com/api...

[4]compiler-hooks: https://webpack.js.org/api/co...

[5]Compilation Hooks: https://webpack.js.org/api/co...

[6]github: https://github.com/webpack/ta...

[7]Plugin API: https://www.webpackjs.com/api...

原文地址(前端大全)

相關文章
相關標籤/搜索