Web Bundler CheatSheet, 選擇合適的構建打包工具
Web Bundler CheatSheet | Web 構建與打包工具盤點
工欲善其事,必先利其器,當我們準備開始某個 Web 相關的項目時,合適的腳手架會讓我們事半功倍。在 2016-我的前端之路:工具化與工程化一文中,我們討論了工具化與工程化相關的內容,其中重要的章節就是關于所謂的打包工具。Grunt、Glup 屬于 Task Runner,即任務執行器; 實際上,npm package.json 中定義的腳本也可以看做 Task Runner,而 Rollup,Parcel 以及 Webpack 則是屬于 Bundler,即打包工具。
尺有所短,寸有所長,不同的構建工具有其不同的適用場景。Webpack 是非常優秀的構建與打包工具,但是其提供了基礎且復雜的功能支持,使得并不適用于全部的場景。Parcel 這樣的零配置打包工具適合于應用型的原型項目構建,而 Rollup 或者 Microbundle 適合于庫的打包,Backpack 則能夠幫我們快速構建 Node.js 項目。筆者在本文中列舉討論的僅是日常工作中會使用的工具,更多的 Browserify、Fusebox 等等構建工具查看 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 是較為為純粹的模塊打包工具,其相較于 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 等。
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 中,以方便客戶端進行緩存等操作。
不同于 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, async 與 all 三個配置,上述配置即是分別針對初始 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 語句,動態地進行塊劃分,實現代碼的按需加載:
- // 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"]
- }
【本文是51CTO專欄作者“張梓雄 ”的原創文章,如需轉載請通過51CTO與作者聯系】