問:這篇文章適合哪些人?
答:適合沒接觸過Webpack或者瞭解不全面的人。
問:這篇文章的目錄怎麼安排的?
答:先介紹背景,由背景引入Webpack的概念,進一步介紹Webpack基礎、核心和一些常用配置案例、優化手段,Webpack的plugin和loader確實非常多,短短2w多字還只是覆蓋其中一小部分。
問:這篇文章的出處?
答:此篇文章知識來自付費視訊(連結在文章末尾),文章由自己獨立撰寫,已獲得講師授權並首發於掘金
上一篇:從今天開始,學習Webpack,減少對腳手架的依賴(上)
如果你覺得寫的不錯,請給我點一個star,原部落格地址:原文地址
PWA配置
PWA全稱Progressive Web Application
(漸進式應用框架),它能讓我們主動快取檔案,這樣使用者離線後依然能夠使用我們快取的檔案開啟網頁,而不至於讓頁面掛掉,實現這種技術需要安裝workbox-webpack-plugin
外掛。
如果你的谷歌瀏覽器還沒有開啟支援PWA,請開啟它再進行下面的測試。
安裝外掛
$ npm install workbox-webpack-plugin -D
複製程式碼
webpack.config.js檔案配置
// PWA只有線上上環境才有效,所以需要在webpack.prod.js檔案中進行配置
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const prodConfig = {
// 其它配置
plugins: [
new MiniCssExtractPlugin({}),
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]
}
module.exports = merge(commonConfig, prodConfig);
複製程式碼
以上配置完畢後,讓我們使用npm run build
打包看一看生成了哪些檔案,dist
目錄的打包結果如下:
|-- dist
| |-- index.html
| |-- main.f28cbac9bec3756acdbe.js
| |-- main.f28cbac9bec3756acdbe.js.map
| |-- precache-manifest.ea54096f38009609a46058419fc7009b.js
| |-- service-worker.js
複製程式碼
我們可以程式碼塊高亮的部分,多出來了precache-manifest.xxxxx.js
檔案和service-worker.js
,就是這兩個檔案能讓我們實現PWA。
改寫index.js
需要判斷瀏覽器是否支援PWA,支援的時候我們才進行註冊,註冊的.js
檔案為我們打包後的service-worker.js
檔案。
console.log('hello,world');
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js').then((register) => {
console.log('註冊成功');
}).catch(error => {
console.log('註冊失敗');
})
}
複製程式碼
PWA實際效果
在npm run dev
後,我們利用webpack-dev-server
啟動了一個小型的伺服器,然後我們停掉這個伺服器,重新整理頁面,PWA的實際結果如下圖所示
WebpackDevServer請求轉發
在這一小節中,我們要學到的技能有:
- 如何進行介面代理配置
- 如何使用介面路徑重寫
- 其他常見配置的介紹
假設我們現在有這樣一個需求:我有一個URL地址(http://www.dell-lee.com/react/api/header.json
),我希望我請求的時候,請求的地址是/react/api/header.json
,能有一個什麼東西能自動幫我把請求轉發到http://www.dell-lee.com
域名下,那麼這個問題該如何解決呢?可以使用 Webpack 的webpack-dev-server
這個外掛來解決,其中需要配置proxy
屬性。
如何進行介面代理配置
既然我們要做請求,那麼安裝axios
來發請求再合適不過了,使用如下命令安裝axios
:
$ npm install axios --save-dev
複製程式碼
因為我們的請求代理只能在開發環境下使用,線上的生產環境,需要走其他的代理配置,所以我們需要在webpack.dev.js
中進行代理配置
const devConfig = {
// 其它配置
devServer: {
contentBase: './dist',
open: false,
port: 3000,
hot: true,
hotOnly: true,
proxy: {
'/react/api': {
target: 'http://www.dell-lee.com'
}
}
}
}
複製程式碼
以上配置完畢後,我們在index.js
檔案中引入axios
模組,再做請求轉發。
import axios from 'axios';
axios.get('/react/api/header.json').then((res) => {
let {data,status} = res;
console.log(data);
})
複製程式碼
使用npm run dev
後, 我們可以在瀏覽器中看到,我們已經成功請求到了我們的資料。
如何使用介面路徑重寫
現在依然假設有這樣一個場景:http://www.dell-lee.com/react/api/header.json
這個後端介面還沒有開發完畢,但後端告訴我們可以先使用http://www.dell-lee.com/react/api/demo.json
這個測試介面,等介面開發完畢後,我們再改回來。解決這個問題最佳辦法是,程式碼中的地址不能變動,我們只在proxy
代理中處理即可,使用pathRewrite
屬性進行配置。
const devConfig = {
// 其它配置
devServer: {
contentBase: './dist',
open: false,
port: 3000,
hot: true,
hotOnly: true,
proxy: {
'/react/api': {
target: 'http://www.dell-lee.com',
pathRewrite: {
'header.json': 'demo.json'
}
}
}
}
}
複製程式碼
同樣,我們打包後在瀏覽器中可以看到,我們的測試介面的資料已經成功拿到了。
其他常見配置的含義
轉發到https: 一般情況下,不接受執行在https
上,如果要轉發到https
上,可以使用如下配置
module.exports = {
//其它配置
devServer: {
proxy: {
'/react/api': {
target: 'https://www.dell-lee.com',
secure: false
}
}
}
}
複製程式碼
跨域: 有時候,在請求的過程中,由於同源策略的影響,存在跨域問題,我們需要處理這種情況,可以如下進行配置。
module.exports = {
//其它配置
devServer: {
proxy: {
'/react/api': {
target: 'https://www.dell-lee.com',
changeOrigin: true,
}
}
}
}
複製程式碼
代理多個路徑到同一個target: 代理多個路徑到同一個target
,可以如下進行配置
module.exports = {
//其它配置
devServer: {
proxy: [{
context: ['/vue/api', '/react/api'],
target: 'http://www.dell-lee.com'
}]
}
}
複製程式碼
多頁打包
現在流行的前端框架都推行單頁引用(SPA),但有時候我們不得不相容一些老的專案,他們是多頁的,那麼如何進行多頁打包配置呢?
現在我們來思考一個問題:多頁運用,即 多個入口檔案+多個對應的html檔案 ,那麼我們就可以配置 多個入口+配置多個html-webpack-plugin
來進行。
場景:假設現在我們有這樣三個頁面:index.html
, list.html
, detail.html
,我們需要配置三個入口檔案,新建三個.js
檔案。
在webpack.common.js
中配置多個entry
並使用html-webpack-plugin
來生成對應的多個.html
頁面。
HtmlWebpackPlugin引數說明:
template
:代表以哪個HTML頁面為模板filename
:代表生成頁面的檔名chunks
:代表需要引用打包後的哪些.js
檔案
module.exports = {
// 其它配置
entry: {
index: './src/index.js',
list: './src/list.js',
detail: './src/detail.js',
},
plugins: [
new htmlWebpackPlugin({
template: 'src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new htmlWebpackPlugin({
template: 'src/index.html',
filename: 'list.html',
chunks: ['list']
}),
new htmlWebpackPlugin({
template: 'src/index.html',
filename: 'detail.html',
chunks: ['detail']
}),
new cleanWebpackPlugin()
]
}
複製程式碼
在src
目錄下新建三個.js
檔案,名字分別是:index.js
,list.js
和detail.js
,它們的程式碼如下:
// index.js程式碼
document.getElementById('root').innerHTML = 'this is index page!'
// list.js程式碼
document.getElementById('root').innerHTML = 'this is list page!'
// detail.js程式碼
document.getElementById('root').innerHTML = 'this is detail page!'
複製程式碼
執行npm run build
進行打包:
$ npm run build
複製程式碼
打包後的dist
目錄:
|-- dist
| |-- detail.dae2986ea47c6eceecd6.js
| |-- detail.dae2986ea47c6eceecd6.js.map
| |-- detail.html
| |-- index.ca8e3d1b5e23e645f832.js
| |-- index.ca8e3d1b5e23e645f832.js.map
| |-- index.html
| |-- list.5f40def0946028db30ed.js
| |-- list.5f40def0946028db30ed.js.map
| |-- list.html
複製程式碼
隨機選擇list.html
在瀏覽器中執行,結果如下:
思考:現在只有三個頁面,即我們要配置三個入口+三個對應的html
,如果我們有十個入口,那麼我們也要這樣做重複的勞動嗎?有沒有什麼東西能幫助我們自動實現呢?答案當然是有的!
我們首先定義一個makeHtmlPlugins
方法,它接受一個 Webpack 配置項的引數configs
,返回一個plugins
陣列
const makeHtmlPlugins = function (configs) {
const htmlPlugins = []
Object.keys(configs.entry).forEach(key => {
htmlPlugins.push(
new htmlWebpackPlugin({
template: 'src/index.html',
filename: `${key}.html`,
chunks: [key]
})
)
})
return htmlPlugins
}
複製程式碼
通過呼叫makeHtmlPlugins
方法,它返回一個html
的plugins
陣列,把它和原有的plugin
進行合併後再複製給configs
configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs));
module.exports = configs;
複製程式碼
以上配置完畢後,打包結果依然還是一樣的,請自行測試,以下是webpack.commom.js
完整的程式碼:
const path = require('path');
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const miniCssExtractPlugin = require('mini-css-extract-plugin');
const optimizaCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const configs = {
entry: {
index: './src/index.js',
list: './src/list.js',
detail: './src/detail.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: miniCssExtractPlugin.loader,
options: {
hmr: true,
reloadAll: true
}
},
'css-loader'
]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: [
{
loader: "babel-loader"
},
{
loader: "imports-loader?this=>window"
}
]
}
]
},
plugins: [
new cleanWebpackPlugin(),
new miniCssExtractPlugin({
filename: '[name].css'
}),
new webpack.ProvidePlugin({
'$': 'jquery',
'_': 'lodash'
})
],
optimization: {
splitChunks: {
chunks: 'all'
},
minimizer: [
new optimizaCssAssetsWebpackPlugin()
]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist')
}
}
const makeHtmlPlugins = function (configs) {
const htmlPlugins = []
Object.keys(configs.entry).forEach(key => {
htmlPlugins.push(
new htmlWebpackPlugin({
template: 'src/index.html',
filename: `${key}.html`,
chunks: [key]
})
)
})
return htmlPlugins
}
configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs))
module.exports = configs
複製程式碼
如何打包一個庫檔案(Library)
在上面所有的 Webpack 配置中,幾乎都是針對業務程式碼的,如果我們要打包釋出一個庫,讓別人使用的話,該怎麼配置?在下面的幾個小節中,我們將來講一講該怎麼樣打包一個庫檔案,並讓這個庫檔案在多種場景能夠使用。
建立一個全新的專案
步驟:
- 建立library專案
- 使用
npm init -y
進行配置package.json
- 新建
src
目錄,建立math.js
檔案、string.js
檔案、index.js
檔案 - 根目錄下建立
webpack.config.js
檔案 - 安裝
webpack
、webpack-cli
:::
按上面的步驟走完後,你的目錄大概看起來是這樣子的:
|-- src
| |-- index.js
| |-- math.js
| |-- string.js
|-- webpack.config.js
|-- package.json
複製程式碼
初始化package.json
// 初始化後,改寫package.json
{
"name": "library",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "MIT"
}
複製程式碼
建立src目錄,並新增檔案
在src
目錄下新建math.js
,它的程式碼是四則混合運算的方法,如下:
export function add(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function division(a, b) {
return a / b;
}
複製程式碼
在src
目錄下新建string.js
,它有一個join
方法,如下:
export function join(a, b) {
return a + '' + b;
}
複製程式碼
在src
目錄下新建index.js
檔案,它引用math.js
和string.js
並匯出,如下:
import * as math from './math';
import * as string from './string';
export default { math, string };
複製程式碼
新增webpack.config.js
因為我們是要打包一個庫檔案,所以mode
只配置為生產環境(production
)即可。
在以上檔案新增完畢後,我們來配置一下webpack.config.js
檔案,它的程式碼非常簡單,如下:
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'library.js',
path: path.resolve(__dirname, 'dist')
}
}
複製程式碼
安裝Webpack
因為涉及到 Webpack 打包,所以我們需要使用npm instll
進行安裝:
$ npm install webpack webpack-cli -D
複製程式碼
進行第一次打包
使用npm run build
進行第一次打包,在dist
目錄下會生成一個叫library.js
的檔案,我們要測試這個檔案的話,需要在dist
目錄下新建index.html
$ npm run build
$ cd dist
$ touch index.html
複製程式碼
在index.html
中引入library.js
檔案:
<script src="./library.js"></script>
複製程式碼
至此,我們已經基本把專案目錄搭建完畢,現在我們來考慮一下,可以在哪些情況下使用我們打包的檔案:
- 使用
ES Module
語法引入,例如import library from 'library'
- 使用
CommonJS
語法引入,例如const library = require('library')
- 使用
AMD
、CMD
語法引入,例如require(['library'], function() {// todo})
- 使用
script
標籤引入,例如<script src="library.js"></script>
針對以上幾種使用場景,我們可以在output中配置library和libraryTarget屬性(注意:這裡的library和libraryTarget和我們的庫名字library.js沒有任何關係,前者是Webpack固有的配置項,後者只是我們隨意取的一個名字)
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
library: 'library',
libraryTarget: 'umd'
}
}
複製程式碼
配置屬性說明:
library
:這個屬性指,我們庫的全域性變數是什麼,類似於jquery
中的$
符號libraryTarget
: 這個屬性指,我們庫應該支援的模組引入方案,umd
代表支援ES Module
、CommomJS
、AMD
以及CMD
在配置完畢後,我們再使用npm run build
進行打包,並在瀏覽器中執行index.html
,在console
控制檯輸出library
這個全域性變數,結果如下圖所示:
以上我們所寫的庫非常簡單,在實際的庫開發過程中,往往需要使用到一些第三方庫,如果我們不做其他配置的話,第三方庫會直接打包進我們的庫檔案中。
如果使用者在使用我們的庫檔案時,也引入了這個第三方庫,就造成了重複引用的問題,那麼如何解決這個問題呢?可以在webpack.config.js
檔案中配置externals
屬性。
在string.js
檔案的join
方法中,我們使用第三方庫lodash
中的_join()
方法來進行字串的拼接。
import _ from 'lodash';
export function join(a, b) {
return _.join([a, b], ' ');
}
複製程式碼
在修改完畢string.js
檔案後,使用npm run build
進行打包,發現lodash
直接打包進了我們的庫檔案,造成庫檔案積極臃腫,有70.8kb。
$ npm run build
Built at: 2019-04-05 00:47:25
Asset Size Chunks Chunk Names
library.js 70.8 KiB 0 [emitted] main
複製程式碼
針對以上問題,我們可以在webpack.config.js
中配置externals
屬性,更多externals
的用法請點選externals
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
externals: ['lodash'],
output: {
filename: 'library.js',
path: path.resolve(__dirname, 'dist'),
library: 'library',
libraryTarget: 'umd'
}
}
複製程式碼
配置完externals
後,我們再進行打包,它的打包結果如下,我們可以看到我們的庫檔案又變回原來的大小了,證明我們的配置起作用了。
$ npm run build
Built at: 2019-04-05 00:51:22
Asset Size Chunks Chunk Names
library.js 1.63 KiB 0 [emitted] main
複製程式碼
如何釋出並使用我們的庫檔案
在打包完畢後,我們如何釋出我們的庫檔案呢,以下是釋出的步驟:
- 註冊
npm
賬號 - 修改
package.json
檔案的入口,修改為:"main": "./dist/library.js"
- 執行
npm adduser
新增賬戶名稱 - 執行
npm publish
命令進行釋出 - 執行
npm install xxx
來進行安裝
為了維護npm倉庫的乾淨,我們並未實際執行npm publish命令,因為我們的庫是無意義的,釋出上去屬於垃圾程式碼,所以請自行嘗試釋出。另外自己包的名字不能和npm倉庫中已有的包名字重複,所以需要在package.json中給name屬性起一個特殊一點的名字才行,例如"name": "why-library-2019"
TypeScript配置
隨著TypeScript
的不斷髮展,相信未來使用TypeScript
來編寫 JS 程式碼將變成主流形式,那麼如何在 Webpack 中配置支援TypeScript
呢?可以安裝ts-loader
和typescript
來解決這個問題。
新建一個專案webpack-typescript
新建立一個專案,命名為webpack-typescript
,並按如下步驟處理:
- 使用
npm init -y
初始化package.json
檔案,並在其中新增build
Webpack打包命令 - 新建
webpack.config.js
檔案,並做一些簡單配置,例如entry
、output
等 - 新建
src
目錄,並在src
目錄下新建index.ts
檔案 - 新建
tsconfig.json
檔案,並做一些配置 - 安裝
webpack
和webpack-cli
- 安裝
ts-loader
和typescript
按以上步驟完成後,專案目錄大概如下所示:
|-- src
| |-- index.ts
|-- tsconfig.json
|-- webpack.config.js
|-- package.json
複製程式碼
在package.json
中新增好打包命令命令:
"scripts": {
"build": "webpack"
},
複製程式碼
接下來我們需要對webpack.config.js
做一下配置:
const path = require('path');
module.exports = {
mode: 'production',
module: {
rules: [
{
test: /\.(ts|tsx)?$/,
use: {
loader: 'ts-loader'
}
}
]
},
entry: {
main: './src/index.ts'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
複製程式碼
在tsconfig.json
裡面進行typescript
的相關配置,配置項的說明如下
module
: 表示我們使用ES6
模組target
: 表示我們轉換成ES5
程式碼allowJs
: 允許我們在.ts
檔案中通過import
語法引入其他.js
檔案
{
"compilerOptions": {
"module": "ES6",
"target": "ES5",
"allowJs": true
}
}
複製程式碼
在src/index.ts
檔案中書寫TypeScript
程式碼,像下面這樣
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message;
}
greet() {
return 'hello, ' + this.greeting;
}
}
let greeter = new Greeter('why');
console.log(greeter.greet());
複製程式碼
打包測試
- 執行
npm run build
進行打包 - 在生成
dist
目錄下,新建index.html
,並引入打包後的main.js
檔案 - 在瀏覽器中執行
index.html
使用其他模組的型別定義檔案
如果我們要使用lodash庫,必須安裝其對應的型別定義檔案,格式為@types/xxx
安裝lodash
對應的typescript
型別檔案:
$ npm install lodash @types/lodash -D
複製程式碼
安裝完畢後,我們在index.ts
中引用lodash
,並使用裡面的方法:
import * as _ from 'lodash'
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message;
}
greet() {
return _.join(['hello', this.greeting], '**');
}
}
let greeter = new Greeter('why');
console.log(greeter.greet());
複製程式碼
打包測試
使用npm run build
,在瀏覽器中執行index.html
,結果如下:
Webpack效能優化
打包分析
在進行 Webpack 效能優化之前,如果我們知道我們每一個打包的檔案有多大,打包時間是多少,它對於我們進行效能優化是很有幫助的,這裡我們使用webpack-bundle-analyzer
來幫助我們解決這個問題。
首先需要使用如下命令去安裝這個外掛:
$ npm install webpack-bundle-analyzer --save-dev
複製程式碼
安裝完畢後,我們需要在webpack.prod.js
檔案中做一點小小的改動:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const prodConfig = {
// 其它配置項
mode: 'production',
plugins: [
new BundleAnalyzerPlugin()
]
}
複製程式碼
配置完畢後,我們執行npm run build
命令來檢視打包分析結果,以下打包結果僅供參考:
縮小檔案的搜尋範圍
首先我們要弄明白 Webpack 的一個配置引數(Resolve
)的作用:它告訴了 Webpack 怎麼去搜尋檔案,它同樣有幾個屬性需要我們去理解:
extensions
:它告訴了 Webpack 當我們在匯入模組,但沒有寫模組的字尾時應該如何去查詢模組。mainFields
:它告訴了 Webpack 當我們在匯入模組,但並沒有寫模組的具體名字時,應該如何去查詢這個模組。alias
:當我們有一些不得不引用的第三方庫或者模組的時候,可以通過配置別名,直接引入它的.min.js
檔案,這樣可以庫內的直接解析- 其它
include
、exclude
、test
來配合loader進行限制檔案的搜尋範圍
extensions引數
就像上面所說的那樣,extensions
它告訴了 Webpack 當我們在匯入模組,但沒有寫模組的字尾時,應該如何去查詢模組。這種情況在我們開發中是很常見的,一個情形可能如下所示:
// 書寫了模組字尾
import main from 'main.js'
// 沒有書寫模組字尾
import main from 'main'
複製程式碼
像上面那樣,我們不寫main.js
的.js
字尾,是因為 Webpack 會預設幫我們去查詢一些檔案,我們也可以去配置自己的檔案字尾配置:
extensions引數應儘可能只配置主要的檔案型別,不可為了圖方便寫很多不必要的,因為每多一個,底層都會走一遍檔案查詢的工作,會損耗一定的效能。
module.exports = {
// 其它配置
resolve: {
extensions: ['.js', '.json', '.vue']
}
}
複製程式碼
如果我們像上面配置後,我們可以在程式碼中這樣寫:
// 省略 .vue檔案擴充套件
import BaseHeader from '@/components/base-header';
// 省略 .json檔案擴充套件
import CityJson from '@/static/city';
複製程式碼
mainFields引數
mainFields
引數主要應用場景是,我們可以不寫具體的模組名稱,由 Webpack 去查詢,一個可能的情形如下:
// 省略具體模組名稱
import BaseHeader from '@components/base-header/';
// 以上相當於這一段程式碼
import BaseHeader from '@components/base-header/index.vue';
// 或者這一段
import BaseHeader from '@components/base-header/main.vue';
複製程式碼
我們也可以去配置自己的mainFields
引數:
同extensions引數類似,我們也不建議過多的配置mainFields的值,原因如上。
module.exports = {
// 其它配置
resolve: {
extensions: ['.js', '.json', '.vue'],
mainFields: ['main', 'index']
}
}
複製程式碼
alias引數
alias
引數更像一個別名,如果你有一個目錄很深、檔名很長的模組,為了方便,配置一個別名這是很有用的;對於一個龐大的第三方庫,直接引入.min.js
而不是從node_modules
中引入也是一個極好的方案,一個可能得情形如下:
通過別名配置的模組,會影響Tree Shaking,建議只對整體性比較強的庫使用,像lodash庫不建議通過別名引入,因為lodash使用Tree Shaking更合適。
// 沒有配置別名之前
import main from 'src/a/b/c/main.js';
import React from 'react';
// 配置別名之後
import main from 'main.js';
import React from 'react';
複製程式碼
// 別名配置
const path = require('path');
module.exports = {
// 其它配置
resolve: {
extensions: ['.js', '.json', '.vue'],
mainFields: ['main', 'index'],
alias: {
main: path.resolve(__dirname, 'src/a/b/c'),
react: path.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
}
}
複製程式碼
Tree Shaking去掉冗餘的程式碼
Tree Shaking
配置我們已經在上面講過,配置Tree Shaking
也很簡單。
module.exports = {
// 其它配置
optimization: {
usedExports: true
}
}
複製程式碼
如果你對Tree Shaking
還不是特別理解,請點選Tree Shaking閱讀更多。
DllPlugin減少第三方庫的編譯次數
對於有些固定的第三方庫,因為它是固定的,我們每次打包,Webpack 都會對它們的程式碼進行分析,然後打包。那麼有沒有什麼辦法,讓我們只打包一次,後面的打包直接使用第一次的分析結果就行。答案當然是有的,我們可以使用 Webpack 內建的DllPlugin
來解決這個問題,解決這個問題可以分如下的步驟進行:
- 把第三方庫單獨打包在一個
xxx.dll.js
檔案中 - 在
index.html
中使用xxx.dll.js
檔案 - 生成第三方庫的打包分析結果儲存在
xxx.manifest.json
檔案中 - 當
npm run build
時,引入已經打包好的第三方庫的分析結果 - 優化
單獨打包第三方庫
為了單獨打包第三方庫,我們需要進行如下步驟:
- 根目錄下生成
dll
資料夾 - 在
build
目錄下生成一個webpack.dll.js
的配置檔案,並進行配置。 - 在
package.json
檔案中,配置build:dll
命令 - 使用
npm run build:dll
進行打包
生成dll
資料夾:
$ mkdir dll
複製程式碼
在build
資料夾下生成webpack.dll.js
:
$ cd build
$ touch webpack.dll.js
複製程式碼
建立完畢後,需要在webpack.dll.js
檔案中新增如下程式碼:
const path = require('path');
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash', 'jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
}
}
複製程式碼
最後需要在package.json
檔案中新增新的打包命令:
{
// 其它配置
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js",
"build:dll": "webpack --config ./build/webpack.dll.js"
}
}
複製程式碼
使用npm run build:dll
打包結果,你的打包結果看起來是下面這樣的:
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.dll.js
| |-- webpack.prod.js
|-- dll
| |-- vendors.dll.js
|-- src
| |-- index.html
| |-- index.js
|-- package.json
複製程式碼
引用xxx.dll.js
檔案
在上一小節中我們成功拿到了xxx.dll.js
檔案,那麼如何在index.html
中引入這個檔案呢?答案是需要安裝add-asset-html-webpack-plugin
外掛:
$ npm install add-asset-html-webpack-plugin -D
複製程式碼
在webpack.common.js
中使用add-asset-html-webpack-plugin
外掛:
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const configs = {
// 其它配置
plugins: [
new addAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
})
]
}
module.exports = configs;
複製程式碼
我們將第三方庫全域性暴露了一個vendors
變數,現引入xxx.dll.js
檔案結果如下所示:
生成打包分析檔案
在webpack.dll.js
中使用 Webpack 內建的DllPlugin
外掛,進行打包分析:
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash', 'jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json')
})
]
}
複製程式碼
引用打包分析檔案
在webpack.common.js
中使用 Webpack 內建的DllReferencePlugin
外掛來引用打包分析檔案:
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
module.exports = {
// 其它配置
plugins: [
new cleanWebpackPlugin(),
new htmlWebpackPlugin({
template: 'src/index.html'
}),
new addAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
]
}
複製程式碼
優化
現在我們思考一個問題,我們目前是把lodash
和jquery
全部打包到了vendors
檔案中,那麼如果我們要拆分怎麼辦,拆分後又該如何去配置引入?一個可能的拆分結果如下:
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash'],
jquery: ['jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json')
})
]
}
複製程式碼
根據上面的拆分結果,我們需要在webpack.common.js
中進行如下的引用配置:
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const path = require('path');
const configs = {
// ... 其他配置
plugins: [
new cleanWebpackPlugin(),
new htmlWebpackPlugin({
template: 'src/index.html'
}),
new addAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
}),
new addAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/jquery.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/jquery.manifest.json')
})
]
}
module.exports = configs;
複製程式碼
我們可以發現:隨著我們引入的第三方模組越來越多,我們不斷的要進行 Webpack 配置檔案的修改。對於這個問題,我們可以使用Node
的核心模組fs
來分析dll
資料夾下的檔案,進行動態的引入,根據這個思路我們新建一個makePlugins
方法,它返回一個 Webpack 的一個plugins
陣列:
const makePlugins = function() {
const plugins = [
new cleanWebpackPlugin(),
new htmlWebpackPlugin({
template: 'src/index.html'
}),
];
// 動態分析檔案
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
// 如果是xxx.dll.js檔案
if(/.*\.dll.js/.test(file)) {
plugins.push(
new addAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
})
)
}
// 如果是xxx.manifest.json檔案
if(/.*\.manifest.json/.test(file)) {
plugins.push(
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
})
)
}
})
return plugins;
}
configs.plugins = makePlugins(configs);
module.exports = configs;
複製程式碼
使用npm run build:dll
進行打包第三方庫,再使用npm run build
打包,打包結果如下:
本次試驗,第一次打包時間為1100ms+,後面的打包穩定在800ms+,說明我們的 Webpack效能優化已經生效。
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.dll.js
| |-- webpack.prod.js
|-- dist
| |-- index.html
| |-- jquery.dll.js
| |-- main.1158fa9f961c50aaea21.js
| |-- main.1158fa9f961c50aaea21.js.map
|-- dll
| |-- jquery.dll.js
| |-- jquery.manifest.json
| |-- vendors.dll.js
| |-- vendors.manifest.json
|-- src
| |-- index.html
| |-- index.js
|-- package.json
|-- postcss.config.js
複製程式碼
小結:Webpack 效能優化是一個長久的話題,本章也僅僅只是淺嘗輒止,後續會有關於 Webpack 更加深入的解讀部落格,敬請期待(立個flag)。
編寫自己的Loader
在我們使用 Webpack 的過程中,我們使用了很多的loader
,那麼那些loader
是哪裡來的?我們能不能寫自己的loader
然後使用?
答案當然是可以的,Webpack 為我們提供了一些loader
的API,通過這些API我們能夠編寫出自己的loader
並使用。
如何編寫及使用自己的Loader
場景:
我們需要把.js
檔案中,所有出現Webpack is good!
,改成Webpack is very good!
。實際上我們需要編寫自己的loader
,所以我們有如下的步驟需要處理:
- 新建
webpack-loader
專案 - 使用
npm init -y
命令生成package.json
檔案 - 建立
webpack.config.js
檔案 - 建立
src
目錄,並在src
目錄下新建index.js
- 建立
loaders
目錄,並在loader
目錄下新建replaceLoader.js
- 安裝
webpack
、webpack-cli
按上面的步驟新建後的專案目錄如下:
|-- loaders
| | -- replaceLoader.js
|-- src
| | -- index.js
|-- webpack.config.js
|-- package.json
複製程式碼
首先需要在webpack.config.js
中新增下面的程式碼:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
module: {
rules: [
{
test: /\.js$/,
use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
}
]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
複製程式碼
隨後在package.json
檔案新增build
打包命令:
// 其它配置
"scripts": {
"build": "webpack"
}
複製程式碼
接下來在src/index.js
檔案中新增一行程式碼:這個檔案使用最簡單的例子,只是列印一句話。
console.log('Webpack is good!');
複製程式碼
最後就是在loader/replaceLoader.js
編寫我們自己loader
檔案中的程式碼:
- 編寫
loader
時,module.exports
是固定寫法,並且它只能是一個普通函式,不能寫箭頭函式(因為需要this
指向自身) source
是打包檔案的原始檔內容
const loaderUtils = require('loader-utils');
module.exports = function(source) {
return source.replace('good', 'very good');
}
複製程式碼
使用我們的loader
: 要使用我們的loader
,則需要在modules
中寫loader
,
resolveLoader
它告訴了 Webpack 使用loader
時,應該去哪些目錄下去找,預設是node_modules
,做了此項配置後,我們就不用去顯示的填寫其路徑了,因為它會自動去loaders
資料夾下面去找。
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
resolveLoader: {
modules: ['node_modules', './loaders']
},
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: 'replaceLoader',
options: {
name: 'wanghuayu'
}
}]
}
]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
複製程式碼
最後我們執行npm run build
,在生成的dist
目錄下開啟main.js
檔案,可以看到檔案內容已經成功替換了,說明我們的loader
已經使用成功了。
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("console.log('Webpack is very good!');\n\n//# sourceURL=webpack:///./src/index.js?");
/***/ })
/******/ });
複製程式碼
如何向Loader傳參及返回多個值
問題:
- 我們如何返回多個值?
- 我們如何向自己的Loader傳遞引數?
如何返回多個值
Webpack 的 API允許我們使用callback(error, result, sourceMap?, meta?)
返回多個值,它有四個引數:
Error || Null
:錯誤型別, 沒有錯誤傳遞null
result
:轉換後的結果sourceMap
:可選引數,處理分析後的sourceMap
meta
: 可選引數,元資訊
返回多個值,可能有如下情況:
// 第三,第四個引數是可選的。
this.callback(null, result);
複製程式碼
如何傳遞引數
我們知道在使用loader
的時候,可以寫成如下的形式:
// options裡面可以傳遞一些引數
{
test: /\.js$/,
use: [{
loader: 'replaceLoader',
options: {
word: 'very good'
}
}]
}
複製程式碼
再使用options
傳遞引數後,我們可以使用官方提供的loader-utils來獲取options
引數,可以像下面這樣寫:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
var options = loaderUtils.getOptions(this);
return source.replace('good', options.word)
}
複製程式碼
如何在Loader中寫非同步程式碼
在上面的例子中,我們都是使用了同步的程式碼,那麼如果我們有必須非同步的場景,該如何實現呢?我們不妨做這樣的假設,先寫一個setTimeout
:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
var options = loaderUtils.getOptions(this);
setTimeout(() => {
var result = source.replace('World', options.name);
return this.callback(null, result);
}, 0);
}
複製程式碼
如果你執行了npm run build
進行打包,那麼一定會報錯,解決辦法是:使用this.async()
主動標識有非同步程式碼:
const loaderUtils = require('loader-utils');
module.exports = function(source) {
var options = loaderUtils.getOptions(this);
var callback = this.async();
setTimeout(() => {
var result = source.replace('World', options.name);
callback(null, result);
}, 0);
}
複製程式碼
至此,我們已經掌握瞭如何編寫、如何引用、如何傳遞引數以及如何寫非同步程式碼,在下一小節當中我們將學習如何編寫自己的plugin
。
編寫自己的Plugin
與loader
一樣,我們在使用 Webpack 的過程中,也經常使用plugin
,那麼我們學習如何編寫自己的plugin
是十分有必要的。
場景:編寫我們自己的plugin
的場景是在打包後的dist
目錄下生成一個copyright.txt
檔案
plugin基礎
plugin
基礎講述了怎麼編寫自己的plugin
以及如何使用,與建立自己的loader
相似,我們需要建立如下的專案目錄結構:
|-- plugins
| -- copyWebpackPlugin.js
|-- src
| -- index.js
|-- webpack.config.js
|-- package.json
複製程式碼
copyWebpackPlugins.js
中的程式碼:使用npm run build
進行打包時,我們會看到控制檯會輸出hello, my plugin
這段話。
plugin與loader不同,plugin需要我們提供的是一個類,這也就解釋了我們必須在使用外掛時,為什麼要進行new操作了。
class copyWebpackPlugin {
constructor() {
console.log('hello, my plugin');
}
apply(compiler) {
}
}
module.exports = copyWebpackPlugin;
複製程式碼
webpack.config.js
中的程式碼:
const path = require('path');
// 引用自己的外掛
const copyWebpackPlugin = require('./plugins/copyWebpackPlugin.js');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
// new自己的外掛
new copyWebpackPlugin()
]
}
複製程式碼
如何傳遞引數
在使用其他plugin
外掛時,我們經常需要傳遞一些引數進去,那麼我們如何在自己的外掛中傳遞引數呢?在哪裡接受呢?
其實,外掛傳參跟其他外掛傳參是一樣的,都是在建構函式中傳遞一個物件,外掛傳參如下所示:
const path = require('path');
const copyWebpackPlugin = require('./plugins/copyWebpackPlugin.js');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
// 向我們的外掛傳遞引數
new copyWebpackPlugin({
name: 'why'
})
]
}
複製程式碼
在plugin
的建構函式中呼叫:使用npm run build
進行打包,在控制檯可以列印出我們傳遞的引數值why
class copyWebpackPlugin {
constructor(options) {
console.log(options.name);
}
apply(compiler) {
}
}
module.exports = copyWebpackPlugin;
複製程式碼
如何編寫及使用自己的Plugin
apply
函式是我們外掛在呼叫時,需要執行的函式apply
的引數,指的是 Webpack 的例項compilation.assets
打包的檔案資訊
我們現在有這樣一個需求:使用自己的外掛,在打包目錄下生成一個copyright.txt
版權檔案,那麼該如何編寫這樣的外掛呢?
首先我們需要知道plugin
的鉤子函式,符合我們規則鉤子函式叫:emit
,它的用法如下:
class CopyWebpackPlugin {
constructor() {
}
apply(compiler) {
compiler.hooks.emit.tapAsync('CopyWebpackPlugin', (compilation, cb) => {
var copyrightText = 'copyright by why';
compilation.assets['copyright.txt'] = {
source: function() {
return copyrightText
},
size: function() {
return copyrightText.length;
}
}
cb();
})
}
}
module.exports = CopyWebpackPlugin;
複製程式碼
使用npm run build
命名打包後,我們可以看到dist
目錄下,確實生成了我們的copyright.txt
檔案。
|-- dist
| |-- copyright.txt
| |-- main.js
|-- plugins
| |-- copyWebpackPlugin.js
|-- src
| |-- index.js
|-- webpack.config.js
|-- package.json
複製程式碼
我們開啟copyright.txt
檔案,它的內容如下:
copyright by why
複製程式碼
本篇部落格由慕課網視訊從基礎到實戰手把手帶你掌握新版Webpack4.0閱讀整理而來,觀看視訊請支援正版。