此項目總共 24 小節,目錄:css
前 15 節基於 Webpack4 漸進式教程 爲基礎,加上本身的實踐和理解得出,感謝 godbmw 😄html
在 Webpack4 漸進式教程 的基礎上升級:前端
隨後的章節以 mooc 網 手把手帶你掌握新版 Webpack4.0 整理的學習筆記,感謝 DellLee 老師 😄vue
環境以下:node
OS: 「win10」 node: 「10.5.0」 npm: 「6.1.0」 webpack: 「4.29.6」 webpack-cli: 「3.2.3」
每個章節對應一個 demo 👉 源碼地址, clone 源碼後注意運行 npm install
安裝依賴webpack
第一次寫文章,錯誤的地方但願各位大佬指出,第一時間修改git
demo1 源碼地址es6
建立空文件夾,經過運行如下命令初始化 package.jsongithub
npm init -y
npm init 用來初始化生成一個新的 package.json 文件。它會向用戶提問一系列問題,若是你以爲不用修改默認配置,一路回車就能夠了。
若是使用了 -y(表明 yes),則跳過提問階段,直接生成一個新的 package.json 文件。
引入 webpack4:web
npm i webpack --save-dev
還須要 webpack-cli ,做爲一個單獨的包引入,若是不裝 webpack-cli 是沒法在命令行裏使用 webpack 的
npm i webpack-cli --save-dev
此項目 webpack 版本以下:
"webpack": "^4.29.6", "webpack-cli": "^3.2.3"
如今打開 package.json 並添加一個 build(構建) 腳本:
嘗試運行看看會發生什麼:
npm run build
在 webpack4 之前的版本中,必須在名爲 webpack.config.js 的配置文件中 經過 entry 屬性定義 entry point(入口點),就像這樣:
從 webpack4 開始,再也不必須定義 entry point(入口點) :它將默認爲 ./src/index.js
測試這個新功能,首先建立 ./src/index.js 文件
再運行 npm run build
試試
打包成功,並在當前的根目錄中獲得打包後的文件夾,也就是 dist 文件夾
它將查找 ./src/index.js 做爲默認入口點。 並且,它會在 ./dist/main.js 中輸出模塊包,目前代碼量小,能夠格式化看效果
至此,打包 JS 結束
參考:webpack 官網入門
擁有 2 個配置文件在 webpack 中是的常見模式。
一個典型的項目可能有:
雖然較大的項目可能仍然須要 2 個配置文件,但在 webpack4 中,你能夠在沒有一行配置的狀況下完成
webpack4 引入了 production(生產) 和 development(開發) 模式。
細心的朋友會發如今 npm run build
打包後會有一段報警提示
'mode' 選項還沒有設置,webpack 將回退到 'production'。 將 「mode」 選項設置爲 'development' 或 'production' 以啓用每一個環境的默認值。您還能夠將其設置爲 'none' 以禁用任何默認行爲。 瞭解更多
"dev": "webpack --mode development", "build": "webpack --mode production"
npm run dev
打開 ./dist/main.js 文件,是一個 bundle(包) 文件,並無壓縮!
npm run build
能夠看到 ./dist/main.js 文件已經被壓縮了
其實在終端裏也能發現,看構建完的大小, dev 模式下文件大小是 3.8 KB, prod 模式下文件大小僅爲 960 字節
production mode(生產模式) 能夠開箱即用地進行各類優化。 包括壓縮,做用域提高,tree-shaking 等。
另外一方面,development mode(開發模式) 針對速度進行了優化,僅僅提供了一種不壓縮的 bundle
在 webpack4 中,能夠在沒有一行配置的狀況下完成任務! 只需定義 –mode 參數便可得到全部內容!
1. 檢驗 webpack 規範支持
webpack 支持 ES6, CommonJS, AMD 規範
建立 vendor 文件夾,其中 minus.js、multi.js 和 sum.js 分別用 CommonJS、AMD 和 ES6 規範編寫
// minus.js module.exports = function(a, b) { return a - b } // multi.js define(function(require, factory) { 'use strict' return function(a, b) { return a * b } }) // sum.js export default function(a, b) { return a + b }
在 app.js 文件中引入以上三個 js 文件
/** * webpack 支持 ES六、CommonJs 和 AMD 規範 */ // ES6 import sum from './vendor/sum' console.log('sum(1, 2) = ', sum(1, 2)) // CommonJs var minus = require('./vendor/minus') console.log('minus(1, 2) = ', minus(1, 2)) // AMD require(['./vendor/multi'], function(multi) { console.log('multi(1, 2) = ', multi(1, 2)) })
2. 編寫配置文件覆蓋 entry/output
webpack.config.js 是 webpack 默認的配置文件名,在根目錄下建立
const path = require('path') module.exports = { entry: { app: './app.js' // 須要打包的文件入口 }, output: { publicPath: __dirname + '/dist/', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: 'bundle.js' // 打包後生產的 js 文件 } }
path.resolve()
方法會把一個路徑或路徑片斷的序列解析爲一個絕對路徑。
__dirname
: 當前模塊的文件夾名稱。
可使用 console.log
輸出一下就明白了
const path = require('path') console.log('__dirname: ', __dirname) console.log('path.resolve: ', path.resolve(__dirname, 'dist')) module.exports = { entry: { app: './app.js' // 須要打包的文件入口 }, output: { publicPath: __dirname + '/dist/', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: 'bundle.js' // 打包後生產的 js 文件 } }
執行 npm run build
打包 js 文件
會發現生成了 dist 文件夾,並生成了兩個打包後的文件
這跟 AMD 的引入方式有關,若是在 app.js 中註釋掉 AMD 的寫法,則只會打包出一個 bundle.js 文件
在實際寫代碼的時候,最好使用 ES6 和 CommonJS 的規範來寫
當你註釋 AMD 後,打包完 dist 中有多個文件,這是由於打包的時候,沒有先刪除 dist 文件,再打包,咱們須要使用一個插件來幫咱們實現,GitHub 連接:clean-webpack-plugin
① 安裝插件
npm install clean-webpack-plugin --save-dev
② 修改 webpack 配置文件
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') module.exports = { entry: { app: './app.js' // 須要打包的文件入口 }, output: { publicPath: __dirname + '/dist/', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: 'bundle.js' // 打包後生產的 js 文件 }, plugins: [ new CleanWebpackPlugin() // 默認狀況下,此插件將刪除 webpack output.path目錄中的全部文件,以及每次成功重建後全部未使用的 webpack 資產。 ] }
以後再執行 npm run build
就能夠了
打包後的 js 文件會按照咱們的配置放在 dist 目錄下,建立一個 html 文件,引用打包好的 js 文件,打開 F12 就能看到效果了
現代 Javascript 主要是用 ES6 編寫的。但並不是每一個瀏覽器都知道如何處理 ES6。 咱們須要某種轉換,這個轉換步驟稱爲 transpiling(轉譯)。transpiling(轉譯) 是指採用 ES6 語法,轉譯爲舊瀏覽器能夠理解的行爲。
Webpack 不知道如何進行轉換可是有 loader(加載器) :將它們視爲轉譯器。
babel-loader 是一個 webpack 的 loader(加載器),用於將 ES6 及以上版本轉譯至 ES5
要開始使用 loader ,咱們須要安裝一堆依賴項,如下已 Babel7 爲主,升級建議
若是是用 babel7 來轉譯,須要安裝 @babel/core、 @babel/preset-env 和 @babel/plugin-transform-runtime,而不是 babel-core、babel-preset-env 和 babel-plugin-transform-runtime,它們是用於 babel6 的使用 @babel/plugin-transform-runtime 的緣由:Babel 使用很是小的助手來完成常見功能。默認狀況下,這將添加到須要它的每一個文件中。這種重複有時是沒必要要的,尤爲是當你的應用程序分佈在多個文件上的時候。
transform-runtime 能夠重複使用 Babel 注入的程序代碼來節省代碼,減少體積。使用 @babel/polyfill 的緣由:Babel 默認只轉換新的 JavaScript 句法(syntax),而不轉換新的 API,好比 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局對象,以及一些定義在全局對象上的方法(好比 Object.assign)都不會轉碼。必須使用 @babel/polyfill,爲當前環境提供一個墊片。
所謂墊片也就是墊平不一樣瀏覽器或者不一樣環境下的差別
① 安裝依賴
npm i @babel/core babel-loader @babel/preset-env @babel/plugin-transform-runtime --save-dev
npm i @babel/polyfill @babel/runtime
② 在項目的根目錄中建立名爲 .babelrc 的新文件來配置 Babel:
{ "presets": ["@babel/preset-env"], "plugins": ["@babel/plugin-transform-runtime"] }
感謝評論區 xcsweb 的指出,若是遇到以下報錯
WARNING: We noticed you're using the `useBuiltIns` option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via the `corejs` option. You should also be sure that the version you pass to the `corejs` option matches the version specified in your `package.json`'s `dependencies` section. If it doesn't, you need to run one of the following commands: npm install --save core-js@2 npm install --save core-js@3 yarn add core-js@2 yarn add core-js@3
不只僅要安裝 npm install --save core-js@3 還須要設置 .babelrc 設置 "corejs": 3
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 } ] ], "plugins": ["@babel/plugin-transform-runtime"] }
③ webpack 配置 loader(加載器)
module: { rules: [ { test: /\.js$/, // 使用正則來匹配 js 文件 exclude: /node_modules/, // 排除依賴包文件夾 use: { loader: 'babel-loader' // 使用 babel-loader } } ] }
webpack.config.js 最終配置:
④ 在 app.js 全局引入 @babel/polyfill
並寫入 ES6 語法,並執行 npm run build
打包
// 全局引入 import '@babel/polyfill' // 測試 ES6 語法是否經過 babel 轉譯 const array = [1, 2, 3] const isES6 = () => console.log(...array) isES6() const arr = [new Promise(() => {}), new Promise(() => {})] arr.map(item => { console.log(item) })
⑤ 打包完以後使用 IE 瀏覽器打開 index.html 文件,看控制檯是否有輸出,若是是新版的 chrome,是可使用 es6 語法的,因此要用 IE 這個萬惡之源試試
全局引入 @babel/polyfill
的這種方式可能會導入代碼中不須要的 polyfill,從而使打包體積更大
更改 .babelrc
,只轉譯咱們使用到的
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage" } ] ], "plugins": ["@babel/plugin-transform-runtime"] }
同時,將全局引入這段代碼註釋掉,再次打包
// 全局引入 // import '@babel/polyfill'
體積就減少了不少,可是更多的狀況是咱們並不確切的知道項目中引起兼容問題的具體緣由,因此仍是全局引入比較好
browserslistrc 用於在不一樣前端工具之間共享目標瀏覽器和 Node.js 版本的配置
能夠看看 browserslist 兼容瀏覽器的頁面
當您將如下內容添加到 package.json
時,全部工具都會自動找到目標瀏覽器:
"browserslist": [ "> 1%", "last 2 version", "not ie <= 8" ]
也能夠建立 .browserslistrc
文件單獨寫配置
# 所支持的瀏覽器版本 > 1% # 全球使用狀況統計選擇的瀏覽器版本 last 2 version # 每一個瀏覽器的最後兩個版本 not ie <= 8 # 排除小於 ie8 如下的瀏覽器
該項目仍是使用單首創建配置文件的方式,便於理解,若是以爲配置文件很差,也能夠寫在 package.json
中
package.json
文件所用依賴,npm install 安裝:
{ "scripts": { "dev": "webpack --mode development", "build": "webpack --mode production" }, "devDependencies": { "clean-webpack-plugin": "^2.0.0", "webpack": "^4.29.6", "webpack-cli": "^3.2.3" }, "dependencies": { "lodash": "^4.17.11" } }
咱們在 src/ 文件夾下建立 index.js 文件
import _ from 'lodash' console.log(_.join(['a', 'b', 'c']))
目錄結構爲:
配置 webpack.config.js 文件
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') module.exports = { entry: { main: './src/index.js' }, output: { publicPath: __dirname + '/dist/', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 }, plugins: [new CleanWebpackPlugin()] }
運行 npm run build
打包
在 index.html 中使用打包後的文件
<!DOCTYPE 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>代碼分割</title> </head> <body> <script src="./dist/main.bundle.js"></script> </body> </html>
使用瀏覽器打開 index.html
文件,進入控制檯,能夠看到以下信息:a,b,c
若是咱們再改動業務代碼,將 index.js 中的代碼改成
import _ from 'lodash' console.log(_.join(['a', 'b', 'c'], '***'))
再打包,刷新頁面能夠看到 a***b*****c**
咱們引用的第三方框架和咱們的業務代碼一塊兒被打包,這樣會有一個什麼問題?
假設 lodash 爲 1M,業務代碼也爲 1M,打包後假設爲 2M
瀏覽器每次打開頁面,都要先加載 2M 的文件,才能顯示業務邏輯,這樣會使得加載時間變長,
業務代碼更新會比較頻繁,第三方代碼基本不會更改,這樣從新打包後,假設爲 2M,用戶從新打開網頁後,又會再加載 2M 文件
瀏覽器是有緩存的,若是文件沒變更的話,就不用再去發送 http 請求,而是直接從緩存中取,這樣在刷新頁面或者第二次進入的時候能夠加快網頁加載的速度。
怎麼解決呢,能夠利用 webpack 中的代碼分割
在 webpack4 以前是使用 commonsChunkPlugin 來拆分公共代碼,v4 以後被廢棄,並使用 splitChunksPlugins
在使用 splitChunksPlugins 以前,首先要知道 splitChunksPlugins 是 webpack 主模塊中的一個細分模塊,無需 npm 引入
如今咱們來配置 webpack.config.js 文件
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') module.exports = { entry: { main: './src/index.js' }, output: { publicPath: __dirname + '/dist/', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 }, optimization: { splitChunks: { chunks: 'all' } }, plugins: [new CleanWebpackPlugin()] }
上面高亮的代碼段就是告訴 webpack,要作代碼分割了,這裏的 chunks: 'all'
是分割全部代碼,包括同步代碼和異步代碼,webpack 默認是 chunks: 'async'
分割異步代碼
咱們使用 npm run dev
來打包開發環境下的代碼,這樣代碼就不會壓縮,方便咱們來觀察,能夠看到代碼被分割成兩個文件了
打開 dist/main.bundle.js 文件,在最底部能夠看到 src/index.js 文件,裏面放的是業務邏輯的代碼,可是並無 lodash 的代碼
打開 dist/vendors~main.js 文件,在最上面能夠看到 lodash 模塊
再次打開頁面,控制檯也輸出了內容,這樣就實現了 Code Splitting(代碼分割)
其實沒有 webpack 的時候,也是有代碼分割的,不過是須要咱們本身手動的分割,而如今使用了 webpack,經過這種配置項的方式,它會自動幫咱們去作代碼分割
仔細看分割完的代碼名稱,vendors~main.js
,咱們對分割完的名稱進行更改
仍是在 splitChunks
的配置項中,添加 cacheGroups
對象
optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendors: { name: 'vendors' } } } }
再次打包就能夠看到效果了,cacheGroups 的默認配置會定義 vendors 和 default
test: /[\\/]node_modules[\\/]/,
使用正則過濾,只有 node_modules 引入的第三方庫會被分割
爲了驗證默認配置,咱們將 splitChunks 屬性設置爲空對象,再次打包
打包完發現只有一個文件,這是爲何?
由於 chunks
默認爲 async
,只會分割異步的代碼,而以前咱們寫的都是同步的代碼,先 import lodash,再去寫業務邏輯,如今使用異步的方式來作,將 index.js 中的代碼改成如下:
function getComponent() { // 使用 異步的形式導入 lodash,default: _ 表示用 _ 代指 lodash return import('lodash').then(({ default: _ }) => { var element = document.createElement('div') element.innerHTML = _.join(['hello', 'world'], '-') return element }) } getComponent().then(element => { document.body.appendChild(element) })
這裏分割出了 0.js
和 main.bundle.js
,0 是以 id 爲編號來命名
因此通常咱們設置 chunks 爲 all,異步、同步代碼都打包
如今咱們將 webpack 官網上的默認配置拷貝到咱們的 webpack.config.js 中來分析一下
optimization: { splitChunks: { chunks: 'async', minSize: 30000, maxSize: 0, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~', name: true, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } } } }
webpack 代碼分割的配置是這樣的,好比咱們要分割 jQuery 和 lodash 這樣的第三方庫,它會先通過 chunks、minSize、maxSize、minChunks
等等,知足條件後生成 jQuery 和 lodash 兩個文件,而後放入 cacheGroup 中緩存着,再根據你在 cacheGroup 中配置的組來決定是將兩個文件整合到一個文件打包,仍是單獨分開打包,好比上面代碼中的 vendors,就是將 node_modules
中全部的第三方庫都打包到 vendors.js 文件中,若是你還想繼續分割能夠這麼作
cacheGroups: { lodash: { name: 'lodash', test: /[\\/]node_modules[\\/]lodash[\\/]/, priority: 5 // 優先級要大於 vendors 否則會被打包進 vendors }, vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } }
感謝評論區 anlinsir 的指出,若是打包有報錯 Support for the experimental syntax ‘dynamicImport’ isn't currently enabled,這是由於 dynamicImport 仍是實驗性的語法,webpack 不支持,須要安裝插件來支持,具體步驟能夠參考: https://www.cnblogs.com/chaoy...
再次打包,就能夠看到 lodash 被分割出來了,之後使用第三方庫均可以用這種配置來單獨分割成一個 js 文件,好比 element-ui,注意設置 priority 的值很重要,優先級越高的會越先被打包
若是 index.js 引入了 A.js 和 B.js,同時 A、B 又引入了 common,common 被引入了兩次,能夠被稱爲公共模塊
目錄結構爲:
代碼以下:
// a,js import './common' console.log('A') export default 'A' // b.js import './common' console.log('B') export default 'B' // common.js console.log('公共模塊') export default 'common' // index.js import './a.js' import './b.js' // 異步代碼 function getComponent() { // 使用異步的形式導入 lodash,default: _ 表示用 _ 代指 lodash return import('lodash').then(({ default: _ }) => { var element = document.createElement('div') element.innerHTML = _.join(['hello', 'world'], '-') return element }) } getComponent().then(element => { document.body.appendChild(element) })
上面那種異步的寫法可能比較繞,如今精簡一下,而且 webpack 對異步代碼經過註釋能夠直接修改打包後的名稱,如下代碼所有以異步的形式引入
// 異步代碼 import(/* webpackChunkName: 'a'*/ './a').then(function(a) { console.log(a) }) import(/* webpackChunkName: 'b'*/ './b').then(function(b) { console.log(b) }) import(/* webpackChunkName: 'use-lodash'*/ 'lodash').then(function(_) { console.log(_.join(['1', '2'])) })
將 minChunks 設置爲 2,最小公用 2 次才分割
optimization: { splitChunks: { chunks: 'all', minSize: 30000, maxSize: 0, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~', name: true, cacheGroups: { lodash: { name: 'lodash', test: /[\\/]node_modules[\\/]lodash[\\/]/, priority: 10 }, commons: { name: 'commons', minSize: 0, //表示在壓縮前的最小模塊大小,默認值是 30kb minChunks: 2, // 最小公用次數 priority: 5, // 優先級 reuseExistingChunk: true // 公共模塊必開啓 }, vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } } } }
這裏分割出了 lodash
和咱們在註釋中定義的 use-lodash
,前者是第三庫,後者是使用第三庫寫的業務代碼,也能被分割出來
這裏之因此會自動引入分割後的依賴,能夠查看打包後的 main.bundle.js 文件
甚至咱們能夠打斷點來看它是怎麼運行的,如下爲 gif 動圖演示
經常使用的配置項在下面的表格中,更多配置詳情見官網
配置項 | 說明 | 示例 |
---|---|---|
chunks | 匹配的塊的類型 | initial(初始塊),async(按需加載的異步塊),all(全部塊) |
name | 用以控制分離後代碼塊的命名 | chunk-libs |
test | 用於規定緩存組匹配的文件位置 | /[\/]node_modules[\/]/ |
priority | 分離規則的優先級,優先級越高,則優先匹配 | priority: 20 |
minSize | 超過多少大小就進行壓縮 | minSize: 30000 默認值是 30kb |
minChunks | 分割前必須共享模塊的最小塊數 | minChunks: 2 |
reuseExistingChunk | 若是當前塊已從主模塊拆分出來,則將重用它而不是生成新的塊 | true |
webpack4 系列教程 (三): 多頁面解決方案 -- 提取公共代碼
在 demo5 的基礎上修改 index.js 文件,並刪除了多餘的 js 文件
document.addEventListener('click', function() { import(/* webpackChunkName: 'use-lodash'*/ 'lodash').then(function(_) { console.log(_.join(['3', '4'])) }) })
這段代碼表示的是,當點擊頁面的時候,異步加載 lodash 並輸出內容,打包後打開 index.html 文件,演示以下:
第一次進入頁面的時候,並無加載 lodash 和 use-lodash,當我點擊網頁的時候,瀏覽器再去加載,而且控制檯輸出內容,這就是代碼懶加載,若是有用過 vue-router 的朋友應該會知道路由懶加載,而且官方也推薦使用懶加載的寫法,就是爲告終合 webpack,下圖是 vue-cli3 生成的項目
其實懶加載就是經過 import 去異步的加載一個模塊,具體何時加載,這個要根據業務來寫,好比彈窗組件,模態框組件等等,都是點擊按鈕後再出現。
懶加載能加快網頁的加載速度,若是你把詳情頁、彈窗等頁面所有打包到一個 js 文件中,用戶若是隻是訪問首頁,只須要首頁的代碼,不須要其餘頁面的代碼,加入多餘的代碼只會使加載時間變長,因此咱們能夠對路由進行懶加載,只有當用戶訪問到對應路由的時候,再去加載對應模塊
懶加載並非 webpack 裏的概念,而是 ES6 中的 import 語法,webpack 只是可以識別 import 語法,能進行代碼分割而已。import 後面返回的是一個 then,說明這是一個 promise 類型,一些低端的瀏覽器不支持 promise,好比 IE ,若是要使用這種異步的代碼,就要使用 babel 以及 babel-polyfill 來作轉換,由於我使用的是 73 版本的 chrome 瀏覽器,對 ES6 語法是支持的,因此我並無安裝 babel 也能使用
更改 index.js 文件
document.addEventListener('click', function() { const element = document.createElement('div') element.innerHTML = 'Hello World' document.body.appendChild(element) })
從新打包,並打開 index.html ,打開瀏覽器控制檯,按 ctrl + shift + p
,輸入 coverage
就能看到當前頁面加載的 js 代碼未使用率,紅色區域表示未被使用的代碼段
演示:
打開 coverage
若是沒出現分析的文件,記得刷新一下
這裏一開始紅色區域的代碼未被使用,當我點擊了頁面後,變成綠色,頁面出現了 Hello World
,說明代碼被使用了
頁面剛加載的時候,異步的代碼根本就不會執行,可是咱們卻把它下載下來,實際上就會浪費頁面執行性能,webpack 就但願像這樣交互的功能,應該把它放到一個異步加載的模塊去寫
新建一個 click.js 文件
function handleClick() { const element = document.createElement('div') element.innerHTML = 'Dell Lee' document.body.appendChild(element) } export default handleClick
而且將 index.js 文件改成異步的加載模塊:
document.addEventListener('click', () => { import('./click.js').then(({ default: func }) => { func() }) })
從新打包,使用 coverage 分析
演示:
當加載頁面的時候,沒有加載業務邏輯,當點擊網頁的時候,纔會加載 1.js 模塊,也就是咱們在 index.js 中異步引入的 click.js
這麼去寫代碼,纔是使頁面加載最快的一種方式,寫高性能前端代碼的時候,不關是考慮緩存,還要考慮代碼使用率
因此 webpack 在打包過程當中,是但願咱們多寫這種異步的代碼,才能提高網站的性能,這也是爲何 webpack 的 splitChunks 中的 chunks 默認是 async,異步的
異步能提升你網頁打開的性能,而同步代碼是增長一個緩存,對性能的提高是很是有限的,由於緩存通常是第二次打開網頁或者刷新頁面的時候,緩存頗有用,可是網頁的性能通常是用戶第一次打開網頁,看首屏的時候。
固然,這也會出現另外一個問題,就是當用戶點擊的時候,纔去加載業務模塊,若是業務模塊比較大的時候,用戶點擊後並無立馬看到效果,而是要等待幾秒,這樣體驗上也很差,怎麼去解決這種問題
一:若是訪問首頁的時候,不須要加載詳情頁的邏輯,等用戶首頁加載完了之後,頁面展現出來了,頁面的帶寬被釋放出來了,網絡空閒了,再「偷偷」的去加載詳情頁的內容,而不是等用戶去點擊的時候再去加載
這個解決方案就是依賴 webpack 的 Prefetching/Preloading 特性
修改 index.js
document.addEventListener('click', () => { import(/* webpackPrefetch: true */ './click.js').then(({ default: func }) => { func() }) })
webpackPrefetch: true
會等你主要的 JS 都加載完了以後,網絡帶寬空閒的時候,它就會預先幫你加載好
從新打包後刷新頁面,注意看 Network
當網頁打開的時候,main.bundle.js 被加載完了,網絡空閒了,就會預先加載 1.js 耗時 14ms,等我去點擊頁面的時候,Network 又多了一個 1.js,耗時 2ms,這是由於第一次加載完了 1.js,被瀏覽器給緩存起來了,等我點擊的時候,瀏覽器直接從緩存中取,響應速度很是快
這裏咱們使用的是 webpackPrefetch
,還有一種是 webpackPreload
與 prefetch 相比,Preload 指令有不少 不一樣之處:Prefetch 會等待覈心代碼加載完以後,有空閒以後再去加載。Preload 會和核心的代碼並行加載,仍是不推薦
針對優化,不只僅是侷限於緩存,緩存能帶來的代碼性能提高是很是有限的,而是如何讓代碼的使用率最高,有一些交互後才用的代碼,能夠寫到異步組件裏面去,經過懶加載的形式,去把代碼邏輯加載進來,這樣會使得頁面訪問速度變的更快,若是你以爲懶加載會影響用戶體驗,可使用 Prefetch 這種方式來預加載,不過在某些遊覽器不兼容,會有兼容性的問題,重點不是在 Prefetch 怎麼去用,而是在作前端代碼性能優化的時候,緩存不是最重要的點,最重要的是代碼使用的覆蓋率上(coverage)
通過上面幾個小節的操做,有沒有以爲每次要去更改 index.html 中引入 js 文件很麻煩,一旦打包的名字變動後,也要對應的去修改 index.html 引入的 js 名稱,這個時候就要使用一個插件來幫助咱們,打包完以後自動生成 HTML 文件,並自動引入打包後的 js 文件
npm i html-webpack-plugin html-loader --save-dev
package.json 以下:
{ "scripts": { "dev": "webpack --mode development", "build": "webpack --mode production" }, "devDependencies": { "clean-webpack-plugin": "^2.0.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "webpack": "^4.29.6", "webpack-cli": "^3.2.3" }, "dependencies": { "lodash": "^4.17.11" } }
module.exports = { plugins: [ new HtmlWebpackPlugin({ // 打包輸出HTML title: '自動生成 HTML', minify: { // 壓縮 HTML 文件 removeComments: true, // 移除 HTML 中的註釋 collapseWhitespace: true, // 刪除空白符與換行符 minifyCSS: true // 壓縮內聯 css }, filename: 'index.html', // 生成後的文件名 template: 'index.html' // 根據此模版生成 HTML 文件 }) ] }
HtmlWebpackPlugin 是在 plugin 這個選項中配置的。經常使用參數含義以下:
因爲使用了 title
選項,則須要在 template
選項對應的 html 的 title 中加入 <%= htmlWebpackPlugin.options.title %>
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') // 引入插件 module.exports = { entry: { page: './src/page.js' }, output: { publicPath: __dirname + '/dist/', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ // 打包輸出HTML title: '自動生成 HTML', minify: { // 壓縮 HTML 文件 removeComments: true, // 移除 HTML 中的註釋 collapseWhitespace: true, // 刪除空白符與換行符 minifyCSS: true // 壓縮內聯 css }, filename: 'index.html', // 生成後的文件名 template: 'index.html' // 根據此模版生成 HTML 文件 }) ], optimization: { splitChunks: { chunks: 'all', cacheGroups: { lodash: { name: 'chunk-lodash', // 單獨將 lodash 拆包 priority: 10, // 優先級要大於 commons 否則會被打包進 commons test: /[\\/]node_modules[\\/]lodash[\\/]/ }, commons: { name: 'chunk-commons', minSize: 1, //表示在壓縮前的最小模塊大小,默認值是 30kb minChunks: 2, // 最小公用次數 priority: 5, // 優先級 reuseExistingChunk: true // 公共模塊必開啓 } } } } }
運行 npm run build
打開 dist 文件夾裏自動生成的 index.html
在瀏覽器中打開 index.html 文件,打開控制檯也發現有輸出,OK,自動生成 HTML 文件成功
細心的朋友可能會發現一個問題,生成後的 HTML 文件引入的 JS 爲絕對路徑,可是真實的項目打完包以後都是部署在服務器上,用絕對路徑確定不行,由於你本地電腦的絕對路徑放在服務器上確定找不到
咱們要將引入的 js 文件從絕對路徑改成相對路徑
修改 webpack.config.js 文件
找到 output 輸出配置,更改 publicPath 公共路徑,修改成 ./
絕對路徑
output: { publicPath: './', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 },
再次打包,看打包後的 index.html 文件,打開瀏覽器測試,也是沒問題的
CSS 在 HTML 中的經常使用引入方法有 <link>
標籤和 <style>
標籤兩種,因此此次就是結合 webpack 特色實現如下功能:
下圖展現了此次的目錄代碼結構:
此次咱們須要用到 css-loader
,style-loader
等 loader,跟 babel 同樣,webpack 不知道將 CSS 提取到文件中。須要使用 loader 來加載對應的文件
css-loader:負責解析 CSS 代碼,主要是爲了處理 CSS 中的依賴,例如 @import 和 url() 等引用外部文件的聲明
style-loader 會將 css-loader 解析的結果轉變成 JS 代碼,運行時動態插入 style 標籤來讓 CSS 代碼生效。
npm i css-loader style-loader --save-dev
package.json
以下:
{ "scripts": { "dev": "webpack --mode development", "build": "webpack --mode production" }, "devDependencies": { "clean-webpack-plugin": "^2.0.0", "css-loader": "^2.1.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.5.0", "style-loader": "^0.23.1", "webpack": "^4.29.6", "webpack-cli": "^3.2.3" } }
更改配置文件
module.exports = { module: { rules: [ { test: /\.css$/, // 針對 .css 後綴的文件設置 loader use: ['style-loader', 'css-loader'] } ] } }
配置 module 中的 rules 屬性,和配置 babel 同樣,首先在 test 中使用正則來過濾 .css
文件,對 .css
文件使用 loader,'style-loader', 'css-loader'
在 base.css 中寫入樣式
*, body { margin: 0; padding: 0; } html { background: red; }
並在 app.js 中引入 base.css
import style from './css/base.css'
配置文件完整代碼:
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') // 引入插件 module.exports = { entry: { app: './src/app.js' }, output: { publicPath: './', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 }, module: { rules: [ { test: /\.css$/, // 針對 .css 後綴的文件設置 loader use: ['style-loader', 'css-loader'] // 使用 loader } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ // 打包輸出HTML title: '自動生成 HTML', minify: { // 壓縮 HTML 文件 removeComments: true, // 移除 HTML 中的註釋 collapseWhitespace: true, // 刪除空白符與換行符 minifyCSS: true // 壓縮內聯 css }, filename: 'index.html', // 生成後的文件名 template: 'index.html', // 根據此模版生成 HTML 文件 chunks: ['app'] // entry中的 app 入口才會被打包 }) ] }
項目打包,查看 dist 文件夾
發現並沒有生成 CSS 文件,可是打開 index.html 是有樣式的
緣由是:style-loader
, css-loader
兩個 loader 的處理後,CSS 代碼會轉變爲 JS,和 index.js 一塊兒打包
能夠發現是經過 <style>
標籤注入的 css
若是須要單獨把 CSS 文件分離出來,咱們須要使用 mini-css-extract-plugin 插件。
以前是使用 extract-text-webpack-plugin
插件,此插件與 webpack4 不太匹配,如今使用 mini-css-extract-plugin
確保將 webpack 更新到 4.2.0 版及以上。不然 mini-css-extract-plugin 將無效!
目前還不支持熱更新,也就是在開發環境下更改了 css,須要手動的刷新頁面纔會看到效果,目前這個插件通常在生產環境中使用,開發環境下仍是使用 'style-loader',具體見官網配置
npm i mini-css-extract-plugin --save-dev
更改配置文件:
const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { module: { rules: [ { test: /\.css$/, // 針對 .css 後綴的文件設置 loader use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader' ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css' }) ] }
完整代碼:
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 將 css 單獨打包成文件 module.exports = { entry: { app: './src/app.js' }, output: { publicPath: './', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 }, module: { rules: [ { test: /\.css$/, // 針對 .css 後綴的文件設置 loader use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ // 打包輸出HTML title: '自動生成 HTML', minify: { // 壓縮 HTML 文件 removeComments: true, // 移除 HTML 中的註釋 collapseWhitespace: true, // 刪除空白符與換行符 minifyCSS: true // 壓縮內聯 css }, filename: 'index.html', // 生成後的文件名 template: 'index.html', // 根據此模版生成 HTML 文件 chunks: ['app'] // entry中的 app 入口才會被打包 }), new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css' }) ] }
這樣只是生成了單獨的 css 文件,可是並無壓縮,引入 optimize-css-assets-webpack-plugin 插件來實現 css 壓縮
npm install optimize-css-assets-webpack-plugin --save-dev
完整代碼:
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 將 css 單獨打包成文件 const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') // 壓縮 css module.exports = { entry: { app: './src/app.js' }, output: { publicPath: './', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 }, module: { rules: [ { test: /\.css$/, // 針對 .css 後綴的文件設置 loader use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ // 打包輸出HTML title: '自動生成 HTML', minify: { // 壓縮 HTML 文件 removeComments: true, // 移除 HTML 中的註釋 collapseWhitespace: true, // 刪除空白符與換行符 minifyCSS: true // 壓縮內聯 css }, filename: 'index.html', // 生成後的文件名 template: 'index.html', // 根據此模版生成 HTML 文件 chunks: ['app'] // entry中的 app 入口才會被打包 }), new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css' }), new OptimizeCssAssetsPlugin({ assetNameRegExp: /\.css$/g, cssProcessor: require('cssnano'), //用於優化\最小化 CSS 的 CSS處理器,默認爲 cssnano cssProcessorOptions: { safe: true, discardComments: { removeAll: true } }, //傳遞給 cssProcessor 的選項,默認爲{} canPrint: true //布爾值,指示插件是否能夠將消息打印到控制檯,默認爲 true }) ] }
再打開 css 文件能夠發現已經被壓縮了,而且打開 index.html 也是有樣式的
安裝 sass 依賴:
npm i node-sass sass-loader --save-dev
在 src 文件夾下新增 scss 文件夾及 main.scss 文件
main.scss 引入樣式
$bgColor: black !default; *, body { margin: 0; padding: 0; } html { background-color: $bgColor; }
在 app.js 中引入 main.scss 文件
import './css/base.css' import './scss/main.scss'
修改配置文件
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 將 css 單獨打包成文件 const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') // 壓縮 css module.exports = { entry: { app: './src/app.js' }, output: { publicPath: './', // js 引用的路徑或者 CDN 地址 path: path.resolve(__dirname, 'dist'), // 打包文件的輸出目錄 filename: '[name].bundle.js', // 代碼打包後的文件名 chunkFilename: '[name].js' // 代碼拆分後的文件名 }, module: { rules: [ { test: /\.(scss|css)$/, // 針對 .scss 或者 .css 後綴的文件設置 loader use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader', 'sass-loader' // 使用 sass-loader 將 scss 轉爲 css ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ // 打包輸出HTML title: '自動生成 HTML', minify: { // 壓縮 HTML 文件 removeComments: true, // 移除 HTML 中的註釋 collapseWhitespace: true, // 刪除空白符與換行符 minifyCSS: true // 壓縮內聯 css }, filename: 'index.html', // 生成後的文件名 template: 'index.html', // 根據此模版生成 HTML 文件 chunks: ['app'] // entry中的 app 入口才會被打包 }), new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[id].css' }), new OptimizeCssAssetsPlugin({ assetNameRegExp: /\.css$/g, cssProcessor: require('cssnano'), //用於優化\最小化 CSS 的 CSS處理器,默認爲 cssnano cssProcessorOptions: { safe: true, discardComments: { removeAll: true } }, //傳遞給 cssProcessor 的選項,默認爲{} canPrint: true //布爾值,指示插件是否能夠將消息打印到控制檯,默認爲 true }) ] }
module.rules.use 數組中,loader 的位置。根據 webpack 規則: 放在最後的 loader 首先被執行,從上往下寫的話是下面先執行,從左往右寫的話是右邊先執行。
;['style-loader', 'css-loader', 'sass-loader']
執行順序爲 sass-loader --> css-loader --> style-loader
首先應該利用 sass-loader 將 scss 編譯爲 css,剩下的配置和處理 css 文件相同。
打包後再打開 index.html 文件會發現樣式已經被 main.scss 中寫的覆蓋了,處理 scss 成功
安裝 postcss-loader 和 autoprefixer 依賴
npm install postcss-loader autoprefixer --save-dev
給 src/scss/main.css
中添加這段代碼
.example { display: grid; transition: all 0.5s; user-select: none; background: linear-gradient(to bottom, white, black); }
有兩種方式來配置 postcss,第一種是直接寫在 webpack.config.js 中
module: { rules: [ { test: /\.(sa|sc|c)ss$/, // 針對 .sass .scss 或者 .css 後綴的文件設置 loader use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader', 'sass-loader', // 使用 sass-loader 將 scss 轉爲 css // 使用 postcss 爲 css 加上瀏覽器前綴 { loader: 'postcss-loader', options: { plugins: [require('autoprefixer')] } } ] } ] }
打包完以後,查看 dist/app.css 文件
第二種方式,在 webpack.config.js 同級目錄下,新建 postcss.config.js 配置文件
module.exports = { plugins: [require('autoprefixer')] }
同時在 webpack.config.js 中
module: { rules: [ { test: /\.(sa|sc|c)ss$/, // 針對 .sass .scss 或者 .css 後綴的文件設置 loader use: [ { loader: MiniCssExtractPlugin.loader }, 'css-loader', 'sass-loader', // 使用 sass-loader 將 scss 轉爲 css 'postcss-loader' // 使用 postcss 爲 css 加上瀏覽器前綴 ] } ] },
因爲 module 中的 rules 是倒着執行的,以上的執行順序是postcss-loader
->sass-loader
->css-loader
->MiniCssExtractPlugin.loader
`
`postcss-loader
要放在最下面,也就是第一個執行的 loader
在 css-loader 中使用 importLoaders 屬性
module: { rules: [ { test: /\.(sa|sc|c)ss$/, // 針對 .sass .scss 或者 .css 後綴的文件設置 loader use: [ { loader: MiniCssExtractPlugin.loader }, { loader: css - loader, options: { importLoaders: 2 } }, 'sass-loader', // 使用 sass-loader 將 scss 轉爲 css 'postcss-loader' // 使用 postcss 爲 css 加上瀏覽器前綴 ] } ] }
importLoaders: 2 表示:在一個 css 中引入了另外一個 css,也會執行以前兩個 loader,即 postcss-loader 和 sass-loader
參考:webpack 官網指南