Webpack2初識

騰訊大資料發表於2019-02-23

本文為騰訊移動分析MTA 產品UI工程師linji的分享。

聚焦webpack2的部分目錄:

溫故或知新

簡單地概括–webpack是一個智慧模組依賴分析的打包工具,它通過入口js檔案以及一系列的外掛能把各種資原始檔編譯、打包為可釋出使用的靜態資源。如官網中的檢視:萬物皆模組。

Webpack2初識

安裝

文章撰寫時webpack最新正式版版本號為2.5.1。

命令列工具中安裝:

# 全域性安裝
npm install -g webpack
# 初始化專案
npm init
#安裝到專案目錄中
npm install --save-dev webpack複製程式碼

快速開始

在根目錄建立如下檔案:
entry.js — 入口檔案

import fn from `./fn`;
fn();複製程式碼

fn.js — 被引用的模組

export default function fn() {
  document.write(`I am a Function`);
}複製程式碼

webpack.config.js — webpack配置檔案

module.exports = {
  entry: `./entry.js`,
  output: {
    filename: `bundle.js`
  }
}複製程式碼

index.html — 靜態頁面

<!DOCTYPE html>
<html>
  <body>
    <script src="./bundle.js"></script>
  </body>
</html>複製程式碼

在專案目錄中執行命令 webpack,然後在瀏覽器開啟上面的靜態頁面就可以看到效果了

Webpack2初識

變化

現在簡單列出我認為對比webpack1版比較明顯的變化

  1. ES6模組的靜態匯入(import等部分ES6語法的原生支援): 更快捷的初始化工程

  2. ES6模組的動態匯入:按官方文件描述,這種動態匯入方式正嘗試加入到ES的規範之中

    • 建立async.js
      export default function async() {
        console.log(`非同步引用`);
      }複製程式碼
    • entry.js 加入程式碼

      import(`./async`).then(async => {
        async.default();
      });
      // 等價於
      /*
      require.ensure([], (require) => {
        const async = require(`./async`);
        async.default();
      });
      */複製程式碼

      import()返回的是promise例項,所以我們還可以通過Promise.all([import(`./async`)]).then(([async]) => {})的形式去引用多個模組;不過,當你需要相容IE8-的瀏覽器時,你需要額外匯入promise-polyfill

      System.import()和import()方法作用相同,在本文撰寫期間的2.5.x版本前者依然可以使用,但官方文件有指出這個方法不建議使用

  3. 修復ugulifyJS壓縮css檔案時誤刪除[dpr]問題:當然我們可以通過別的工具去代替

  4. 函式方式匯出配置:可以通過引數來代替設定複雜的環境變數

    • 修改 webpack.config.js, 並執行 webpack --env.dev
      module.exports = function(env) {
      return {
       entry: `./entry.js`,
       output: {
         filename: `bundle.js`
       },
       // 新的“環境變數”使用方式 (此處新增了sourcemap)
       devtool: env.dev
         ? `cheap-module-eval-source-map`
         : false
       };
      };複製程式碼
  5. 配置更清晰簡潔, 更多檢視後文配置“升級”
  6. Tree shaking:去除無用程式碼,適用於檔案目錄結構複雜的專案,更多檢視後文瞭解 Tree shaking

配置“升級”

  • module.loaders –> modules.rules
    使用loader時需要把loader名字寫全,如 style 需要寫為 style-loader

  • 移除 module.preLoaders, module.postLoaders
    現在通過module.rules.enforce屬性來配置

  • 不再需要手動加入 DedupePlugin, OccurrenceOrderPlugin, json-loader, devServer.inline 等配置
    畢竟幾乎大家都需要用到它們

  • webpack-dev-server, extract-text-webpack-plugin, html-webpack-plugin 這三個常用的工具必須升級到2.x版本,否則無法和webpack2作用

  • 合併分散的resolve配置 resolve.root, resolve.fallback, resolve.modulesDirectories
    通過配置好 resolve.modules 有助於提高定位引用檔案的速度

    const path = require(`path`);
    module.exports = function(env) {
      return {
        // ...
        resolve: {
          modules: [
            path.resolve(__dirname, `node_modules`),
          ],
        }
       };
    };複製程式碼

下圖為以前一次把個迷你專案從webpack1升級到webpack2的基本配置變更

Webpack2初識

