問:這篇文章適合哪些人?
答:適合沒接觸過Webpack或者瞭解不全面的人。
css
問:這篇文章的目錄怎麼安排的?
答:先介紹背景,由背景引入Webpack的概念,進一步介紹Webpack基礎、核心和一些經常使用配置案例、優化手段,Webpack的plugin和loader確實很是多,短短2w多字還只是覆蓋其中一小部分。
html
問:這篇文章的出處?
答:此篇文章知識來自付費視頻(連接在文章末尾),文章由本身獨立撰寫,已得到講師受權並首發於掘金前端
上一篇:從今天開始,學習Webpack,減小對腳手架的依賴(上)vue
若是你以爲寫的不錯,請給我點一個star,原博客地址:原文地址node
PWA全稱Progressive Web Application
(漸進式應用框架),它能讓咱們主動緩存文件,這樣用戶離線後依然可以使用咱們緩存的文件打開網頁,而不至於讓頁面掛掉,實現這種技術須要安裝workbox-webpack-plugin
插件。react
若是你的谷歌瀏覽器尚未開啓支持PWA,請開啓它再進行下面的測試。 jquery
$ npm install workbox-webpack-plugin -D
複製代碼
// 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
目錄的打包結果以下:webpack
|-- 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。ios
須要判斷瀏覽器是否支持PWA,支持的時候咱們才進行註冊,註冊的.js
文件爲咱們打包後的service-worker.js
文件。git
console.log('hello,world');
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js').then((register) => {
console.log('註冊成功');
}).catch(error => {
console.log('註冊失敗');
})
}
複製代碼
在npm run dev
後,咱們利用webpack-dev-server
啓動了一個小型的服務器,而後咱們停掉這個服務器,刷新頁面,PWA的實際結果以下圖所示
在這一小節中,咱們要學到的技能有:
假設咱們如今有這樣一個需求:我有一個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
複製代碼
在上面全部的 Webpack 配置中,幾乎都是針對業務代碼的,若是咱們要打包發佈一個庫,讓別人使用的話,該怎麼配置?在下面的幾個小節中,咱們未來講一講該怎麼樣打包一個庫文件,並讓這個庫文件在多種場景可以使用。
步驟:
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
{
"name": "library",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "MIT"
}
複製代碼
在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 };
複製代碼
由於咱們是要打包一個庫文件,因此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 打包,因此咱們須要使用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
來編寫 JS 代碼將變成主流形式,那麼如何在 Webpack 中配置支持TypeScript
呢?能夠安裝ts-loader
和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-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
它告訴了 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
參數主要應用場景是,咱們能夠不寫具體的模塊名稱,由 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
參數更像一個別名,若是你有一個目錄很深、文件名很長的模塊,爲了方便,配置一個別名這是頗有用的;對於一個龐大的第三方庫,直接引入.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
也很簡單。
module.exports = {
// 其它配置
optimization: {
usedExports: true
}
}
複製代碼
若是你對Tree Shaking
還不是特別理解,請點擊Tree Shaking閱讀更多。
對於有些固定的第三方庫,由於它是固定的,咱們每次打包,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)。
在咱們使用 Webpack 的過程當中,咱們使用了不少的loader
,那麼那些loader
是哪裏來的?咱們能不能寫本身的loader
而後使用? 答案固然是能夠的,Webpack 爲咱們提供了一些loader
的API,經過這些API咱們可以編寫出本身的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?");
/***/ })
/******/ });
複製代碼
問題:
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)
}
複製代碼
在上面的例子中,咱們都是使用了同步的代碼,那麼若是咱們有必須異步的場景,該如何實現呢?咱們不妨作這樣的假設,先寫一個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
。
與loader
同樣,咱們在使用 Webpack 的過程當中,也常用plugin
,那麼咱們學習如何編寫本身的plugin
是十分有必要的。 場景:編寫咱們本身的plugin
的場景是在打包後的dist
目錄下生成一個copyright.txt
文件
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;
複製代碼
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閱讀整理而來,觀看視頻請支持正版。