重拾 Webpack(上卷)

Ozzie發表於2019-12-12

花了幾天時間看了一本書《Webpack入門、進階與調優》,之前看書評不錯就去詳細閱讀了一遍,雖然感覺有些內容並非屬於 webpack 而是不深不淺地介紹了一些在實戰中的內容,但作為一本系統介紹 webpack 的解析書,確實寫的比較清晰了,在這裡歸納書中的前一部分的知識點,這也足以入門 webpack 了


初識Webpack

模組打包工具

  1. 模組打包工具的任務:解決模組之間的依賴,使其打包後能執行在瀏覽器上
  2. 模組打包工具的工作方式(主要分為以下兩種)
    • 將存在依賴關係的模組按照特定的規則合併為單個的 JS 檔案,一次全部載入進入頁面中
    • 在頁面初始時載入一個入口模組,其他模組非同步地進行載入
  3. 有哪些模組打包工具?
    • Webpack
    • Parcel
    • Rollup
  4. 為什麼選擇 Webpack ?
    • 支援多種模組標準,如 AMD 規範Commonjs 規範ES6 模組規範 等等
    • 完備的程式碼分割方案,通俗地說,就是首屏只載入必要的部分,不太重要的部分放到後面動態地載入
    • 處理各種型別的資源,除了能處理 JavaScript 檔案,還能處理樣式、模板、甚至圖片
    • 龐大的社群支援

安裝

  1. 注意:確保已經安裝了 Node.js,並且該 Node 的版本要儘量新
  2. 初始化專案
    • 新建 MyWebpack 資料夾,並輸入:
      npm init -y
  3. 安裝 webpack
    • 我們採用區域性安裝的方式,輸入:
      npm install webpack webpack-cli --save-dev 

      注:webpack 是核心模組,webpack-cli 是命令列工具,在這裡是需要的

  4. 檢驗安裝
    • 由於我們將 webpack 安裝在了本地,所以這裡無法使用 "webpack" 指令,
      工程內部只能使用 npx webpack <command> 的方式,所以我們輸入以下命令檢驗版本:
      npx webpack -v
      npx webpack-cli -v

打包第一個應用

  1. 在根目錄下新增以下幾個檔案:
    • 新建 index.js 並輸入:
      import addContent from './addContent'
      document.write('My first Webpack app <br/>')
      addContent()
    • 新建 addContent.js 並輸入:
      export default function(){
          document.write('Hello World')
      }
    • 新建 index.html 並輸入:
      <!DOCTYPE html>
      <html>
      <head>
          <meta charset="utf-8">
          <title></title>
      </head>
      <body>
          <script src="./dist/bundle.js"></script>
      </body>
      </html>
  2. 在命令列中輸入:
    npx webpack --entry=./index.js --output-filename=bundle.js --mode=development
  3. 瀏覽器開啟 index.html 即可看到內容
  4. 回顧剛才的命令:
    • entry
      • 資源打包的入口,Webpack 將從這裡開始進行模組依賴的查詢,webpack 便知道了專案中包含 index.jsaddContent.js 兩個模組,通過他們來生成產物
    • output-filename
      • 打包後的檔名
    • mode=development
      • 打包模式,Webpack 提供了 development、production、none 三種模式
      • 當選擇 developmentproduction 模式時,它會自動新增適用於當前模式下的一系列配置,一般在開發環境下,我們選擇 development 就行了
  5. 使用 npm scripts
    • package.json 中新增一下命令:
      "scripts": {
        "build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development"
      }
    • 現在不需要像剛才那樣輸入冗長的命令,直接輸入:
      npm run build
  6. 使用預設目錄配置
    • 通常情況下,我們會設定兩個目錄,分別為原始碼目錄和資源輸出目錄
      工程原始碼放在 /src 中,輸出資源放在 /dist
    • Webpack 預設的原始碼入口就是 src/index.js ,所以現在我們可以省略掉 entry 的配置,編輯 package.json 如下:
      "scripts": {
          "build": "webpack --output-filename=bundle.js --mode=development"
      }
  7. 使用配置檔案
    • webpack 有非常多的配置項以及相應的命令列引數,我們可以通過以下命令檢視:
      npx webpack -h
    • 新建 webpack.config.js,輸入如下:
      module.exports = {
          entry: './src/index.js',
          output: {
              filename: 'bundle.js',
          },
          mode: 'development'
      }
    • 現在我們可以去掉 package.json 中配置的打包引數了:
      "scripts": {
          "build": "webpack"
      }
    • 輸入 npm run build 即可重新打包
  8. webpack-dev-server
    • 由於我們現在每次更新內容都需要重新打包一次,比較麻煩,我們可以使用 Webpack 社群提供的一個開發工具 —— webpack-dev-server
    • 安裝工具
      npm install webpack-dev-server --save-dev
    • package.json 中新增一項:
      "scripts": {
          "dev": "webpack-dev-server"
      }
    • 最後,我們需要對 webpack.config.js 進行配置,如下:
      module.exports = {
          entry: './src/index.js',
          output: {
              filename: './bundle.js',
          },
          mode: 'development',
          devServer: {
              publicPath: '/dist',
          },
      }
    • webpack.config.jsdevServer物件 是專門配置 webpack-dev-server 的,webpack-dev-server 主要工作就是接受瀏覽器的請求,然後將資源返回
      當服務啟動時,會先讓 webpack 進行模組打包,當 webpack-dev-server 接收到 瀏覽器的資源請求時,它會首先進行 URL 地址校驗,如果地址是資源服務地址(即配置中的 publicPath),那麼就從 webpack 將打包結果返回給瀏覽器,否則直接從硬碟讀取原始檔並返回
    • 總結 webpack-dev-server 的職能:
      • 令 webpack 進行模組打包,並處理打包結果的資源請求
      • 作為 web server,處理靜態資原始檔請求
    • 輸入命令,並開啟 http://localhost:8080/
      npm run dev
    • 注意事項
      • 直接用 webpack 開發和使用 webpack-dev-server 有一個很大的區別:前者每次都會生成 bundle.js,而後者只是將打包結果放在記憶體中,並沒有實際寫入 bundle.js 中,每次都是將記憶體中的打包結果返回給瀏覽器,可以通過刪除 dist 目錄來檢驗此區別
    • 當然,還需說明的一點是,webpack-dev-server 中的很便捷的的特點就是 live-reloading,來保持本地服務啟動以及瀏覽器開啟的狀態

