webpack
立刻要出5了,徹底手寫一個優化後的腳手架是不可或缺的技能。css
本文書寫時間 2019年5月9日
, webpack
版本 4.30.0
最新版本html
要轉載必須聯繫本人通過贊成纔可轉載 謝謝!node
杜絕5分鐘
的技術,咱們先深刻原理再寫配置,那會簡單不少。react
實現需求:webpack
識別JSX
文件git
tree shaking
搖樹優化 刪除掉無用代碼es6
PWA
功能,熱刷新,安裝後當即接管瀏覽器 離線後仍讓能夠訪問網站 還能夠在手機上添加網站到桌面使用github
CSS
模塊化,不怕命名衝突web
小圖片的base64
處理面試
文件後綴省掉jsx js json
等
實現懶加載,按需加載 , 代碼分割
支持less sass stylus
等預處理
code spliting
優化首屏加載時間 不讓一個文件體積過大
提取公共代碼,打包成一個chunk
每一個chunk
有對應的chunkhash
,每一個文件有對應的contenthash
,方便瀏覽器區別緩存
圖片壓縮
CSS
壓縮
增長CSS
前綴 兼容各類瀏覽器
對於各類不一樣文件打包輸出指定文件夾下
緩存babel
的編譯結果,加快編譯速度
每一個入口文件,對應一個chunk
,打包出來後對應一個文件 也是code spliting
刪除HTML
文件的註釋等無用內容
每次編譯刪除舊的打包代碼
將CSS
文件單獨抽取出來
等等....
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)
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][2]
高潮來了 ,
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
很是類似。下面正式開始開發環境的配置:
chunk
vendor
,能夠code spliting
,將這些公共的複用代碼最終抽取成一個chunk
,單獨打包出來HMTL
文件也熱更新,須要加入·index.html
爲入口文件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"]
},
}
},
複製代碼
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'
}
```
複製代碼
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const WorkboxPlugin = require('workbox-webpack-plugin')
module.exports = {
entry: {
app: ['./src/index.js', './src/index.html'],
vendor: ['react', ]
},
output: {
filename: '[name].[hash:8].js',
path: resolve(__dirname, '../build')
},
module: {
rules: [
{
enforce:'pre',
test:/\.js$/,
exclude:/node_modules/,
include:resolve(__dirname,'/src/js'),
loader:'eslint-loader'
},
{
oneOf: [{
test: /\.(html)$/,
loader: 'html-loader'
},
{
test: /\.(js|jsx)$/,
use:
{
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react", ["@babel/preset-env", { "modules": false }]],
plugins: ["@babel/plugin-syntax-dynamic-import"]
},
}
},
{
test: /\.(less)$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader', options: {
modules: true,
localIdentName: '[local]--[hash:base64:5]'
}
},
{ loader: 'less-loader' }
]
}, {
test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[name].[hash:8].[ext]',
}
}, {
exclude: /\.(js|json|less|css|jsx)$/,
loader: 'file-loader',
options: {
outputPath: 'media/',
name: '[name].[hash].[ext]'
}
}
]
}]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), //打包時候能夠看到文件名的插件
],
mode: 'development',
devServer: {
contentBase: '../build',
open: true,
port: 3000,
hot: true
},
resolve: {
extensions: [".js", ".json", ".jsx"]
}
}
複製代碼
必須瞭解的
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 請求,獲取到最新的模塊代碼。這就是上圖中 七、八、9 步驟。
而第 10 步是決定 HMR 成功與否的關鍵步驟,在該步驟中,HotModulePlugin
將會對新舊模塊進行對比,決定是否更新模塊,在決定更新模塊後,檢查模塊之間的依賴關係,更新模塊的同時更新模塊間的依賴引用。
最後一步,當 HMR
失敗後,回退到 live reload
操做,也就是進行瀏覽器刷新來獲取最新打包代碼。
參考文章 [webpack面試題-騰訊雲][4]
正式開始生產環節:
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`,
//打包後就生成一個單獨的文件
}
}
複製代碼
{
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 }
]
})
]
}
}
]
}
```
* 加入單獨抽取`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,
}
複製代碼
}),
* 加入`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"
}
}
# 整個項目和`webpack`配置的源碼地址 : [源碼地址啊 看得見嗎親][5]
# **路過的小夥伴麻煩點個贊給個star,寫得好辛苦啊!!!!**
[1]: /img/bVbspk3
[2]: /img/bVbspEk
[3]: /img/bVbspE4
[4]: webpackjs.com/configuration/module/#rule-oneof
[5]: https://github.com/JinJieTan/React-webpack複製代碼