Web Bundler CheatSheet, 選擇合適的構建打包工具

王下邀月熊發表於2018-05-24

題注:Web Bundler CheatSheet 屬於 Awesome-CheatSheet 系列,盤點數個常用的開發打包工具清單。歡迎加入阿里南京前端團隊,歡迎關注阿里南京技術專刊瞭解更多訊息。

Web Bundler CheatSheet | Web 構建與打包工具盤點

工欲善其事,必先利其器,當我們準備開始某個 Web 相關的專案時,合適的腳手架會讓我們事半功倍。在 2016-我的前端之路:工具化與工程化一文中,我們討論了工具化與工程化相關的內容,其中重要的章節就是關於所謂的打包工具。Grunt、Glup 屬於 Task Runner,即任務執行器; 實際上,npm package.json 中定義的指令碼也可以看做 Task Runner,而 Rollup,Parcel 以及 Webpack 則是屬於 Bundler,即打包工具。

webpack

尺有所短,寸有所長,不同的構建工具有其不同的適用場景。Webpack 是非常優秀的構建與打包工具,但是其提供了基礎且複雜的功能支援,使得並不適用於全部的場景。Parcel 這樣的零配置打包工具適合於應用型的原型專案構建,而 Rollup 或者 Microbundle 適合於庫的打包,Backpack 則能夠幫我們快速構建 Node.js 專案。筆者在本文中列舉討論的僅是日常工作中會使用的工具,更多的 BrowserifyFusebox 等等構建工具檢視 Web 構建與打包工具資料索引或者現代 Web 開發實戰/進階篇

Parcel

Parcel 是著名的零配置的應用打包工具,在 TensorflowJS 或者 gh-craft 等演算法實驗/遊戲場景構建中,都能夠快速地搭建應用。

# 安裝 Parcel
$ npm install -g parcel-bundler

# 啟動開發伺服器
$ parcel index.html

# 執行線上編譯
$ parcel build index.js

# 指定編譯路徑
$ parcel build index.js -d build/output
複製程式碼

Parcel 會為我們自動地下載安裝依賴,並且內建了 ES、SCSS 等常見的處理器。在 fe-boilerplate 中提供了 React, React & TypeScript, Vue.js 等 Parcel 常見的示例,這裡以 React 為例,首先定義元件與渲染:

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import logo from '../public/logo.svg';
import './index.css';

const App = () => (
  <div className="App">
    <img className="App-Logo" src={logo} alt="React Logo" />
    <h1 className="App-Title">Hello Parcel x React</h1>
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));

// Hot Module Replacement
if (module.hot) {
  module.hot.accept();
}
複製程式碼

然後定義入口的 index.html 檔案:

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Parcel React Example</title>

</head>

<body>
  <div id="root"></div>
  <script src="./index.js"></script>
</body>

</html>
複製程式碼

然後使用 parcel index.html 執行開發伺服器即可。Parcel 中同樣也是支援非同步載入的,假設我們將部分程式碼定義在 someModule.js 檔案中,然後在使用者真實需要時再進行載入:

// someModule.js
console.log('someModule.js loaded');
module.exports = {
  render: function(element) {
    element.innerHTML = 'You clicked a button';
  }
};
複製程式碼

在入口檔案中使用 import 進行非同步載入:

console.log('index.js loaded');
window.onload = function() {
  document.querySelector('#bt').addEventListener('click', function(evt) {
    console.log('Button Clicked');
    import('./someModule').then(function(page) {
      page.render(document.querySelector('.holder'));
    });
  });
};
複製程式碼

最後值得一提的是,Parcel 內建支援 WebAssembly 與 Rust,通過簡單的 import 匯入,即可以使用 WASM 模組:

// synchronous import
import {add} from './add.wasm';
console.log(add(2, 3));

// asynchronous import
const {add} = await import('./add.wasm');
console.log(add(2, 3));

// synchronous import
import {add} from './add.rs';
console.log(add(2, 3));

// asynchronous import
const {add} = await import('./add.rs');
console.log(add(2, 3));
複製程式碼

這裡 add.rs 是使用 Rust 編寫的簡單加法計算函式:

#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
  return a + b
}
複製程式碼

Rollup + Microbundle

Rollup 是較為為純粹的模組打包工具,其相較於 Parcel 與 Webpack 等,更適合於構建 Library,譬如 React、Vue.js、Angular、D3、Moment、Redux 等一系列優秀的庫都是採用 Rollup 進行構建。。Rollup 能夠將按照 ESM(ES2015 Module)規範編寫的原始碼構建輸出為 IIFE、AMD、CommonJS、UMD、ESM 等多種格式,並且其較早地支援 Tree Shaking,Scope Hoisting 等優化特性,保證模組的簡潔與高效。這裡我們使用的 Rollup 示例配置專案存放在了 fe-boilerplate/rollup。最簡單的 rollup.config.js 檔案配置如下:

export default {
  // 指定模組入口
  entry: 'src/scripts/main.js',
  // 指定包體檔名
  dest: 'build/js/main.min.js',
  // 指定檔案格式
  format: 'iife',
  // 指定 SourceMap 格式
  sourceMap: 'inline'
};
複製程式碼

如果我們只是對簡單的 sayHello 函式進行打包,那麼輸出的檔案中也只是會簡單地連線與呼叫,並且清除未真實使用的模組:

(function() {
  'use strict';
  ...
  function sayHelloTo(name) {
    ...
  }
  ...
  const result1 = sayHelloTo('Jason');
  ...
})();
//# sourceMappingURL=data:application/json;charset=utf-8;base64,...
複製程式碼

Rollup 同樣具有豐富的外掛系統,在 fe-boilerplate/rollup 中我們也引入了常見的別名、ESLint、環境變數定義、包體壓縮與分析等外掛。這裡我們以最常用的 Babel 與 TypeScript 為例,如果我們需要在專案中引入 Babel,則同樣在根目錄配置 .babelrc 檔案,然後引入 rollup-plugin-babel 外掛即可:

import { rollup } from 'rollup';
import babel from 'rollup-plugin-babel';

rollup({
  entry: 'main.js',
  plugins: [
    babel({
      exclude: 'node_modules/**'
    })
  ]
}).then(...)
複製程式碼

對於 TypeScript 則是引入 rollup-plugin-typescript 外掛:

import typescript from 'rollup-plugin-typescript';

export default {
  entry: './main.ts',

  plugins: [typescript()]
};
複製程式碼

Microbundle 則是 Developit 基於 Rollup 封裝的零配置的輕量級打包工具,其目前已經內建支援 TypeScript 與 Flow,不需要額外的配置;筆者在 js-swissgear/x-fetch 專案的打包中也使用了該工具。

{
  "scripts": {
    "build": "microbundle",
    "dev": "microbundle watch"
  }
}
複製程式碼
  • index.js 是 CommonJS 模組,是 Node.js 內建的模組型別,使用類似於 require('MyModule') 語法匯入
  • index.m.js 是 ECMAScript 模組,使用類似於 import MyModule from 'my-module' 語法匯入
  • index.umd.js 是 UMD 模組
  • index.d.ts 是 TypeScript 的型別宣告檔案

Webpack

作為著名的打包工具,Webpack 允許我們指定專案的入口地址,然後自動將用到的資源,經由 Loader 與 Plugin 的轉換,打包到包體檔案中。Webpack 相關的專案模板可以參考:fe-boilerplate/react-webpack, fe-boilerplate/react-webpack-ts, fe-boilerplate/vue-webpack 等。

538c4af0d21e375d6d252d38cbb8a993

Webpack 目前也支援零配置執行

$ npm install webpack webpack-cli webpack-dev-server --save-dev
複製程式碼
"scripts": {
  "start": "webpack-dev-server --mode development",
  "build": "webpack --mode production"
},
複製程式碼

基礎配置

const config = {
  // 定義入口
  entry: {
    app: path.join(__dirname, 'app')
  },
  // 定義包體檔案
  output: {
    // 輸出目錄
    path: path.join(__dirname, 'build'),

    // 輸出檔名
    filename: '[name].js'
    // 使用 hash 作為檔名
    // filename: "[name].[chunkhash].js",
  },
  // 定義如何處理
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  // 新增額外外掛操作
  plugins: [new webpack.DefinePlugin()]
};
複製程式碼

Webpack 同樣支援新增多個配置:

module.exports = [{
  entry: './app.js',
  output: ...,
  ...
}, {
  entry: './app.js',
  output: ...,
  ...
}]
複製程式碼

我們程式碼中的 require 與 import 解析規範,則由 resolve 模組負責,其包含了擴充套件、別名、模組等部分:

const config = {
  resolve: {
    alias: {
      /*...*/
    },
    extensions: [
      /*...*/
    ],
    modules: [
      /*...*/
    ]
  }
};
複製程式碼

資源載入

const config = {
  module: {
    rules: [
      {
        // **Conditions**
        test: /\.js$/, // Match files
        enforce: 'pre', // "post" too

        // **Restrictions**
        include: path.join(__dirname, 'app'),
        exclude: path => path.match(/node_modules/),

        // **Actions**
        use: 'babel-loader'
      }
    ]
  }
};
複製程式碼
// Process foo.png through url-loader and other matches
import 'url-loader!./foo.png';

// Override possible higher level match completely
import '!!url-loader!./bar.png';
複製程式碼

babel-loader 或者 awesome-typescript-loader 來處理 JavaScript 或者 TypeScript 檔案

/******/ (function(modules) { // webpackBootstrap
...
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = ((text = "Hello world") => {
  const element = document.createElement("div");

  element.innerHTML = text;

  return element;
});

/***/ })
/******/ ]);
複製程式碼

use: ["style-loader", "css-loader"] css-loader 會自動地解析 @import 與 url(),而 style-loader 則會將 CSS 注入到 DOM 中,並且實現 HMR 的特性,而對於 SASS、LESS 等 CSS 前處理器,也有專門的 sass-loader 或者 less-loader 來處理;在生產環境下,我們也常常會將 CSS 抽取到獨立的樣式檔案中,此時就可以使用 mini-css-extract-plugin (MCEP) 等工具。同樣,我們可以使用 url-loader/file-loader 來處理圖片等資原始檔,

程式碼分割

程式碼分割是提升 Web 效能表現的重要分割,我們常做的程式碼分割也分為公共程式碼提取與按需載入等方式。公共程式碼提取即是將第三方渲染模組或者庫與應用本身的邏輯程式碼分割,或者將應用中多個模組間的公共程式碼提取出來,劃分到獨立的 Chunk 中,以方便客戶端進行快取等操作。

cc11f7e53c579fff28de1b3ed5b9f53a

不同於 Webpack 3 中需要依賴 CommonChunksPlugin 進行配置,Webpack 4 引入了 SplitChunksPlugin,併為我們提供了開箱即用的程式碼優化特性,Webpack 會根據以下情況自動進行程式碼分割操作:

  • 新的塊是在多個模組間共享,或者來自於 node_modules 目錄;
  • 新的塊在壓縮之前的大小應該超過 30KB;
  • 頁面所需併發載入的塊數量應該小於或者等於 5;
  • 初始頁面載入的塊數量應該小於或者等於 3;

SplitChunksPlugin 的預設配置如下:

splitChunks: {
    chunks: "async",
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
    default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
複製程式碼

值得一提的是,這裡的 chunks 選項有 initial, asyncall 三個配置,上述配置即是分別針對初始 chunks、按需載入的 chunks 與全部的 chunks 進行優化;如果將 vendors 的 chunks 設定為 initial,那麼它將忽略通過動態匯入的模組包包含的第三方庫程式碼。而 priority 則用於指定某個自定義的 Cache Group 捕獲程式碼的優先順序,其預設值為 0。在 common-chunk-and-vendor-chunk 例子中,我們即針對入口進行優化,提取出入口公共的 vendor 模組與業務模組:

{
splitChunks: {
			cacheGroups: {
				commons: {
					chunks: "initial",
					minChunks: 2,
					maxInitialRequests: 5, // The default limit is too small to showcase the effect
					minSize: 0 // This is example is too small to create commons chunks
				},
				vendor: {
					test: /node_modules/,
					chunks: "initial",
					name: "vendor",
					priority: 10,
					enforce: true
				}
			}
		}
}
複製程式碼

Webpack 的 optimization 還包含了 runtimeChunk 屬性,當該屬性值被設定為 true 時,即會為每個 Entry 新增僅包含執行時資訊的 Chunk; 當該屬性值被設定為 single 時,即為所有的 Entry 建立公用的包含執行時的 Chunk。我們也可以在程式碼中使用 import 語句,動態地進行塊劃分,實現程式碼的按需載入:

c4e91fafb1a08e7733ac2688222eb65a

// Webpack 3 之後支援顯式指定 Chunk 名
import(/* webpackChunkName: "optional-name" */ './module')
  .then(module => {
    /* ... */
  })
  .catch(error => {
    /* ... */
  });
複製程式碼
webpackJsonp([0], {
  KMic: function(a, b, c) {
    ...
  },
  co9Y: function(a, b, c) {
    ...
  },
});
複製程式碼

如果是使用 React 進行專案開發,推薦使用 react-loadable 進行元件的按需載入,他能夠優雅地處理元件載入、服務端渲染等場景。Webpack 還內建支援基於 ES6 Module 規範的 Tree Shaking 優化,即僅從匯入檔案中提取出所需要的程式碼。

更多關於 Webpack 的使用技巧可以參閱 Webpack CheatSheet 或者現代 Web 開發基礎與工程實踐/Webpack 章節。

Backpack

Backpack 是面向 Node.js 的極簡構建系統,受 create-react-app, Next.js 以及 Nodemon 的影響,能夠以零配置的方式建立 Node.js 專案。Backpack 為我們處理了檔案監控、熱載入、轉換、打包等工作,預設支援 ECMAScript 最新的 async/await, 物件擴充套件、類屬性等語法。我們可以使用 npm 安裝依賴:

$ npm i backpack-core --save
複製程式碼

然後在 package.json 中配置執行指令碼:

{
  "scripts": {
    "dev": "backpack",
    "build": "backpack build"
  }
}
複製程式碼

Backend-Boilerplate/node 中可以檢視 Backpack 的典型應用,我們也可以覆蓋預設的 Webpack 配置:

// backpack.config.js
module.exports = {
  webpack: (config, options, webpack) => {
    // Perform customizations to config
    // Important: return the modified config
    return config;
  }
};
複製程式碼

或者新增 Babel 外掛:

{
  "presets": ["backpack-core/babel", "stage-0"]
}
複製程式碼

相關文章