webpack4 從零學習經常使用配置梳理

webpack 的核心價值就是前端源碼的打包,即將前端源碼中每個文件(不管任何類型)都當作一個 pack ,而後分析依賴,將其最終打包出線上運行的代碼。webpack 的四個核心部分css

  • entry 規定入口文件,一個或者多個html

  • output 規定輸出文件的位置前端

  • loader 各個類型的轉換工具vue

  • plugin 打包過程當中各類自定義功能的插件node

webpack 現在已經進入 v4.x 版本,v5.x 估計也會很快發佈。不過看 v5 的變化相比於 v4 ,經常使用的配置沒有變,這是一個好消息,說明基本穩定。jquery

前端工程師須要瞭解的 webpackwebpack

前端工程化是近幾年前端發展迅速的主要推手之一,webpack 無疑是前端工程化的核心工具。目前前端工程化工具尚未到一鍵生成,或者重度繼承到某個 IDE 中(雖然有些 cli 工具能夠直接建立),仍是須要開發人員手動作一些配置。css3

所以,做爲前端開發人員,熟練應用 webpack 的經常使用配置、經常使用優化方案是必備的技能 ―― 這也正是本文的內容。另外,webpack 的實現原理算是一個加分項,不要求全部開發人員掌握,本文也沒有涉及。git

基礎配置github

初始化環境

npm init -y 初始化 npm 環境,而後安裝 webpack npm i webpack webpack-cli -D

新建 src 目錄並在其中新建 index.js ,隨便寫點 console.log('index js') 。而後根目錄建立 webpack.config.js ,內容以下

const path = require('path')

module.exports = {
// mode 可選 development 或 production ,默認爲後者
// production 會默認壓縮代碼並進行其餘優化(如 tree shaking)
mode: 'development',
entry: path.join(__dirname, 'src', 'index'),
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist')
}
}

而後增長 package.json 的 scripts

"scripts": {
"build": "webpack"
},

而後運行 npm run build 便可打包文件到 dist 目錄。

區分 dev 和 build

使用 webpack 須要兩個最基本的功能:第一,開發的代碼運行一下看看是否有效;第二,開發完畢了將代碼打包出來。這兩個操做的需求、配置都是徹底不同的。例如,運行代碼時不須要壓縮以便 debug ,而打包代碼時就須要壓縮以減小文件體積。所以,這裏咱們仍是先把二者分開,方便接下來各個步驟的講解。

首先,安裝 npm i webpack-merge -D ,而後根目錄新建 build 目錄,其中新建以下三個文件。

// webpack.common.js 公共的配置
const path = require('path')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = {
entry: path.join(srcPath, 'index')
}
// webpack.dev.js 運行代碼的配置(該文件暫時用不到,先建立了,下文會用到)
const path = require('path')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = smart(webpackCommonConf, {
mode: 'development'
})
// webpack.prod.js 打包代碼的配置
const path = require('path')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = smart(webpackCommonConf, {
mode: 'production',
output: {
filename: 'bundle.[contentHash:8].js', // 打包代碼時,加上 hash 戳
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改全部靜態文件 url 的前綴(如 cdn 域名),這裏暫時用不到
}
})

修改 package.json 中的 scripts

"scripts": {
"build": "webpack --config build/webpack.prod.js"
},

從新運行 npm run build 便可看到打包出來的代碼。最後,別忘了將根目錄下的 webpack.config.js 刪除。

這將引起一個新的問題:js 代碼中將如何判斷是什麼環境呢?須要藉助 webpack.DefinedPlugin 插件來定義全局變量。能夠在 webpack.dev.js 和 webpack.prod.js 中作以下配置:

// 引入 webpack
const webpack = require('webpack')

