webpack
馬上要出5了,完全手寫一個優化後的腳手架是不可或缺的技能。
- 本文書寫時間
2019年5月9日
,webpack
版本4.30.0
最新版本 - 本人所有程式碼均手寫,親自試驗過可以執行達到優化效果。
- 歡迎關注我的專欄
《前端進階》
以後都是高贊高質量文章 - 要轉載必須聯絡本人經過同意才可轉載 謝謝!
- 杜絕
5分鐘
的技術,我們先深入原理再寫配置,那會簡單很多。
實現需求:
- 識別
JSX
檔案 -
tree shaking
搖樹優化 刪除掉無用程式碼 -
PWA
功能,熱重新整理,安裝後立即接管瀏覽器 離線後仍讓可以訪問網站 還可以在手機上新增網站到桌面使用 -
CSS
模組化,不怕命名衝突 - 小圖片的
base64
處理 - 檔案字尾省掉
jsx js json
等 - 實現React懶載入,按需載入 , 程式碼分割 並且支援服務端渲染
- 支援
less sass stylus
等預處理 -
code spliting
優化首屏載入時間 不讓一個檔案體積過大 - 提取公共程式碼,打包成一個
chunk
- 每個
chunk
有對應的chunkhash
,每個檔案有對應的contenthash
,方便瀏覽器區別快取 - 圖片壓縮
-
CSS
壓縮 - 增加
CSS
字首 相容各種瀏覽器 - 對於各種不同檔案打包輸出指定資料夾下
- 快取
babel
的編譯結果,加快編譯速度 - 每個入口檔案,對應一個
chunk
,打包出來後對應一個檔案 也是code spliting
- 刪除
HTML
檔案的註釋等無用內容 - 每次編譯刪除舊的打包程式碼
- 將
CSS
檔案單獨抽取出來 - 讓babel不僅快取編譯結果,還在第一次編譯後開啟多執行緒編譯,極大加快構建速度
- 等等....
-
webpack
中文官網的標語是 :讓一切都變得簡單
概念:
本質上,webpack
是一個現代 JavaScript
應用程式的靜態模組打包器(module bundler
)。當 webpack
處理應用程式時,它會遞迴地構建一個依賴關係圖(dependency graph
),其中包含應用程式需要的每個模組,然後將所有這些模組打包成一個或多個 bundle
。
webpack v4.0.0
開始,可以不用引入一個配置檔案。然而,webpack 仍然還是高度可配置的。在開始前你需要先理解四個核心概念:
- 入口(
entry
) - 輸出(
output
) loader
- 外掛(
plugins
)
本文旨在給出這些概念的高度概述,同時提供具體概念的詳盡相關用例。
讓我們一起來複習一下最基礎的Webpack
知識,如果你是高手,那麼請直接忽略這些往下看吧....
-
入口
- 入口起點`(entry point)指示 webpack 應該使用哪個模組,來作為構建其內部依賴圖的開始。進入入口起點後,webpack 會找出有哪些模組和庫是入口起點(直接和間接)依賴的。
- 每個依賴項隨即被處理,最後輸出到稱之為
bundles
的檔案中,我們將在下一章節詳細討論這個過程。 - 可以通過在
webpack
配置中配置entry
屬性,來指定一個入口起點(或多個入口起點)。預設值為./src
。 -
接下來我們看一個
entry
配置的最簡單例子:webpack.config.js module.exports = { entry: './path/to/my/entry/file.js' };
-
入口可以是一個物件,也可以是一個純陣列
entry: { app: ['./src/index.js', './src/index.html'], vendor: ['react'] }, entry: ['./src/index.js', './src/index.html'],
- 有人可能會說,入口怎麼放
HTML
檔案,因為開發模式下熱更新如果不設定入口為HTML
,那麼更改了HTML
檔案內容,是不會重新整理頁面的,需要手動重新整理,所以這裡給了入口HTML
檔案,一個細節。
-
出口(output)
- output 屬性告訴 webpack 在哪裡輸出它所建立的 bundles,以及如何命名這些檔案,預設值為 ./dist。基本上,整個應用程式結構,都會被編譯到你指定的輸出路徑的資料夾中。你可以通過在配置中指定一個 output 欄位,來配置這些處理過程:
webpack.config.js
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};
在上面的示例中,我們通過 output.filename
和 output.path
屬性,來告訴 webpack bundle
的名稱,以及我們想要 bundle
生成(emit
)到哪裡。可能你想要了解在程式碼最上面匯入的 path 模組是什麼,它是一個 Node.js
核心模組,用於操作檔案路徑。
loader
- loader 讓 webpack 能夠去處理那些非 JavaScript 檔案(webpack 自身只理解 JavaScript)。loader 可以將所有型別的檔案轉換為 webpack 能夠處理的有效模組,然後你就可以利用 webpack 的打包能力,對它們進行處理。
- 本質上,webpack loader 將所有型別的檔案,轉換為應用程式的依賴圖(和最終的 bundle)可以直接引用的模組。
- 注意,loader 能夠 import 匯入任何型別的模組(例如 .css 檔案),這是 webpack 特有的功能,其他打包程式或任務執行器的可能並不支援。我們認為這種語言擴充套件是有很必要的,因為這可以使開發人員建立出更準確的依賴關係圖。
- 在更高層面,在 webpack 的配置中 loader 有兩個目標:
- test 屬性,用於標識出應該被對應的 loader 進行轉換的某個或某些檔案。
-
use 屬性,表示進行轉換時,應該使用哪個 loader。
webpack.config.js const path = require('path'); const config = { output: { filename: 'my-first-webpack.bundle.js' }, module: { rules: [ { test: /\.txt$/, use: 'raw-loader' } ] } }; module.exports = config;
- 以上配置中,對一個單獨的
module
物件定義了 rules 屬性,裡面包含兩個必須屬性:test 和 use。這告訴 webpack 編譯器(compiler
) 如下資訊:
- “嘿,
webpack
編譯器,當你碰到「在require()/import
語句中被解析為'.txt'
的路徑」時,在你對它打包之前,先使用raw-loader
轉換一下。” - 重要的是要記得,在
webpack
配置中定義loader
時,要定義在module.rules
中,而不是 rules。然而,在定義錯誤時webpack
會給出嚴重的警告。為了使你受益於此,如果沒有按照正確方式去做,webpack
會“給出嚴重的警告” -
loader
還有更多我們尚未提到的具體配置屬性。 - 這裡引用這位作者的優質文章內容,手寫一個
loader
和plugin
手寫一個loader和plugin
高潮來了 ,webpack
的編譯原理 ,為什麼要先學學習原理? 因為你起碼得知道你寫的是幹什麼的!
-
webpack
打包原理- 識別入口檔案
- 通過逐層識別模組依賴。(
Commonjs、amd
或者es6的import,webpack
都會對其進行分析。來獲取程式碼的依賴) -
webpack
做的就是分析程式碼。轉換程式碼,編譯程式碼,輸出程式碼 - 最終形成打包後的程式碼
- 這些都是
webpack
的一些基礎知識,對於理解webpack
的工作機制很有幫助。
-
什麼是
loader
?-
loader
是檔案載入器,能夠載入資原始檔,並對這些檔案進行一些處理,諸如編譯、壓縮等,最終一起打包到指定的檔案中 - 處理一個檔案可以使用多個
loader
,loader
的執行順序是和本身的順序是相反的,即最後一個loader
最先執行,第一個loader
最後執行。 - 第一個執行的
loader
接收原始檔內容作為引數,其他loader
接收前一個執行的loader
的返回值作為引數。最後執行的loader
會返回此模組的JavaScript
原始碼 - 在使用多個
loader
處理檔案時,如果要修改outputPath
輸出目錄,那麼請在最上面的loader中options設定
-
什麼是
plugin?
- 在
Webpack
執行的生命週期中會廣播出許多事件,Plugin
可以監聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結果。
- 在
-
plugin和loader
的區別是什麼? - 對於
loader
,它就是一個轉換器,將A檔案進行編譯形成B檔案,這裡操作的是檔案,比如將A.scss或A.less轉變為B.css,單純的檔案轉換過程 -
plugin
是一個擴充套件器,它豐富了wepack
本身,針對是loader
結束後,webpack
打包的整個過程,它並不直接操作檔案,而是基於事件機制工作,會監聽webpack
打包過程中的某些節點,執行廣泛的任務。
-
webpack
的執行-
webpack
啟動後,在讀取配置的過程中會先執行new MyPlugin(options)
初始化一個 MyPlugin 獲得其例項。在初始化compiler 物件後,再呼叫myPlugin.apply(compiler)
給外掛例項傳入compiler
物件。外掛例項在獲取到compiler
物件後,就可以通過compiler.plugin
(事件名稱, 回撥函式) 監聽到Webpack
廣播出來的事件。並且可以通過compiler
物件去操作webpack
- 看到這裡可能會問
compiler
是啥,compilation
又是啥? -
Compiler
物件包含了 Webpack 環境所有的的配置資訊,包含options,loaders,plugins
這些資訊,這個物件在 Webpack 啟動時候被例項化,它是全域性唯一的,可以簡單地把它理解為Webpack
例項; -
Compilation
物件包含了當前的模組資源、編譯生成資源、變化的檔案等。當Webpack
以開發模式執行時,每當檢測到一個檔案變化,一次新的Compilation
將被建立。Compilation
物件也提供了很多事件回撥供外掛做擴充套件。通過Compilation
也能讀取到Compiler
物件。 -
Compiler
和Compilation
的區別在於: -
Compiler
代表了整個Webpack
從啟動到關閉的生命週期,而Compilation
只是代表了一次新的編譯。
-
-
事件流
-
webpack
通過Tapable
來組織這條複雜的生產線。 -
webpack
的事件流機制保證了外掛的有序性,使得整個系統擴充套件性很好。 -
webpack
的事件流機制應用了觀察者模式,和Node.js 中的 EventEmitter
非常相似。
-
下面正式開始開發環境的配置:
-
入口設定 :
- 設定APP,幾個入口檔案,即會最終分割成幾個
chunk
- 在入口中配置
vendor
,可以code spliting
,將這些公共的複用程式碼最終抽取成一個chunk
,單獨打包出來 - 要想在開發模式中
HMTL
檔案也熱更新,需要加入·index.html
為入口檔案
- 設定APP,幾個入口檔案,即會最終分割成幾個
entry: {
app: ['./src/index.js', './src/index.html'],
vendor: ['react'] //這裡還可以加入redux react-redux better-scroll等公共程式碼
},
-
output
出口-
webpack
基於Node.js
環境執行,可以使用Node.js
的API
,path
模組的resolve
方法 - 對輸出的
JS
檔案,加入contenthash
標示,讓瀏覽器快取檔案,區別版本。
-
output: {
filename: '[name].[contenthash:8].js',
path: resolve(__dirname, '../dist')
},
-
mode: 'development'
模式選擇,這裡直接設定成開發模式,先從開發模式開始。 -
resolve
解析配置,為了為了給所有檔案字尾省掉js jsx json
,加入配置resolve: { extensions: [".js", ".json", ".jsx"] }
-
加入外掛 熱更新
plugin
和html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin') const webpack = require('webpack') new HtmlWebpackPlugin({ template: './src/index.html' }), new webpack.HotModuleReplacementPlugin(),
-
加入
babel-loader
還有 解析JSX ES6
語法的babel preset
-
@babel/preset-react
解析jsx語法
-
@babel/preset-env
解析es6
語法 -
@babel/plugin-syntax-dynamic-import
解析react-loadable
的import
按需載入,附帶code spliting
功能
-
{
test: /\.(js|jsx)$/,
use:
{
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react", ["@babel/preset-env", { "modules": false }]],
plugins: ["@babel/plugin-syntax-dynamic-import"]
},
cacheDirectory: true//開啟babel編譯快取
}
},
- 加入
thread-loader
,在babel
首次編譯後開啟多執行緒
const os = require('os')
{
loader: 'thread-loader',
options: {
workers: os.cpus().length
}
}
-
React
的按需載入,附帶程式碼分割功能 ,每個按需載入的元件打包後都會被單獨分割成一個檔案
import React from 'react'
import loadable from 'react-loadable'
import Loading from '../loading'
const LoadableComponent = loadable({
loader: () => import('../Test/index.jsx'),
loading: Loading,
});
class Assets extends React.Component {
render() {
return (
<div>
<div>這即將按需載入</div>
<LoadableComponent />
</div>
)
}
}
export default Assets
- 加入
html-loader
識別html
檔案
{
test: /\.(html)$/,
loader: 'html-loader'
}
- 加入
eslint-loader
{
enforce:'pre',
test:/\.js$/,
exclude:/node_modules/,
include:resolve(__dirname,'/src/js'),
loader:'eslint-loader'
}
- 開發模式結束 程式碼在下面的
git
倉庫裡
必須瞭解的webpack
熱更新原理 :
webpack
的熱更新又稱熱替換(Hot Module Replacement
),縮寫為HMR
。 這個機制可以做到不用重新整理瀏覽器而將新變更的模組替換掉舊的模組。-
首先要知道server端和client端都做了處理工作
- 第一步,在
webpack 的 watch
模式下,檔案系統中某一個檔案發生修改,webpack
監聽到檔案變化,根據配置檔案對模組重新編譯打包,並將打包後的程式碼通過簡單的JavaScript
物件儲存在記憶體中。 - 第二步是
webpack-dev-server
和webpack
之間的介面互動,而在這一步,主要是dev-server
的中介軟體webpack-dev-middleware 和 webpack
之間的互動,webpack-dev-middleware
呼叫webpack
暴露的 API對程式碼變化進行監控,並且告訴webpack
,將程式碼打包到記憶體中。 - 第三步是
webpack-dev-server
對檔案變化的一個監控,這一步不同於第一步,並不是監控程式碼變化重新打包。當我們在配置檔案中配置了devServer.watchContentBase
為 true 的時候,Server 會監聽這些配置資料夾中靜態檔案的變化,變化後會通知瀏覽器端對應用進行 live reload。注意,這兒是瀏覽器重新整理,和 HMR 是兩個概念。 - 第四步也是
webpack-dev-server
程式碼的工作,該步驟主要是通過 sockjs(webpack-dev-server 的依賴)在瀏覽器端和服務端之間建立一個 websocket 長連線,將 webpack 編譯打包的各個階段的狀態資訊告知瀏覽器端,同時也包括第三步中 Server 監聽靜態檔案變化的資訊。瀏覽器端根據這些 socket 訊息進行不同的操作。當然服務端傳遞的最主要資訊還是新模組的 hash 值,後面的步驟根據這一 hash 值來進行模組熱替換。 -
webpack-dev-server/client
端並不能夠請求更新的程式碼,也不會執行熱更模組操作,而把這些工作又交回給了webpack,webpack/hot/dev-server
的工作就是根據webpack-dev-server/client
傳給它的資訊以及dev-server
的配置決定是重新整理瀏覽器呢還是進行模組熱更新。當然如果僅僅是重新整理瀏覽器,也就沒有後面那些步驟了。 -
HotModuleReplacement.runtime
是客戶端 HMR 的中樞,它接收到上一步傳遞給他的新模組的hash
值,它通過JsonpMainTemplate.runtime
向 server 端傳送 Ajax 請求,服務端返回一個json
,該json
包含了所有要更新的模組的 hash 值,獲取到更新列表後,該模組再次通過 jsonp 請求,獲取到最新的模組程式碼。這就是上圖中 7、8、9 步驟。 - 而第 10 步是決定 HMR 成功與否的關鍵步驟,在該步驟中,
HotModulePlugin
將會對新舊模組進行對比,決定是否更新模組,在決定更新模組後,檢查模組之間的依賴關係,更新模組的同時更新模組間的依賴引用。 - 最後一步,當
HMR
失敗後,回退到live reload
操作,也就是進行瀏覽器重新整理來獲取最新打包程式碼。 - 參考文章 webpack面試題-騰訊雲
- 第一步,在
正式開始生產環節:
-
加入
WorkboxPlugin
,PWA
的外掛-
pwa
這個技術其實要想真正用好,還是需要下點功夫,它有它的生命週期,以及它在瀏覽器中熱更新帶來的副作用等,需要認真研究。可以參考百度的lavas
框架發展歷史~
-
const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
clientsClaim: true, //讓瀏覽器立即servece worker被接管
skipWaiting: true, // 更新sw檔案後,立即插隊到最前面
importWorkboxFrom: 'local',
include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],
}),
- 加入每次打包輸出檔案清空上次打包檔案的外掛
const CleanWebpackPlugin = require('clean-webpack-plugin')
new CleanWebpackPlugin()
- 加入
code spliting
程式碼分割
optimization: {
runtimeChunk:true, //設定為 true, 一個chunk打包後就是一個檔案,一個chunk對應`一些js css 圖片`等
splitChunks: {
chunks: 'all' // 預設 entry 的 chunk 不會被拆分, 配置成 all, 就可以了拆分了,一個入口`JS`,
//打包後就生成一個單獨的檔案
}
}
- 加入單獨抽取
CSS
檔案的loader
和外掛
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
{
test: /\.(less)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader', options: {
modules: true,
localIdentName: '[local]--[hash:base64:5]'
}
},
{loader:'postcss-loader'},
{ loader: 'less-loader' }
]
}
new MiniCssExtractPlugin({
filename:'[name].[contenthash:8].css'
}),
- 加入壓縮
css
的外掛
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
new OptimizeCssAssetsWebpackPlugin({
cssProcessPluginOptions:{
preset:['default',{discardComments: {removeAll:true} }]
}
}),
- 殺掉
html
一些沒用的程式碼
new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
}
}),
- 加入圖片壓縮
{
test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
use:[
{loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[name].[hash:8].[ext]',
outputPath:'/img'
}},
{
loader: 'img-loader',
options: {
plugins: [
require('imagemin-gifsicle')({
interlaced: false
}),
require('imagemin-mozjpeg')({
progressive: true,
arithmetic: false
}),
require('imagemin-pngquant')({
floyd: 0.5,
speed: 2
}),
require('imagemin-svgo')({
plugins: [
{ removeTitle: true },
{ convertPathData: false }
]
})
]
}
}
]
}
- 加入
file-loader
把一些檔案打包輸出到固定的目錄下
{
exclude: /\.(js|json|less|css|jsx)$/,
loader: 'file-loader',
options: {
outputPath: 'media/',
name: '[name].[contenthash:8].[ext]'
}
}
裡面有一些註釋可能不詳細,程式碼都是自己一點點寫,試過的,肯定沒用任何問題
- 需要的依賴
{
"name": "webpack",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"autoprefixer": "^9.5.1",
"babel-loader": "^8.0.5",
"clean-webpack-plugin": "^2.0.2",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
"eslint-loader": "^2.1.2",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"imagemin": "^6.1.0",
"imagemin-gifsicle": "^6.0.1",
"imagemin-mozjpeg": "^8.0.0",
"imagemin-pngquant": "^7.0.0",
"imagemin-svgo": "^7.0.0",
"img-loader": "^3.0.1",
"less": "^3.9.0",
"less-loader": "^5.0.0",
"mini-css-extract-plugin": "^0.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"postcss-loader": "^3.0.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-loadable": "^5.5.0",
"react-redux": "^7.0.3",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.2",
"webpack-dev-server": "^3.3.1",
"workbox-webpack-plugin": "^4.3.1"
},
"scripts": {
"start": "webpack-dev-server --config ./config/webpack.dev.js",
"dev": "webpack-dev-server --config ./config/webpack.dev.js",
"build": "webpack --config ./config/webpack.prod.js "
},
"devDependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.2.0"
}
}