再談模組打包

各種模組規範

模組打包原理

  1. 新建兩個檔案,內容分別如下:
    • index.js
      const mod = require('./mod.js')
      const sum = mod.add(2,3)
      console.log('sum',sum)
    • mod.js
      module.exports = {
          add: function(a, b){
              return a + b;
          }
      }
  2. 打包之後的 JS 檔案如下:
    (function(modules) {
        var installedModules = {};
        function __webpack_require__(moduleId) {
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;
            }
            var module = installedModules[moduleId] = {
                i: moduleId,
                l: false,
                exports: {}
            };
            modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
            module.l = true;
            return module.exports;
        }
        __webpack_require__.m = modules;
        __webpack_require__.c = installedModules;
        __webpack_require__.d = function(exports, name, getter) {
            if (!__webpack_require__.o(exports, name)) {
                Object.defineProperty(exports, name, {
                    enumerable: true,
                    get: getter
                });
            }
        };
        __webpack_require__.r = function(exports) {
            if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
                Object.defineProperty(exports, Symbol.toStringTag, {
                    value: 'Module'
                });
            }
            Object.defineProperty(exports, '__esModule', {
                value: true
            });
        };
        __webpack_require__.t = function(value, mode) {
            if (mode & 1) value = __webpack_require__(value);
            if (mode & 8) return value;
            if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
            var ns = Object.create(null);
            __webpack_require__.r(ns);
            Object.defineProperty(ns, 'default', {
                enumerable: true,
                value: value
            });
            if (mode & 2 && typeof value != 'string')
                for (var key in value) __webpack_require__.d(ns, key, function(key) {
                    return value[key];
                }.bind(null, key));
            return ns;
        };
        __webpack_require__.n = function(module) {
            var getter = module && module.__esModule ?
                function getDefault() {
                    return module['default'];
                } :
                function getModuleExports() {
                    return module;
                };
            __webpack_require__.d(getter, 'a', getter);
            return getter;
        };
        __webpack_require__.o = function(object, property) {
            return Object.prototype.hasOwnProperty.call(object, property);
        };
        __webpack_require__.p = "";
        return __webpack_require__(__webpack_require__.s = "./index.js");
    })
    ({
        "./index.js": (function(module, exports, __webpack_require__) {
            eval(
                "const mod = __webpack_require__(/*! ./mod.js */ \"./mod.js\")\r\nconst sum = mod.add(2,3)\r\nconsole.log('sum',sum)\r\n\n\n//# sourceURL=webpack:///./index.js?"
            );
        }),
        "./mod.js": (function(module, exports) {
            eval(
                "module.exports = {\r\n\tadd: function(a, b){\r\n\t\treturn a + b;\r\n\t}\r\n}\n\n//# sourceURL=webpack:///./mod.js?"
            );
        })
    });
  3. 上述這個結果可以很清晰地展示它是如何將具有依賴關係的模組串聯在一起的,此檔案可以分為以下幾個部分:
    • 最外層立即執行匿名函式,用來包裹整個檔案,並形成自己的作用域
    • installedModules物件:每個模組只在第一次被載入的時候執行,然後匯出的值就存在這個物件裡面,當再次被載入的時候直接從裡面取值,而不會重新執行
    • __webpack_require__函式:對於模組載入的實現,在瀏覽器中可以通過 __webpack_require__(module.id) 來完成模組的匯入
    • module物件:工程中所有產生了依賴關係的檔案都會以 key-value 的形式存放在這裡
      • key 可以理解為一個模組的 id,由數字或者很短的 hash 字串組成
      • value 是一個匿名函式包裹的模組實體,匿名函式的每個引數賦予了模組的匯入和匯出的功能
  4. 打包後的檔案在瀏覽器中的執行過程:
    • 最外層的匿名函式初始化瀏覽器的執行環境,為模組的載入和執行做準備工作,比如定義 installedModules 物件、__webpack_require__ 函式等等
    • 載入入口模組,每個打包後的檔案都有一個入口模組,上述例項中,index.js 是入口模組,瀏覽器即從入口模組開始執行
    • 執行模組程式碼:
      • 如果執行到了 module.exports,則記錄下模組的匯出值
      • 如果執行時遇到了 __webpack_require__,則會暫時交出執行權,進入 __webpack_require__ 函式體內載入其他模組的內容
    • 在 __webpack_require__ 中判斷即將載入的模組是否存在於 installedModules 中,如果存在則直接取值,否則返回上一步 —— 執行模組程式碼獲取匯出值
    • 當所有依賴的模組均已執行完畢,則最後的執行權顯然又會回到入口模組,當入口模組的程式碼執行結束,也就標緻著整個模組打包過程結束