// 增長 webpack 配置
plugins: [
new webpack.DefinePlugin({
// 注意:此處 webpack.dev.js 中寫 'development' ,webpack.prod.js 中寫 'production'
ENV: JSON.stringify('development')
})

最後,修改 src/index.js 只需加入一行 console.log(ENV) ,而後重啓 npm run dev 便可看到效果。

JS 模塊化

webpack 默認支持 js 各類模塊化,如常見的 commonJS 和 ES6 Module 。可是推薦使用 ES6 Module ,由於 production 模式下,ES6 Module 會默認觸發 tree shaking ,而 commonJS 則沒有這個福利。究其緣由,ES6 Module 是靜態引用,在編譯時便可肯定依賴關係,而 commonJS 是動態引用。

不過使用 ES6 Module 時,ES6 的解構賦值語法這裏有一個坑,例如 index.js 中有一行 import {fn, name} from './a.js' ,此時 a.js 中有如下幾種寫法,你們要注意!

// 正確寫法一
export function fn() {
console.log('fn')
}
export const name = 'b'
// 正確寫法二
function fn() {
console.log('fn')
}
const name = 'b'
export {
fn,
name
}
// 錯誤寫法
function fn() {
console.log('fn')
}
export default {
fn,
name: 'b'
}

該現象的具體緣由可參考https://www.jb51.net/article/162079.htm 。下文立刻要講解啓動本地服務,讀者能夠立刻寫一個 demo 本身驗證一下這個現象。

啓動本地服務

上文建立的 webpack.dev.js 一直沒使用,下面就要用起來。

使用 html

啓動本地服務,確定須要一個 html 頁面做爲載體,新建一個 src/index.html 並初始化內容

<!DOCTYPE html>
<html>
<head><title>Document</title></head>
<body>
<p>this is index html</p>
</body>
</html>

要使用這個 html 文件,還須要安裝 npm i html-webpack-plugin -D ,而後配置 build/webpack.common.js ,由於不管 dev 仍是 prod 都須要打包 html 文件。

plugins: [
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html'
})
]

從新運行 npm run build 會發現打包出來了 dist/index.html ,且內部已經自動插入了打包的 js 文件。

webpack-dev-server

有了 html 和 js 文件,就能夠啓動服務了。首先安裝 npm i webpack-dev-server -D ,而後打開 build/webpack.dev.js配置。只有運行代碼才須要本地 server ,打包代碼時不須要。

devServer: {
port: 3000,
progress: true, // 顯示打包的進度條
contentBase: distPath, // 根目錄
open: true, // 自動打開瀏覽器
compress: true // 啓動 gzip 壓縮
}

打開 package.json 修改 scripts ,增長 "dev": "webpack-dev-server --config build/webpack.dev.js", 。而後運行 npm run dev ,打開瀏覽器訪問 localhost:3000 便可看到效果。

解決跨域

實際開發中,server 端提供的端口地址和前端可能不一樣,致使 ajax 收到跨域限制。使用 webpack-dev-server 可配置代理,解決跨域問題。若有須要,在 build/webpack.dev.js 中增長以下配置。

