webapck學習筆記
- 2020 年 3 月 30 日
- 筆記
該筆記是部落客學習webpack課程時做的筆記,裡面加了一些自己的一些理解,也踩了一些坑,在筆記中基本上都更正過來了,分享給大家,如果發現什麼問題,望告知,非常感謝。
1. 為什麼要學webapck
為什麼要學webpack
? webpack
有什麼作用?
由於項目一般比較大,為了項目的可維護性和可擴展性,我們一般需要把程式碼分成好多個模組,但是一個大型的項目模組有特別多甚至有幾千個,我們不可能通過手工引入這幾千個模組,需要藉助工具來管理我們的模組,webpack
就是一個這樣的工具。
類似 webpack
的工具還有 Gulp, browserify
,最火的是 webpack
,目前三大框架都在使用 webpack
作為腳手架工具。
webpack
和其它腳手架工具相比有哪些優勢?
-
Tree Shaking
-
懶載入
-
程式碼分割
-
……
2. webpack 初探
2-1 webpack 究竟是什麼?
總結: 使得瀏覽器認識 import 語句了,webpack 是一個模組打包工具。
沒有 webpack 之前瀏覽器不認識 import 語句,引入 js 文件時需要利用 script 標籤的形式在 html 文件中引入,這種做法有很多弊端,比如程式碼不利於維護,而且會顯得 html 文件比較亂。
有了 webpack 之後,瀏覽器可以識別 import 語句了,所以有些 js 文件可以在某個 js 文件中通過 import 的方式進行引入,而不需要在 HTML 文件中引入了。
經過這節課的學習之後會認為 webpack 是一個 js 的翻譯器,實際上是不對的,因為 webpack 只是翻譯 import 其它的語法它做不到翻譯。
其實根據在 webpack 中做的一些配置來理解 webpack 到底是什麼還是比較好理解的。webpack 首先要做的就是對輸入的各種類型的文件進行一些轉換,例如將 ES6 的程式碼轉換為 ES5 的程式碼,其次就是根據程式碼中各個模組之間的導入導出規則來打包實現程式碼中所寫的依賴關係。(這個再總結一下根據打包配置來說一來比較好說,二來可以顯示出你是用過 webpack 的。)
2-2 什麼是模組打包工具?
Bundler: 模組打包工具
怎樣理解模組打包工具呢?
從字面意義上就是可以將一些模組( js 文件)在一個 js 文件中引入,也就是將一些模組打包到一個 js 文件中,即模組打包工具。
通常我們在 react 或者 Vue 中使用的 import 屬於 ES Module 規範,對於 node 採用的 CommonJS 模組引入規範,還有 CMD,ADM 模組引入規範都是有效的。『
除此之外,webpack 還可以打包 css 文件等。
2-3 搭建 webpack 環境
-
創建一個項目,進入到這個項目中,運行
npm init
如果想直接使用默認配置可以加上-y
參數,即npm init -y
。為什麼要運行 npm init ?
和 git init 結合起來理解,如果不執行這條語句,接下來的操作不知道按照哪個規範來執行,執行完 npm init 之後就可以按照 npm 規範來管理這個項目了,由於 npm 是 node 開發的,也就是按照 node 的規範來管理這個項目。
-
全局安裝 webpack(不推薦使用,因為有可能有webpak3的項目)
npm install webpack webpack-cli -g
卸載全局安裝的 webpack。
npm uninstall webpack webpack-cli -g
在某個項目中安裝 webpack:
- 進入到那個項目中所在的目錄,然後
# --save-dev 表示 webpack和webpack-cli不會被打包線上環境中,只在開發環境中使用。這是合理的,因為線上環境需要的是webpack打包後的文件並不需要webpack這個工具了。 npm install webpack webpack-cli -save-dev # 等價於 npm install webpack webpack-cli -D # 自動安裝依賴包 npm install # 查看版本 webpack -v
這時候會出現 `bash: webpack: command not found`,原因就在於運行 webpack 時系統會去找全局的 webpack 。 但是我們可以通過 ```bash npx webpack
這種形式來運行 webpack 命令, npx
會幫助我們去找當前項目下的 webpack 命令(在 node_modules 中就有)。
安裝特定版本的 webpack 。
-
查看某個版本是否存在
# 顯示 webpack 現在可用的所有版本號,想查看其它的將 webpack 替換掉即可 npm info webpack
-
安裝特定版本的 webpack
npm install [email protected] webpack-cli -D
2-4 使用 webpack 的配置文件
使用 webpack 的配置文件可以在項目下新建 webpack.config.js 文件,對 webpack 進行配置。
如果沒有 webpack.config.js 文件,那麼在沒有人為指定入口文件時,運行 npx webpack
會報錯,因為 webpack 並不知道將哪一個文件作為入口文件。
當有 webpack.config.js 文件,在運行 npx webpack
時,webpack 會去查看webpack.config.js 文件的配置資訊,找到其中的入口文件等資訊,並按照其中的配置進行打包。
const path = require('path'); module.exports = { // 打包的模式,,默認的模式是 production,還有一種是 development 模式,該模式下生成的輸出文件不會被壓縮。 mode: 'production', // 入口文件也就是要打包的文件 entry: { main: './src/index.js', }, // 打包好文件的資訊 output: { filename: 'bundle.js', // 不能直接寫相對路徑,必須藉助node中的path模組 path: path.resolve(__dirname, 'dist'), } };
注意:
- output 中的 path 不能寫相對路徑只能利用 node 提供的 path 模組將其轉換為絕對路徑。
- 在定義 entry 時就算是相同的的目錄下,
./
也一定要寫上,如果不寫 webpack 會提示找不到入口文件。
在默認情況下,webpack 只會將 webpack.config.js 作為配置文件,如果想指定某個文件作為 webpack 的配置文件可以使用下面這條命令。
npx webpack --config 配置文件名
修改 webpack 打包的指令:
項目下的 package.json 文件中的 scripts 定義了一些常用命令的簡化形式。
"scripts": { // 注意在這裡寫 webpack 時,會先在本項目下找是否有 webpack,而並不會直接就去找全局的 webpack "bundle": "webpack" }
經過上面的修改之後,以後就可以利用 npm run bundle
命令來替代 npx webpack
命令。
2.5 淺談 webpack 打包知識點
運行打包命令後的輸出內容:
Hash: 0071acf88b3dabc88234 // 此次打包獨一無二的哈希值 Version: webpack 4.41.3 Time: 133ms Built at: 2019-12-18 4:19:29 PM // Chunks:存放的是輸出文件的id值,在大型項目中輸出文件不止一個,裡面還會存放與之相關聯的輸出文件的id值。 // Chunks Name:和Chunks同理,只是裡面存放的是名字,名字的來源就是entry中的屬性名。 Asset Size Chunks Chunk Names bundle.js 1.29 KiB 0 [emitted] main Entrypoint main = bundle.js [0] ./src/index.js + 3 modules 707 bytes {0} [built] | ./src/index.js 140 bytes [built] | ./src/header.js 185 bytes [built] | ./src/sidebar.js 191 bytes [built] | ./src/content.js 191 bytes [built]
3. webpack 的核心概念
3.1 什麼是 Loader?
loader 其實就是一個打包方案,由於 webpack 只能識別出 js 文件,本身只能對 js 文件進行打包,當遇到其它類型的文件時,loader 的作用就是告訴 webpack 針對這種類型的文件應該如何去進行打包。
以後只要遇到打包的文件不是 js 文件,第一要想到的就是需要引入相應的 loader 了。
// index.js const App = require('./App.js'); const JSPng = require('./JS.jpg'); console.log(JSPng);
現在有一張圖片需要打包,如果不引入相應的 loader 那麼 webpack 就會報錯。現在我們引入 file-loader
對圖片進行打包,需要先安裝 file-loader,配置文件修改如下。
// 安裝 fileloader npm install file-loader // webpack.config.js const path = require('path'); module.exports = { mode: 'development', entry: { main: './src/index.js' }, module: { rules: [{ test: /.(jpg|png|gif)$/, use: { loader: 'file-loader', } }] }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } };
打包的資訊如下:
Hash: a6218893dcb1a8e0e3e6 Version: webpack 4.42.0 Time: 985ms Built at: 2020-03-22 10:01:15 PM Asset Size Chunks Chunk Names 2b65174ed9bfd3d62c292c8f2d3171ab.jpg 19 KiB [emitted] bundle.js 4.63 KiB main [emitted] main Entrypoint main = bundle.js [./src/App.js] 20 bytes {main} [built] [./src/JS.jpg] 80 bytes {main} [built] [./src/index.js] 88 bytes {main} [built]
console.log(JSPng)
的結果是 Object [Module] { default: '2b65174ed9bfd3d62c292c8f2d3171ab.jpg' }
當使用 file-loader 打包圖片文件時,實際上它幫我們做了兩件事,首先它先將圖片移動到配置文件指定的目錄下,然後將圖片的url返回給 JSPng 變數。
const img = new Image(); img.src = JSPng; const root = document.getElementById('root'); root.append(img);
那張圖片會被顯示在頁面上。
3.2 使用 loader 打包靜態資源(圖片篇)
主要介紹了 file-loader
和 url-loader
這兩種方式來打包圖片資源。
url-loader
是將一個圖片轉換為 base64 字元串的形式,這個字元串解析完後就是這張圖片,img 標籤的 src 屬性的值是 base64 字元串,因此不用發起http請求就可以將圖片展示出來。但是問題是如果圖片很大,那麼這個js文件就非常大,那麼請求這個js文件過來再載入出來這張圖片的時間會很長。
所以,對於 url-loader
最合理的使用方法是在配置 webpack 時使用 limit 屬性,當圖片小於 limit 時,就按 url-loader
將圖片轉換為 base64 字元串的方式進行載入,如果大於 limit 就按 file-loader
將圖片的 url 賦值給 img 的 src 屬性的方式進行載入,但時候請求圖片時需要發送 http 請求。
在處理大於 limit 的圖片時可以說 url-loader
和 file-loader
是一模一樣的,所以就沒必要用 file-loader 了。
url-loader
webpack 配置:
module: { rules: [{ test: /.(jpg|png|gif)$/, use: { loader: 'url-loader', options: { // 設置打包後文件的名稱,[name]指打包後文件的名字是原文件的名字,[ext]指原文件的後綴,[hash]打包後文件名字上加一個hash值 name: '[name]_[hash].[ext]', // 將文件打包到指定的目錄,它的上一級路徑是output中設置的輸出路徑 outputPath: './images/', // 設定文件的大小,如果小於limit的值就按url-loader的方式去處理 // 如果大於 limit 的值那麼就會按 file-loader 的方式去處理。 // 單位為位元組(B) limit: 20480, } } }] }
當圖片小於設置的 limit 時會將圖片轉換為 base64 字元串,將 base64 字元串傳遞給 img.src 也可以在頁面上顯示出來。
3.3 使用 loader 打包靜態資源(樣式篇上)
css 文件的打包:
style-loader 和 css-loader 配合使用,css-loader 通過 css 文件之間的引用關係(例如使用@import語法在一個 css 文件中引入另一個css 文件)負責將 css 內容整合起來,style-loader 將整合好的 css 內容掛載到 html 文件 head 中的 style 上。由於掛載到了 style 上,因此頁面上的所有標籤都可以訪問這些樣式,這也說明了 css 樣式沒有局部作用域的概念。這一部分可以參考 聊一聊React中的CSS樣式方案這篇文章。
webpack.config.js
module: { rules: [{ test: /.(jpg|png|gif)$/, use: { loader: 'url-loader', options: { name: '[name].[ext]', outputPath: './images', limit: 10240 } } }, { test: /.css$/, use: ['style-loader', 'css-loader'] }] },
需要注意的一點是當遇到一種文件類型需要用到多個 loader 時,use 的值就應該是一個數組,而不是一個對象了。如果需要給某個 loader 加上 option 可以將相應的 loader 寫成對象的形式。
{ test: /.css$/, use: [ 'style-loader', { loader: 'css-loader', option: { importLoaders: 2 } } ] }
sass 文件的打包:
需要安裝 sass-loader
和 node-sass
這兩個 loader。
在配置文件中 style-loader , css-loader, sass-loader 結合使用。
在 webpack 中使用 loader 的順序是從下往上,從右到左。
module: { rules: [{ test: /.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] }
因此,sass-loader 先將 sass 文件轉換成 css 文件,css-loader 將 css 文件根據引用關係將 css 內容整合起來,進而交給 style-loader 將樣式掛載到 head 中的 style 標籤上。
為什麼要加廠商前綴
在標準還未確定時,部分瀏覽器已經根據最初草案實現了部分功能,為了與之後確定下來的標準進行兼容,所以每種瀏覽器使用了自己的私有前綴與標準進行區分,當標準確立後,各大瀏覽器將逐步支援不帶前綴的css3新屬性
目前已有很多私有前綴可以不寫了,但為了兼容老版本的瀏覽器,可以仍沿用私有前綴和標準方法,逐漸過渡 。
對於 css3 一些新增加的標籤我們一般會加上廠商前綴, postcss-loader
會自動幫我們加上廠商前綴。
CSS3 屬性自動添加廠商前綴方法:
-
安裝 postcss-loader。
npm i -D postcss-loader
-
創建 postcss.config.js 文件,做一些配置。
使用 autoprefixer 插件,安裝 autoprefixer,
npm install autoprefixer -D
// postcss.config.js module.exports = { plugins: [ require('autoprefixer')({ browsers: ['last 10 versions','Firefox >= 20','Android >= 4.0','iOS >= 8'] }) ] }
3.4 使用 loader 打包靜態資源(樣式篇下)
此時的 webpack 的配置文件如下:
module: { rules: [{ test: /.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader', 'postcss-loader', ]} }
當在 index.js
中引入 index.scss
文件後 webpack 的打包過程是這樣的:
從下往上,先是利用 post-loader 進行打包,緊接著是 sass-loader,依次向上。
現在有個問題是:
如果在 index.scss
中又引入了一個 scss 文件,即 @import blockChain.scss
,在對 index.scss 進行打包時,webpack 到了 css-loader 這一步,然後遇到 blockChain.scss 也需要打包,那麼這時候只能從 css-loader 進行打包,而不會從下面的 postcss-loader 進行打包。
那麼怎樣解決這個問題呢?
對 css-loader 進行配置:
上面的 webpack 配置文件修改為:
module: { rules: [{ test: /.scss$/, use: [ 'style-loader', { loader: 'css-loader', option: { importLoaders: 2 } }, 'sass-loader', 'postcss-loader', ]} }
importLoaders: 2
的含義是在遇到 .scss
文件時,再重新執行下面的兩個 loader。
import './index.scss'
和 import style from './index.scss' 有什麼區別?
前者是全局引入,頁面上的所有標籤都可以使用引入的樣式。
後者是 CSS Modules,引入了樣式局部作用域的概念,只在當前的組件中有用。
想要在 webpack 中配置 CSS Modules 需要在 css-loader 的 option 中配置 modules: true
。
{ loader: 'css-loader', option: { importLoaders: 2, // 模組化的 css,默認值為 false modules: true } }
通過 import './index.scss'
引入的樣式在整個模組中都會起作用,引入的樣式是全局的。比如在 index.js 中引入 index.scss 之後,在 index.js 文件中還引入了 createBlockChain.js 文件,那麼在 createBlockChain.js 文件中 index.scss 的樣式也會起作用。
在 index.js 中 import style from './index.scss'
,只會在 index.js 中起作用,而且只能通過 style.
的形式來使用樣式。
參考文章:聊一聊React中的CSS樣式方案
如何打包並使用字體圖標?
字體圖標的獲取就不再詳述了,阿里圖標庫。
-
解壓下載下來的字體圖標文件
-
將
.eot, .ttf, .svg, .woff
文件複製粘貼到src/font
文件夾下 -
將
iconfont.css
文件的內容複製粘貼到index.scss
文件中。 -
修改
index.scss
文件,主要是裡面的一些路徑不對。 -
index.scss
中最下面的類名就是字體圖標,將 html 元素的 class 賦值為 index.scss 的類名之後就可以使用相應的字體圖標了。import './index.scss'; const root = document.getElementById('root'); root.innerHTML = '<div class="iconfont icon-dengpao">abc</div>';
-
使用
file-loader
對eot, ttf, svg, woff
文件進行打包,對應的 webpack 配置文件如下:{ test: /.(eot|ttf|svg|woff)$/, use: { loader: 'file-loader', } }
3.5 使用 plugins 讓打包更便捷
plugin 可以在 webpack 運行到某個時刻的時候,去做一些事情,這一點類似於 React 和 Vue 中的生命周期函數,既然類似於函數,那麼在 webpack 配置文件中,plugin 需要首先被引入,在配置的時候需要 new Pluginin()生成一個對象,其實仔細觀察後會發現 webpack 中返回的都是對象,所以要實例化一個對象。
從 plugin 類似於函數這一點要理解和loader配置時的區別。
例如 html-webpack-plugin 這個插件就會在 webpack 打包結束的時刻,生成一個 index.html 文件。
html-webpack-plugin 的作用?
當我們把 dist 目錄刪除後,直接執行 npm run bundle
後在 dist 中並不會生成新的 index.html 文件,必須手動新建一個 index.html 文件。
使用 html-webpack-plugin 之後,再進行打包,在 dist 目錄下就會自動生成一個新的 index.html 文件,並把打包生成的 js 自動引入到這個 HTML 文件中。
<script type="text/javascript" src="bundle.js"></script>
在 webpack 配置文件中使用 html-webpack-plugin 時可以配置生成的 html 文件的模板,那麼在 html-webpack-plugin 生成 html 文件時就會在這個模板文件的基礎上再通過 script 標籤引入生成的打包文件。
這樣以後我們只需要提供相應的模板文件,不再需要關心打包文件的引入了,html-webpack-plugin 會自動結合兩者幫我們生成最終的 index.html 文件。
html-webpack-plugin 的使用流程
-
安裝相應的 html-webpack-plugin。
-
在 webpack 的配置文件中引入html-webpack-plugin,如
// webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin');
-
在 webpack 的配置文件中使用 html-webpack-plugin,並配置生成 html 文件時的模板文件。
modules: { ... }, plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })], output: { ... }
clean-webpack-plugin 有什麼作用?
在生成打包文件的目錄中,如果下一次生成的打包文件的名字變了,那麼之前生成的打包文件還是存在的,這樣有可能會導致一些問題。
clean-webpack-plugin 腳本可以在這次打包之前刪除上一次的打包文件。
你可以在 dist 目錄下自己人為建一個 1.js
,使用這個插件之後會發現再次打包之後這個文件就不見了。
使用流程和 html-webpack-plugin 是一樣的,只是注意新版的 clean-webpack-plugin 在webpack 的設置方式上發生了變化,參考部落格 clean-webpack-plugin 升級踩坑。
3.6 Entry 和 Output 的基礎配置
Entry 配置:
entry: { // 要打包的文件是 index.js,打包後文件的名字是 main.js(如果output中filename的值沒有設置那麼打包後文件的名字就是 main.js) main: './src/index.js', } // 上面的寫法等價於 entry: './src/index.js'
現在有一個需求,將 index.js 反覆打包兩次,第一次打包生成 main.js,第二次打包生成 sub.js。
entry: { main: './src/index.js', sub: './src/index.js' } output: { filename: '[name].js' }
簡單解釋一下:
main: './src/index.js'
會將 index.js 打包到 main.js 中,sub: './src/index.js'
會將 index.js 打包到 sub.js 中,output 中的 [name]
是一個佔位符(place holder),在這裡表示的是 main 和 sub。
現在又有一個需求,在現實開發中我們一般都需要將 js 文件上傳到 CDN 上,假設現在我們將 js 文件都需要上傳到 http://cdn.com.cn 上面,那麼我們希望 index.html 中利用 script 標籤引入打包後的文件時要在前面添加 http://cdn.com.cn
的前綴,應該如何實現?
output: { publicPath: 'http://cdn.com.cn', }
3.7 SourceMap 的配置
sourceMap 是一種映射關係,它能夠知道打包後的文件中的某行程式碼對應打包前哪個文件的哪行程式碼。
DellLee:這部分除了要掌握常見的 sourceMap 的配置方式還會問到 sourceMap 的原理。
假設你在書寫程式碼的過程中出錯了,瀏覽器中報錯時只會報打包後的文件中程式碼的出錯位置,但這並不是我們想要的,我們需要的是打包前的源文件是是哪行程式碼寫錯了。
通過 sourceMap 就可以滿足我們上面的需求,當使用 source-map 時瀏覽器中報錯就會提示打包前文件中程式碼的出錯位置。
如何開啟 sourceMap?
// webpack.config.js // none -> 不使用 sourceMap devtool: 'source-map',
inline-spurce-map 和 source-map 有什麼區別?
使用 source-map 時會在打包後的文件下生成一個 .map
文件。
使用 inline-source-map 會將這個 map 關係直接寫在打包後的文件中。
使用 sourceMap 是很消耗性能的,可以使用 inline-cheap-source-map , 如果出錯時它只會告訴你是第幾行出錯了,但不會告訴你具體是這行的哪個地方出錯了,其實這樣也就足夠了。
在 devtool 官方介紹中有很多 sourceMap 的選擇,在實際項目中怎樣進行選擇呢?
devtool | build | rebuild | production | quality |
(none) | fastest | fastest | yes | bundled code |
eval | fastest | fastest | no | generated code |
eval-cheap-source-map | fast | faster | no | transformed code (lines only) |
eval-cheap-module-source-map | slow | faster | no | original source (lines only) |
eval-source-map | slowest | fast | no | original source |
eval-nosources-source-map | ||||
eval-nosources-cheap-source-map | ||||
eval-nosources-cheap-module-source-map | ||||
cheap-source-map | fast | slow | yes | transformed code (lines only) |
cheap-module-source-map | slow | slower | yes | original source (lines only) |
inline-cheap-source-map | fast | slow | no | transformed code (lines only) |
inline-cheap-module-source-map | slow | slower | no | original source (lines only) |
inline-source-map | slowest | slowest | no | original source |
inline-nosources-source-map | ||||
inline-nosources-cheap-source-map | ||||
inline-nosources-cheap-module-source-map | ||||
source-map | slowest | slowest | yes | original source |
hidden-source-map | slowest | slowest | yes | original source |
hidden-nosources-source-map | ||||
hidden-nosources-cheap-source-map | ||||
hidden-nosources-cheap-module-source-map | ||||
hidden-cheap-source-map | ||||
hidden-cheap-module-source-map | ||||
nosources-source-map | slowest | slowest | yes | without source content |
nosources-cheap-source-map | ||||
nosources-cheap-module-source-map |
一般情況下,在 development 模式下,建議使用 cheap-module-eval-source-map
這種形式的 devtool。
devtool: 'cheap-module-eval-source-map',
因為提示的錯誤比較全面,打包速度也比較快。
在 production 模式下,建議使用 cheap-module-source-map
這種形式。
devtool: 'cheap-module-source-map',
錯誤提示比較全面,打包速度也比較快。
3.8 使用 webpackDevServer 提升開發效率
現在我們每次修改完程式碼之後都需要重新運行打包命令後,修改的程式碼才會生效,如何解決這個問題,提高開發效率呢?
三種方法:
1)修改 package.json 文件
// package.json "scripts": { "watch": "webpack --watch" }, // 執行打包命令 npm run watch
加上 --watch
參數再次運行 npm run watch
命令後,每當程式碼發生變化時,webpack 會自動監聽程式碼的變化,一旦有變化會自動打包。
2)使用 webpack-dev-server
-
當程式碼改變的時候不僅會重新打包,而且會重新刷新瀏覽器,上面 –watch 的方法並不會幫助我們重新刷新瀏覽器。
-
React,Vue 的官方腳手架工具都使用了 webpack-dev-server.
-
打包後的文件並不會存放在 webpack.config.js 文件中規定的輸出路徑中,而是存放在記憶體中,這樣做可以明顯地提升性能。
-
webpack 等配置文件改變後只有重啟伺服器之後才能生效。
使用方法
-
安裝 webpack-dev-server
-
修改 webpack 配置文件
// 配置 devServer // 也就是啟動一個webpack dev 伺服器 devServer: { // 伺服器的根路徑是 ./dist(裡面有index.html文件) contentBase: './dist', open: true, // 自動地打開一個瀏覽器並訪問這個伺服器的地址 port: 9000 },
-
兩種啟動 webpack-dev-server 的方法
-
找到 webpack-dev-server 運行
cd node_modules cd .bin webpack-dev-server
-
使用 npm
首先修改 package.json 文件
"scripts": { "bundle": "webpack", "dev": "webpack-dev-server" },
然後在命令行窗口中運行
npm run dev
。
如果自動打開瀏覽器顯示出內容,並且埠號是 9000,表示啟動成功。
-
3)手動實現一個感知程式碼變化自動打包的伺服器。
-
安裝 express, webpack-dev-middleware
npm run express
-
修改 package.json,在 script 中添加下面一行程式碼,就可以使用
npm run server
來啟動自己實現的伺服器了。scripts:{ .... "server": "node server.js" }
-
修改 webpack.config.js 文件。
添加下面這條語句:
output: { publicPath: '/', }
-
編寫 server.js
const express = require('express'); const webpack = require('webpack'); const webpackkDevMiddleware = require('webpack-dev-middleware'); const config = require('./webpack.config'); const path = require('path'); // webpack(config) 會返回一個編譯器,這個編譯器執行一次就會重新打包一次. const complier = webpack(config); const app = express(); // 只要程式碼發生變化,那麼就會執行 complier 重新將程式碼打包,並將打包好的程式碼 // 放在 localhost:3000 的 publicPath 下。 app.use(webpackkDevMiddleware(complier, { publicPath: config.output.publicPath, })); app.listen(3000, () => { console.log('server is running on 3000'); console.log(path.resolve(__dirname)); });
-
啟動服務
npm run server
幾個注意點:
-
webpack-dev-middleware
webpack-dev-middleware
是一個封裝器(wrapper),它可以把 webpack 處理過的文件發送到一個 server。app.use(webpackkDevMiddleware(complier, { publicPath: config.output.publicPath, }));
當程式碼發生變化時,complier 就會執行將程式碼重新打包,假設打包完成後生成的文件是 main.js , 這個時候 webpack-dev-middleware 會把 main.js 存放到伺服器 localhost:3000 的 publicPath 目錄下。
為什麼這裡的 publicPath 必須一定和 webpack 配置文件 output 中的 publicPath 相同呢?
從這個角度去理解,之前當 publicPath 是
http://cdn.com.cn
時,在script標籤中引入時,src='http://cdn.com.cn/main.js'
所以只有前綴和 webpack 配置文件中的 publicPath 一致時才能訪問到 main.js 文件。所以在這裡 webpack 配置文件中是/
, 所以 server.js 中也必須是/
。所以令publicPath = config.output.publicPath
可以保證兩者永遠是一致的。 如果 webpack 配置文件中的 publicPath 是
/test
那麼在瀏覽器中訪問時應該輸入localhost:3000/test
-
為什麼不會訪問 src 下的 index.html ?
注意瀏覽器中顯示的實際上是打包後新生成的文件 ,而不是 src 中的 index.html。
-
3.9 Hot module replacement 熱模組更新(1)
簡寫 HMR
什麼是熱更新?
我的理解就是無需刷新頁面當文件中的程式碼改變時頁面就會自動發生變化。例如樣式發生變化了,當前頁面也不需要刷新,將改變後的樣式應用到當前頁面即可。
下面說明的是當文件中的樣式程式碼發生變化時 HMR 帶來的變化
以下程式碼實現的是,點擊一下 add 按鈕新創建一個 item div元素,並且當是偶數時背景色為黃色。
// index.js import './index/css'; var btn = document.createElement('button'); btn.innerHTML = 'add'; document.body.appendChild(btn); btn.onclick = function() { var div = document.createElement('div'); div.innerHTML = 'item'; document.body.appendChild(div); } // style.css div:nth-of-type(odd) { background-color: yellow; }
使用 webpack-dev-server 時,當改變文件中的程式碼(例如將 style.css 中 background-color 改為 pink),webpack-dev-server 感知到文件中的程式碼發生了變化,會重新打包,頁面自然會重新刷新,那麼之前頁面上通過 add 按鈕新增的 div 標籤自然也就沒有了。
那有沒有什麼方法,在改變文件中的程式碼後,當前頁面的 DOM 元素不用變化呢?
有,用熱更新就可以實現。
熱更新可以實現在文件中的程式碼改變之後,在當前頁面不刷新的情況下根據文件中程式碼的改變作出改變,例如當文件中的樣式發生變化時,可以在當前頁面不刷新的情況下應用改變後的樣式。
在 webpack.config.js 中新增的程式碼如下:
// HotModuleReplacementPlugin 是 webpack 中的方法,所以需要引入 webpack const webpack = require('webpack'); devServer: { ... hot: true, // 使用 HMR 功能 hotOnly: true // 當 HMR 失效時不需要瀏覽器做其它的工作,失效就失效了,如果不開啟這個,當 HMR 失效時,瀏覽器可能會進行刷新等操作。 } plugin: [ ... new webpack.HotModuleReplacementPlugin() ]
下面說明的是當文件中的 JS 程式碼發生變化時 HMR 帶來的變化
利用熱更新實現改變 js 程式碼時不需要刷新頁面即可在頁面上展示改變後的結果
結合下面兩個 js 文件來驗證這個功能的實現。
counter.js
創建了一個 div 標籤,每次點擊時 innerHTML 的值加1.
number.js
創建了一個 div 標籤,innerHTML 是 1000。
// counter.js function counter() { var div = document.createElement('div'); div.innerHTML = '1'; div.setAttribute('id', 'counter'); div.onclick = function() { div.innerHTML = parseInt(div.innerHTML, 10) + 1; }; document.body.appendChild(div) } export default counter; // number.js function number() { var div = document.createElement('div'); div.innerHTML = '1000'; div.setAttribute('id', 'number'); document.body.appendChild(div); } export default number;
// index.js import number from './number.js'; import counter from './counter.js'; counter(); number();
webpack.confug.js 的配置和上面是一樣的,表明開啟了熱更新。
運行 webpack-dev-server (npm run dev)後,頁面上出現了 1 和 1000,現在點擊 1,發現數值再加 1 變為 2,當我們改變 number.js 中的 innerHTML 變為 2000後,發現頁面不會刷新,但是第二個值仍然是 1000,並沒有改為 2000.
這是因為當改變 JS 程式碼時還要加上下面這幾句程式碼熱更新才會生效。
index.js 中的程式碼如下:
// index.js import number from './number.js'; import counter from './counter.js'; counter(); number(); // 如果開啟了熱更新 if(module.hot) { // 第一個參數指的是依賴的文件,這裡是 number.js,如果 number.js 這個文件中的程式碼發生變化,那麼就會執行後面的回調函數。這裡要做的是,如果number文件發生變化,那麼需要先清除掉之前在頁面上顯示的那個div,然後再去執行number函數去產生一個修改值後的div顯示在頁面上。 module.hot.accept('./number', () => { // 先清除掉之前在頁面上的那個 DOM 元素 document.body.removeChild(document.getElementById('number')); // 將更改程式碼之後產生的 DOM 元素掛載到頁面上 number(); }) }
第 9 – 17 行程式碼是核心所在,改變樣式時不需要這幾行程式碼的原因是 webpack 中的 css-loader 幫我們實現了這幾行程式碼的功能。在 Vue,React 這些高級框架中都實現了 HMR,因此也不需要上述的程式碼,無論是樣式改變還是 JS 改變 HMR 都會生效。
注意有些文件的 loader 是不支援 HMR,這個時候上面的那幾行程式碼就需要自己編寫了。
3.11 使用 Babel 處理 ES6 語法(1)
目前有很多瀏覽器還並不支援或者只是支援部分的 ES6 語法,所以要想實現 ES6 的程式碼能夠在各個瀏覽器上運行,就需要藉助 Bable 將 ES6 的語法轉換為瀏覽器能夠支援的 ES5 程式碼。
在 Babel 官網 的 setup 中提供了許多實用 babel 的方法,其中就有如何在 webpack 中使用 babel。
在 webpack 中安裝 Babel
npm install --save-dev babel-loader @babel/core
babel-loader 在 webpack 中配置時需要用到,當遇到 js 文件時提供給 webpack 一份打包方案,而不是使用默認的 js 打包方案。
@babel/core 是 babel 的核心庫。
安裝好 babel-loader 之後,只是將 webpack 和 babel 建立起了連接,也就是遇到 js 文件時告訴 webpack 用 babe-loader 提供的方案進行打包,但是 babel-loader 並不能夠對 ES6 程式碼進行轉換,要想真正實現轉換需要藉助 @babel/preset-env ,安裝上它之後就可以在打包的過程中將 ES6 的程式碼轉換成 ES5 的程式碼。
npm install @babel/preset-env --save-dev
在 webpack.config.js 中的配置如下:
// webpack.congfig.js rules: [ ... { test: /.js$/, // 第三方的程式碼沒必要進行轉換,其實那些程式碼已經做好了轉換。 exclude: /node_modules/, loader: 'babel-loader', options: { presets: ["@babel/presets-env"] } } ]
經過上面的轉換後,ES6 的程式碼確實轉換成了 ES5 的程式碼,例如 const 語法轉化為了 var 語法,箭頭函數轉換為了普通函數。
但是 ES6 中新增的一些對象和方法是不能用 babel 進行轉換的,比如 Promise,經過轉換之後還是 Promise。

這個時候需要藉助 polyfill,來讓低版本的瀏覽器也能支援 Promise 等高級的用法。
Polyfill 是一塊程式碼(通常是 Web 上的 JavaScript),用來為舊瀏覽器提供它沒有原生支援的較新的功能。
比如說 polyfill 可以讓 IE7 使用 Silverlight 插件來模擬 HTML Canvas 元素的功能,或模擬 CSS 實現 rem 單位的支援,或 text-shadow,或其他任何你想要的功能。
安裝 polyfill
npm install --save @babel/polyfill
Because this is a polyfill (which will run before your source code), we need it to be a
dependency
, not adevDependency
因為 polyfill 實際上就是一塊 JS 程式碼用來模擬 ES6 中一些高級的語法,因此在線上環境中仍然需要這塊程式碼,因此不能將其設置為 devDependencies
。
webpack.config.js 配置:
{ test: /.js$/, // 第三方的程式碼沒必要進行轉換,其實那些程式碼已經做好了轉換。 exclude: /node_modules/, loader: 'babel-loader', options: { presets: [['@babel/preset-env', { // 用到什麼語法就轉換什麼語法,而不是所有的都進行轉換 useBuiltIns: 'usage' }]] } }
在不加 useBuiltIns 時,所有的 語法都會加進去,這樣無疑會使打包生成的 main.js 文件很大,加上 useBuiltIns 之後,程式碼中用到了什麼就去轉換什麼,比如上面的程式碼中用到了 Promise 那麼就去轉換 Promise,其它的沒有用到的並不會去轉換。
使用 polyfill:
// index.js import "@babel/polyfill";
因為 polyfill 實際上就是一塊 JS 程式碼用來模擬 ES6 中一些高級的語法,因此需要在文件中進行引入之後才能使用裡面的程式碼。
這樣引入之後 polyfill 的所有程式碼都在 index.js 文件中,經過上面 webpack 中 useBuilIns: 'usage'
配置之後,只有 polyfill 中被用到的語法才會被打包,這樣就減少了打包文件的大小。
3.12 使用 Babel 處理 ES6 語法(2)
設定瀏覽器的版本,讓 Babel 根據瀏覽器的版本進行轉換:
Babel 轉換後的程式碼需要在 chrome 67 及以上的版本上支援運行,由於 Chrome 67 版本對 ES6 語法已經支援地很好了,所以會發現 ES6 的語法幾乎不需要轉換,例如, const 等語法都不會被轉換。
// webpack.config.js { test: /.js$/, exclude: /node_modules/, loader: 'babel-loader', options: { presets: [['@babel/preset-env', { useBuiltIns: 'usage', targets: { chrome: '>67' } }]] } }
使用 polyfill 的形式不適合開發第三方庫,因為經過它轉換的 Promise 方法會通過全局變數的形式(因為在引入polyfill的時候是通過 import '@babel/polyfill';
形式在 index.js 中引入的,全局都會生效),會污染全局環境。
When setting useBuiltIns: ‘usage’, polyfills are automatically imported when needed. 當在webpack 中配置了 useBuiltIns: ‘usage’ 時不需要再人為地引入 @babel/polyfill。
所以在打包第三方庫的情況下,需要去換一種打包的方式。
藉助 plugin-transform-runtime 插件
安裝 plugin-transform-runtime
npm install --save-dev @babel/plugin-transform-runtime
安裝 runtime
npm install --save @babel/runtime
修改 webpack.config.js
test: /.js$/, // 第三方的程式碼沒必要進行轉換,其實那些程式碼已經做好了轉換。 exclude: /node_modules/, loader: 'babel-loader', options: { "plugins": [ [ "@babel/plugin-transform-runtime", { "absoluteRuntime": false, "corejs": 2, "helpers": true, "regenerator": true, "useESModules": false, "version": "7.0.0-beta.0" } ] ] } }]
總結: 開發類庫時使用 plugin-transform-runtime 插件,因為它不會污染全局環境,開發業務程式碼時可以使用 polyfill, 使用 polyfill 實際上是在 windows 對象上加了一些 Promise 等的屬性,所以它會污染全局環境。
一般 babel 的配置比較多,可以將 options 的內容單獨取出來放到根目錄下的 .babelrc
文件下,然後將 webpack babel 配置文件的 options 一項直接刪除掉即可。
3.13 webpack 實現對 React 程式碼的打包
首先安裝 react,react-dom,然後安裝 preset-react:。
# 因為 react,react-dom 在線上環境中也是需要的,所以要用 --save npm install --save react react-dom npm install --save-dev @babel/preset-react
修改 .babelrc
文件。
注意
.babelrc
文件中是不能有注釋的,因為它的格式是什麼都不清楚,用什麼符號注釋都不知道。
{ "presets": [ [ "@babel/preset-env", { "targets": { "chrome": "67" }, "useBuiltIns": "usage" } ], "@babel/preset-react" ] }
注意 webpack 的執行順序是從下到上,從右往左,所以在遇到 .js
(因為 babel 就是用來轉換 js 文件的)文件時,是先採用 @babel/preset-react
轉換 React 的程式碼,然後才是 @babel/preset-env
轉換 ES6 的程式碼。
3.14 webpack 核心概念總結
// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { // 打包的模式,默認的模式是 production, development 模式下打包後的程式碼不會被壓縮 mode: 'development', // none -> 不使用 sourceMap // cheap: 生成 source-map 只有行資訊,不帶列資訊,同時不需要包含 loader 裡面程式碼的 source-map,只需要生成業務程式碼的 source-map 就可以了。 // module:對 loader 裡面的業務程式碼也生成 source-map // eval: 一種執行方式 devtool: 'cheap-module-eval-source-map', // 配置 devServer, devServer可以使我們開啟一個伺服器當程式碼變化時,自動打包並刷新瀏覽器。 // contnetBase: 在哪一個目錄下去啟動伺服器 // open:啟動伺服器時會幫我們自動打開一個頁面 // port: 埠號 // hot: 是否啟用熱更新,true 啟用 // hotOnly: true 表示即使熱更新出現了問題也不幫我們刷新瀏覽器。 devServer: { contentBase: './dist', open: true, port: 8080, hot: true, hotOnly: true }, // 入口文件也就是要打包的文件 entry: { // main 指的是打包後文件的名字,如果output配置中有指定filename那麼就用output中指定的名字。 main: './src/index.js', }, // module: 遇到一個文件(模組)時怎樣去打包,告訴webpack遇到哪種文件時應該如何去打包。 // rules: webpack 進行打包的規則 // test: 後面跟的是一種文件類型 // use: 使用哪些loader // loader:使用哪一種loader module: { rules: [{ test: /.css$/, use: [ 'style-loader', 'css-loader', 'postcss-loader' ] }, { test: /.(eot|ttf|svg|woff)$/, use: { loader: 'file-loader', } }, { test: /.js$/, // 第三方的程式碼沒必要進行轉換,其實那些程式碼已經做好了轉換。 // 使用 babel-loader 時,options 項可以寫在根目錄下的 .babelrc 文件下 // exclude 除去什麼模組 exclude: /node_modules/, loader: 'babel-loader', }] }, // plugin: 插件 // HtmlWebpackPlugin 自動生成一個 index.html 文件 // CleanWebpackPlugin 自動刪除上一次打包生成的文件 // webpack.HotModuleReplacementPlugin 實現 HMR(熱更新)功能 plugins: [ new HtmlWebpackPlugin({ template: 'src/index.html' }), new CleanWebpackPlugin({ cleanStaleWebpackAssets:true, //自動刪除未被使用的webpack資源 }), new webpack.HotModuleReplacementPlugin() ], // 打包好文件的資訊 // publicPath: 打包後文件的前綴,如 /main.js // filename: 打包後的文件名,[name] 是一個佔位符,[name]的值就是前面entry中的main // path: 生成的打包文件的存放路徑,注意必須藉助node中的path模組。並且在使用 npx webpack 執行打包時才會生成dist目錄 // 在使用 dev-server 時生成的打包文件直接保存在記憶體中,不會存放在dist目錄下。 output: { publicPath: '/', filename: '[name].js', // 不能直接寫相對路徑,必須藉助node中的path模組 path: path.resolve(__dirname, 'dist'), } };
4. webpack 高級概念
4.1 Tree Shaking
什麼是 Tree Shaking?
舉例說明吧:
// math.js export const add = (a, b) => { console.log(a + b); }; export const minus = (a, b) => { console.log(a - b); }; // index.js import { add } from './math'; add(1, 2);
在 math.js 中有兩個方法,在 index.js 中只使用了一個方法,但是在打包生成的打包文件中這兩個方法的程式碼都被打包進去了,這顯得有點沒必要。
那能不能用到了什麼就打包什麼呢?
可以的,TreeShaking 就可以實現,math.js 中的內容可以比作一個樹形結構,通過 TreeShaking 將 minus 這個樹枝 shaking 掉,這樣打包的時候就只打包 add 這個方法就好了。
TreeShaking 的作用就是只打包用到的東西,沒有用到的東西不會出現在打包後的文件中。
當 TreeShaking 發現 import 語句時就會去看從哪個被引入的模組中引入了什麼東西到當前的模組中。如:
import './style.css'; // 並未從 style.css 中導出任何東西,TreeShaking 會直接將 style.css 忽略 import { add } from './math' // 從 math.js 中導出了 add 方法,TreeShaking 會將 math.js 中的 add 方法打包到文件中,其它的東西都將被忽略。
如何實現 TreeShaking ?
TreeShaking 只支援 ES Module 的模組引入方式(也就是只支援 import 這種模組引入方式),TreeShaking 只支援靜態引入的方式,CommonJS 模組化是動態引入的方式。
關於 ES Module 與 CommonJS 模組化的區別見《ES6標準入門》P457。
在 development 模式下:
-
webpack.config.js 文件中添加
optimization: { usedExports: true }
-
package.json
一般會直接寫
*.css
,除了樣式文件還有例如@babel/polyfill
同樣沒有導出內容但是不能被忽略。{ ... "sideEffects": [ "@babel/polyfill", "style.css", ] ... }
設置 TreeShaking 的忽略模組,就像前面講的當 `import './style.css'` 時由於沒有導出任何東西,所以 TreeShaking 直接會將其忽略,打包文件中不會出現 `style.css`,這是我們不希望看到的。 `sideEffects` 數組中的模組就是 TreeShaking 忽略的模組,即使沒有導入任何東西也會被打包到打包文件中。 設置完成之後再進行打包之後,main.js 中會發生下面的變化: ```js /*! exports provided: add, minus */ /*! exports used: add */
註:
1)在 development 模式下,TreeShaking 不會將沒有使用的程式碼忽略掉,而是也將其打包到打包文件中,只是會有提示一共導出了哪些,哪些被使用了。
提示方式:
/*! exports provided: add, minus */ /*! exports used: add */
2)在 production 模式下,TreeShaking 會直接將沒有使用程式碼忽略,不會將其打包到打包文件中。
在 production 模式下:
-
webpackage.config.js 中不需要加 optimization
-
package.json
{ ... "sideEffects": [ 'style.css', ] ... }
-
再次打包後,minus 的程式碼將不會打包到打包文件中。
4.2 Development 和 Production 模式的區分打包
Development 模式下程式碼不會被壓縮,Production 模式下程式碼會被壓縮,Development 模式下 source map 需要詳細一些,Production 模式下比較簡單就可以
如果 development 和 production模式都是使用的 webpack.config.js 這個文件,那麼在打包的時候,如果現在要有 development 模式轉為 production 模式,那麼 webpack.config.js 會改動比較多,而且不方便。
現在創建 webpack.dev.js 和 webpack.prod.js 這兩個文件來分別配置兩種模式下的打包就不會出現上面的問題了。
接下來需要配置 package.json
"scripts": { "dev": "webpack-dev-server --config webpack.dev.js", "build": "webpack --config webpack.prod.js" },
--config
後面跟著的表示使用哪一個 webpack 配置文件。
那麼以後
npm run dev # 啟動 development 模式下的打包 npm run build # 啟動 production 模式下的打包
現在有個問題是 webpack.dev.js 和 webpack.prod.js 重複的程式碼有很多,現在建立一個 webpack.common.js 來存放兩者共同的程式碼,然後再利用 webpack-merge 庫將它們結合起來。
npm install webpack-merge -D
webpack.dev.js
const webpack = require('webpack'); const merge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); const devConfig = { mode: 'development', devtool: 'cheap-module-eval-source-map', devServer: { contentBase: './dist', open: true, port: 9000, hot: true, // hotOnly: true }, plugins: [ new webpack.HotModuleReplacementPlugin() ], optimization: { usedExports: true }, }; module.exports = merge(devConfig, commonConfig);
webpack.prod.js
const merge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); const prodConfig = { mode: 'production', devtool: 'cheap-module-source-map', }; module.exports = merge(prodConfig, commonConfig);
webpack.common.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { entry: { main: './src/index.js', }, module: { rules: [{ test: /.js$/, exclude: /node_modules/, loader: 'babel-loader', },{ test: /.(jpg|png|gif)$/, use: { loader: 'url-loader', options: { name: '[name].[ext]', outputPath: './images', limit: 10240 } } }, { test: /.css$/, use: [ 'style-loader', 'css-loader', ] }, { test: /.(eot|ttf|svg|woff)$/, use: { loader: 'file-loader' } }] }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html' }), new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [ path.resolve(__dirname, 'dist'), "**/*", "!images", ], }), ], output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), } }
一般會建一個 build 文件夾,然後將這三個配置文件放在裡面。
看到這有沒有發現點什麼,vue-cli 腳手架工具自動生成的 Vue 項目中就有 build 這個文件夾,而且裡面也有這三個文件。
4.3 webpack 和 Code Splitting(1)
lodash: 一個一致性、模組化、高性能的 JavaScript 實用工具庫。
一般我們會將 lodash 起名為 _ ,即
import _ from 'lodash'
。
所謂程式碼分割就是將一部分程式碼單獨寫在一個 js 文件中,如果某個文件比較大,就可以考慮使用程式碼分割,如果有的程式碼會經常改變,而有的程式碼不會經常改變也比較適合利用程式碼分割。
舉例說明這樣做的意義:
// index.js import _ from 'lodash'; // 假設大小為 1 MB 業務程式碼 // 假設大小為 1 MB
- 在首次訪問時, index.js 文件的大小為 2 MB,需要載入的大小是 2 MB
- 業務程式碼改變用戶再次訪問時,index.js 的大小為 2 MB,需要載入的大小還是 2 MB
現在做一點改變:
webpack.common.js 中 將 entry 添加 lodash: 'src/lodash.js'
。
// src/index.js 業務程式碼 // 假設大小為 1 MB // src/lodash.js import _ from 'lodash'; window._ = _; // 以後在其它文件中使用 _ 就可以使用 lodash 庫了。
-
首次訪問時,index.js 1 MB,lodash.js 1 MB , 需要載入的大小是 2 MB,而且此時可以進行並行載入,速度一般會比上面的快。
-
業務程式碼改變用戶再次訪問時,index.js 1 MB,由於 lodash.js 文件並沒有發生變化,所以無需再次載入,因為瀏覽器的快取中有,所以此次只需載入 1 MB。
程式碼分割和 webpack 是沒有什麼關係的,上面實現的程式碼分割使我們人為地去實現的,不夠智慧,在 webapck 中使用自帶的 SplitChunksPlugins 就可以很容易地實現程式碼分割。
**在 webpack 中使用 SplitChunksPlugins 實現程式碼分割的兩種方式 **
這一部分總結的有點亂。
-
同步程式碼:只需要在 webpack.common.js 中做 optimization 的配置即可。
// splitChunks 是 webpack 中自帶的,無需導入。 optimization: { splitChunks: { chunks: 'all' }, }
-
非同步程式碼(利用 import 語法引入的組件或庫)會自動進行程式碼分割
// 引入 lodash 之後將其賦給 _,並執行後面的函數 function getComponent() { return import('lodash').then(({default: _}) => { ... }) }
import 非同步語法之前沒接觸過,補上。
要想使用 import 非同步語法需要安裝 dynamic-import-webpack。
npm install babel-plugin-dynamic-import-webpack -D
4.5 SplitChunksPlugin 配置參數詳解
總結了一篇部落格:
4.6 Lazy Loading 懶載入, Chunk 是什麼?
懶載入通俗點講就是當用到了之後再去載入,即按需載入。
舉例說明:
實現當點擊頁面時才載入 lodash.
function getComponent() { return import(/* webpackChunkName:"lodash" */ 'lodash') .then(({ default: _}) => { var element = document.createElement('div'); element.innerHTML = _.join(['happy', 'Coding'], '_'); return element; }) } document.addEventListener('click', () => { getComponent().then(element => { document.body.appendChild(element); }); });
上面的程式碼實現的就是一個懶載入的過程,當點擊頁面時才開始載入 lodash.
懶載入有什麼好處?
只有當用到了才會載入這無疑提升了性能,避免了無用資源的載入。
註:
我在實現懶載入時,index.html 文件中會有兩個 script 標籤不知道為什麼?這樣實際上並沒有實現懶載入,因為一開始的時候兩個打包文件就都載入了,檢查了一下配置文件也不清楚是什麼原因?主要是不清楚怎樣設置 index.html 中的 script 標籤的數量。
什麼是 chunk?
在進行打包之後會生成很多 js 打包文件,每個 js 打包文件都被稱為是一個 chunk。
打包時輸出資訊:
4.8 打包分析,Preloading,Prefectching
什麼是打包分析?
對生成的打包文件藉助一些分析工具來進行分析,檢查打包是否合理。
打包分析流程:
-
配置 package.json
添加
--profile --json > stats.json
參數,目的是為了生成 stats.json 文件。"scripts": { "dev-build": "webpack --profile --json > stats.json --config build/webpack.dev.js" }
-
藉助分析工具,導入 stats.json 文件獲得結果,常用的分析工具如下:
關於 Preloading 和 Prefetching 參考下面總結的這篇部落格:
聊一聊 webpack 中的 preloading 和 Prefetching
4.9 CSS 文件的程式碼分割
用到的腳本是:MiniCssExtractPlugin。
如果不使用這個腳本就會將 css 文件一起打包到 js 文件中。
關於具體的使用可以參考MiniCssExtractPlugin 的介紹,就不再說了,有一點需要注意的是, TreeShaking 要除去 css 和 scss 文件,因此在 package.json 中應該添加以下配置。
"sideEffects": [ "*.css", "*.scss" ],
4.10 webpack 與瀏覽器快取(Caching)
舉例說明這一小節中解決的問題:
import _ from 'lodash'; import $ from 'jquery'; const dom = $('<div>'); dom.html(_.join(['hello', 'webpack']), ' '); $('body').append(dom);
當瀏覽器訪問過一次後,就會在在快取中留下 main.js 和 vendors~main.chunk.js 文件,這時候如果我改變源程式碼,因為我再次打包後生成的文件名是不會發生變化的,所以瀏覽器普通刷新之後是不會重新向伺服器請求更改過後的文件的,因為它會直接利用瀏覽器中的文件,名字還是那個名字,它會認為伺服器中的文件沒有發生變化,這就有問題了。
在 webpack 中通過給文件名中加一個獨一無二的哈希值來實現這個問題,當文件發生變化時這個哈希值就會發生變化否則不會發生變化。
webpack 的配置如下:
output: { filename: '[name].[contentHash].js', chunkFilename: '[name].[contentHash].chunk.js', path: path.resolve(__dirname, '../dist'), }
在名字後面加上一個 contentHash 即可。
4.11 Shimming 的作用
shim: n. 楔子;墊片;填隙片 v. 用填隙片填入
還是以一個例子來引出 Shimming 的作用:
// jq.ui.js function ui() { $('<div>hello webpack</div>').appendTo($('#root')); } export default ui; // index.js import $ from 'jquery'; import ui from './jq.ui'; ui();
上面的程式碼是不能正常運行的,會提示 $ is not defined.
原因就在於,在一個模組中定義的變數只能在當前的模組中起作用,程式碼中 index.js
中引入的 $
變數並不能在 jq.ui.js
中起作用。
下面有一種解決方案就是在 index.js 中:
// index.js import $ from 'jquery'; import ui from './jq.ui'; window.$ = $; // 這樣無論在哪個模組中都能夠使用 $ 了。 ui();
這一種並不是本節想說的,本節說的是通過在 webpack 中增加配置的方法解決這個問題。
// webpack.common.js new webpack.ProvidePlugin({ $: 'jquery', _join: ['lodash', 'join'] }),
通過上面的配置之後,$
就可以在全局使用了,和下面程式碼實現的功能類似
import $ from 'jquery'; window.$ = $;
_join
和下面的程式碼功能類似:
import _ from 'lodash'; window._join = _.join;
上面實現的只是墊片的一種,實際上墊片是一種思想,下面這種實現模組中的 this 指向 window 也是墊片應用的一種。
老樣子還是用程式碼來說明:
// index.js console.log(this === window)
false, 因為 this 指向的是當前這個模組並不是全局 window 對象。
使用 imports-loader
腳本,配置如下:
// webpack.common.js { test: /.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader', }, { loader: 'imports-loader?this=>window' } ] }
經過這樣的配置之後每個模組中的 this 就都指向 window 對象了。
4.12 環境變數的使用
這一小節主要講了如何通過 env 這個環境變數來區分不同的環境,實現的功能就是通過不同的通過傳入不同的 env ,在都使用 webpack.common.js 的情況下實現不同環境下的程式碼打包。
實現的主要程式碼如下:
// webpack.common.js import merge from 'webpack-merge'; import prodConfig = './webpack.prod.js'; import devConfig = './webpack.dev.js'; const commonConfig = { ... }; export default = (env) => { if (env && env.production) { return(merge(prodConfig, commonConfig)); }else { return(merge(devConfig, commonConfig)); } } // package.json scripts: { 'dev-build': 'webpack --config build/webpack-common.js' 'dev': 'webpack-dev-server --config build/webpack-common.js' 'build': 'webpack --env.production --config build/webpack-common.js ' }
·dev-build
和 dev
都沒有傳 --env.production
參數,所以會 return(merge(prodConfig, commonConfig));
build
都傳了 --env.production
默認值為 true
, 所以會 return(merge(devConfig, commonConfig));
5. Webpack 實戰配置案例講解
5.1 Library 的打包
之前都是業務程式碼進行打包,如果現在想要開發一個庫,那怎樣對庫進行打包呢?
還是結合程式碼來說明:
-
新建 library 文件夾
-
npm init -y 初始化,生成 package.json 文件
-
安裝webpack
npm install webpack webpack-cli --save
-
新建 src 文件夾,添加 math.js 文件, index.js 文件.
// src/math.js export default function add(a, b) { return a + b; } // src/index.js import * as math from './math'; export default math;
-
新建 webpack.config.js 文件
const path = require('path'); module.exports = { mode: 'production', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'library.js', library: 'lib', libraryTarget: 'umd' } };
-
打包
npm run build
有幾點需要注意:
-
用戶引用庫的方式有很多:
// ES Module import library from 'library'; // CommonJS const library = require('library'); // AMD require(['library'], function { })
想讓用戶能夠以各種各樣的方式引用我們的庫,需要多加一條配置,即在 webpack.config.js 中的 output 中添加
libraryTarget: 'umd'
。 -
如果用戶想通過
<script src='library.js'></script>
引入庫,並且通過lib
就可以使用我們的庫,我們還需要添加一條配置。output: { ... library: 'lib', ... }
-
由於我們想讓用戶使用的是打包後的 library 文件,所以需要將 package.json 文件下的main 修改為我們打包好的文件。
"main": "./dist/library.js",
-
發布到 npm 上
-
npm 官網 上註冊一個帳號,並登錄。
-
在 library 文件夾下打開命令行介面,添加 npm 賬戶。
npm adduser
輸入完此命令後會讓你填寫 用戶名,密碼,郵箱。
-
登錄到 npm。
npm login
-
修改 package.json 文件中的 name 屬性,因為不能和 npm 上已有的包重名。
-
發布
npm publish
-
5.2 PWA 的打包配置
先簡單說一下 PWA 吧,PWA 即漸進式 web 應用,當用戶瀏覽過一次網頁之後能夠將網頁內容快取下來,即使伺服器掛掉了,用戶依然可以訪問那個網頁。
PWA 打包配置的流程:
-
安裝 workbox-webpack-plugin
npm install workbox-webpack-plugin -D
-
配置 webpack.prod.js
const workboxPlugin = require('workbox-webpack-plugin'); plugins: [ new workboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true }) ],
-
編寫 index.js
// index.js if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('service-worker registed'); }).catch(error => { console.log('service-worker register error'); }) }) }
上面程式碼要表達式的意思是: 如果 navigator 中有 serviceWorker 那麼就在 window 上註冊一個 load 事件,等頁面載入完後,如果 serviceWorker 註冊成功,那麼就快取內容並列印
service-worker registed
,如果失敗就列印service-worker register error
。 -
安裝 http-server
npm install http-server -D
配置 package.json
"scripts": { "start": "http-server dist", ... }
真正的開發過程中是將我們打包生成的 dist 目錄下的文件放到伺服器上,現在我們將 http-server 運行在 dist 目錄下來模擬。
-
在瀏覽器中訪問
localhost:8080
,然後關掉http-server
,刷新瀏覽器再次訪問 localhost:8080 你會發現依然可以正常訪問,這就是 PWA 的作用。這個地方我有一個疑問就是,就算沒有 PWA 不是也有快取嗎,不是照樣可以訪問嗎?
我現在的理解是這樣的,當沒有 PWA 的時候就算本地有快取,也是需要向伺服器上查看是不是訪問的文件有變化,訪問的時候由於伺服器掛掉了,所以沒辦法訪問,所以並不會訪問本地的快取。
而 PWA 的機制時當訪問不到伺服器時就訪問之前的快取,這也許就是兩者的區別吧。
不是很確定,需要再進行核實。
5.3 TypeScript 的打包配置
先說一下 TypeScript 的打包流程;
-
新建一個 type-script 的文件夾,並運行
npm init -y
進行初始化 -
創建一個 src 文件夾,新建一個 index.ts 文件。
import * as _ from 'lodash'; class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { // return "hello, " + this.greeting; return _.join(['hello', ' ', this.greeting], ' '); } } let greeter = new Greeter('world'); alert(greeter.greet());
-
由於是 .ts 文件,所以 webpack 並不知道應該如何去打包,所以需要引入
ts-loader
來告訴 webpack 應該如何去打包 .ts 文件,因此創建一個 webpack.config.js 並進行以下配置。const path = require('path'); module.exports = { mode: "production", entry: './src/index.ts', // 使用 ts-loader module: { rules: [{ test: /.ts?$/, use: 'ts-loader', exclude: /node_modules/ } ] }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } };
-
這個時候打包會報錯,原因在於對 .ts 文件打包需要一個 tsconfig.json 。
// 注意 json 文件是不能進行注釋的 { "compilerOptions": { "module": "es6", // 模組的規範是 ES6 模組規範,也就是使用 import 這種形式引入模組 "target": "es5", // 轉換成 ES5 的程式碼 "allowJs": true, // 允許在 ts 文件中使用 js 的模組 }, }
-
這個時候打包是沒有問題了,但是有個問題在於當按在 js 文件那樣引入模組時,ts 並不會給予語法上的提示,不會報錯,(例如函數的參數要求是 number ,傳入的是 string,ts 文件也不會給出錯誤)這顯然就失去了我們使用 ts 的初衷。
解決方法是,引入相應 js 模組的類型文件,例如 lodash,需要
npm install @types/lodash -D
。關於哪些 js 模組有相應的 types ,可以在 TypeSearch 中搜索。
5.4 使用 WebpackDevServer 實現請求轉發
前後端聯調時伺服器是不同的,在開發過程中前端使用的是後端開發測試的伺服器,在真正上線後使用的是線上的伺服器,很明顯這兩個伺服器的地址是不同的。
這樣的話就會有一個問題,如果向服務端請求數據時使用絕對路徑,那麼在由開發轉到線上時就要去更改使用絕對路徑的程式碼,這樣是很不方便的。那麼我們可不可以使用相對路徑,然後通過一個代理來實現使用相對路徑訪問的是我們想訪問的伺服器呢,答案是肯定的。使用 WebpackDevServer 實現請求轉發就可以做到。
實現流程:
-
配置 dev-server
// webpack.config.js devServer: { ... proxy: { '/react/api': 'http://www.dell-lee.com' } }
經過上面的配置之後,當在 axios 中請求
/react/api
時就相當於請求的是http://www.dell-lee.com
這台伺服器。 -
index.js 中請求數據
import axios from 'axios'; axios.get('/react/api/header.json') .then((res) => { console.log(res); });
我們實際要訪問的絕對路徑是 http://www.dell-lee.com/react/api/header.json
,經過上面的配置之後,遇到 /react/api
時就會去請求 http://www.dell-lee.com
這台伺服器,因此實際訪問的地址就是 http://www.dell-lee.com/react/api/header.json
。
現在有一個問題是如果後端告訴你目前 header.json 暫時不能用,你先用 demo.json 吧,由於header.json 是前後端商量好後在線上時要使用的介面,所以一般不要更改這裡的 header.json,可以通過下面的配置來實現路徑的重寫,只需這樣配置 WebpackDevServer :
// webpack.config.js proxy: { '/react/api': { target: 'http://www.dell-lee.com', pathRewrite: { 'header.json': 'demo.json' } }, }
這樣在請求 header.json 時實際上請求的是 demo.json。
注意:
這裡配置的 proxy 是 WebpackDevServer 中的,只有在開發環境下才會去使用 WebpackDevServer,是為了方便開發。
真正到了線上,請求 /react/api/header.json
就是去請求線上伺服器的 /react/api/header.json
。
在之前開發不使用 webpack 作為打包工具的時候還需要使用 charles 類似的工具,使用 webpack 之後完全可以替代這些軟體,而且功能也很強大。
5.5 WebpackDevServer 解決 React 單頁面應用路由的問題
流程:
-
安裝 react-router-dom
npm install react-router-dom -save
-
index.js 文件中,使用 react 路由,並且處理路由邏輯.
import React, { Component } from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; import ReactDom from 'react-dom'; import Home from './home'; import List from './list'; class App extends Component { render() { return ( <BrowserRouter> <div> // 當訪問根路徑時,展示 Home 這個組件 <Route path="/" exact component={Home} /> // 當訪問 /list 時,展示 List 這個組件 <Route path="/list" component={List} /> </div> </BrowserRouter> ); } } ReactDom.render(<App />, document.getElementById('root'));
程式碼中 Home 和 list 組件一個在頁面上顯示 Home, 另一個在頁面上顯示 List。
當我們訪問
http://localhost:8080/list
時,後端以為我們要訪問的是 list.html 這個文件,但是這個文件在後端中並不存在,所以顯示找不到頁面。那怎樣去解決這個問題呢?
通過配置 WebpackDevServer 就可以解決這個問題。
-
配置 WebpackDevServer
devServer: { ... historyApiFallback: true, ... }
當進行了上面的配置之後,只要是訪問這台伺服器,無論訪問哪一個文件,都會將訪問的地址變為訪問根路徑,也就是訪問根路徑下的
index.html
這個文件,然後再通過路由處理使得訪問真正想要訪問的頁面,這就是為什麼稱為單頁面應用的原因。關於
historyApiFallback
的配置可以是一個對象,可配置的參數還有很多,但是一般在平時的開發中使用historyApiFallback: true
就可以了。注意:
上面的配置是在 WepackDevServer 中配置的,只在開發環境中有效,因此想要在線上實現相同的效果,需要後端的小夥伴在後端的伺服器上進行類似的配置,也就是將用戶請求的路徑都變為請求 index.html 。
其實,只要是在 WevpackDevServer 中進行的配置,是為了模擬後端伺服器的環境,真正到了線上的環境都是需要後端的小夥伴在後端的伺服器上進行相應的配置的。
5.6 ESLint 在 Webpack 中的配置(1)
ESLint 本身和 Webpack 是沒有多大的關係的。
在 webpack 中配置 ESLint 的流程:
- 安裝 ESLint 。
npm install eslint -D
-
配置 ESLint ,生成 ESLint 的配置文件,利用
npx eslint --init
可以根據嚮導進行配置快速生成 eslint 配置文件。註:在 windows 下運行
npx eslint --init
時使用 window 自帶的命令行工具比較靠譜,使用 GitBash 總是不知道選項選沒選上。 -
使用 ESLint 。
有兩種方式:
-
使用
npx eslint 要檢查的文件或目錄
,在命令行找那個就會顯示出檢查結果。缺點,只在命令行中顯示錯誤,需要根據命令行中的提示一行一行地去找錯誤然後再在程式碼中去更改。
-
使用 eslint 插件
-
5.7 ESLint 在 Webpack 中的配置(2)
前面兩種方式中,第一種方式查找錯誤比較費事,第二種方式在有些編輯器中使用無法安裝插件比如 sublime,那怎樣來解決這個問題呢?
還是使用 webpack ?
藉助 webpack 中的 eslint-loader
, wbpack 的配置文件如下:
use: [ 'babel-loader', 'eslint-loader' ]
即,在遇到 js 文件時,先使用 eslint-loader 檢查是否有問題,再去使用 babel-loader 進行程式碼的轉換。
在 webpack 配置文件的 devServer 配置中添加 :
devServer: { overlay: true, ... }
這樣在進行打包時就會出現 ESLint 的檢查結果,包括行數和列數,配合 vi 可以快速定位到問題行。
ESLint 檢查結果:
eslint-loader 的配置有很多,例如:
{ loader: 'eslint-loader', options: { fix: true // 對於一些簡單的 ESLint 檢查出的錯誤進行自動修正 } }
在實際工作中一般是不會使用 eslint-loader 的,因為使用它會影響打包的時間。
最佳的實踐方式是在使用 git 將程式碼提交到倉庫是進行 ESLint 程式碼規範的檢查,如果程式碼不規範就不能提交到遠程的倉庫。
實現方式是使用 git 中的一個鉤子,命令如下:
git 鉤子 eslint src
在提交的命令窗口中會出現 ESLint 的檢查結果,如果出現錯誤可以根據那個結果來修改程式碼。
當然這種方式的缺點在於錯誤就不直觀了,到底使用哪一種方式也要根據實際的情況去定。
5.8 webpack 性能優化
在打包大型項目的時候一次打包需要消耗特別長的時間,可以從以下方面出發提升 webpack 的打包速度。
-
跟上技術的迭代(Node,Npm,Yarn)
使用新的版本,因為新的版本一般都會有性能優化,webpack 是建立在 node 之上的,如果 node 的性能得到提升,那麼 webpack 的性能自然也就能得到提升,npm,yarn 這種的包管理工具如果性能得到提升,那麼對於 webpack 中處理包的引入等自然也就能得到提升。
-
在儘可能少的模組上應用 Loader
使用 exclude 或者 include 這種語法來規定 loader 的作用範圍從而減少 loader 的使用
-
Plugin 儘可能精簡併確保可靠
Plugin 儘可能少地使用,並且盡量使用社區推薦的,可靠的 Plugin。
-
resolve 參數合理配置
resolve: { extensions: ['js', 'jsx'], mainFiles: ['index', 'child'] }
上面的配置的作用是:當引入 a.js 時,
import a from './a'
,會在當前目錄下先去查找是否有 a.js ,如果沒有會再去查找是否有 a.jsx.mainFiles: ['index', 'child']
的作用是當引入只寫了目錄時會先去查找當前目錄下是否有 index 文件,如果沒有會再去查找是否有 child 文件。mainFiles: ['index', 'child']
這個參數的配置實際上就是多餘的,因為會默認查找 當前目錄下的 index 文件,沒有必要寫 child 文件。因此 resolve 的配置一定要合理,因為如果很多選項會很消耗查詢文件的時間。
-
使用 DllPlugin
思想就是:第一次打包的時候將第三方模組都打包到一個 dll 文件中,之後再打包的時候第三方模組就不用再進行打包了,直接從 dll 文件中引入,這樣在以後的打包過程中就會節省很多時間。
-
控制包文件的大小
要控制打包生成的文件儘可能地小,如果某個文件比較大要進行合理地拆分。
-
thread-loader, parallel-webpack, happypack 多進程打包
webpack 使用 node 來運行的,所以默認單進程的打包過程,可以通過一些方法實現多進程打包提高性能。
-
合理使用 sourceMap
sourceMap 生成的越詳細打包的時間就會越長,所以要選擇合理的生成 sourceMap 的方式。
-
結合 stats 分析打包結果
例如通過分析哪個模組打包分析的時間比較長等資訊及時發現問題作出修正。
-
開發環境和記憶體編譯
使用 webpackDevServer 打包後的打包文件不會寫入本地硬碟中而是存儲在記憶體中,記憶體的讀取比硬碟要快得多,所以在開發環境下使用 webpackDevServer 比直接用 webpack 在 dist 目錄下生成打包文件要快一些。
-
開發環境無用插件剔除
例如在開發環境下卻把 mode 設置成了
production
,這樣還會有一個將程式碼壓縮的過程也會浪費打包的時間。