webpack4:連奏中的進化


webpack4在2月底的時候發布,這次webpack4有了一個名字"Legato",也就是"連奏"的意思,寓意webpack在不斷進化,而且是無縫(no-gaps)的進化。webpack的進化點是通過捐贈者和用戶投票來決定的,之前在介紹webpack3的時候,曾給出過投票數在前幾名的優化點,集中在用戶體驗、構建性能(速度和產出大小)、通用和適配性(es module、typescript、web assemble)等。webpack4發布了,下面將結合文檔和實踐,驗證一下webpack是否兌現了當初的諾言。

webpack4中的新特性

webpack3官方發布的時候,提到了下個版本可能的改動點,翻譯過來如下所示:

  • 高性能的構建緩存
  • 提升初始化速度和增量構建效率
  • 更好的支持Type Script
  • 修訂長期緩存
  • 支持WASM 模塊支持
  • 提升用戶體驗

webpack4官方發布的文檔之中主要提及了以下新特性:

支持零配置(Zero Configuration)

該特性主要用於解決webpack的門檻高問題,webpack是一個配置聲明式的操作模式,npm、gulp是指令式的,需要描述每一步是干什么的,而webpack的配置項凌亂且無序,讓很多開發者頭疼。
webpack4提供了零配置方案,默認入口屬性為./src,默認輸出路徑為./dist,不再需要配置文件,實現了開箱即用的封裝能力,更通俗的講,webpack會自動查找項目中src目錄下的index.js文件,然后選擇的模式進行相應的打包操作,最后新建dist目錄並生成一個main.js文件。此外針對開發環境和線上環境提供了兩種打包模式:"production""development""production"模式內置了項目產出時的基本配置項,"development"模式基本滿足了快速構建和開發體驗。使用的方法是在運行webapck命令的時候,設置好mode參數的值即可,默認是production屬性。

"scripts": {
    "dev": "webpack --mode development",
    "build": "webpack --mode production" 
}

具體的案例可以前往github進行下載

下面根據官方的文檔介紹一下兩種模式。

  • Production模式

提供了發布程序時的優化配置項,旨在更小的產出文件、更快的運行速度、不暴露源碼和路徑。會默認采用代碼壓縮(minification),作用域提升(scope hoisting),tree-shaking,NoEmitOnErrorsPlugin,無副作用模塊修剪(side-effect-free module pruning)等。

  • Development模式

旨在提升開發調試過程中的體驗,如更快的構建速度、調試時的代碼易讀性、暴露運行時的錯誤信息等。會默認采用bundle的輸出包含路徑名和eval-source-map等,提升代碼的可讀性和構建速度。

兩種模式就是兩個箱子,箱子里面就是各種插件工具,只是有些是開啟的,有些是關閉的,具體有哪些工具可以參考這篇文章

廢棄CommonsChunkPlugin

webpack4廢棄了CommonsChunkPlugin,引入了optimization.splitChunksoptimization.runtimeChunk,旨在優化chunk的拆分。先介紹一下code splitting下的CommonsChunkPlugin有什么缺點,然后仔介紹一下chunk split是怎么進行優化的。

  • CommonsChunkPlugin的問題

CommmonsChunkPlugin的思路是Create this chunk and move all modules matching minChunks into the new chunk,即將滿足minChunks配置想所設置的條件的模塊移到一個新的chunk文件中去,這個思路是基於父子關系的,也就是這個新產出的new chunk是所有chunk的父親,在加載孩子chunk的時候,父親chunk是必須要提前加載的。舉個例子:

example:
entryA:  vue  vuex  someComponents
entryB:  vue axios someComponents
entryC: vue vux axios someComponents
minchunks: 2 

產出后的chunk:

vendor-chunk:vue vuex axios
chunkA~chunkC: only the components

帶來的問題是:對entryA和entryB來說,vendor-chunk都包含了多余的module。
CommonsChunkPlugin另外一個問題是:對異步的模塊不友好。下面也舉例說明:

example:
entryA:  vue  vuex  someComponents
asyncB:vue axios someComponents
entryC: vue vux axios someComponents
minchunks: 2 

