• QQ咨詢:4001806960
  • 咨詢熱線:400-180-6960

webpack4 打包優化策略(圖文)

作者:珠峰培訓日期:2019-03-18 17:05:49 點擊:447

1、優化loader配置

1.1 縮小文件匹配範圍(include/exclude)

通過排除node_modules下的文件 從而縮小了loader加載搜索範圍 高概率命中文件

module: { rules: [ { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/, // 排除不處理的目錄 include: path.resolve(__dirname, 'src') // 精確指定要處理的目錄 } ] }

1.2 緩存loader的執行結果(cacheDirectory)

acheDirectory是loader的一個特定的選項,默認值是false。指定的目錄(use: ‘babel-loader?cacheDirectory=cacheLoader’)將用來緩存loader的執行結果,減少webpack構建時Babel重新編譯過程。如果設置一個空值(use: ‘babel-loader?cacheDirectory’) 或true(use: ‘babel-loader?cacheDirectory=true’) 將使用默認的緩存目錄(node_modules/.cache/babel-loader),如果在任何根目錄下都沒有找到 node_modules 目錄,將會降級回退到操作系統默認的臨時文件目錄。

module: { rules: [ { test: /\.js$/, use: 'babel-loader?cacheDirectory', // 緩存loader執行結果 發現打包速度已經明顯提升了 exclude: /node_modules/, include: path.resolve(__dirname, 'src') } ] }

2、resolve優化配置

2.1 優化模塊查找路徑 resolve.modules

Webpack的resolve.modules配置模塊庫(即 node_modules)所在的位置,在 js 裏出現 import ‘vue’ 這樣不是相對、也不是絕對路徑的寫法時,會去 node_modules 目錄下找。但是默認的配置,會采用向上遞歸搜索的方式去尋找,但通常項目目錄裏只有一個 node_modules,且是在項目根目錄,爲了減少搜索範圍,可以直接寫明 node_modules 的全路徑;同樣,對于別名(alias)的配置,亦當如此

const path = require('path'); function resolve(dir) { // 轉換爲絕對路徑 return path.join(__dirname, dir); } resolve: { modules: [ // 優化模塊查找路徑 path.resolve('src'), path.resolve('node_modules') // 指定node_modules所在位置 當你import 第三方模塊時 直接從這個路徑下搜索尋找 ] }
配置好src目錄所在位置後,由于util目錄是在src裏面 所以可以用下面方式引入util中的工具函數
// main.js import dep1 from 'util/dep1'; import add from 'util/add';

2.2 resolve.alias 配置路徑別名

創建 import 或 require 的路徑別名,來確保模塊引入變得更簡單。配置項通過別名來把原導入路徑映射成一個新的導入路徑 此優化方法會影響使用Tree-Shaking去除無效代碼

例如,一些位于 src/ 文件夾下的常用模塊:

alias: { Utilities: path.resolve(__dirname, 'src/utilities/'), Templates: path.resolve(__dirname, 'src/templates/') }

現在,替換「在導入時使用相對路徑」這種方式,就像這樣:

import Utility from '../../utilities/utility';

你可以這樣使用別名:

import Utility from 'Utilities/utility'; resolve: { alias: { // 別名配置 通過別名配置 可以讓我們引用變的簡單 'vue$': 'vue/dist/vue.common.js', // $表示精確匹配 src: resolve('src') // 當你在任何需要導入src下面的文件時可以 import moduleA from 'src/moduleA' src會被替換爲resolve('src') 返回的絕對路徑 而不需要相對路徑形式導入 } }

也可以在給定對象的鍵後的末尾添加 $,以表示精准匹配:

import Utility from 'Utilities/utility'; ralias: { util$: resolve('src/util/add.js') }

這將産生以下結果:

iimport Test1 from 'util'; // 精確匹配,所以 src/util/add.js 被解析和導入 import Test2 from 'util/dep1.js'; // 精確匹配,觸發普通解析 util/dep1.js

2.3 resolve.extensions

當引入模塊時不帶文件後綴 webpack會根據此配置自動解析確定的文件後綴

  • 後綴列表盡可能小
  • 頻率最高的往前放
  • 導出語句盡可能帶上後綴
resolve: { extensions: ['.js', '.vue'] }

3、module.noParse

用了noParse的模塊將不會被loaders解析,所以當我們使用的庫如果太大,並且其中不包含import require、define的調用,我們就可以使用這項配置來提升性能, 讓 Webpack 忽略對部分沒采用模塊化的文件的遞歸解析處理。

// 忽略對jquery lodash的進行遞歸解析 module: { // noParse: /jquery|lodash/ // 從 webpack 3.0.0 開始 noParse: function(content) { return /jquery|lodash/.test(content) } }

4、HappyPack

HappyPack是讓webpack對loader的執行過程,從單一進程形式擴展爲多進程模式,也就是將任務分解給多個子進程去並發的執行,子進程處理完後再把結果發送給主進程。從而加速代碼構建 與 DLL動態鏈接庫結合來使用更佳。

webpack.config.js

const HappyPack = require('happypack'); const os = require('os'); // node 提供的系統操作模塊 // 根據我的系統的內核數量 指定線程池個數 也可以其他數量 const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}) module: { rules: [ { test: /\.js$/, use: 'happypack/loader?id=babel', exclude: /node_modules/, include: path.resolve(__dirname, 'src') } ] }, plugins: [ new HappyPack({ // 基礎參數設置 id: 'babel', // 上面loader?後面指定的id loaders: ['babel-loader?cacheDirectory'], // 實際匹配處理的loader threadPool: happyThreadPool, // cache: true // 已被棄用 verbose: true }); ]

happypack提供的loader,是對文件實際匹配的處理loader。這裏happypack提供的loader與plugin的銜接匹配,則是通過id=happypack來完成。

5、DLL動態鏈接庫

在一個動態鏈接庫中可以包含其他模塊調用的函數和數據,動態鏈接庫只需被編譯一次,在之後的構建過程中被動態鏈接庫包含的模塊將不會被重新編譯,而是直接使用動態鏈接庫中的代碼。

  • 將web應用依賴的基礎模塊抽離出來,打包到單獨的動態鏈接庫中。一個鏈接庫可以包含多個模塊。
  • 將當需要導入的模塊存在于動態鏈接庫,模塊不會再次打包,而是去動態鏈接庫中去獲取。
  • 頁面依賴的所有動態鏈接庫都需要被加載。

5.1 定義DLL配置

依賴的兩個內置插件:DllPlugin 和 DllReferencePlugin

5.1.1 創建一個DLL配置文件webpack_dll.config.js

module.exports = { entry: { react: ['react', 'react-dom'] }, output: { filename: '[name].dll.js', // 動態鏈接庫輸出的文件名稱 path: path.join(__dirname, 'dist'), // 動態鏈接庫輸出路徑 libraryTarget: 'var', // 鏈接庫(react.dll.js)輸出方式 默認'var'形式賦給變量 b library: '_dll_[name]_[hash]' // 全局變量名稱 導出庫將被以var的形式賦給這個全局變量 通過這個變量獲取到裏面模塊 }, plugins: [ new webpack.DllPlugin({ // path 指定manifest文件的輸出路徑 path: path.join(__dirname, 'dist', '[name].manifest.json'), name: '_dll_[name]_[hash]', // 和library 一致,輸出的manifest.json中的name值 }) ] }

5.1.2 output.libraryTarget

規定了以哪一種導出你的庫 默認以全局變量形式 浏覽器支持的形式

具體包括如下:
  • “var” - 以直接變量輸出(默認library方式) var Library = xxx (default)
  • “this” - 通過設置this的屬性輸出 this[“Library”] = xxx
  • “commonjs” - 通過設置exports的屬性輸出 exports[“Library”] = xxx
  • “commonjs2” - 通過設置module.exports的屬性輸出 module.exports = xxx
  • “amd” - 以amd方式輸出
  • “umd” - 結合commonjs2/amd/root

5.1.3 打包生成動態鏈接庫

webpack --config webpack_dll.config.js --mode production

在dist目錄下 多出react.dll.js 和 react.manifest.json

  • react.dll.js 動態鏈接庫 裏面包含了 react和react-dom的內容
  • react.manifest.json 描述鏈接庫(react.dll)中的信息

5.2 在主配置文件中使用動態鏈接庫文件

// webpack.config.js const webpack = require('webpack'); plugins: [ // 當我們需要使用動態鏈接庫時 首先會找到manifest文件 得到name值記錄的全局變量名稱 然後找到動態鏈接庫文件 進行加載 new webpack.DllReferencePlugin({ manifest: require('./dist/react.manifest.json') }) ]

5.3 將動態鏈接庫文件加載到頁面中

需要借助兩個webpack插件

  • html-webpack-plugin 産出html文件
  • html-webpack-include-assets-plugin 將js css資源添加到html中 擴展html插件的功能
npm i html-webpack-plugin html-webpack-include-assets-plugin -D

配置webpack.config.js

const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlIncludeAssetsPlugin = require('html-webpack-include-assets-plugin'); pluings: [ new webpack.DllReferencePlugin({ manifest: require('./dist/react.manifest.json') }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'src/index.html') }), new HtmlIncludeAssetsPlugin({ assets: ['./react.dll.js'], // 添加的資源相對html的路徑 append: false // false 在其他資源的之前添加 true 在其他資源之後添加 }); ]

此時react.dll.js和main.js被自動引入到頁面中,並且dll文件在main.js之前加載

6、ParallelUglifyPlugin

這個插件可以幫助有很多入口點的項目加快構建速度。把對JS文件的串行壓縮變爲開啓多個子進程並行進行uglify。

cnpm i webpack-parallel-uglify-plugin -D // webpck.config.js const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); plugins: [ new ParallelUglifyPlugin({ workerCount: 4, uglifyJS: { output: { beautify: false, // 不需要格式化 comments: false // 保留注釋 }, compress: { // 壓縮 warnings: false, // 刪除無用代碼時不輸出警告 drop_console: true, // 刪除console語句 collapse_vars: true, // 內嵌定義了但是只有用到一次的變量 reduce_vars: true // 提取出出現多次但是沒有定義成變量去引用的靜態值 } } }); ]

執行壓縮

webpack --mode production

7、Tree shaking

剔除JavaScript中用不上的代碼。它依賴靜態的ES6模塊化語法,例如通過impot和export導入導出

commonJS模塊 與 es6模塊的區別

commonJS模塊:
1.動態加載模塊
commonJS 是運行時加載 能夠輕松實現懶加載,優化用戶體驗
2.加載整個模塊
commonJS模塊中,導出的是整個模塊
3.每個模塊皆爲對象
commonJS模塊被視作一個對象
4.值拷貝
commonJS的模塊輸出和函數的值傳遞相似,都是值得拷貝
es6模塊:
1.靜態解析
es6模塊時 編譯時加載 即在解析階段就確定輸出的模塊的依賴關系,所以es6模塊的import一般寫在被引入文件的開頭
2.模塊不是對象
在es6裏,每個模塊並不會當做一個對象看待
3.加載的不是整個模塊
在es6模塊中 一個模塊中有好幾個export導出
4.模塊的引用
es6模塊中,導出的並不是模塊的值得拷貝,而是這個模塊的引用

7.1 保留ES6模塊化語法

// .babelrc { "presets": [ [ "env", { modules: false // 不要編譯ES6模塊 }, "react", "stage-0" ] ] }

7.2 執行生産編譯 默認已開啓Tree Shaking

webpack --mode production

有個funs.js 裏面有兩個函數

// funs.js export const sub = () => 'hello webpack!'; export const mul = () => 'hello shaking!';

main.js 中依賴funs.js

// main.js import {sub} from './funs.js' sub();

在main.js只使用了裏面的 sub函數 默認情況下也會將funs.js裏面其他沒有的函數也打包進來, 如果開啓tree shaking 生産編譯時

webpack --mode production //此時funs.js中沒有被用到的代碼並沒打包進來 而被剔除出去了

區分開發和生産環境

通常我們在開發網頁時需要區分構建環境

  • 開發環境(development) 開發過程中方便開發調試的環境
  • 生産環境(production) 發布到線上使用的運行環境

通過npm命令區分

通過cross-env模塊設置環境變量

cross-env 跨平台地設置及使用環境變量,而不必擔心爲平台正確設置或使用環境變量。

npm i cross-env -D

Usage

npm scripts中: { "scripts": { "build": "cross-env NODE_ENV=production webpack --mode production", "dev": "cross-env NODE_ENV=development webpack-dev-server --mode development", } }

執行npm命令切換環境

npm run build // 生産環境 process.env.NODE_ENV === 'production' npm run dev // 開發環境 process.env.NODE_ENV === 'development'

接下來我們就可以在webpack.config.js 通過process.env.NODE_ENV來得知當前環境標識

代碼中區分環境

1、定義環境常量

webpack4以前都是通過DefinePlugin來定義NODE_ENV環境變量,以決定library中應該引用哪些內容。

NODE_ENV是一個由Node.js暴露給執行腳本的環境變量。通常用于決定在開發環境與生産環境下,服務工具、構建腳本和客戶端library的行爲。

在webpack.config.js 中添加DefinePlugin插件

const webpack = require('webpack'); plugins: [ new webpack.DefinePlugn({ 'process.env': { 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) } }) ]

在main.js中通過判斷process.env.NODE_ENV常量 來執行相應邏輯

2、mode模式配置

在webpack4 增加了mode模式配置 在浏覽器環境下指定了 process.env.NODE_ENV 的值,默認是development,但node環境中還是需要cross-env來設置。

mode 是 webpack 4 中新增加的參數選項,其有兩個可選值:production 和 development。mode 不可缺省,需要二選一:

3、production 模式:

  1. 生産環境默認開啓了很多代碼優化(minify,splite等)
  2. 開發時開啓注視和驗證,並且自動加上了eval devtool
  3. 生産環境不支持watching,開發環境優化了重新打包的速度
  4. 默認開啓了Scope hoisting和Tree-shaking(原ModuleConcatenationPlugin)
  5. 自動設置process.env.NODE_ENV到不同環境,也就是不需要DefinePlugin來做這個了
  6. 如果你給mode設置爲none,所有默認配置都去掉了
  7. 如果不加這個配置webpack會出現提醒,所以還是加上吧

4、development 模式:

  1. 主要優化了增量構建速度和開發體驗
  2. process.env.NODE_ENV 的值不需要再定義,默認是 development
  3. 開發模式下支持注釋和提示,並且支持 eval 下的 source maps
const NODE_ENV = process.env.NODE_ENV; if (NODE_ENV === 'development') { // 開發環境下執行下面代碼 console.log('development', NODE_ENV); } else { // 生産環境則執行以下環境 console.log('production', NODE_ENV); }

5、DefinePlugn

DefinePlugin 允許創建一個在編譯時可以配置的全局常量。這可能會對開發模式和發布模式的構建允許不同的行爲非常有用。

webpack配置中區分環境

在項目目錄中添加webpack配置文件

  • webpack.base.config.js 保存webpack基礎通用的配置的文件
  • webpack.dev.config.js 保存webpack開發環境配置的文件
  • webpack.prod.config.js 保存webpack生成環境配置的文件
  • webpack.config.js webpack執行配置文件 保存相應環境的配置和webpack基礎配置文件合並後的配置

基礎配置 webpack.base.config.js

webpack一些loader配置

const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const HappyPack = require('happypack'); const os = require('os'); // 系統操作函數 const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); // 指定線程池個數 function resolve(dir) { return path.join(__dirname, dir); } module.exports = { entry: { app: './src/main.js' }, output: { path: path.join(__dirname, 'dist'), filename: '[name].js' }, module: { rules: [ { test: /\.js$/, // use: 'babel-loader?cacheDirectory' use: 'happypack/loader?id=babel', // 緩存loader執行結果 exclude: /node_modules/, // 排除不要加載的文件夾 include: path.resolve(__dirname, 'src') // 指定需要加載的文件夾 } ], noParse: function(content) { // content 從入口開始解析的模塊路徑 return /no-parser/.test(content); // 返回true則忽略對no-parser.js的解析 } }, resolve: { modules: [ // 優化模塊查找路徑 resolve('src'), resolve('node_modules') // 指定node_modules所在位置 當你import第三方模塊式 直接從這個路徑下搜尋 ], alias: { funs$: resolve('src/util/funs.js') }, extensions: ['.js', '.vue'] }, plugins: [ new webpack.DefinePlugin({ // 定義環境變量 "process.env": JSON.stringify(process.env.NODE_ENV) }), new HappyPack({ id: 'babel', loaders: ['babel-loader?cacheDirectory'], threadPool: happyThreadPool, verbose: true }), new HtmlWebpackPlugin({ template: resolve('index.html'), title: 'hello webpack!' }) ] }

開發配置webpack.dev.config.js

開發時的輸入輸出以及開發調試配置 如 devServer devtool 配置

const webpack = require('webpack'); module.exports = { plugins: [ new webpack.DefinePlugin({ 'process.env': { 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) } }) ], devServer: { contentBase: resolve('dist'), compress: true, port: 9000 } };

生成環境webpack.prod.config.js

生成環境 進行壓縮 代碼分離等代碼優化 線上配置

const webpack = require('webpack'); const path = require('path'); const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlIncludeAssetsPlugin = require('html-webpack-include-assets-plugin'); module.exports = { plugins: [ new webpack.DllReferencePlugin({ manifest: require(path.join(__dirname, 'dll', 'react.manifest.json')) }), new ParallelUglifyPlugin({ workerCount: 4, // 開啓幾個子進程去並發的執行壓縮,默認是當前電腦的cpu數量減1 uglifyJS: { output: { beautify: false, // 不需要格式化 comments: false // 保留注釋 }, compress: { warnings: false, // Uglifyjs 刪除沒有代碼時,不輸出警告 // drop_console: true, // 刪除所有console語句 collapse_vars: true, reduce_vars: true } } }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'index.html') }), new HtmlIncludeAssetsPlugin({ assets: ['/dll/react.dll.js'], append: false }) ] };

webpack.config.js 配置合並

借助webpack-merge 將base配置和相應環境配置 合並到’webpack.config.js’npm i webpack-merge -D

webpack.config.js const base = require('./webpack.base.config'); const merge = require('webpack-merge'); let config; if (process.env.NODE_ENV === 'production') { config = require('./webpack.prod.config'); } else { config = require('./webpack.dev.config'); } module.exports = merge(base, config);

運行生産配置

npm run build

運行開發配置

npm run dev

實時重新加載(live reloading) 和 模塊熱替換(HMR)

webpack-dev-server 爲你提供了一個簡單的 web 服務器,並且能夠實時重新加載(live reloading)。

讓我們設置以下:

npm i webpack-dev-server -D

配置webpack.dev.config.js

devServer: { contentBase: path.join(__dirname, 'dist'), // 將 dist 目錄下的文件,作爲可訪問文件。 compress: true, // 開啓Gzip壓縮 port: 9000, // 端口號 inline: true // 在打包後文件裏注入一個websocket客戶端 }

npm scripts

{ "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --mode development" } }

啓動server

npm run dev

浏覽器訪問localhost:9000 當修改代碼ctrl+s 將自動刷新浏覽器

啓用HMR

模塊熱替換(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允許在運行時更新各種模塊,而無需進行完全刷新。

配置webpack.dev.config.js

devServer: { contentBase: path.join(__dirname, 'dist'), compress: true, port: 9000, inline: true, hot: true // 開啓HMR }

注意 此外我們還需添加NamedModulesPlugin 和 HotModuleReplacementPlugin 插件

webpack.dev.config.js

其他代碼和框架開啓熱替換

1.React Hot Loader(實時調整 react 組件)

npm install react-hot-loader

Getting started

1.1 Add react-hot-loader/babel to your .babelrc:

// .babelrc { "plugins": ["react-hot-loader/babel"] }

1.2 Mark your root component as hot-exported:

// App.js import React from 'react' import { hot } from 'react-hot-loader' const App = () =>
Hello World!
export default hot(module)(App)

2.Vue loader

vue-cli 已經集成 只需用vue-cli腳手架開發即可

  • react-hot-loader
  • vue-loader

HMR修改樣式表

借助于style-loader的幫助,CSS的模塊熱替換實際上是相當簡單的。

安裝如下loader:

npm i style-loader css-loader -D

上一篇: Promise 原理解析與實現(遵循Promise/A+規範)

下一篇: 彎道超車,5分鍾快速理解構造器函數與原型對象之間的關系