在前一篇文章已經介紹了webpack4從入門到一些核心經常使用的用法,你們能夠從上一篇文章看起。帶你由淺入深探索webpack4(一)css
接着上一章,接下來咱們會繼續探討webpack4中的各類實用用法,讓咱們共同探討認知前端的另外一個世界。html
三: webpack中的高級配置前端
3.1代碼分離node
在咱們以前的項目中,打包的文件都是放在一個main.js中,每次使用時,都是加載整個main.js,形成性能上的浪費。jquery
代碼分離是webpack中最引人注目的特性之一。他能夠把代碼分離到不一樣的budle中,而後能夠按需加載或者並行加載這些文件。webpack
代碼分離通常有3個經常使用的分離方法:web
1.入口起點:使用ertry配置手動分離代碼。chrome
2.防止重複:配置optimization去重和分離chunk.npm
3.動態導入:(異步代碼)經過模塊的內聯函數調用來分離代碼。json
3.1.1入口起點
這個很簡單,就是本身手動分離代碼,將代碼分離出多個js文件,將其配置到打包入口文件,這樣就能夠打包出不一樣模塊。
其缺點很明顯,要咱們手動進行分離不夠靈活,而且重複都模塊都會被引入到打包後的文件中。
3.1.2防止重複
假設咱們打包多個js入口文件,且他們又引用了同一個第三庫,這樣打包出來的每一個出口文件都含用一份第三方庫,性能消耗是巨大的。
因此咱們可使用CommonsChunkPlugin插件幫助咱們去重,其是將重複的模塊分離出單獨的chunk。
咱們在src下建立兩個入口文件.another1.js、another2.js分別引入第三方模塊lodash
npm install lodash -D
src/another1.js、src/another2.js:
import _ from 'lodash' console.log( _.json(['A','B','C'],' ') )
webpack.common.js:
entry:{ //配置入口文件 another1:'./src/another1.js', anotehr2:'./src/another2.js' }
直接運行npx webpack打包,能夠看到打包出來的文件都挺大的:
咱們如今webpack.common.js配置一下先:
....... module.exports = { entry:{ //配置入口文件 another1:'./src/another1.js', anotehr2:'./src/another2.js' },
...... output:{ //打包文件的出口 filename:'[name].js', //打包後的文件名 path:path.resolve(__dirname,'dist') //打包後文件存放的位置 }, optimization:{ splitChunks:{ cacheGroups: { commons: { name: 'commons', //+++抽離出公共模塊的名稱 chunks: 'initial', //+++入口文件中的共享代碼 minChunks: 2 //+++最少的入口文件數 } } } } }
而後咱們再運行npx webpack就會發現抽離出了一個commons.js文件,整體代碼減輕了一大截。
在這裏可能查看中文文檔可能有個坑,因爲中文文檔更新有點慢,其推薦的配置是使用CommonsChunkPlugin,但在最新版webpack中其已經被刪除了,採用optimization代替了。
3.13動態導入
首先咱們先移除another2.js先,再刪除optimization配置。
要實現異步打包,須要藉助一個插件:babel-plugin-syntax-dynamic-import。因此咱們先安裝一下:
npm install babel-plugin-syntax-dynamic-import -D
咱們引入了插件,由於其是異步代碼分離,因此咱們先將another1.js修改成異步代碼
src/another1.js:
function getComponent() { return import(/* webpackChunkName:"lodash" */ 'lodash').then(({ default: _ }) => { var element = document.createElement('div'); element.innerHTML = _.join(['1', '2'], '-'); return element; }) } getComponent().then(element => { document.body.appendChild(element); });
由於其實babel的插件,接着咱們還須要修改一下.babelrc文件的配置.
.babelrc:
{ "presets": [ [ "babel-preset-env", { "targets": { //在這些版本以上的瀏覽器中不轉換爲es5 "chrome": "67", "firfox":"60", "edge":"17", "safari":"11.1" }, "useBuiltIns": "usage" } ] ], "plugins": ["babel-plugin-syntax-dynamic-import"] //+++ }
分類方便區分哪些是抽離出的公共模塊,咱們配置一下出口文件名:
output:{ //打包文件的出口 filename:'[name].js', //打包後的文件名 chunkFilename:'[name].chunk.js', //+++抽離出的公共模塊的文件名 path:path.resolve(__dirname,'dist') //打包後文件存放的位置 },
最後,咱們直接運行打包,能夠看到lodash被抽離出來了。
3.2css代碼分割
若是咱們將所有代碼打包到js文件中,這就會顯得什麼臃腫,並且不利於代碼分離按需加載!
咱們要將css分離出來單獨的模塊,就須要用到官方推薦的插件:mini-css-extract-plugin(4.3版本以前不支持模塊熱替換)
首先,咱們先安裝一下這個插件:
npm install mini-css-extract-plugin --save-dev
因此咱們須要修改一下css的翻譯官由於style-loader會與其發生衝突,接着須要配置一下plugin,修改以下
webpack.common.js
...... const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { plugins: [ new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: '[name].css', //分離後css的文件名 chunkFilename: '[id].css', }), ], module: { rules: [ { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { // you can specify a publicPath here // by default it uses publicPath in webpackOptions.output publicPath: '../', hmr: process.env.NODE_ENV === 'development', }, }, 'css-loader',
'postcss-loader' ], }, ], }, };
上面這個是建議在生產版本中使用,若是你想要在開發版本中使用,特別是使用HMR,可使用以下配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const devMode = process.env.NODE_ENV !== 'production'; module.exports = { plugins: [ new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: devMode ? '[name].css' : '[name].[hash].css', chunkFilename: devMode ? '[id].css' : '[id].[hash].css', }), ], module: { rules: [ { test: /\.(sa|sc|c)ss$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { hmr: process.env.NODE_ENV === 'development', }, }, 'css-loader', 'postcss-loader', 'sass-loader', ], }, ], }, };
在這裏,咱們成功css文件,而且,當咱們引入多個css文件時,mini-css-extract-plugin插件會幫咱們自動合併爲一個文件,可是,若是咱們須要將css文件壓縮,咱們還須要引用一個插件:
npm install optimize-css-assets-webpack-plugin -D
這時咱們修改一下webpack.common.js:
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); //+++ module.exports = { optimization: { minimizer: [ new OptimizeCSSAssetsPlugin({})], //+++ }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css', }), ], module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, ], }, };
3.3懶加載
在上面的代碼中,咱們須要將公共模塊抽離出來。可是,有時候還沒要用到某些模塊,其都會在頁面加載時請求他們,對性能產生負面影響。
懶加載和tree shaking又是不一樣概念,不要搞混。
tree shaking:刪除掉沒有用到的冗餘代碼。
懶加載:又叫延遲加載,即沒用到該資源的時候不加載該資源,等用到時纔開始加載該資源。
咱們引用官網的實例看,咱們動態添加了一個按鈕,當咱們觸發這個按鈕時,咱們纔開始加載‘lodash'模塊,大大提高了初始加載頁面的速度。
function component() { var element = document.createElement('div'); var button = document.createElement('button'); var br = document.createElement('br'); button.innerHTML = 'Click me and look at the console!'; element.innerHTML = _.join(['Hello', 'webpack'], ' '); element.appendChild(br); element.appendChild(button); // Note that because a network request is involved, some indication // of loading would need to be shown in a production-level site/app. button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => { var print = module.default; print(); }); return element; } document.body.appendChild(component());
3.4 緩存
當咱們加載一次頁面時,瀏覽器會使用一種叫「緩存」的技術,在必定時間保存咱們請求的資源。當咱們再次修改上存而沒有修改資源文件名時,瀏覽器就會認爲它沒有被更新,而請求其以前緩存的資源,致使用戶沒法實時獲取新的資源代碼。
首先咱們上個例子將入口文件換回src/another2.js文件
因此咱們在輸出文件配置中,使其打包後的文件名發生改變。
output:{ //打包文件的出口 filename:'[name].[contenthash].js', //打包後的文件名 path:path.resolve(__dirname,'dist') //打包後文件存放的位置 },
在之前的版本包括如今的webpack中文文檔(估計沒更新)都是使用chunkhash值的,而最新的版本改用爲contenthash值。
這裏插入一下大體講解下hash、chunkhash、contenthash他們之間有什麼區別。
hash:這個跟整個項目構建有關,只要項目文件有更改,整個項目構建的hash值都會改變,而且所有文件都共用相同的hash值。
chunkhash:它會根據不一樣的入口文件進行依賴文件解析,構建對應的chunk,生成對應的哈希值。且只要咱們不改動公共庫的代碼時, 就能夠保證哈希值不會受到影響。可是其有個問題,就是當咱們將css分離出單獨的模塊(後面會有介紹)時,其hash值會與主入口的文件 公用同一個哈希值,當咱們修改css或主入口文件時,其也會致使主入口文件和css文件的哈希值都發生改變。
contenthash:它只會根據文件內容的變化而改變其hash值,既即便css文件所處的模塊裏的其餘模塊文件內容文件發生變化,只要css文 件內容不變,其就不會重複構建。
在這裏咱們給他們添加了一個contenthash值,咱們看一下打包後的文件名。
按照常理說當咱們內容文件不改變時,是不會改變其的hash值。當咱們再運行多幾回打包看下它的哈希值會不會改變,然而。。。
(個人電腦運行並不會出現下列問題,官網說多是有些版本差別纔會出現這種問題,但爲了更可靠起見,仍是建議用下面介紹的方法)
在某些版本中遇到上述問題,是由於webpack中包括了某些樣板,特別是runtime和manifest。下面介紹下解決方法:
提取模塊
提取模塊主要用到CommonsChunkPlugin插件,在前面代碼分離第二part咱們已經用過這個插件,咱們主要用來提取公共模塊,這章咱們用其來將代碼拆分紅單獨塊:
optimization:{ runtimeChunk:'single' }
當咱們運行時,其就會幫助咱們將第三方模塊拆分爲單獨的模塊
讓咱們在配置一下其參數,當咱們再將node_modules中的模塊給分離出來
optimization:{ splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } }, runtimeChunk:'single' }
咱們從新打包一下就會發現,剩下的main.js居然只有2.9KB了!!!
3.5預取/預加載模塊
當咱們要加載一個組件時,若是使用懶加載,等到觸發的時候纔開始加載時,性能上會有必定損耗,給用戶的體驗感也不是很好。
因此咱們能夠在頁面加載完成後有空閒時間時,再加載一下未來須要用到的模塊,這樣在觸發該功能時,就能夠直接調用緩存中的模塊,這就能夠大大提高用戶的體驗感。
在webpack4.6+的版本中webpack添加了預取和預加載模塊。
下面介紹一下怎樣使用預取/預加載模塊,很簡單,只須要在導入模塊中加入一個註釋即可。
import(/* webpackPrefetch: true */ 'LoginModal');
其會在頁面頭部附加<link rel=「prefetch」href=「login modal chunk.js」>
這裏可使用webpackPrefetch和webpackPreload這兩個屬性,其仍是有些區別的:
webpackPrefetch: true :先把主加載流程加載完畢,在空閒時在加載其模塊,等再點擊其餘時,只須要從緩存中讀取便可,性能更好。推薦使用,提升代碼利用率。把一些交互後才能用到的代碼寫到異步組件裏,經過懶加載的形式,去把這塊的代碼邏輯加載進來,性能提高,頁面訪問速度更快。
webpackPreload: true : 和主加載流程一塊兒並行加載。而一個預加載的塊應該在加載完成時當即被其父類調用。
3.6Slimming(墊片)
當咱們引入相似lodash、jquery這些第三方庫的時候,這些庫可能回建立一些須要被導出的全局變量,形成了環境的污染。
slimming在這裏就起到了很大的做用,它能夠只導出須要使用到的全局變量。來看下面的例子
咱們添加一個plugin插件的配置以下:
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } }, plugins: [ new webpack.ProvidePlugin({ //+++ _: 'lodash', //+++引用全局變量_時,自動引用lodash這個庫 join:['lodash','join'] //+++引動全局變量join時,調用lodash庫中的join方法 }) ] };
這樣,咱們就無需在每一個模塊中都import這個第三方模塊了,咱們修改一下src/index.js
src/index.js:
function component() { var element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); //+++調用lodash element.innerHTML = join(['Hello', 'webpack'], ' '); //+++直接調用lodash中的方法 return element; } document.body.appendChild(component());
當咱們只須要使用join方法,就無須要導出整個庫,這樣就能夠很好的和tree shaking相配合。
在這裏有一個問題就是,一些傳統的模塊依賴中的this指向的是window對象,當模塊以運行在CommonJS環境下就可能指向的是module.exports,因此咱們須要修改一下模塊中this的指向,將其指向window。通常狀況下不須要設置。
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.js$/, //+++當解析index.js的時候 use: 'imports-loader?this=>window' //+++將this指向window } ] }, plugins: [ new webpack.ProvidePlugin({ join: ['lodash', 'join'] }) ] };
3.7library
當咱們須要開發一個庫時,咱們但願外包能夠經過各類方式引用咱們的庫,這時,咱們就須要用到library
例如咱們建立一個libray.js的方法類。
[{ "num": 1, "word": "One" }, { "num": 2, "word": "Two" }, { "num": 3, "word": "Three" }, { "num": 4, "word": "Four" }, { "num": 5, "word": "Five" }, { "num": 0, "word": "Zero" }]
而後經過src/index.js引入其:
src/index.js
import _ from 'lodash'; import numRef from './ref.json'; export function numToWord(num) { return _.reduce(numRef, (accum, ref) => { return ref.num === num ? ref.word : accum; }, ''); }; export function wordToNum(word) { return _.reduce(numRef, (accum, ref) => { return ref.word === word && word.toLowerCase() ? ref.num : accum; }, -1); };
而後咱們將其打包,若是咱們想要經過各類方式引入該庫,應該怎麼作:
// ES2015 模塊引入 import * as webpackNumbers from 'webpack-numbers'; // CommonJS 模塊引入 var webpackNumbers = require('webpack-numbers'); // ... // ES2015 和 CommonJS 模塊調用 webpackNumbers.wordToNum('Two'); // ... // AMD 模塊引入 require(['webpackNumbers'], function ( webpackNumbers) { // ... // AMD 模塊調用 webpackNumbers.wordToNum('Two'); // ... }); <script src="index.js">
咱們就須要在入口文件中配置一下:
webpack.config.js:
const path = require('path') module.exports = { mode:'production', entry:'./src/index.js', output:{ path:path.resolve(__dirname,'dist'), filename:'index.js', library:'index', libraryTarget:'umd' } }
可是,咱們在src/index.js引入了lodash的庫,若是在外部可能引入了lodash這就顯得什麼耗費性能,因此咱們能夠這樣設置
const path = require('path') module.exports = { mode:'production', entry:'./src/index.js', output:{ path:path.resolve(__dirname,'dist'), filename:'index.js', library:'index', libraryTarget:'umd' }, externals: { lodash: { commonjs: 'lodash', commonjs2: 'lodash', amd: 'lodash', root: '_' } } }
這樣,當咱們引入index.js庫時,就必須須要引入一個lodash的依賴,這樣就能防止外部與內部引入重複的庫。