devServer: {
// 設置代理
proxy: {
// 將本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',

// 將本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}

處理 ES6

使用 babel

因爲如今瀏覽器還不能保證徹底支持 ES6 ,將 ES6 編譯爲 ES5 ,須要藉助 babel 這個神器。安裝 babel npm i babel-loader @babel/core @babel/preset-env -D ,而後修改 build/webpack.common.js 配置

module: {
rules: [
{
test: /\.js$/,
loader: ['babel-loader'],
include: srcPath,
exclude: /node_modules/
},
]
},

還要根目錄下新建一個 .babelrc json 文件,內容下

{
"presets": ["@babel/preset-env"],
"plugins": []
}

在 src/index.js 中加入一行 ES6 代碼,如箭頭函數 const fn = () => { console.log('this is fn') } 。而後從新運行 npm run dev,能夠看到瀏覽器中加載的 js 中,這個函數已經被編譯爲 function 形式。

使用高級特性

babel 能夠解析 ES6 大部分語法特性,可是沒法解析 class 、靜態屬性、塊級做用域,還有不少大於 ES6 版本的語法特性,如裝飾器。所以,想要把平常開發中的 ES6 代碼所有轉換爲 ES5 ,還須要藉助不少 babel 插件。

安裝 npm i @babel/plugin-proposal-class-properties @babel/plugin-transform-block-scoping @babel/plugin-transform-classes -D ,而後配置 .babelrc

{
"presets": ["@babel/preset-env"],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-classes"
]
}

在 src/index.js 中新增一段 class 代碼,而後從新運行 npm run build ,打包出來的代碼會將 class 轉換爲 function 形式。

source map

source map 用於反解析壓縮代碼中錯誤的行列信息,dev 時代碼沒有壓縮,用不到 source map ,所以要配置 build/webpack.prod.js

// webpack 中 source map 的可選項,是狀況選擇一種:

// devtool: 'source-map' // 1. 生成獨立的 source map 文件
// devtool: 'eval-source-map' // 2. 同 1 ,但不會產生獨立的文件,集成到打包出來的 js 文件中
// devtool: 'cheap-module-source-map' // 3. 生成單獨的 source map 文件,但沒有列信息(所以文件體積較小)
devtool: 'cheap-module-eval-source-map' // 4. 同 3 ,但不會產生獨立的文件,集成到打包出來的 js 文件中

生產環境下推薦使用 1 或者 3 ,即生成獨立的 map 文件。修改以後,從新運行 npm run build ,會看到打包出來了 map 文件。

處理樣式

在 webpack 看來,不只僅是 js ,其餘的文件也是一個一個的模塊,經過相應的 loader 進行解析並最終產出。

處理 css

安裝必要插件 npm i style-loader css-loader -D ,而後配置 build/webpack.common.js

module: {
rules: [
{ /* js loader */ },
{
test: /\.css$/,
loader: ['style-loader', 'css-loader'] // loader 的執行順序是:從後往前
}
]
},

新建一個 css 文件,而後引入到 src/index.js 中 import './css/index.css' ,從新運行 npm run dev 便可看到效果。

處理 less

less sass 都是經常使用 css 預處理語言,以 less 爲例講解。安裝必要插件 npm i less less-loader -D ,而後配置 build/webpack.common.js

 

{
test: /\.less$/,
loader: ['style-loader', 'css-loader', 'less-loader'] // 增長 'less-loader' ,注意順序
}

新建一個 less 文件,而後引入到 src/index.js 中 import './css/index.less' ,從新運行 npm run dev 便可看到效果。

自動添加前綴

一些 css3 的語法,例如 transform: rotate(45deg); 爲了瀏覽器兼容性須要加一些前綴,如 webkit- ,能夠經過 webpack 來自動添加。安裝 npm i postcss-loader autoprefixer -D ,而後配置

{
test: /\.css$/,
loader: ['style-loader', 'css-loader', 'postcss-loader'] // 增長 'postcss-loader' , 注意順序
}

還要新建一個 postcss.config.js 文件,內容是

module.exports = {
plugins: [require('autoprefixer')]
}

從新運行 npm run dev 便可看到效果,自動增長了必要的前綴。

抽離 css 文件

默認狀況下,webpack 會將 css 代碼所有寫入到 html 的 <style> 標籤中,可是打包代碼時須要抽離到單獨的 css 文件中。安裝 npm i mini-css-extract-plugin -D 而後配置 build/webpack.prod.js(打包代碼時才須要,運行時不須要)

// 引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

// 增長 webpack 配置
module: {
rules: [
{
test: /\.css$/,
loader: [
MiniCssExtractPlugin.loader, // 注意,這裏再也不用 style-loader
'css-loader',
'postcss-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
})
]

如須要壓縮 css ,須要安裝 npm i terser-webpack-plugin optimize-css-assets-webpack-plugin -D ,而後增長配置

// 引入插件
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

// 增長 webpack 配置
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},

運行 npm run build 便可看到打包出來的 css 是獨立的文件,而且是被壓縮過的。

處理圖片

要在 js 中 import 圖片,或者在 css 中設置背景圖片。安裝 npm i file-loader -D 而後配置 build/webpack.common.js

{
test: /\.(png|jpg|gif)$/,
use: 'file-loader'
}

若是想要處理 html 代碼中 <img src="..."/> 的形式,則安裝 npm i html-withimg-loader -D 而後配置 build/webpack.common.js

{
test: /\.html$/,
use: 'html-withimg-loader'
}

打包以後,dist 目錄下會生成一個相似 917bb63ba2e14fc4aa4170a8a702d9f8.jpg 的文件,並被引入到打包出來的結果中。

若是想要將小圖片用 base64 格式產出,則安裝 npm i url-loader -D ,而後配置 build/webpack.common.js

{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
// 小於 5kb 的圖片用 base64 格式產出
// 不然,依然延用 file-loader 的形式,產出 url 格式
limit: 5 * 1024,

// 打包到 img 目錄下
outputPath: '/img/',

// 設置圖片的 cdn 地址(也能夠統一在外面的 output 中設置,那將做用於全部靜態資源)
// publicPath: 'http://cdn.abc.com'
}
}
},

