詳解webpack打包
技術標籤:javascriptwebpack
模組化打包工具的由來
- ES Modules存在環境相容問題
- 模組檔案過多,網路請求頻繁
- 所有的前端資源都需要模組化
- 毋庸置疑,模組化是必要的
解決問題
- 將es6等新功能變成es5等瀏覽器能識別的語言
- 將很多個模組打包成一個bundle.js(在開發模式下用模組化開發 在生產模式下打包成一個bundle.js檔案)
模組化打包工具概述
- webpack 模組化打包工具
- 模組載入器(Loader)
- 程式碼拆分(Code splitting)
- 資源模組(Asset Module)
上面的到爆工具解決的是前端整體的模組化並不單指Javascript模組化
webpack快速上手
- 初始化 yarn init -y
- webpack資料工具模組需要安裝 yarn add webpack webpack-cli --dev
- 配置webpack.config.js檔案(執行在nodejs的檔案所以要符合commjs)
const path = require('path');
module.exports={
entry:'./src/main.js', //檔案入口
output:{ //輸出目錄
filname:'bundle.js', //輸出檔名稱
path:path.join(_dirname,'output' ) //在outuput檔案裡面生成bundle.js檔案 //指定輸出檔案的目錄(需要絕對路徑)
}
}
webpack模式
- none 最原始狀態打包
- production
- development
webpack資源模組載入
藉助loader可以載入任何形式的資源(loader對於同一個源可以依次使用多個loader,通過多個loader完成同一個功能。例如style-loader,css-loader)
dataUrl 可以用base64編碼進行表示任何檔案 url-loader
- 解析css 模組 安裝
- yarn add style-loader --dev
- yarn add css-loader --dev
- 檔案資源載入器 例如有些內容是不能轉化成js的例如圖片、字型 就需要使用file-loader
- yarn add file-loader --dev
- main.js中
- 使用dataUrl載入任何檔案
- yarn add url-loader
- 適合體積比較小的檔案,因為這樣減少請求次數,如果檔案過大會影響打包速度 url-
- 較大的檔案使用file-loader單個檔案存放,提高載入速度
- 超過10kb檔案單獨提取存放
- 小於10kb轉換為dataUrl嵌入程式碼中
- 使用babel-loader轉化新特性
- yarn add babel-loader @babel/core @babel/preset-env --dev
- babel只是一個轉化程式碼的平臺,具體轉化需要轉化工具進行配置
import icon from './icon.png' const img = new Image(); img.src = icon; document.body.append(img);
- 解析html(使用html-loader)
- yarn add html-loader --dev
<!-- footer.html --> <footer> <a href="better.png">download</a> </footer>
<!-- main.js --> import footerHtml from './footer.html' documnet.write(footerHtml)
// 使用module:
module:{
rules:[
{
test:'/.js$/',
use: {
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
},
{ //將css轉換成js的模組
test:'/.css$/',
//這裡需要注意 陣列中的執行順序是從右往左執行的 必須先解析css在載入style-loader
use:['style-loader',css-loader']
},
{
test:'/.png$',
// use:'file-loader'url
// use:'url-loader'
use:{
loader:'url-loader',
options:{
limit:10*1024 //10kb
}
}
},
{
test:'/.html$/',
loader:'html-loader'
//html中img src屬性觸發打包如果想讓別的也觸發可以進行如下配置
options:{
attrs:['img:src','a:href']
}
}
]
}
需要注意如果我們配置了url-loader 的大小限制,小於走url-loader否則走file-loader 但是需要注意的是解除安裝掉file-loader會報錯(可不配置file-loader但是不能解除安裝)
webpack只是一個打包工具,如果你想處理程式碼編譯轉化的功能需要使用對應的loader進行轉化
常用的載入器
* 編譯轉化載入類(css-loader)
* 檔案操作型別載入器(file-loader)
* 程式碼檢查載入器
webpack匯入資源模組
- 在使用的js中使用import ‘./main.css’
需要注意我們平時寫的專案要求前後端分離 webpack建議我們根據程式碼的需要動態匯入資源,真正需要資源的不是應用,而是程式碼,程式碼什麼時候需要就要載入,程式碼更新不需要資源這個時候就不需要載入,webpack驅動了整個前端的應用。
- 邏輯合理,js需要這些資原始檔
- 確保上線資源不缺失,都是必要的
webpack中模組的載入方式
- 遵循ESmodules 標準的import
- 支援commonJS標準的require函式 但需要注意如果載入一個ESmodule的defalut匯出 需要使用
require('./header.js').default;
- 支援AMD標準的define函式和require函式
- 需要注意的是隻建議在一個專案中使用一種引用方式,不然會增加維護成本
- 樣式程式碼中的@import指令和url函式
- html程式碼中圖片標籤的src屬性
webpack打包的工作原理
專案當中都會散落著各種各樣的程式碼和資原始檔,weboack會根據配置找到一個主入口檔案一般都是.js的檔案根據程式碼import export等依賴的模組和整個的模組依賴,形成依賴數,webpack會遞迴遍歷依賴數,找到每個節點的所對應的資原始檔,根據模組對應的ruls找到模組對應的載入器,然後交給對應的載入器載入模組,載入到的結果放到bundle.js檔案中,實現專案的打包。loader是webpack的核心。
webpack loader的工作原理
自己開發一個markdown-loader,注意你可以使用多個loader但是最終返回的結果是一個javascript的程式碼
- 安裝一個 marked的解析模組
- yarn add marked --dev
- 匯出的檔案支援module.exports 和esModule的格式 export.defaule
- 如果選擇第二種方式需要安裝處理html的loader add html-loader
markdown-loader.js
const marked = required('marked');
module.exports = source =>{
const html = marked(source);
// 第一種匯出將 匯出內容直接轉化成javascript的格式
//以下兩種格式都支援匯出
<!--return `module.exports = ${JSON.stringify(html)}`
return `export.defaule = ${JSON.stringify(html)}`
-->
//第二種格式 返回的html字串直接交給下一個loader來處理
return html;
<!--console.log(source)-->
<!--return source;-->
}
配置webpack.json
//返回javascript的格式
{
test:'./md$/',
use:'./markdown-loader' //不僅可以使用模組還可以使用路徑地址
}
//返回html的格式
{
test:'./md$/',
use:['html-loader','./markdown-loader'] //再次強調陣列的執行順序是從後面像前面執行的
}
webpack外掛功能
loader處理資源 plugin處理那些非資源的內容實現了大多的功能
- 自動清除目錄的外掛 clean-webpack-plugin
- yarn add clean-webpack-plugin --dev
- 自動生成html檔案(如果不輸出html檔案 那麼我就就需要將html引入的script路徑手動指向打包的dist目錄下相應用的路徑地址如果打包的js檔名有所變化那我們就需要手動更改。)
- yarn add html-webpack-plugin --dev
- webpack動態生成html檔案
- html中可以自己建立 用loadsh的語法輸出動態的內容
<h1><%= htmlwebpackPlugin.options.title %></h1>
- webpack多頁面輸出
- webpack c處理不參與打包的靜態檔案需要複製到對應目錄
- yarn add copy-webpack-plugin --dev
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin') //預設匯出的外掛型別
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
new CleanWebpackPlugin()
<!--new HtmlWebpackPlugin()-->
new HtmlWebpackPlugin({ //生成index.html
title:'webpack',
meta:{
viewport:'width=device-width'
},
template:'./src/index.html'
})
//新增多個例項輸出多個檔案
new HtmlWebpackPlugin({
filename:'about.html'
}),
//接受的是一個數組,可以寫路徑,目錄,萬用字元等
new CopyWebpackPlugin([
'public'
//public/**
])
]
webpack開發一個外掛
定義外掛一個函式或者是一個包含apply方法物件
// 清除bundle.js中沒用註釋 emit在webpack輸出檔案時候執行
class MyPlugin {
apply(compiler) {
console.log('Myplugin');
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation => 可以理解為此次打包的上下文
for (const name in compilation.assets) {
//檔案的名稱
// console.log(compilation.assets[name].source()) //內容
if (name.endsWith('.js')) {
const contents = compilation.assets[name].source();
console.log(contents);
const withoutComments = contents.replace(/\/\*\*+\*\//g, '');
// /******/ \/\*\*+\*\/
compilation.assets[name] = { .//webpack必須要暴露出來的兩個方法
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
在plugin中new MyPlugin();
webpack增強開發體驗 實現自動編譯
- webpack dev server 高度整合工具 自動編譯,自動重新整理瀏覽器
yarn add webpack-dev-server --dev
// 通過yarn執行cli
yarn webpack-dev-server 執行命令會直接打包應用
*處理靜態資源 webpack-dev-server
// 註釋:上面程式碼中new CopyWebpackPlugin 可以實現資源的copy,但是一般new CopyWebpackPlugin都留在上線前的打包使用而已,因為在開發過程中每次打包都copy會影響速度。
module.exports = {
devServer:{
contentBase:['./public'] //指定靜態資源
}
}
webpack-dev-server 支援代理服務
devServer:{
contentBase:'./public',
proxy:{
// 請求字首
'/api':{
// http://localhost:8080/api/users -> https:api.geithub.com/api/users
target:"https:api.github.com", //代理目標
pathRewrite:{
'^/api':'' //代理路徑重寫
},
//不能使用localhost:8080 作為請求GitHub的主機名
changeOrign:true
}
}
}
source Map
- 執行程式碼和原始碼之間有很大差異。
- 幫助開發者除錯和定位錯誤
- sourceMappingUrl = jquery-3.4.1.min.map 根據對映關係定位到原始碼
- 解決了原始碼與執行程式碼不一致所導致的問題
如何配置source Map
devtool 開發過程中的輔助工具
- 目前source Map支援12種
- 每種方式的功效和效率都不相同
webpack配置source Map (eval模式下的source Map)
- eval(‘console.log(123)’)
- eval(‘console.log(123) //# sourceURL=./foo/’)
- 將webpack.config.js中的屬性設定為 devtool:‘eval’
- 使用eval 構建速度最快 但是不知道錯誤的行內資訊
模式
- eval 將模組程式碼放到eval中但是不會生成對應的sourvcemao
- eval-source-map 生成sourceMAP
- cheap-eval-source-map 生成速度較快可以定義行錯誤資訊不能定義到列,加工過後的結果,loader處理過後的結果
- cheap-module-eval-source-map 能夠定義到行 沒有經過加工手寫的原始碼
- inline-source-map和普通的相同inline-source-map以dataurl嵌入進來和eval相同 這個不常用因為體積較大
- hidden-source-map在這個模式下程式碼並沒有看到引用,但是確實是已經生成了
- nosources-source-map 能看到錯誤資訊,但是沒有原始碼開發過程中看不到原始碼,為了在生產環境中不爆露原始碼
webpack選擇Source Map模式
- 開發環境打包cheap-eval-source-map
原因:
個人編寫程式碼風格每一行不會超過80個字元,所以定義到行就可以了
使用框架比較多,loader轉化之後和之前都有很多差別,所以要選擇有module
首次打包啟動會慢,但是重寫打包速度較快
- none或者生產環境使用nosources-source-map
原因:
會暴露原始碼
除錯是開發階段的事情,開發階段儘量把所有的問題都找出來解決
## webpack dev server
webpack dev server重新整理會對樣式有影響,每次構建都需要重新整理頁面
HRM
- 開啟HRM 整合在webpack-dev-server 中
第一種
## webpack-dev-server --host
第二種
##devServer:{
hot:true
// 如果出錯可以報錯不會自動重新整理 hot:only
}
const webpack = require('webpack');
plugins:[
new webpack.HotModuleReplacementPlugin();
]
//啟動 yarn webpack-dev-server --open
HMR疑惑
- webpack中的HRM需要手動配置
- 樣式檔案可以自動處理,js指令碼不可以
HMR API
module.hot.accept('./editor',()=>{
})
webpack 不同環境下的配置
- 配置檔案根據環境不同匯出不同配置
module.exports = (env,argv){
//env cli傳過來的引數
//argv 除了env之後的所有引數
if(env ==='production'){
config.mode = 'production'
config.devtool = false
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopywebpackPlugin(['public'])
]
}
return config
}
//執行 yarn webpack --env production
- 一個環境對應一個配置檔案
// 一般有三個檔案 一個線上一個開發 一個公共檔案
// ## webpack.prod.js
const common = require(./webpack.common)
const merge = require('webpack-merge')
const { CleanWebpackPlugin} = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common,{
mode:'production',
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
//單獨執行 yarn webpack --config webpack.prod.js
// ## webpack.dv.js
// ## webpack.common.js
// 公共配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry:'./src/main.js',
output:{
filename:'js/bundle.js'
},
module:{
rules:[
{
test:'/.js$/',
use: {
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
},
},
{ //將css轉換成js的模組
test:'/.css$/',
//這裡需要注意 陣列中的執行順序是從右往左執行的 必須先解析css在載入style-loader
use:['style-loader',css-loader']
},
{
test:'/.png$',
// use:'file-loader'url
// use:'url-loader'
use:{
loader:'url-loader',
options:{
limit:10*1024 //10kb
}
}
},
{
test:'/.html$/',
loader:'html-loader'
//html中img src屬性觸發打包如果想讓別的也觸發可以進行如下配置
options:{
attrs:['img:src','a:href']
}
}
]
}
}
DefinePlugin
- 為程式碼注入全域性成員 預設啟動 預設為全域性註冊了 process.env.NODE_ENV
const webpack = require('webpack');
PLUGINS :[
new webpack.DefinePlugin({
API_BASE_URL:'"https://api.example.com"'
//JSON.stringigy 如果生產和開發的值不一樣可以通過這個來注入
})
]
tree-shaking
在生產模式下自動開啟
集中配置webpack的優化功能
module.exports = [
mode :'none',
entry:'./src/index.js',
output:{
filename:'bundle.js'
},
optimization:{ //優化
usedExports:true, //只標記使用的程式碼,
minimize:true, //把這些無用的搖掉
}
]
webpack 模組合併
optimization:{ //優化
concatenatModules:true
//註釋:平時打包會有很多模組函式,這樣合併成一個,儘可能將所有的模組合併一個模組被稱為 Scope Hoisting 作用域的提升,既提升了執行效率,又減少了程式碼體積
}
Tree-shaking & Babel
Tree-shaking前提是ES Modles,由webpack打包的程式碼必須使用ESM,babel-loader處理程式碼時有可能會把esmodule轉化成了comminjs 所以 tree-shaking就不會生效 在最新版本中babel支援了tree-shaking原因是 原始碼中並沒有轉化esmodule
//自己配置 如果使用commonjs這個時候就不會執行tree-shaking
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:[['@babel/preset-env',{modules:'commonjs'}]
//{modules:'commonjs'} 就不會執行 將modules:false就會禁止轉化esmodule 所以會執行tree-shking
}
}
}
sideEffects 副作用
模組執行除了匯出成員之外所做的其他的事情 一般用於npm包標記是否有副作用
- 使用場景 在多模組開發中,加入我們將多個元件都放到index.js中引用 但是 我們在外部呼叫的時候只應用了其中的button元件 但是這個元件匯入的是index.js這個時候打包會把所有的元件都打包進去這個時候我們就需要使用 optimization中的sideEffects
optimization:{
sideEffects:true, //打包看是否有標識看是否有副作用 如果沒有則別移除
}
//在package.json中增加
"sideEffects":false //標識當前專案沒有副作用
- 注意你的程式碼真的沒有副作用 css都屬於副作用
解決方法:
在package.json中
"sideEffects":['./extend.js','./extnd.css'] 陣列進行配置
程式碼分割 Code Splitting
並不是每個模組在啟動的時候都需要載入 要不然bundle的檔案過大
- 根據不同需求 可分為多入口打包
- 動態匯入按需載入
多頁面打包 Multi Entry (多入口打包)
應用於多應用程式 一個頁面提供一個打包入口 公共的可以提出
## 在entry 中進行配置多頁面 記住是物件
entry:{
index:'./src/index.js',
album:'./src/album.js'
}
## 出口自動設定相應的檔案
output:[name].bundle.js
## html中 new html-webpack-plgins中設定chunks:['index']另一個設定 chunks:['album']
公共模組的提取
多入口打包會存在公共的模組
例如index.js中和album.js中公共的模組使用了global.css的模組
optimization:{
splitChunks:{
chunks:'all'
}
}
MiniCssExtractPlugin提取css單位件
- 安裝外掛 yarn add mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
//style-loader //將樣式是通過style標籤注入
MiniCssExtractPlugin.loader,//如果檔案小於150kb可以不用單獨提出來
css-loader //css 解析
壓縮樣式檔案
- 安裝 yarn add optimize-css-assets-webpack-plugin --dev
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
plugins:[
new OptimizeCssAssetsWebpackPlugin(); //配資到plugins中 都可以使用
]
optimization:{
minimizer:[
new OptimizeCssAssetsWebpackPlugin(); //配置到這裡 在沒有開啟壓縮的時候外掛就不會工作
//需要注意的是配置了minimizer 這個時候打包預設手動填寫,需要配置js打包壓縮不然js無法壓縮
new TerserWebpackPlugin() //需要安裝和引用
]
}
const TerserWebpackPlugin = ('terser-webpack-plugin')
輸出檔名Hash
在生產模式下,檔名使用hash
output:{
//針對專案,有一個發生改變就會重新生成
filename:'[name]-[hash].bundle.js'
// 打包過程中同一類,改動哪裡那個類js.css都會改變
filename:'[name]-[chunkhash].bundle.js'
//解決快取最好的方法 每個檔案都有hashcontenthash:8 也可以指定位數 預設20
filename:'[name]-[contenthash].bundle.js'
}
註釋
-
output 輸出的必須是絕對路徑
-
webpack載入資源的方式:
- 遵循ES Modules標準的import宣告
- 遵循CommonJS標準的require函式
- 遵循AMD標準的define函式和require函式
- 樣式程式碼中的@import指令和url函式
- HTML程式碼中圖片標籤的src屬性
-
webpack本身只能打包js檔案,但對於其他資源例如css,圖片,或者其他的語法集比如jsx,是沒有辦法載入的,這就需要對應的loader將資源轉化,載入進來
-
TreeShaking在生產模式會自動開啟,他是一組功能搭配使用後的效果,起鬨usedExports負責標記(枯樹葉),sideEffects一般標記npm包標記是否有副作用