瞭解TreeShaking

  • Tree shaking 示例
    修改fn.js

    // ...
    export function unused(){
      document.write(`I am an unused Function`);
    }複製程式碼

    修改entry.js

    import fn, {unused} from `./fn`;
    fn();
    // ...複製程式碼

    執行 webpack --env(這裡不使用env.dev來檢視較為美觀的打包程式碼)後,我們會發現生成的bundle程式碼中出現如下片段

    Webpack2初識

    此時再執行 webpack --optimize-minimize --env ,打包的程式碼經過壓縮處理後,unused()這個沒有被呼叫的函式被徹底刪掉了。
    簡單而然,webpack2 中的 tree shaking就是一個按需打包模組一種實現。但需要tree shaking生效,被引用的模組必須遵循es6模組匯出的規範,詳看後文。

  • Tree shaking 起源
    在前端圈子中,該解釋 “起源”於 rollup.js 作者。
    Rollup中的 Tree-shaking 是無用程式碼移除(DCE, dead code elimination)的一種實現。利用AST(Abstract Syntax Tree,譯作 抽象語法樹),找到被使用的程式碼塊然後注入到輸出檔案中。而Webpack2“原生”支援的ES6模組靜態性引用,可以幫助標記編譯後的多餘程式碼,並在壓縮時”甩掉”它們。

    Webpack2初識

    更多關於抽象語法樹的理解可以通過線上抽象語法樹解釋工具去自行體驗

  • ES6模組的靜態性的作用:

    1. DCE
    2. 快速定位程式碼

      • 使用Commonjs去引用庫的時候,呼叫其子屬性會觸發物件屬性查表,這會降低編譯的效率
        const lib = require(`lib`); lib.someFunc(); // 屬性查表

      • 而import的靜態特性使其跳過這個步驟;
        import * as lib from `lib`; lib.someFunc(); // 靜態處理

    3. 提高變數校驗效率

    4. 為macro(巨集)做準備,現有macro庫 sweetjs.org/
  • Tree shaking 橫向對比
    babel-plugin-import 是一個類tree shaking的babel外掛,它能夠把 import { Button } from ‘antd’; 這種程式碼轉換為 import Button ‘antd/lib/button’; 的形式。

    • 優點在於
      o 不硬性要求引用ES6模組
      o 構建速度較快
    • 缺點在於
      x 需指定按需打包的模組
      x 自己建立的模組使用不便
      x 無法把同一個檔案裡的多個export分離
      x 配置錯誤將會導致依賴丟失
      Webpack2初識

    如今的webpack2的tree shaking 主要適用範圍如下
    import {lastName} form `name`; — named import
    import name from `name`; — default import
    import * as all from `name;
    const name = require(`name`);

    如今並不是所有開源元件都提供ES6模組的程式碼,所以如果想最有效的去除多餘程式碼,把 babel-plugin-import 和 webpack2 Tree shaking組合使用來打包壓縮生產環境的程式碼為當下最優選擇。

  • Webpack2 和 Babel 組合的短暫演變
    在使用webpack的同時,我們大多都需要使用到babel來支援轉換ES6、ES7、TS等一系列語法糖,那一旦使用babel,務必要防止babel對import語法先行處理。為了達到這個目的,babel的配置也經歷過比較明顯的轉變最終簡單易懂:

Webpack2初識

1-2速度以及打包大小對比

該速度對比資料為同樣程式碼每項執行5次後取最大時長的資料,程式碼如下:

import react from `react`; // react15.5.4 不提供ES6模組
import {compose} from `refux`; // redux3.6.0 提供ES6模組複製程式碼

速度

webpack1 webpack2
dev-server 啟動服務 2187ms 2252ms
dev-server 檔案更改 41ms 27ms
production 1991ms 2057ms

打包大小

webpack1 webpack2
dev-server 1.38MB 1.75MB
production 53.3kB 48.5kB

由此可見,在開發過程中,webpack2打包速度較快但需載入的程式碼檔案大小較大;生產環境程式碼webpack2生成的檔案體積更小(但這要求開發者遵循使用ES6模組的寫法)

Webpack2優缺點彙總

個人最後總結的優缺點如下

  • 優點
    o change檔案打包效率提高
    o 升級快捷
    o 加強code splitting時chunk檔案的錯誤定位(這個真心棒)
    o 官方文件更加清晰
    o 和babel6完美契合
  • 缺點
    x 自帶的tree shaking對非ES6模組和物件動態屬性無效
    x 動態import實現方式不標準,還需要 bable-plugin-syntax-dynamic-import
    x Chrome下devtool為cheap-module-source-map等時原始碼定
位有數行誤差
    x 後文中的Issue

    隨著webpack2版本迭代,以上缺點正慢慢改正

結語

我想不到任何一個不升級的理由,畢竟這是正式版了。:) 如文中有與實際不對等的情況,務必告訴我好讓我能撇除誤解~~~

Issue

webpack2處理原生ES6 Module時,無法使用babel-plugin-add-module-exports外掛。該外掛能讓babel6處理export時,如同babel5一樣,生成語句 module.exports = exports["default"];webpack2中export default轉換後為 __webpack_exports__["default"] 而非 exports["default"],故執行程式碼時會報錯。

參考

What`s new in webpack 2