多頁應用

src 下有 index.js index.html 和 other.js other.html ,要打包輸出兩個頁面,且分別引用各自的 js 文件。

第一,配置輸入輸出

entry: {
index: path.join(srcPath, 'index.js'),
other: path.join(srcPath, 'other.js')
},
output: {
filename: '[name].[contentHash:8].js', // [name] 表示 chunk 的名稱,即上面的 index 和 other
path: distPath
},

第二,配置 html 插件

plugins: [
// 生成 index.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示該頁面要引用哪些 chunk (即上面的 index 和 other),默認所有引用
chunks: ['index'] // 只引用 index.js
}),
// 生成 other.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other'] // 只引用 other.js
}),

抽離公共代碼

公共模塊

多個頁面或者入口,若是引用了同一段代碼,如上文的多頁面例子中,index.js 和 other.js 都引用了 import './common.js' ,則 common.js 應該被做爲公共模塊打包。webpack v4 開始棄用了 commonChunkPlugin 改用 splitChunks ,可修改 build/webpack.prod.js 中的配置

optimization: {
// 分割代碼塊
splitChunks: {
// 緩存分組
cacheGroups: {
// 公共的模塊
common: {
chunks: 'initial',
minSize: 0, // 公共模塊的大小限制
minChunks: 2 // 公共模塊最少複用過幾回
}
}
}
},

從新運行 npm run build ,便可看到有 common 模塊被單獨打包出來,就是 common.js 的內容。

第三方模塊

同理,若是咱們的代碼中引用了 jquery lodash 等,也但願將第三方模塊單獨打包,和本身開發的業務代碼分開。這樣每次從新上線時,第三方模塊的代碼就能夠藉助瀏覽器緩存,提升用戶訪問網頁的效率。修改配置文件,增長下面的 vendor: {...} 配置。

optimization: {
// 分割代碼塊
splitChunks: {
// 緩存分組
cacheGroups: {
// 第三方模塊
vendor: {
priority: 1, // 權限更高,優先抽離,重要!!!
test: /node_modules/,
chunks: 'initial',
minSize: 0, // 大小限制
minChunks: 1 // 最少複用過幾回
},

// 公共的模塊
common: {
chunks: 'initial',
minSize: 0, // 公共模塊的大小限制
minChunks: 2 // 公共模塊最少複用過幾回
}
}
}
},

重啓 npm run build ,便可看到 vendor 模塊被打包出來,裏面是 jquery 或者 lodash 等第三方模塊的內容。

懶加載

webpack 支持使用 import(...) 語法進行資源懶加載。安裝 npm i @babel/plugin-syntax-dynamic-import -D 而後將插件配置到 .babelrc 中。

新建 src/dynamic-data.js 用於測試,內容是 export default { message: 'this is dynamic' } 。而後在 src/index.js 中加入

setTimeout(() => {
import('./dynamic-data.js').then(res => {
console.log(res.default.message) // 注意這裏的 default
})
}, 1500)

從新運行 npm run dev 刷新頁面,能夠看到 1.5s 以後打印出 this is dynamic 。並且,dynamic-data.js 也是 1.5s 以後被加載進瀏覽器的 ―― 懶加載,雖然文件名變了。

從新運行 npm run build 也能夠看到 dynamic-data.js 的內容被打包一個單獨的文件中。