產出后的chunk:

vendor-chunk:vue vuex 
chunkA: only the components
chunkB: vue axios someComponents
chunkC: axios someComponents

帶來的問題是:如果asyncB在entryA中動態引入,則會引入多余的module。
總的來說CommonsChunkPlugin有以下三個問題:

  1. 產出的chunk在引入的時候,會包含重復的代碼;
  2. 無法優化異步chunk;
  3. 高優的chunk產出需要的minchunks配置比較復雜。
  • SplitChunksPlugin

與CommonsChunkPlugin的父子關系思路不同的是,SplitChunksPlugin引入了chunkGroup的概念,在入口chunk和異步chunk中發現被重復使用的模塊,將重疊的模塊以vendor-chunk的形式分離出來,也就是vendor-chunk可能有多個,不再受限於是所有chunk中都共同存在的模塊,原理的示意如下圖所示:

其中,可以發現SplitChunksPlugin產出的vendor-chunk有多個,對於入口A來說,引入的代碼只有chunkA、vendor-chunkA-B、vendor-chunkA-C、vendor-chunkA-B-C;這時候chunkA、vendor-chunkA-B、vendor-chunkA-C、vendor-chunkA-B-C形成了一個chunkGroup。下面舉個列子:

example:
entryA:  vue  vuex  someComponents
entryB:vue axios someComponents
entryC: vue vux axios someComponents

產出后的chunk:

vendor-chunkA-C:vuex 
vendor-chunkB-C:axios
vendor-chunkA-B-C:vue
chunkA: only the components
chunkB: only the components
chunkC: only the components

SplitChunksPlugin能夠解決掉CommonsChunkPlugin中提到的三個問題,SplitChunksPlugin在production模式下是默認開啟的,但是它默認只作用於異步chunk,如果要作用於入口chunk的話,需要配置optimization.splitChunks.chunks: "all",同時webpack自動split chunks是有幾個限制條件的:

  1. 新產出的vendor-chunk是要被共享的,或者模塊來自npm包;
  2. 新產出的vendor-chunk的大小得大於30kb;
  3. 並行請求vendor-chunk的數量不能超出5個;
  4. 對於entry-chunk而言,並行加載的vendor-chunk不能超出3個。

這些限制可以在SplitChunks的默認配置項中可以一一對應的看到。

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

其實不難理解這些限制,因為SplitChunksPlugin產生的結果就是原來chunk被拆分了,引入的文件數量會變多,因此需要在文件數量上進行限制。

  • runtimeChunkPlugin

在使用CommonsChunkPlugin的時候,我們通常會把webpack runtime的基礎函數提取出來,單獨作為一個chunk,畢竟code splitting想把不變的代碼單獨抽離出來,方便瀏覽器緩存,提升加載速度。webpack4廢棄了CommonsChunkPlugin,采用了runtimeChunkPlugin可以將每個entry chunk中的runtime部分的函數分離出來,只需要一個簡單的配置:optimization.runtimeChunk: true

sideEffects

在webapck2開始支持ESModule后,webpack提出了tree-shaking進行無用模塊的消除,主要依賴ES Module的靜態結構。在webapck4之前,主要通過在.babelrc文件中設置"modules": false來開啟無用的模塊檢測,該方法顯然比較粗暴。webapck4靈活擴展了如何對某模塊開展無用代碼檢測,主要通過在package.json文件中設置sideEffects: false來告訴編譯器該項目或模塊是pure的,可以進行無用模塊刪除。
官方的github上提供了一個sideEffects的demo示例供參考,如果對tree-shaking的概念不是太了解,可去官方的文檔中tree-shaking部分詳細了解。下面是官方提供的一個減包效果示例,僅供欣賞。

支持壓縮ES6+代碼

在webapck4之前,webpack.prod.conf.js中關於UglifyJsPlugin的注釋會有這么一段話:

// UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify

意思就是UglifyJs無法對ES6+的代碼進行壓縮,需使用babel-minify獲取更好的treeshaking效果。webapck4目前已經支持壓縮ES6+的代碼。

