Web Bundler CheatSheet, 選擇合適的構(gòu)建打包工具
Web Bundler CheatSheet | Web 構(gòu)建與打包工具盤點
工欲善其事,必先利其器,當(dāng)我們準(zhǔn)備開始某個 Web 相關(guān)的項目時,合適的腳手架會讓我們事半功倍。在 2016-我的前端之路:工具化與工程化一文中,我們討論了工具化與工程化相關(guān)的內(nèi)容,其中重要的章節(jié)就是關(guān)于所謂的打包工具。Grunt、Glup 屬于 Task Runner,即任務(wù)執(zhí)行器; 實際上,npm package.json 中定義的腳本也可以看做 Task Runner,而 Rollup,Parcel 以及 Webpack 則是屬于 Bundler,即打包工具。
尺有所短,寸有所長,不同的構(gòu)建工具有其不同的適用場景。Webpack 是非常優(yōu)秀的構(gòu)建與打包工具,但是其提供了基礎(chǔ)且復(fù)雜的功能支持,使得并不適用于全部的場景。Parcel 這樣的零配置打包工具適合于應(yīng)用型的原型項目構(gòu)建,而 Rollup 或者 Microbundle 適合于庫的打包,Backpack 則能夠幫我們快速構(gòu)建 Node.js 項目。筆者在本文中列舉討論的僅是日常工作中會使用的工具,更多的 Browserify、Fusebox 等等構(gòu)建工具查看 Web 構(gòu)建與打包工具資料索引或者現(xiàn)代 Web 開發(fā)實戰(zhàn)/進階篇。
Parcel
Parcel 是著名的零配置的應(yīng)用打包工具,在 TensorflowJS 或者 gh-craft 等算法實驗/游戲場景構(gòu)建中,都能夠快速地搭建應(yīng)用。
- # 安裝 Parcel
 - $ npm install -g parcel-bundler
 - # 啟動開發(fā)服務(wù)器
 - $ parcel index.html
 - # 執(zhí)行線上編譯
 - $ parcel build index.js
 - # 指定編譯路徑
 - $ parcel build index.js -d build/output
 
Parcel 會為我們自動地下載安裝依賴,并且內(nèi)置了 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 運行開發(fā)服務(wù)器即可。Parcel 中同樣也是支持異步加載的,假設(shè)我們將部分代碼定義在 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 內(nèi)建支持 WebAssembly 與 Rust,通過簡單的 import 導(dǎo)入,即可以使用 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 編寫的簡單加法計算函數(shù):
- #[no_mangle]
 - pub fn add(a: i32, b: i32) -> i32 {
 - return a + b
 - }
 
Rollup 是較為為純粹的模塊打包工具,其相較于 Parcel 與 Webpack 等,更適合于構(gòu)建 Library,譬如 React、Vue.js、Angular、D3、Moment、Redux 等一系列優(yōu)秀的庫都是采用 Rollup 進行構(gòu)建。。Rollup 能夠?qū)凑?ESM(ES2015 Module)規(guī)范編寫的源碼構(gòu)建輸出為 IIFE、AMD、CommonJS、UMD、ESM 等多種格式,并且其較早地支持 Tree Shaking,Scope Hoisting 等優(yōu)化特性,保證模塊的簡潔與高效。這里我們使用的 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 函數(shù)進行打包,那么輸出的文件中也只是會簡單地連接與調(diào)用,并且清除未真實使用的模塊:
- (function() {
 - 'use strict';
 - ...
 - function sayHelloTo(name) {
 - ...
 - }
 - ...
 - const result1 = sayHelloTo('Jason');
 - ...
 - })();
 - //# sourceMappingURL=data:application/json;charset=utf-8;base64,...
 
Rollup 同樣具有豐富的插件系統(tǒng),在 fe-boilerplate/rollup 中我們也引入了常見的別名、ESLint、環(huán)境變量定義、包體壓縮與分析等插件。這里我們以最常用的 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 封裝的零配置的輕量級打包工具,其目前已經(jīng)內(nèi)建支持 TypeScript 與 Flow,不需要額外的配置;筆者在 js-swissgear/x-fetch 項目的打包中也使用了該工具。
- {
 - "scripts": {
 - "build": "microbundle",
 - "dev": "microbundle watch"
 - }
 - }
 
- index.js 是 CommonJS 模塊,是 Node.js 內(nèi)置的模塊類型,使用類似于 require('MyModule') 語法導(dǎo)入
 - index.m.js 是 ECMAScript 模塊,使用類似于 import MyModule from 'my-module' 語法導(dǎo)入
 - index.umd.js 是 UMD 模塊
 - index.d.ts 是 TypeScript 的類型聲明文件
 
Webpack
作為著名的打包工具,Webpack 允許我們指定項目的入口地址,然后自動將用到的資源,經(jīng)由 Loader 與 Plugin 的轉(zhuǎn)換,打包到包體文件中。Webpack 相關(guān)的項目模板可以參考: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"
 - },
 
基礎(chǔ)配置
- 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 解析規(guī)范,則由 resolve 模塊負責(zé),其包含了擴展、別名、模塊等部分:
- 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 中,并且實現(xiàn) HMR 的特性,而對于 SASS、LESS 等 CSS 預(yù)處理器,也有專門的 sass-loader 或者 less-loader 來處理;在生產(chǎn)環(huán)境下,我們也常常會將 CSS 抽取到獨立的樣式文件中,此時就可以使用 mini-css-extract-plugin (MCEP) 等工具。同樣,我們可以使用 url-loader/file-loader 來處理圖片等資源文件,
代碼分割
代碼分割是提升 Web 性能表現(xiàn)的重要分割,我們常做的代碼分割也分為公共代碼提取與按需加載等方式。公共代碼提取即是將第三方渲染模塊或者庫與應(yīng)用本身的邏輯代碼分割,或者將應(yīng)用中多個模塊間的公共代碼提取出來,劃分到獨立的 Chunk 中,以方便客戶端進行緩存等操作。
不同于 Webpack 3 中需要依賴 CommonChunksPlugin 進行配置,Webpack 4 引入了 SplitChunksPlugin,并為我們提供了開箱即用的代碼優(yōu)化特性,Webpack 會根據(jù)以下情況自動進行代碼分割操作:
- 新的塊是在多個模塊間共享,或者來自于 node_modules 目錄;
 - 新的塊在壓縮之前的大小應(yīng)該超過 30KB;
 - 頁面所需并發(fā)加載的塊數(shù)量應(yīng)該小于或者等于 5;
 - 初始頁面加載的塊數(shù)量應(yīng)該小于或者等于 3;
 
SplitChunksPlugin 的默認(rèn)配置如下:
- 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 進行優(yōu)化;如果將 vendors 的 chunks 設(shè)置為 initial,那么它將忽略通過動態(tài)導(dǎo)入的模塊包包含的第三方庫代碼。而 priority 則用于指定某個自定義的 Cache Group 捕獲代碼的優(yōu)先級,其默認(rèn)值為 0。在 common-chunk-and-vendor-chunk 例子中,我們即針對入口進行優(yōu)化,提取出入口公共的 vendor 模塊與業(yè)務(wù)模塊:
- {
 - 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 屬性,當(dāng)該屬性值被設(shè)置為 true 時,即會為每個 Entry 添加僅包含運行時信息的 Chunk; 當(dāng)該屬性值被設(shè)置為 single 時,即為所有的 Entry 創(chuàng)建公用的包含運行時的 Chunk。我們也可以在代碼中使用 import 語句,動態(tài)地進行塊劃分,實現(xiàn)代碼的按需加載:
- // 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 進行項目開發(fā),推薦使用 react-loadable 進行組件的按需加載,他能夠優(yōu)雅地處理組件加載、服務(wù)端渲染等場景。Webpack 還內(nèi)建支持基于 ES6 Module 規(guī)范的 Tree Shaking 優(yōu)化,即僅從導(dǎo)入文件中提取出所需要的代碼。
更多關(guān)于 Webpack 的使用技巧可以參閱 Webpack CheatSheet 或者現(xiàn)代 Web 開發(fā)基礎(chǔ)與工程實踐/Webpack 章節(jié)。
Backpack
Backpack 是面向 Node.js 的極簡構(gòu)建系統(tǒng),受 create-react-app, Next.js 以及 Nodemon 的影響,能夠以零配置的方式創(chuàng)建 Node.js 項目。Backpack 為我們處理了文件監(jiān)控、熱加載、轉(zhuǎn)換、打包等工作,默認(rèn)支持 ECMAScript ***的 async/await, 對象擴展、類屬性等語法。我們可以使用 npm 安裝依賴:
- $ npm i backpack-core --save
 
然后在 package.json 中配置運行腳本:
- {
 - "scripts": {
 - "dev": "backpack",
 - "build": "backpack build"
 - }
 - }
 
在 Backend-Boilerplate/node 中可以查看 Backpack 的典型應(yīng)用,我們也可以覆蓋默認(rèn)的 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專欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請通過51CTO與作者聯(lián)系】


















 
 
 










 
 
 
 