常見性能優化

tree shaking

使用 import 引入,在 production 環境下,webpack 會自動觸發 tree shaking ,去掉無用代碼。可是使用 require 引入時,則不會觸發 tree shaking。這是由於 require 是動態引入,沒法在編譯時判斷哪些功能被使用。而 import 是靜態引入,編譯時便可判斷依賴關係。

noParse

不去解析某些 lib 其內部的依賴,即肯定這些 lib 沒有其餘依賴,提升解析速度。可配置到 build/wepback.common.js 中

module: {
noParse: /jquery|lodash/, // 不解析 jquery 和 lodash 的內部依賴

ignorePlugin

以經常使用的 moment 爲例。安裝 npm i moment -d 而且 import moment from 'moment' 以後,monent 默認將全部語言的 js 都加載進來,使得打包文件過大。能夠經過 ignorePlugin 插件忽略 locale 下的語言文件,不打包進來。

plugins: [
new webpack.IgnorePlugin(/\.\/locale/, /moment/), // 忽略 moment 下的 /locale 目錄

這樣,使用時能夠手動引入中文包,並設置語言

import moment from 'moment'
import 'moment/locale/zh-cn' // 手動引入中文語言包
moment.locale('zh-cn')
const r = moment().endOf('day').fromNow()
console.log(r)

happyPack

多進程打包,參考 https://www.npmjs.com/package/happypack 。注意,小項目使用反而會變慢。只有項目較大,打包出現明顯瓶頸時,才考慮使用 happypack 。

經常使用插件和配置

ProvidePlugin

如要給全部的 js 模塊直接使用 $ ,不用每次都 import $ from 'jquery' ,可作以下配置

plugins: [
new webpack.ProvidePlugin({
$: 'jquery'
}),

externals

若是 jquery 已經在 html 中經過 cdn 引用了,無需再打包,可作以下配置

externals: {
jquery: 'jQuery'
},

alias

設置 alias 別名在實際開發中比較經常使用,尤爲是項目較大,目錄較多時。可作以下配置

resolve: {
alias: {
Utilities: path.join(srcPath, 'utilities')
}
},

在該配置以前,可能須要 import Utility from '../../utilities/utility' 使用。配置以後就能夠 import Utility from 'Utilities/utility' 使用,一來書寫簡潔,二來不用再考慮相對目錄的層級關係。

extensions

若是引用文件時沒有寫後綴名,能夠經過 extensions 來匹配。

resolve: {
extensions: [".js", ".json"]
},

clean-webpack-plugin

因爲使用了 contentHash ,每次 build 時候均可能打包出不一樣的文件,所以要及時清理 dist 目錄。安裝 npm i clean-webpack-plugin -D ,而後在 build/webpack.prod.js 中配置

// 引入插件
const CleanWebpackPlugin = require('clean-webpack-plugin')

// 增長配置
plugins: [
new CleanWebpackPlugin(), // 默認清空 output.path 目錄

copy-webpack-plugin

build 時,將 src 目錄下某個文件或者文件夾,無條件的拷貝到 dist 目錄下,例如 src/doc 目錄拷貝過去。安裝 npm i copy-webpack-plugin -D,而後在 build/webpack.prod.js 中配置

// 引入插件
const CopyWebpackPlugin = require('copy-webpack-plugin')

// 增長配置
plugins: [
new CopyWebpackPlugin([
{
from: path.join(srcPath, 'doc'), // 將 src/doc 拷貝到 dist/doc
to: path.join(distPath, 'doc')
}
]),

bannerPlugin

代碼的版權聲明,在 build/webpack.prod.js 中配置便可。

plugins: [
new webpack.BannerPlugin('by github.com/wangfupeng1988 \r'),

總結

webpack 發展至今配置很是多,該視頻中也沒有所有講解出來,只是一些實際開發中經常使用的。其餘的配置能夠去看官網文檔。

 

您可能感興趣的文章:

文章同步發佈: https://www.geek-share.com/detail/2770037894.html

相關文章
相關標籤/搜索