webpack基本用法及原理(10000+)
1 webpack是什麼
所有工具的出現,都是為了解決特定的問題,那麼前端熟悉的webpack是為了解決什麼問題呢?
1.1 為什麼會出現webpack
js模組化:
瀏覽器認識的語言是HTML,CSS,Javascript,而其中css和javascript是通過html的標籤link
,script
引入進來。
隨著前端項目的越來越複雜,css和js文件會越來越龐大,那麼在開發階段,就必須要把css和js按功能拆分成幾個小文件,方便開發。
那麼拆分的小文件如何引入到html中呢?css可以通過link
標籤或者@import
css語法,但是js因為沒有模組導入的語法(ES6有了import,但還不是所有瀏覽器兼容),就只能通過script
標籤引入。但是這樣的話會導致很多問題:
- http請求大量增多,影響頁面呈現速度。
- 全局變數混亂,難以維護。
針對js模組化出現了很多的解決方案,總結來說有幾種規範:
- CommonJs,語法為:require(), module.exports(同步載入,適用於node伺服器環境)
- ES6 Mode,語法為:import,export(非同步載入,適用於瀏覽器環境)
- AMD,語法為:require(),define()(非同步載入,適用於瀏覽器環境)
工程化:
除了js模組化的問題之外,前端還有很多其他的問題,比如程式碼混淆,程式碼壓縮,scss,less等css預編譯語言的編譯,typescript的編譯,eslint檢驗程式碼規範,如果這些任務都需要手工去執行的話,太繁瑣,也容易出錯。
1.2 webpack能做什麼
-
模組化
其實webpack的核心就是解決js模組化問題的工具,運行在node環境中,同時可以支援commonjs,es6,amd的模組語法(可以使用:reuire/mocule.exports,import/export,require/define的方式來導入導出模組)。可以將開發時候拆分為不同文件的js程式碼,打包成一個js文件。也可以通過配置靈活的拆分js程式碼,通過 tree shaking 刪減沒有使用到的程式碼。
模組化打包時webpack的核心功能,但是它還有兩個非常重要的機制loader和plugin。
-
loader
webpack本身只支援js,json文件的模組化打包,但是有開放出loader介面,通過不同的loader可以將其他格式的文件轉化為可識別的模組,比如:
css-loader可以識別css文件,raw-loader可以直接將文件當作模組,less-loader,sass-loader可以直接識別less,sass文件。 -
plugin
插件機制是webpack的另一個重要的拓展,webpack在打包的過程中,會暴露出不同的生命周期事件,而插件會監聽這些事件,然後做出對應的操作,比如:
UglifyJsPlugin可以混淆壓縮程式碼,EslintWebpackPlugin可以執行eslint的程式碼格式檢測和自動修復。
總結:
webpack是一個運行在node環境下,對js文件進行模組化打包的工具。通過loader機制可以實現除js格式外的其他格式文件,通過plugin機制可以實現自動執行一些工程化需要的任務。
2 webpack怎麼用
那麼webpack要怎麼使用呢?
2.1 安裝運行
首先要安裝webapck,使用npm(npm基本用法及原理),安裝webpack(核心),webpack-cli(命令行工具):
npm install webpack webpack-cli
然後創建以下兩個文件:name.js,index.js,
我們以es6的語法導入導出模組,es6模式的模組變數的導出時按引用導出,就是在模組的變數如果在外部被修改,也會作用到模組內部,而commonjs的模式是按值導出,即模組外部的修改,不會影響到模組內部。
//name.js
let name = "小明"
function say(){
console.log('my name is ',name)
}
export {
name,
say
}
//index.js
import { name,say } from "./name.js";
name = "小紅"
say()
console.log('he name is ',name)
然後運行打包命令:
//用npx直接運行webpack命令
npx webpack
//或者用npm的腳本運行打包
//package.json
{
script:{
pack:'webpack'
}
}
npm run pack
webpack默認從index.js文件開始打包,所以如果開始文件的名稱為index,就可以不需要寫配置文件,就可以直接打包。
默認打包的模式是『production’即生產模式,打包成功後,會自動創建dist文件夾,並生成main.js文件:
//main.js
(()=>{"use strict";let e="小明";e="小紅",console.log("my name is ","小紅"),console.log("he name is ","小紅")})();
我們看到打包後的文件把index.js和name.js兩個文件合成了一個文件,並對程式碼進行了混淆壓縮(生產模式),這是最基本的webpack最核心的功能 6— 打包。
但是顯然,在實際工作中我們不會這麼簡單的使用,那就需要用的配置文件了,下面是一個比較接近實際工作中的例子。
2.2 配置文件 webpack-config.js
以下的項目會有幾個文件:index.js , utils.js, style.scss, index.html, webpack-config.js。
基本功能就是在index.js文件中引入utils.js文件里的方法並調用。然後用scss語法編寫樣式,最後把打包的文件加入到已有的index.html文件中。
通過命令:npx webpack serve(可以放入npm腳本配置中,然後運行 npm run xxx)
,實現的效果是:
- scss自動編譯
- index.js utils.js style.scss 文件打包成一個文件
- 把打包的文件自動添加到index.html中
- 打包完成後,自動打開默認瀏覽器,查看頁面
- 文件有變更的話,會自動重新打包,刷新頁面
//utils.js
export function sayHello(){
console.log('hello world')
}
//index.js
import './styles.scss'
import { sayHello } from "./utils";
sayHello()
/*styles.scss*/
$bg : black;
$fontC:rgb(218, 17, 117);
body{
background:$bg;
h3{
color:$fontC;
}
}
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webpack</title>
<meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body>
<h3>hello webpack</h3>
</body>
</html>
//webpack-config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 打包模式
mode:'development', //development,production,none
devtool:'cheap-source-map', // eval-source-map,source-map,cheap-source-map
// 入口配置
entry: {
app: './src/index.js',
},
// 出口配置
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次打包,清除dist文件夾
},
// 本地伺服器
devServer:{
port:8888,//埠
open:true,//自動打開瀏覽器
hot:true,//啟動熱更新
},
// loader
module:{
// 處理scss文件
rules:[
{
test:/\.s[ac]ss$/i,
use:[
'style-loader',//將js模組生成style標籤節點
'css-loader',//將css轉化成js模組
'sass-loader'//將scss文件編譯成css文件
]
}
]
},
// 插件
plugins: [
// 自動把打包後的文件加入到html文件
new HtmlWebpackPlugin({
// 生成html文件的模板
template: './index.html'
}),
],
};
webpack的打包是在node環境下執行的,所以node的語法這裡都可以用,最終輸出的是一個js對象。
配置可以分成幾個部分(參考webpack配置文檔):
-
打包模式
-
mode 預置了開發環境和生產環境的一些優化。
-
devtool 控制是否生成,以及如何生成 source map。有了source map文件的話,如果程式碼有報錯可以映射到打包之前的程式碼(源程式碼),可以方便定位錯誤。
可以有很多的選擇,一般來說,在開發環境下選擇:eval-cheap-module-source-map,cheap-source-map。生產環境選擇:不配置,source-map。
-
-
入口、出口
- entry 入口文件,webpack會從這個文件開始查找依賴的包,可以配置多個
- output 出口文件,webpack會根據這裡的配置,輸出打包後的文件。
-
本地伺服器
此功能需要安裝webpack-dev-server插件npm install --save-dev webpack-dev-server
,啟動時需要用serve命令npx webpack serve
.
啟動成功後,會在本地開啟一個web伺服器,並且有實時更新,熱模組替換等功能。
配置項是在devServer
中。 -
loader
webpack本是只支援對js文件的打包,但是因為有loader機制,可以通過配置rules
實現對其他文件的打包。
示例程式碼中實現的是對scss/sass文件的打包,同一個relues中的loader的執行順序是從右到左(逆序),所以順序不能亂。第一個執行的loader 會將其結果(被轉換後的資源)傳遞給下一個 要執行的loader。 -
插件plugins
webpack在打包的時候,會暴露其生命周期,插件就是在特定的生命周期執行的操作,通過插件的機制,可以實現很多強大的功能。
示例程式碼中使用了HtmlWebpackPlugin
插件,功能是在打包完成後,自動把打包後的程式碼加入到html文件中。如果沒有任何配置,則會自動生成一個html文件,並通過<script>標籤把js文件引入進來。
3 實踐中的優化
3.1 配置文件拆分與合併–merge
在實際項目中,開發環境和生產環境的配置往往會有很大的區別,所以會有兩個配置文件,而這兩個配置文件又會有一些公共的配置,所以就會有如下三個配置文件:
- webpack.dev.js
- webpack.prod.js
- webpack.common.js
那麼這些配置文件是如何結合的呢?這就要用到webpack-merge插件了。
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口配置
entry: {
app: './src/index.js',
},
// 出口配置
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次打包,清除dist文件夾
},
// 插件
plugins: [
// 自動把打包後的文件加入到html文件
new HtmlWebpackPlugin({
// 生成html文件的模板
template: './index.html'
}),
],
};
//webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
// 本地伺服器
devServer:{
port:8888,//埠
open:true,//自動打開瀏覽器
hot:true,//啟動熱更新
},
});
//webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
devtool:'source-map'
});
// package.json
{
...
"scripts": {
"dev": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
},
...
}
然後分別執行npm run dev
,npm run build
就可以了。
3.2 程式碼分離
webpack會把所有程式碼打包成一個文件(包括業務程式碼,npm包),這樣最後的包就會很大,打包效率也很慢,所以可以有時候需要做程式碼分離。
-
第三方庫分離
有一些第三方庫可能會需要獨立引入,而不是放在業務程式碼裡面,因為不會改動或者需要cdn服務,比如jquery有免費的cdn服務://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.2.min.js 。
那這些獨立引入的js文件就不需要加入到webpack打包,只需要在externals
添加配置就行。// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // 打包模式 mode:'development', //development,production,none devtool:'cheap-source-map', // eval-source-map,source-map,cheap-source-map // 入口配置 entry: { app: './src/index.js', }, // 出口配置 output: { filename: '[name].[contenthash].js',//contenthash 是文件內容的hash值 path: path.resolve(__dirname, 'dist'), clean: true,//每次打包,清除dist文件夾 }, // 插件 plugins: [ // 自動把打包後的文件加入到html文件 new HtmlWebpackPlugin({ // 生成html文件的模板 template: './index.html' }), ], };
這樣的話,雖然index.js里有引入jquery,webpack也不會把jquery打包進來,打包時間會減少,包的體積也會變小。
-
npm包分離
第三方庫除了一些可以用script標籤引入的,大多數是通過npm引入的,這一類的js包也會也會合併到最後的app.js總包之中,使得app.js文件會過大,而且如果業務程式碼有一點改動的話,app.js的包就會全部都變動,導致瀏覽器就會重新下載app.js文件,使用不了瀏覽器內置的快取機制。
我們可以通過配置,讓npm里的包與業務程式碼分開。// index.js import _ from 'lodash' import $ from 'jquery' import { sayHello } from "./utils" $('#title').text('hello jquery')
// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // 打包模式 mode:'development', //development,production,none devtool:'cheap-source-map', // eval-source-map,source-map,cheap-source-map // 入口配置 entry: { app: './src/index.js', }, // 出口配置 output: { filename: '[name].[contenthash].js', //contenthash是文件內容的hash值 path: path.resolve(__dirname, 'dist'), clean: true,//每次打包,清除dist文件夾 }, // 插件 plugins: [ // 自動把打包後的文件加入到html文件 new HtmlWebpackPlugin({ // 生成html文件的模板 template: './index.html' }), ], optimization: { runtimeChunk: 'single',// 把webpack引導文件獨立出來 splitChunks: { cacheGroups: { vendor: { //所有node_modules下的包合併成一個,並獨立出來 test: /[\\/]node_modules[\\/]/, //控制哪種導入方式的js包才分離出來, 'all'-全部的js包,'async'-非同步導入的js包,'initial'-初始導入的js包 chunks: 'all', name:'vendor' //獨立後的包的名稱 } } } }, };
這樣打包目錄下就有三個文件:
- app.js: 業務程式碼
- runtime.js:webpack的引導程式碼
- vendor.js: npm引入的js包程式碼
一般有變動的就只有app.js文件了。npm引入的js包是否可以再分成幾個文件呢?可以的,參看 SplitChunksPlugin文檔
-
業務程式碼分離
有時候不僅第三方庫需要分離,我們自己寫的業務程式碼可能也會很大,也需要分離。要實現業務程式碼的分離只要添加多個入口就可以了。// index.js import { sayHello } from "./utils" sayHello() console.log('hello index')
// utils.js export function sayHello(){ console.log('hello utils') }
// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'development', devtool: 'cheap-source-map', // 入口配置 entry: { app: { import:'./src/index.js', dependOn:'utils' }, utils:'./src/utils.js' }, // 出口配置 output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true,//每次打包,清楚dist文件夾 }, // 插件 plugins: [ // 自動把打包後的文件加入到html文件 new HtmlWebpackPlugin({ // 生成html文件的模板 template: './index.html' }), ], };
這樣utils.js文件也從主包app.js中分離了出來,要注意的是app入口加了
dependOn:'utils'
,為了讓app.js裡面不要重複打包utils.js。 -
動態載入
有時候不是需要頁面一開始的時候,就載入全部的js包,而是等到特定的時機再去載入某些js包,這就需要動態載入了,只需要用到import()
就可以了,注意這是import的函數使用方式。// index.js const element = document.createElement('div'); element.id = 'title' element.innerHTML ='Hello webpack'; const button = document.createElement('button'); button.innerHTML = 'Click me'; button.onclick = importJquery; document.body.appendChild( element); document.body.appendChild( button); async function importJquery(){ const { default: $ } = await import('jquery'); $('#title').text('hello jquery') }
// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'development', devtool: 'cheap-source-map', // 入口配置 entry: { app: { import:'./src/index.js', }, }, // 出口配置 output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), clean: true,//每次打包,清楚dist文件夾 }, // 插件 plugins: [ // 自動把打包後的文件加入到html文件 new HtmlWebpackPlugin({ // 生成html文件的模板 template: './index.html' }), ], };
如果運行程式碼的話,會發現頁面一開始進入的時候,並沒有引入jquery的包,但是點擊按鈕的時候,就開始導入了,這就動態載入。
並且發現webpack.config.js並沒有做什麼特殊的配置,這是因為動態導入的js包webpack會自動給獨立為一個js文件。
import()返回的是一個promise對象。
3.3 動態鏈接庫 dll
webpack每次打包的時候,都會把涉及到的js包都處理一遍。但是實際上有些js包是不會有改動到的,所以打包過後的文件每次都是一樣的,每次都重新打包的話,會加增打包時間。
有一種解決方案是:把不會變動的js包先打包一次,以後每次打包的時候,直接引用就可以了。
先添加一個獨立的配置文件 webpack.dll.config.js
//webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
// 入口文件
entry: {
// 項目中用到該兩個依賴庫文件
jquery_lodash: ['jquery','lodash'],
},
// 輸出文件
output: {
// 文件名稱
filename: '[name].dll.js',
// 將輸出的文件放到dll目錄下
path: path.resolve(__dirname, 'dll'),
// 文件輸出的全局變數
library: '_dll_[name]',
},
plugins: [
// 使用插件 DllPlugin
new webpack.DllPlugin({
// 動態鏈接庫的全局變數名稱,需要和 output.library 中保持一致
// 該欄位的值也就是輸出的 manifest.json 文件 中 name 欄位的值
name: '_dll_[name]',
// 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
path: path.join(__dirname, 'dll', '[name].manifest.json')
}),
]
};
執行打包腳本 npx webpack --config webpack.dll.config.js
,就會再dll目錄下輸出文件:jquery_lodash.dll.js
,jquery_lodash.manifest.json
。
主要用到的插件是:
DllPlugin
的作用就是生成manifest.json文件。
然後配置項目打包用的webpack.config.js文件:
//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: 'cheap-source-map',
// 入口配置
entry: {
app: {
import:'./src/index.js',
},
},
// 出口配置
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true,//每次打包,清楚dist文件夾
},
// 插件
plugins: [
// 自動把打包後的文件加入到html文件
new HtmlWebpackPlugin({
// 生成html文件的模板
template: './index.html'
}),
// 引用dll中的文件,
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/jquery_lodash_dll.manifest.json')
}),
//把dll文件加入到index.html中
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, './dll/jquery_lodash_dll.dll.js'),
publicPath: './',
}),
],
};
主要用到的插件是:
DllReferencePlugin
檢索引用文件的時候,如果發現manifest.json裡面有,就告訴webpack不要打包該文件,因為已經打包好了。AddAssetHtmlWebpackPlugin
因為webpack沒有打包dll里的文件,所以需要手動把它加入到index.html中。
然後就可以正常的打包項目了:
// index.js
import _ from 'lodash'
import $ from 'jquery'
$('#title').text('hello jquery')
執行打包腳本 npx webpack
,會自動使用webpack.config.js配置文件打包。會發現打包速度提高了很多。