webpack4.0優化那些事兒
一 縮小文件搜索範圍
1 include & exclude
1) action
- 限制編譯範圍
2) useage
module: { rules: [ { test: /\.js$/, use: ['babel-loader?cacheDirectory'], include: path.resolve(__dirname, 'src'), exclude: /node_modules/ } ] }
'babel-loader?cacheDirectory'javascript
You can also speed up babel-loader by as much as 2x by using the cacheDirectory option. This will cache transformations to the filesystem.css
QA
命令行warning
[BABEL] Note: The code generator has deoptimised the styling of "/Users/xxx/Documents/xxx/webpack_test/test3/node_modules/lodash/lodash.js" as it exceeds the max of "500KB".html
加上exclude限制範圍就不會報錯了java
2 resolve.modules
1) action
- Tell webpack what directories should be searched when resolving modules.
2) useage
resolve: { modules: [path.resolve('node_modules'), path.resolve('lib')] }
3) note
-
Absolute and relative paths can both be used, but be aware that they will behave a bit differently.node
-
A relative path will be scanned similarly to how Node scans for node_modules, by looking through the current directory as well as it's ancestors (i.e. ./node_modules, ../node_modules, and on).react
-
With an absolute path, it will only search in the given directory.jquery
-
If you want to add a directory to search in that takes precedence over node_modules/:(便是有前後順序的)webpack
modules: [path.resolve(__dirname, "src"), "node_modules"]
4) QA
Module not found: Error: Can't resolve 'ajax' in '/Users/xxx/Documents/xxx/webpack_test/test3/src'git
當你須要指定除node_modules以外的其它模塊目錄的時候能夠在數組中添加屬性es6
3 resolve.mainFields
1) action
- Webpack 配置中的 resolve.mainFields 用於配置第三方模塊使用哪一個入口文件。
安裝的第三方模塊中都會有一個 package.json文件,用於描述這個模塊的屬性,其中有些字段用於描述入口文件在哪裏,resolve.mainFields 用於配置採用哪一個字段做爲入口文件的描述。
能夠存在多個字段描述入口文件的緣由是由於有些模塊能夠同時用在多個環境中,針對不一樣的運行環境須要使用不一樣的代碼。 以 isomorphic-fetch API 爲例,它是 Promise的一個實現,但可同時用於瀏覽器和 Node.js 環境。
2) useage
爲了減小搜索步驟,在你明確第三方模塊的入口文件描述字段時,你能夠把它設置的儘可能少。 因爲大多數第三方模塊都採用 main字段去描述入口文件的位置,能夠這樣配置 Webpack:
module.exports = { resolve: { // 只採用 main 字段做爲入口文件描述字段,以減小搜索步驟 mainFields: ['main'], }, };
4 resolve.alias
1) action
- 配置項經過別名來把原導入路徑映射成一個新的導入路徑
- 此優化方法會影響使用Tree-Shaking去除無效代碼
2) useage
alias: { "bootstrap": "bootstrap/dist/css/bootstrap.css" }
5 resolve.extensions
1) action
- 在導入語句沒帶文件後綴時,Webpack會自動帶上後綴後去嘗試詢問文件是否存在 -
- 默認後綴是 extensions: ['.js', '.json']
2) useage
3) note
- 後綴列表儘量小
- 頻率最高的往前方
- 導出語句裏儘量帶上後綴
6 module.noParse
1) action
- module.noParse 配置項可讓 Webpack 忽略對部分沒采用模塊化的文件的遞歸解析處理
2) useage
module: { noParse: [/react\.min\.js/] }
2) note
被忽略掉的文件裏不該該包含 import 、 require 、 define 等模塊化語句
二 DLL
1 action
dll 爲後綴的文件稱爲動態連接庫,在一個動態連接庫中能夠包含給其餘模塊調用的函數和數據
- 把基礎模塊獨立出來打包到單獨的動態鏈接庫裏
- 當須要導入的模塊在動態鏈接庫裏的時候,模塊不能再次被打包,而是去動態鏈接庫裏獲取
2 usage
定義插件(DLLPlugin) ---> 引用插件(DllReferencePlugin)
本次例子用jquery舉例
1) 定義DLL
- 用於打包出一個個動態鏈接庫
- 須要單獨build
webpack.jquery.config.js
module.exports = { entry: ["jquery"], output: { filename: "vendor.js", path: path.resolve(__dirname, "dist"), libraryTarget: 'var',// 打包的方式,hou library: "vendor_lib_vendor"// DLL的名字 }, plugins: [ new webpack.DllPlugin({ name: "vendor_lib_vendor",// 定義DLL path: path.resolve(__dirname, "dist/vendor-manifest.json") }) ] };
package.json 的scripts添加
"dll": "webpack --config webpack.jquery.config.js --mode development"
配置好上述的文件後,在終端運行 npm run dll
,時候會在dist目錄下生成兩個文件,分別是vendor.js
和 vendor-manifest.json
。vendor.js
包含的就是打包後的jquery
文件代碼,vendor-manifest.json
是用來作關聯的。DLL定義好了,接下來就是應用打包好的DLL了。
2) 引用DLL
webpack.config.js 配置文件中引入DllPlugin插件打包好的動態鏈接庫
plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require("./dist/vendor-manifest.json") }) ],
3) 使用DLL
app.html 在app.html的底部添加
<script src="./vendor.js"></script>
3 note
libraryTarget
和library
當用 Webpack 去構建一個能夠被其餘模塊導入使用的庫時須要用到它們。
- output.libraryTarget 配置以何種方式導出庫。
- output.library 配置導出庫的名稱。 它們一般搭配在一塊兒使用。
output.libraryTarget 是字符串的枚舉類型,支持如下配置。
1) var (默認)
編寫的庫將經過 var 被賦值給經過 library 指定名稱的變量。
假如配置了 output.library='LibraryName',則輸出和使用的代碼以下:
// Webpack 輸出的代碼 var LibraryName = lib_code; //其中 lib_code 代指導出庫的代碼內容,是有返回值的一個自執行函數。 // 使用庫的方法 LibraryName.doSomething();
2) commonjs2
編寫的庫將經過 CommonJS 規範導出。
假如配置了 output.library='LibraryName',則輸出和使用的代碼以下:
// Webpack 輸出的代碼 exports['LibraryName'] = lib_code; // 使用庫的方法 require('library-name-in-npm')['LibraryName'].doSomething(); // 其中 library-name-in-npm 是指模塊發佈到 Npm 代碼倉庫時的名稱。
3) commonjs2
編寫的庫將經過 CommonJS2 規範導出,輸出和使用的代碼以下:
// Webpack 輸出的代碼 module.exports = lib_code; // 使用庫的方法 require('library-name-in-npm').doSomething();
CommonJS2 和 CommonJS 規範很類似,差異在於 CommonJS 只能用 exports 導出,而 CommonJS2 在 CommonJS 的基礎上增長了 module.exports 的導出方式。 在 output.libraryTarget 爲 commonjs2 時,配置 output.library 將沒有意義。
4) this
編寫的庫將經過 this 被賦值給經過 library 指定的名稱,輸出和使用的代碼以下:
// Webpack 輸出的代碼 this['LibraryName'] = lib_code; // 使用庫的方法 this.LibraryName.doSomething();
5) window
編寫的庫將經過 window 被賦值給經過 library 指定的名稱,即把庫掛載到 window 上,輸出和使用的代碼以下:
// Webpack 輸出的代碼 window['LibraryName'] = lib_code; // 使用庫的方法 window.LibraryName.doSomething();
6) global
編寫的庫將經過 global 被賦值給經過 library 指定的名稱,即把庫掛載到 global 上,輸出和使用的代碼以下:
// Webpack 輸出的代碼 global['LibraryName'] = lib_code; // 使用庫的方法 global.LibraryName.doSomething();
三 HappyPack
1 action
HappyPack就能讓Webpack把任務分解給多個子進程去併發的執行,子進程處理完後再把結果發送給主進程。
2 usage
install
因爲webpack 4.0 剛剛發佈,響應的插件尚未更新完,不過能夠在後面加一個@next
來安裝即將發佈的版本
npm i happypack@next -D
webpack.config.js
module: { rules: [ { test: /\.css$/, use: 'happypack/loader?id=css', //把對.js文件的處理轉交給id爲babel的HappyPack實例 //用惟一的標識符id來表明當前的HappyPack是用來處理一類特定文件 include: path.resolve('./src'), exclude: /node_modules/ }, { test: /\.js/, use: 'happypack/loader?id=babel', include: path.resolve('./src'), exclude: /node_modules/ } ] }, plugins: [ new HtmlWebPackPlugin({ template: './src/index.html' }), new HappyPack({ id: 'babel', loaders: ['babel-loader']// 和rules裏的配置相同 }), new HappyPack({ id: 'css', loaders: ['style-loader', 'css-loader']// 和rules裏的配置相同 }), ]
四 ParallelUglifyPlugin
1.action
- 能夠把對JS文件的串行壓縮變爲開啓多個子進程並行執行
2 usage
insatll
npm install webpack-parallel-uglify-plugin -D
webpackage.config.js
new ParallelUglifyPlugin({ workerCount: os.cpus().length - 1,//開啓幾個子進程去併發的執行壓縮。默認是當前運行電腦的 CPU 核數減去1 uglifyJS: { output: { beautify: false, //不須要格式化 comments: true, //不保留註釋 }, compress: { warnings: false, // 在UglifyJs刪除沒有用到的代碼時不輸出警告 drop_console: true, // 刪除全部的 `console` 語句,能夠兼容ie瀏覽器 collapse_vars: true, // 內嵌定義了可是隻用到一次的變量 reduce_vars: true, // 提取出出現屢次可是沒有定義成變量去引用的靜態值 } } })
五 服務器自動刷新
1 文件監聽
1) action
- 能夠監聽文件變化,當文件發生變化的時候從新編譯
2) useage
watch: true, watchOptions: { ignored: /node_modules/, aggregateTimeout: 300, poll: 1 }
3) note
watch
只有在開啓監聽模式時(watch爲true),watchOptions纔有意義
aggregateTimeout
監聽到變化發生後等300(ms)再去執行動做,防止文件更新太快致使編譯頻率過高
poll
經過不停的詢問文件是否改變來判斷文件是否發生變化,默認每秒詢問1000次
文件監聽流程
webpack定時獲取文件的更新時間,並跟上次保存的時間進行比對,不一致就表示發生了變化,poll就用來配置每秒問多少次。
當檢測文件再也不發生變化,會先緩存起來,等等待一段時間後以後再通知監聽者,這個等待時間經過aggregateTimeout配置。
webpack只會監聽entry依賴的文件 咱們須要儘量減小須要監聽的文件數量和檢查頻率,固然頻率的下降會致使靈敏度降低。
2 自動刷新瀏覽器
1) use
devServer: { inline: true },
2) note
webpack負責監聽文件變化,webpack-dev-server負責刷新瀏覽器。這些文件會被打包到chunk中,它們會代理客戶端向服務器發起WebSocket鏈接
3 模塊熱替換
1) action
- 模塊熱替換(Hot Module Replacement)的技術可在不刷新整個網頁的狀況下只更新指定的模塊.
- 原理是當一個源碼發生變化時,只從新編譯發生變化的模塊,再用新輸出的模塊替換掉瀏覽器中對應的老模塊
2) 優勢
- 反應更快,時間更短
- 不刷新網頁能夠保留網頁運行狀態
- 監聽更少的文件
- 忽略掉 node_modules 目錄下的文件
2) use
webpack.config.js
devServer: { hot:true//將hot設置爲true }, // 須要的插件 plugins: [ new webpack.NamedModulesPlugin(),//顯示模塊的相對路徑 new webpack.HotModuleReplacementPlugin()// 啓動熱加載功能 ]
code
if (module.hot) { module.hot.accept('./hot.js', () => { let hot = require('./hot'); document.getElementById('app2').innerHTML = hot + '1'; }) }
3 note
須要熱加載的模塊須要在初始化的時候引入到模塊中,不然不會觸發HMR。
六 區分環境
1 action
在開發網頁的時候,通常都會有多套運行環境,例如,在開發過程當中方便開發調試的環境。發佈到線上給用戶使用的運行環境。
線上的環境和開發環境區別主要有如下不一樣:
- 線上的代碼被壓縮
- 開發環境可能會打印只有開發者才能看到的日誌
- 開發環境和線上環境後端數據接口可能不一樣
2 usage
package.json
cross-env
跨平臺設置環境變量(後面沒有&&)
"scripts": { "build-dev": "cross-env NODE_ENV=development webpack --mode development", "build-prod": "cross-env NODE_ENV=production webpack --mode production" }
webpack.config.js
- 根據環境變量區分生產環境仍是開發環境,而後和
webpack.base.config.js
合併,生產環境(或者開發環境)的優先級高於webpack.base.config.js
的配置。
let merge = require('webpack-merge'); let base = require('./webpack.base.config'); let other = null; if (process.env.NODE_ENV === 'development') { other = require('./webpack.dev.config'); } else { other = require('./webapack.prod.config'); } module.exports = merge(base, other);
webpack.base.config.js
- 基本配置
webpack.DefinePlugin
定義環境變量
基本配置... plugins: [ new webpack.DefinePlugin({ __isDevelopment__: JSON.stringify(process.env.NODE_ENV == 'development') }) ]
webpack.dev.config.js
- 以
output
舉例,若是開發和生產環境的參數不一樣,就會覆蓋webpack.base.config.js
裏面的配置
const path = require('path'); module.exports = { output: { path: path.resolve('./dist'), filename: "[name].dev.[hash:2].js" } };
webpack.prod.config.js
- (以
output
舉例)
const path = require('path'); module.exports = { output: { path: path.resolve('./dist'), filename: "[name].prod.[hash:8].js" } };
base.js
- 配置文件中的
webpack.DefinePlugin
定義的變量(__isDevelopment__
),在入口文件和入口文件引用的其餘文件中均可以獲取到__isDevelopment__
的值
let env = null; if (__isDevelopment__) { env = 'dev'; } else { env = 'prod'; } module.exports = env;
index.js
let env = require('./base.js'); if (__isDevelopment__) { console.log('dev'); } else { console.log('prod'); } console.log('env', env); /* prod env prod */
3 note
webpack.DefinePlugin
定義環境變量的值時用 JSON.stringify 包裹字符串的緣由是環境變量的值須要是一個由雙引號包裹的字符串,而 JSON.stringify('production')的值正好等於'"production"'
七 CDN
CDN 又叫內容分發網絡,經過把資源部署到世界各地,用戶在訪問時按照就近原則從離用戶最近的服務器獲取資源,從而加速資源的獲取速度。
- HTML文件不緩存,放在本身的服務器上,關閉本身服務器的緩存,靜態資源的URL變成指向CDN服務器的地址
- 靜態的JavaScript、CSS、圖片等文件開啓CDN和緩存,而且文件名帶上HASH值
- 爲了並行加載不阻塞,把不一樣的靜態資源分配到不一樣的CDN服務器上
八 Tree Shaking
1 action
tree Shaking 能夠用來剔除JavaScript中用不上的死代碼。
2 useage
- 它依賴靜態的ES6模塊化語法,例如經過import和export導入導出
- 不要編譯ES6模塊
use: { loader: 'babel-loader', query: { presets: [ [ "env", { modules: false //含義是關閉 Babel 的模塊轉換功能,保留本來的 ES6 模塊化語法 } ], "react" ] } },
3 note
須要注意的是它依賴靜態的ES6模塊化語法,例如經過import和export導入導出。也就是說若是項目代碼運行在不支持es6語法的環境上,Tree Shaking也就沒有意義了。
九 提取公共代碼
1 爲何須要提取公共代碼
大網站有多個頁面,每一個頁面因爲採用相同技術棧和樣式代碼,會包含不少公共代碼,若是都包含進來會有問題
相同的資源被重複的加載,浪費用戶的流量和服務器的成本; 每一個頁面須要加載的資源太大,致使網頁首屏加載緩慢,影響用戶體驗。 若是能把公共代碼抽離成單獨文件進行加載能進行優化,能夠減小網絡傳輸流量,下降服務器成本
2 如何提取
1) 分類
不一樣類型的文件,打包後的代碼塊也不一樣:
- 基礎類庫,方便長期緩存
- 頁面之間的公用代碼
- 各個頁面單獨生成文件
2) usage
webpack.config.js
optimization: { 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: {// 基礎類庫 chunks: 'initial', test: /node_modules/, name: "vendor", priority: 10, enforce: true } } } },
./src/pageA.js
require('./utils/utility1.js'); require('./utils/utility2.js'); require('react');
./src/pageB.js
require('./utils/utility2.js'); require('./utils/utility3.js');
./src/pageC.js
require('./utils/utility2.js'); require('./utils/utility3.js');
utils/utility1.js
module.exports = 1;
utils/utility2.js
module.exports = 2;
utils/utility3.js
module.exports = 3;
打包後的結果
上述三種代碼的生成的結果,以下圖:
十 Scope Hoisting
1 action
Scope Hoisting 可讓 Webpack 打包出來的代碼文件更小、運行的更快, 它又譯做 "做用域提高",是在 Webpack3 中新推出的功能。
- 代碼體積更小,由於函數申明語句會產生大量代碼
- 代碼在運行時由於建立的函數做用域更少了,內存開銷也隨之變小 hello.js
2 useage
package.json
"build": "webpack --display-optimization-bailout --mode development",
webpack.config.js
plugins: [ new ModuleConcatenationPlugin() ],
./h.js
export default 'scope hoist'
./index.js
import str from './h.js' console.log(str);
3 note
必須使用ES6語法,不然不起做用(
--display-optimization-bailout
參數會提示)
十一 代碼分離
代碼分離是 webpack 中最引人注目的特性之一。此特性可以把代碼分離到不一樣的 bundle 中,而後能夠按需加載或並行加載這些文件。 有三種經常使用的代碼分離方法:
- 入口起點:使用 entry 配置手動地分離代碼。
- 防止重複:使用 splitChunks 去重和分離 chunk。
- 動態導入:經過模塊的內聯函數調用來分離代碼。
入口起點和防止重複上面已經提到了,下面咱們重點講一下動態導入
1 action
用戶當前須要用什麼功能就只加載這個功能對應的代碼,也就是所謂的按需加載 在給單頁應用作按需加載優化時,通常採用如下原則:
- 對網站功能進行劃分,每一類一個chunk
- 對於首次打開頁面須要的功能直接加載,儘快展現給用戶
- 某些依賴大量代碼的功能點能夠按需加載
- 被分割出去的代碼須要一個按需加載的時機
2 usage
- 使用
import(module)
的語法 - import 異步 加載 模塊是一個es7的語法
- 在webpack裏import是一個自然的分割點
document.getElementById('play').addEventListener('click',function(){ import('./vedio.js').then(function(video){ let name = video.getName(); console.log(name); }); });