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