// 源代碼
import {sayHello} from './libs/js/util.js'
let output = sayHello('hwm');
console.log(output);

// 產出
...let r=(e=>`Hello ${e}!`)("hwm");console.log(r)}...

支持更多的模塊類型

webpack4支持以importexport形式加載和導出本地的WebAssembly模塊,這一塊本人實際項目並未使用到,暫不做介紹;此外,webpack4支持json模塊和tree-shaking,之前json文件的加載需要json-loader的支持,webpack4已經能夠支持json模塊(JSON Module),不需要額外的配置;此外,當json文件用ESModule的語法import引入的時候,webpack4還能支持對json模塊進行tree-shaking處理,把用不到的字段過濾掉,起到減包的作用。下面是個示例:

// 引用數據的三種方法
let jsonData = require('./data/test.json');

import jsonData from './data/test.json'

// 打包時只會提取test.json文件中onePart部分。
import { onePart } from './data/test.json' 

如何遷移升級到webpack4

0配置的局限性

webpack4聲稱能夠0配置,但是0配置有很多局限性,比如只能是單入口的項目,入口和產出的文件名是固定的,entry是src目錄下的index.js,產出是dist目錄下的main.js,很明顯不能滿足實際項目應用。於是,開發者還是得自己配置webpack.config.js文件。

webpack4配置文件的變化點

如何配置webpack4下的配置文件,需要大致了解webapck4的配置項的改動點。
mode:開發模式 development

  • 開啟dev-tool,方便瀏覽器調試
  • 提供詳細的錯誤提示
  • 利用緩存機制,實現快速構建
  • 開啟output.pathinfo,在產出的bundle中顯示模塊路徑信息
  • 開啟NamedModulesPlugin
  • 開啟NoEmitOnErrorsPlugin

mode:生產模式 production

  • 啟動各種優化插件(ModuleConcatenationPlugin、optimization.minimize、ModuleConcatenationPlugin、Tree-shaking),壓縮、合並、拆分,產出更小體積的chunk
  • 關閉內存緩存
  • 開啟NoEmitOnErrorsPlugin

plugin

  • 內置optimization.minimize來壓縮代碼,不用再顯示引入UglifyJsPlugin;
  • 廢棄CommonsChunkPlugin插件,使用optimization.splitChunks和optimization.runtimeChunk來代替;
  • 使用optimization.noEmitOnErrors來替換NoEmitOnErrorsPlugin插件
  • 使用optimization.namedModules來替換NamedModulesPlugin插件

loader

  • 廢棄json-loader,友好支持json模塊,以ESMoudle的語法引入,還可以對json模塊進行tree-shaking處理;

其他的改動信息建議查看webpack的中文升級日志

vue-cli項目如何改造

介紹完了webpack4中核心配置項的變化,接下來結合vue-cli示例項目介紹一下,如何配置webpack.conf.js文件。
1.升級npm包版本

建議升級webpack4,同時升級涉及到的loaders和plugins。webpack4 中 cli 工具分離成了 webpack 核心庫 與 webpack-cli 命令行工具兩個模塊,需要使用 CLI ,必安裝 webpack-cli 至項目中。

npm i webpack webpack-cli webpack-dev-server -D

涉及到的插件有:

"vue-loader": "^15.0.10",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"copy-webpack-plugin": "^4.0.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"html-webpack-plugin": "^3.1.0",
"optimize-css-assets-webpack-plugin": "^4.0.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-middleware": "^3.1.2",
"webpack-dev-server": "^3.1.3",
"webpack-merge": "^4.1.0"

2.修改webpack.base.conf.js

webpack4推薦使用了最新版本的vue-loader("vue-loader": "^15.0.10"),但是最新的vue-loader需要在webapck config文件中設置VueLoaderPlugin插件,否則會報以下錯誤:

vue-loader was used without the corresponding plugin. Make sure to include VueLoaderPlugin in your webpack config.

webpack.base.conf.js文件中的改動主要是添加VueLoaderPlugin插件。

const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
    ...
    plugins: [
        // 添加VueLoaderPlugin,以響應vue-loader
        new VueLoaderPlugin()
  ],
    ...
}