資源輸入與輸出

資源處理流程

  1. 在一切工作開始之前,我們需要指定一個或者多個 入口(entry) 來讓 webpack 知曉應該從哪裡開始打包,如果把各個模組的依賴關係比喻成一顆樹,那麼入口檔案顯然就是樹根,如圖:
  2. 這些存在依賴關係的模組,在打包時會被封裝成一個 chunk,chunk 的字面意思是程式碼塊,在 webpack 中,可以理解為被封裝和抽象過後的一些模組,根據配置不同,webpack可能會形成一個或多個 chunk
  3. 由這個 chunk 得到的打包產物我們稱為 bundleentrychunkbundle的關係如下:
  4. 在工程中可以定義多個入口,每個入口都會產生一個結果,比如我們有兩個入口檔案 index.jslib.js,那麼打包的結果就會生成 dist/bundle.jsdist/lib.js,如圖:

配置資源入口

  • webpack 通過 contextentry 兩個配置項來共同決定入口檔案的路徑,在配置時,實際上做了兩件事:
    • 確定入口模組的位置,告訴 webpack 從哪裡開始打包
    • 定義 chunk name ,如果該工程只有唯一入口,那麼預設為 main,若有多個入口,那麼分別定義對應的 chunk name。

context與entry

  1. context
    • context 可以理解為資源入口的路徑字首,在配置時要求使用絕對路徑的形式,比如下面兩個例子:
      // 指定路徑為:<工程根路徑>/src/home/index.js
      module.exports = {
          context: path.join(__dirname, './src'),
          entry: './home/index.js'
      };
      // 等同於下面這種方式
      module.exports = {
          context: path.join(__dirname, './src/home'),
          entry: './index.js'
      }
    • 配置 context 的目的主要是讓 entry 的編寫更加簡潔,這種作用在多入口的情況下尤其突出,此外,context 是可以省略的,則預設值為當前工程的根目錄
    • context 的配置形式只能為字串
  2. entry
    • 首先,entry 的配置形式可以有多種:字串、陣列、物件、函式,可以根據不同的需求場景來選擇
    • 字串型別入口
      • 直接傳入路徑
        module.exports = {
            entry: './src/index.js',
        }
    • 陣列型別入口:
      • 傳入一個陣列的作用是將多個資源先合併,在打包時 webpack 會將陣列中的最後一個元素作為實際的入口路徑,如:
        module.exports = {
            entry: ['babel-polyfill', './src/index.js'],
        }
      • 以上配置等同於:
        // webpack.config.js
        module.exports = {
            entry: './src/index.js',
        }
        // index.js
        import 'babel-polyfill'
    • 物件型別入口:
      • 如果想要定義多個入口,則必須要使用物件地形式,物件的屬性名(key)是 chunk name,屬性值(value)是入口路徑,如:
        module.exports = {
            entry: {
                // chunk name 為 index,入口路徑為 ./src/index.js
                index: './src/index.js',
                // chunk name 為 lib,入口路徑為 ./src/lib.js
                lib: './src/lib.js',
            }
        }
      • 當然,物件的屬性值也可以為字串或者陣列,如:
        module.exports = {
            index: ['babel-polyfill', './src/index.js'],
            lib: './src/lib.js'
        }
    • 函式型別入口:
      • 用函式定義入口時,只需要返回字串、陣列或者物件中的任何一種配置形式即可,如:
        // 返回字串型的入口
        module.exports = {
            entry: () => './src/index.js',
        }
        // 返回物件型的入口
        module.exports = {
            entry: () => ({
                index: ['babel-polyfill', './src/index.js'],
                lib: './src/lib.js'
            })
        }
      • 使用函式的優勢是我們可以在函式體內新增一些動態的邏輯來獲取入口,而且,函式也支援返回一個 Promise 物件 來進行非同步操作,如:
        module.exports = {
            entry: () => new Promise((resolve) => {
                // 模擬非同步操作
                setTimeout(() => {
                    resolve('./src/index.js');
                }, 1000);
            }),
        };
    • 注:使用字串或陣列定義單入口時,沒有辦法更改 chunk name,只能為預設的 "main"
      使用物件來定義多入口時,則必須為每一個入口定義 chunk name

例項 —— 單頁與多頁

  1. 單頁應用
    • 對於 單頁應用(SPA) 來說,一般定義單一入口即可:
      module.exports = {
          entry: './src/index.js',
      }
    • 這樣做的好處是隻會產生一個 JS 檔案,依賴關係清晰,而弊端就是所有的模組都打包到一個檔案中,可能會導致該輸出檔案體積過大,降低頁面的渲染速度
      在 webpack 的預設配置中,一個輸出檔案大於 250KB 時,會認為這個檔案已經過大了,在打包時會發出警告
  2. 提取 vendor
    • 假如工程產生的 JS 檔案體積很大,那麼一旦程式碼更新,輸出檔案也要響相應地更新,這對頁面的效能影響是比較大的,我們可以通過 vendor 來解決這個問題
    • 在 webpack 中,vendor 一般指的是工程所使用的庫、框架等第三方模組集中打包產生的輸出檔案,如:
      module.exports = {
          context: path.join(__dirname, './src'),
          entry: {
              index: './src/index.js',
              vendor: ['react', 'react-dom', 'react-router'],
          },
      };

      在上述的例子中,我們新增了一個新的 chunk name 作為 vendor 的入口,通過陣列的形式將工程所需的第三方模組放了進去

  3. 多頁應用
    • 我們希望每個頁面都只載入各自必要的邏輯,而不是把所有的內容都打包到一個輸出檔案中,因此每個頁面都需要有一個獨立的輸出檔案,如:
      module.exports = {
          entry: {
              pageA: './src/pageA.js',
              pageB: './src/pageB.js',
              // 提取 vendor 來對公共模組打包
              vendor: ['react', 'react-dom'],
          }
      }

配置資源出口

  • 所有與出口相關的配置都集中在 output 物件 中,此部分最好的學習當然是去查文件啦,給出連結:
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章