關於 webpack
,在我眼裏一直都是蒙了一層「面紗」,一些配置也只能作到 「眼熟」。至於如何從 0 到 1 地去搭建一套「能用」的 webpack
配置更是「癡人說夢」。css
「痛定思痛」以後, 我決定揭開那層「面紗」,從新梳理一遍它的基礎配置,並進行一次從 0 到 1 的搭建。而這篇文章就是我揭開它的 「面紗」的整個過程,但願能對你有所幫助~html
webpack
是一個現代JavaScript
應用程序的靜態模塊打包器。當webpack
處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個bundle
。vue
入口(entry)node
入口起點指示 webpack
應該使用哪一個模塊,來做爲構建其內部依賴圖的開始。webpack
輸出(output)git
output 屬性告訴 webpack
在哪裏輸出它所建立的 bundles
,以及如何命名這些文件,默認值爲 ./dist
。github
loaderweb
loader 讓 webpack
可以去處理那些非 JavaScript
文件。vue-cli
插件(plugins)npm
loader 被用於轉換某些類型的模塊,而插件則能夠用於執行範圍更廣的任務。插件的範圍包括,從打包優化和壓縮,一直到從新定義環境中的變量。
mkdir webpack-demo && cd webpack-demo
npm init -y
# webpack@4.43.0
# webpack-cli@3.3.11
npm install --save-dev webpack webpack-cli
複製代碼
設置目錄結構:
├── src #
| ├── assets # 資源文件
| ├── css # css
| ├── html # html
| |── js # js
| |──index.js # 入口文件
├── static # 靜態資源文件
├── webpack.config.js # webpack配置文件
├── package.json #
複製代碼
在 package.json
中添加命令:
"scripts": {
"dev": "webpack --mode=development",
"build": "webpack --mdoe=production"
}
複製代碼
在 src/js
目錄下新建 index.js
文件,內容以下。並在入口文件 index.js
中引入該 js import './js/index'
:
const arrowFn = () => {
console.log('arrowFn')
}
arrowFn()
複製代碼
因爲 webpack 4
是開箱即用的,因此咱們能夠直接執行命令 npm run dev
(前面已經將 webpack
的命令配置在了 package.json
中)。
執行完成後,在 dist/main.js
中咱們能夠看到剛剛的箭頭函數,而 webpack
並無主動幫助咱們將它轉義爲低版本的代碼。想要實現這個功能,咱們須要經過 loader
對代碼進行轉換。
loader
編寫在 webpack
配置中的 module.rules
數組中。loader
的基本格式爲:{
test: xxx,
use: xxx,
options: {}
}
複製代碼
其中,test
屬性用於標識出應該被對應的 loader 進行轉換的某個或某些文件;use
屬性表示進行轉換時,應該使用哪一個 loader
;options
屬性用於設置單獨的配置。use
屬性有三種寫法:
use: 'babel-loader'
複製代碼
use: {
loader: 'babel-loader',
options: {}
}
複製代碼
use: ['babel-loader']
複製代碼
將 js 轉換爲低版本的代碼,咱們須要使用 babel-loader
,另外還須要添加 babel
相關的配置。
依次執行下面的命令(關於babel 7的詳細介紹):
# babel-loader@8.1.0
npm install --save-dev babel-loader
# @babel/core:babel的核心,包含全部的核心 API
# @babel/preset-env:將 js 引入的新語法轉換爲 ES5 的語法(不包括新增的全局變量、方法等)
# @babel/plugin-transform-runtime:用於構建過程當中的代碼轉換
npm install --save-dev @babel/core @babel/preset-env @babel/plugin-transform-runtime
# @babel/runtime:實際導入項目代碼的功能模塊
# @babel/runtime-corejs3:配合 @babel/plugin-transform-runtime 避免全局污染
npm install --save @babel/runtime @babel/runtime-corejs3
複製代碼
修改 webpack.config.js
,內容以下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
}
]
}
}
複製代碼
添加 babel
配置文件 .babelrc
,內容以下:
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
複製代碼
添加 .browserslist
(控制目標瀏覽器的範圍),內容以下:
> 0.25%
not dead
複製代碼
執行 npm run dev
後,咱們會發現 dist/main.js
中輸出的代碼已經被轉義成低版本的代碼了。
在 src/html
目錄下新建 index.html
文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
這是 Index 頁面
</body>
</html>
複製代碼
打包 html
文件,咱們須要使用 html-webpack-plugin 插件。
# html-webpack-plugin@4.3.0
npm install --save-dev html-webpack-plugin
複製代碼
修改 webpack.config.js
:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
# ...
plugins: [
new HtmlWebpackPlugin({
template: './src/html/index.html',
filename: 'index.html', # 打包後的文件名
minify: { # 設置靜態資源壓縮狀況
collapseWhitespace: false, # 是否摺疊空白
}
})
]
# ...
}
複製代碼
執行 npm run dev
命令後,咱們能夠看到 dist
文件夾下新增了 index.html
文件,而且其中自動引入的是打包以後的 js 文件。此時直接經過瀏覽器訪問 dist/index.html
,一切都那麼天然~
html-webpack-plugin
插件的可擴展性仍是很強的,例如設置 favicon
、meta
等,咱們也能夠本身添加自定義屬性,根據不一樣的環境,在 html 中進行不一樣的設置等,例如能夠手動設置頁面的標題:<title><%= htmlWebpackPlugin.options.title %></title>
,而在 webpack
的配置文件中,title
屬性的值能夠設置爲某個變量,以此達到靈活配置的目的。更多的使用方式還須要咱們本身結合實際狀況進行配置。
webpack
須要藉助 style-loader
、css-loader
才能處理 css
文件,因爲考慮到兼容性的問題,咱們一般還會添加 postcss-loader
進行處理。而當咱們用到 css
預處理器: sass
和 less
時,還須要分別使用 less-loader
和 less-loader
。這裏以 less
爲例。
安裝依賴:
npm install --save-dev style-loader css-loader postcss-loader autoprefixer less-loader less
複製代碼
新建 css/index.less
文件,內容以下:
@bgColor: skyblue;
body {
background: @bgColor;
}
.bold {
font-weight: 500;
}
複製代碼
在 webpack.config.js
文件的 module.rules
下添加:
{
test: /\.(le|c)ss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
},
'less-loader'
]
}
複製代碼
在入口文件 src/index.js
中引入樣式文件:
import './css/index.less'
複製代碼
執行 npm run dev
,在瀏覽器中打開 dist/index.html
,打開控制檯,檢查 html
的 head
標籤,咱們能夠看到新增了 style
標籤以及剛剛添加的樣式。
首先咱們要知道的是,loader
的執行順序是從後往前的,即上面的執行順序是:less-loader
-> postcss-loader
-> css-loader
-> style-loader
。
下面咱們就按順序分析一下,剛剛的幾個 loader
分別作了哪些事情吧~
less-loader
:處理 .less
文件,將其轉爲 .css
文件;postcss-loader
和 autoprefixer
:生成瀏覽器前綴;css-loader
:處理 import
等語句,分析多個 css
文件併合成一段 css
;style-loader
:動態建立 style
標籤,將 css
插入到 head
中;到了這裏咱們應該對 webpack
如何處理樣式文件有了一個比較清晰的認識了。下面咱們來看一下,如何抽離 css
。即:經過 link
標籤的形式從外部引入。
# mini-css-extract-plugin@0.9.0
npm install --save-dev mini-css-extract-plugin
複製代碼
mini-css-extract-plugin
和 extract-text-webpack-plugin
比較:
css
修改 webpack.config.js
文件以下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
# 引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader'
},
{
test: /\.(le|c)ss$/,
use: [
MiniCssExtractPlugin.loader, # 替換以前的 style-loader
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
},
'less-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/html/index.html',
filename: 'index.html',
minify: {
collapseWhitespace: false,
}
}),
# 輸出 css 文件
new MiniCssExtractPlugin({
filename: '[name].[hash:6].css'
})
]
}
複製代碼
執行編譯命令後,在 dist
目錄下咱們能夠看到已經多出了 main.xxxxxx.css
樣式文件,並在 html
中經過 link
標籤的形式進行了引入,「大功告成」~
簡單對比一下,這兩種的方式其本質的不一樣就是在最後一步的寫入。前者經過 style-loader
在 html
中添加內聯樣式表,後者首先經過 MiniCssExtractPlugin
輸出 css
,再經過 MiniCssExtractPlugin.loader
在 html
中添加外部樣式表。
安裝 optimize-css-assets-webpack-plugin
:
npm install --save-dev optimize-css-assets-webpack-plugin
複製代碼
在 webpack.config.js
中添加配置:
plugins: [
new OptimizeCssPlugin(),
]
複製代碼
在樣式文件中使用了圖片資源時,須要使用 url-loader
和 file-loader
進行處理,url-loader
依賴於 file-loader
,能夠理解爲 url-loader
是對 file-loader
的一種封裝。他們的區別在於,url-loader
在 options
配置添加了一個 limit
屬性。當資源大小小於設置的 limit
值時,webpack
會將資源轉換爲 base64,超過 限制則會將圖片拷貝到 dist
目錄下。
安裝 loader
。
# url-loader@4.1.0
# file-loader@6.0.0
npm install --save-dev url-loader file-loader
複製代碼
在 webpack.config.js
的 module.rules
中添加配置:
# ...
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240, # 10K
name: '[name]_[hash:6].[ext]',
outputPath: 'assets'
}
}
]
}
# ...
複製代碼
在 src/assets
目錄下添加兩張圖片:avatar.jpeg
(超過 10k)和 logo.png
(小於 10k)。
在 html/index.html
中添加:
<div class="logo"></div>
<div class="avatar"></div>
複製代碼
在 css/index.less
中添加樣式:
.avatar {
width: 200px;
height: 200px;
background: url('../assets/avatar.jpeg');
}
.logo {
width: 200px;
height: 200px;
background: url('../assets/logo.png');
}
複製代碼
執行編譯命令,在瀏覽器中打開 dist/index.html
,兩個背景圖片均可以正常顯示出來。咱們也能夠很清楚的看到 avatar.jpeg
是直接被拷貝到 dist/assets
目錄下的,而 logo.png
是被轉換成 base64 進行使用的。
在 html/index.html
中添加:
<img src="../assets/avatar.jpeg" alt="">
複製代碼
此時執行編譯命令,打開 dist/index.html
咱們會發現找不到 avatar.jpeg
圖片。這是由於通過 webpack
的構建後,經過相對路徑已經沒法找到圖片了。
咱們能夠經過添加 html-loader
來解決:
# html-loader@1.1.0
npm install --save-dev html-loader
複製代碼
修改 webpack.config.js
:
{
test: /\.html$/,
use: 'html-loader'
}
複製代碼
從新打包後,全部資源均可以正常加載了~
至此,咱們常見的 html
、js
、css
以及圖片資源的使用均可以經過 webpack
打包了。可是這個時候都是使用的 webpack
默認的入口出口配置,下面咱們就來看一下如何設置入口出口的配置吧。
入口配置的字段爲:entry
,值能夠是字符串、數組、對象。單個頁面通常這三種均可以選擇,但用的最多的仍是字符串和對象。數組是在當有「多個主入口」,多個依賴文件一塊兒注入時使用的。
修改 webpack.config.js
:
module.exports = {
# webpack的默認配置
entry: {
index: './src/index.js'
}
}
複製代碼
這裏經過對象的形式進行了設置,執行編譯命令後咱們會發現,以前的 main.[hash].[ext]
如今都變成了 index.[hash].[ext]
,這也就說明咱們設置的入口配置成功了。若是直接是以字符串的形式設置,那麼在配置中使用 [name]
,打包後的輸出結果的默認名就是 main
。(注意:咱們在配置中所使用的 [name]
不是全部地方都是指文件的名稱,部分是指設置的入口的屬性名稱)
出口配置的字段爲:output
。
修改 webpack.config.js
:
const path = require('path')
# ...
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[hash:6].js',
publicPath: '/'
}
# ...
複製代碼
因爲咱們在出口配置中設置了 publicPath: '/'
,則全部的資源默認從根目錄下獲取。因此咱們就不能再使用以前的方式查看編譯後的效果了。
解決方案:
dist
目錄下,經過 http-server
開啓本地服務器。webpack-dev-server
開啓本地服務。提供 mode
配置選項,告知 webpack
使用相應模式的內置優化。
webpack
給咱們提供了兩種選項:development
和 production
。(關於 mode)
一開始咱們就將 mode
配置進了命令行,經過 --mode=xxx
的形式指定。下面咱們將經過環境變量來判斷到底啓用哪一種模式。
安裝 cross-env
(跨平臺設置 NODE_ENV
):
npm install --save-dev cross-env
複製代碼
修改 package.json
:
"scripts": {
"dev": "cross-env NODE_ENV=development webpack",
"build": "cross-env NODE_ENV=production webpack"
},
複製代碼
修改 webpack.config.js
:
const isDev = process.env.NODE_ENV === 'development'
module.exports = {
mode: isDev ? 'development' : 'production'
}
複製代碼
在 webpack.dev.conf.js
中添加 resolve
配置:
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.js', '.less', '.json', '/index.js'],
alias: {
'@': path.join(__dirname, 'src'),
}
}
複製代碼
經過 resolve.module
告訴 webpack
查找模塊時應該搜索哪些目錄,默認值爲 ['node_modules']
。按照數組順序從左往右依次查找。
咱們經過一個例子能夠更加直觀的理解下面的配置:
import { Button } from 'antd'
,此時 webpack
會先查找 src
目錄下有沒有對應的模塊,若是沒有再去 node_modules
目錄下查找。
若是多個文件有着相同的名稱,但具備不一樣的擴展名,則 webpack
將解析其擴展名列在數組中首位的文件,並跳過其他文件。
舉個例子:
common
文件夾下有 index.js
和 index.less
文件。當咱們經過 import './common/index'
的形式引入文件時,webpack
會根據 resolve.extensions
的配置進行判斷,從左往右依次匹配。當找到能夠匹配的目標後,跳過其他文件。此處 webpack
的解析結果就等因而 import './common/index.js'
。
當咱們的項目愈來愈龐大,目錄結構愈來愈複雜時,有些時候經過相對路徑引入文件,代碼會顯得異常不清晰(不少層 「../」)。
所幸 webpack
爲咱們提供了 resolve.alias
配置項來設置別名,幫助咱們優雅地經過絕對路徑引入文件。
例子:引入 css/index.css
。
import '../../../css/index.css'
import '@/css/index.css'
複製代碼
這裏經過別名引入文件,能夠很清晰的知道文件的位置:src/css/index.css
。
前面咱們只是讓 webpack
正常的運行起來,但在實際開發中咱們會須要:提供 HTTP
服務而不是使用本地文件預覽;監聽文件的變化並自動刷新網頁。
官方已經爲咱們準備好了開發工具 DevServer
,它會啓動一個 HTTP
服務用於網頁請求,同時會幫助咱們啓動 webpack
,並接收 webpack
發出的變動信號,自動刷新網頁。
安裝 DevServer
:
# webpack-dev-server@3.11.0
npm install --save-dev webpack-dev-server
複製代碼
修改 webpack.config.js
:
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server",
"build": "cross-env NODE_ENV=production webpack"
},
複製代碼
在 webpack.config.js
中添加配置:
module.exports = {
devServer: {
host: 'localhost', # 服務器地址
port: '8899', # 默認是8080
compress: true # 是否啓用 gzip 壓縮
}
}
複製代碼
執行 npm run dev
後,訪問 http://localhost:8899/
,這樣咱們就能夠在瀏覽器中看到實際的效果啦。
這個時候咱們會發現並無文件輸出到 dist
目錄下,緣由是由於 DevServer
會把 webpack
構建出的文件保存在內存中,在要訪問輸出的文件時,必須經過 HTTP
服務訪問。
經過 DevServer
啓動的 webpack
會開啓監聽模式(默認是關閉的,能夠經過 webpack --watch
手動開啓監聽),當發生變化時從新執行構建,並通知 DevServer
。DevServer
會讓 webpack
在構建出的 js 代碼裏注入一個代理客戶端用於控制網頁,以方便 DevServer
主動向客戶端發送命令。 DevServer
在收到來自 webpack
的文件變化通知時經過注入的客戶端控制網頁刷新。
另外咱們還能夠經過 devServer
解決開發環境跨域的問題。
例子:假設咱們本地代碼運行在 localhost:8899
,而服務端接口在 http://dev.api.com
。此時有 http://dev.api.com/user/login
在 devServer
中添加 proxy
配置:
proxy: {
"/api": { # 起標識做用,表示此處配置用於接口。(名字能夠隨意定)
target: "http://dev.api.com", # 目標服務器
pathRewrite: {
"/api": "" # 重寫 「/api」 爲空
}
}
}
複製代碼
以前咱們每次執行 npm run build
,dist
目錄下都會保留上一次的打包內容,每次手動去刪除豈不是太麻煩了,因此咱們須要經過 clean-webpack-plugin
插件幫助咱們在每次打包前,先將 dist
目錄清空。
安裝 clean-webpack-plugin
:
npm install --save-dev clean-webpack-plugin
複製代碼
修改 webpack.config.js
:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
複製代碼
src/assets
和 static
,它們倆都是用來存放靜態資源文件的,那麼它們有什麼區別呢?
在咱們的項目中,html
和 css
經過 html-loader
和 css-loader
分析靜態資源 URL 的,例如 <img src="../assets/avatar.jpeg" alt="">
、background: url('../assets/avatar.jpeg');
。這些文件都是經過相對路徑引用的,webpack
會將它們做爲依賴模塊處理。相比之下,咱們存放在 static
目錄下的資源文件不會 webpack
處理(這是咱們的初衷),而是直接拷貝到打包後的 dist
目錄下,因此咱們在使用 static
目錄下的文件時,須要使用絕對路徑的方式進行引用。
安裝 copy-webpack-plugin
:
# copy-webpack-plugin@6.0.2
npm install --save-dev copy-webpack-plugin
複製代碼
在 static
目錄下新建 index.js
,並在 html/index.html
中引入:
const fn = () => {
console.log('我是static目錄下的index.js')
}
fn()
複製代碼
修改 webpack.config.js
:
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'static/*',
to: 'static/[name].[ext]'
}
]
}),
]
}
複製代碼
在 html/index.html
中引用 static/index.js
:
<script src="/static/index.js"></script>
複製代碼
重啓本地服務器(當咱們修改了 webpack
配置後,都須要從新啓動才能生效)。訪問 http://localhost:8899/
,一切都很正常~
咱們在配置 loader
的時候,能夠經過 exclude
和 include
來減小 webpack
轉義的文件。這兩個配置只須要設置其中一個便可,exclude
優先級大於 include
。
exclude
:排除某些文件。
include
:指定某些文件。
一般咱們在開發環境和生產環境會採起不一樣的編譯配置,下面咱們就來看看如何對不一樣的環境添加不一樣的配置。
新建 build
目錄,在該目錄下添加如下文件:
util.js
:提供通用方法。webpack.base.conf.js
:提供公共配置。webpack.dev.conf.js
:提供開發環境配置。webpack.prod.conf.js
:提供生產環境配置。新建 config
目錄,在該目錄下添加如下文件:
index.js
:提供webpack編譯參數。dev.env.js
:提供開發環境參數。prod.env.js
:提供生產環境參數。兩個不一樣文件的 webpack
咱們能夠經過 wepack-merge
進行合併。
ps:目前項目的目錄是參考的 vue-cli
建立的項目,這裏能夠根據本身的想法進行設置。
經過前面的配置,咱們單頁應用的打包配置和多環境的配置基本上已經 ok 了,下面咱們來看一下如何進行多頁應用打包。
在 html
目錄下新建 home.html
。
直接看目錄結構吧:
新建 entry
目錄,用於存放入口文件,修改文件內資源引用的路徑。
修改 webpack.base.conf.js
:
module.exports = {
entry: {
index: './src/entry/index.js',
home: './src/entry/home.js'
},
plugins: [
new HtmlWebpackPlugin({
template: './src/html/index.html',
filename: 'index.html',
minify: {
collapseWhitespace: !isDev,
},
chunks: ['index']
}),
new HtmlWebpackPlugin({
template: './src/html/home.html',
filename: 'home.html',
minify: {
collapseWhitespace: !isDev,
},
chunks: ['home']
}),
]
}
複製代碼
如今項目有多個入口JS和HTML,對於這種多頁應用(實際上就是由多個單頁應用組成的),咱們指望的是在開發時能經過路由切換到對應的頁面下,因此咱們須要經過 historyApiFallback
在 devServer
中設置路由規則。
修改 webpack.dev.conf.js
:
devServer: {
host: config.dev.host,
port: config.dev.port,
compress: true,
historyApiFallback: {
rewrites: [
{ from: /^\/index/, to: '/index.html' },
{ from: /^\/home/, to: '/home.html' }
]
}
},
複製代碼
這樣咱們就能夠在開發的時候隨便查看對應的頁面啦~
在多頁應用中,常常會有一些公共的 css
和 js
,咱們須要經過 optimization.splitChunks
將它們進行分割,打包成一個新的模塊後在對應的頁面分別進行引入。
在 webpack.base.conf.js
中添加相關配置:
optimization: {
splitChunks: {
chunks: 'all', # 代碼塊類型,`all`(默認值)、`initial`(初始化)、`async`(動態加載)
minSize: 0, # 生成塊的最小大小
minChunks: 1, # 生成塊前共享模塊的最小塊數
# 緩存組能夠繼承或覆蓋splitChunks.*;中的任何選項配置。
# 可是test,priority和reuseExistingChunk只能用於緩存組級別上的配置。
# 要禁用任何默認緩存組,須要在緩存組配置中添加 default: false。
cacheGroups: {
vendors: { # 處理第三方依賴
priority: 1,
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
minSize: 0,
minChunks: 1
},
commons: { # 處理公共模塊
chunks: 'initial',
name: 'common',
minSize: 0,
minChunks: 2
}
}
}
}
複製代碼
沒有最好的,只有最適合的,採用什麼樣的配置仍是須要根據項目的實際狀況進行分析。
有一段時間沒更新了,一方面是有點忙,另外一方面是本身也鬆懈了。嗐,越學習就會發現本身不會的越多,而「會」的部分有不少也是隻知其一;不知其二,真是使人惆悵。不過仍是要加油呢~文中如有不足之處和錯誤的地方還請大佬們幫忙指正,謝謝~