3.修改webpack.dev.conf.js

添加mode屬性,並設置為development模式;然后注釋掉 webpack4開發模式已經內置的插件,如webpack.NamedModulesPluginwebpack.NoEmitOnErrorsPlugin 插件。

4.修改webpack.prod.conf.js

添加mode屬性,並設置為production模式;然后注釋掉 webpack4生產模式已經內置的插件,如CommonsChunkPluginuglifyjs-webpack-pluginModuleConcatenationPlugin插件;最后根據webpack4提供的文檔配置optimization,使用splitChunksruntimeChunk完成chunk的提取和優化。

const webpackConfig = merge(baseWebpackConfig, {
...
    optimization: {
    // 采用splitChunks提取出entry chunk的chunk Group
    splitChunks: {
      cacheGroups: {
        // 處理入口chunk
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          chunks: 'initial',
          name: 'vendors',
        },
        // 處理異步chunk
        'async-vendors': {
          test: /[\\/]node_modules[\\/]/,
          minChunks: 2,
          chunks: 'async',
          name: 'async-vendors'
        }
      }
    },
    // 為每個入口提取出webpack runtime模塊
    runtimeChunk: { name: 'manifest' }
  }
...
})

經過以上操作,我們已經基本完成了vue-cli項目的改造。運行項目的時候,注意看控制台的報錯,是哪個插件報的錯就去升級那個插件,如果存在找不到模塊之類的錯誤就去升級對應的loader。vue-cli項目webapck4下配置demo已經上傳到github,歡迎下載

實例說話—webpack4的性能如何

webapck4旨在開發模式下提升構建速度、提升用戶體驗,在生產模式下減小產出包的大小,提升加載和運行速度,下面是webapck3和webapck4下vue-cli的webapck模板項目的實際效果截圖:
開發者模式下:


由上圖可以知道:webapck4下的構建速度是3703ms,明顯由於webapck3下的5617ms;

生產模式下:


由上圖可以知道:webapck4下的app-chunk的大小是933byte,明顯小於webapck3下的11.6K;webapck4下vendor-chunk的大小是109K,小於webapck3下的112K。
兩種模式下,webapck4性能上的確是精進不少,雖然有各種坑,還是建議把坑填了,升級到webpack4。

webpack的未來

想了解webpack的未來,建議先過一下webpack的歷史。
webpack1支持CMD和AMD,同時擁有豐富的plugin和loader,webpack逐漸得到廣泛應用。
webpack2相對於webpack最大的改進就是支持ES Module,可以直接分析ES Module之間的依賴關系,而webpack1必須將ES Module轉換成CommonJS模塊之后,才能使用webpack進行下一步處理。除此之外webpack2支持tree sharking,與ES Module的設計思路高度契合。
webpack3相對於webpack2,過渡相對平穩,但是新的特性大都圍繞ES Module提出,如Scope Hoisting和Magic Comment。
webpack4集中發力在用戶體驗、構建性能(速度和產出大小)、通用和適配性(es module、typescript、web assemble)。
展望未來,webpack的未來肯定持續提升用戶體驗、降低使用門檻,進一步提升構建速度和產出代碼的性能,同時webpack的團隊已經承諾會根據社區的投票來決定新特性開發優先權。以下是公告中給出的未來的重點關注點:

  • 繼續修訂長期緩存
  • webapck任務多線程化,提升初始化速度和增量構建效率
  • 提升CSS到一等公民,引入CSS Module Type ,廢棄ExtractTextWebpackPlugin
  • 提升HTML到一等公民,引入HTML Module Type
  • 繼續擴展0CJS(0配置文件),加入更多擴展
  • 優化支持WASM 模塊,從實驗階段過渡到穩定階段
  • 持續提升用戶體驗

參考文獻

webapck4官方medium pr稿
webpack4中文升級日志
webpack4升級指南以及從webpack3.x遷移
Webpack4 新特性 及 Vue-cli項目升級
Webpack4官方指導教程
webpack4.0打包優化策略整理
webapck3新特性


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM