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服務:https://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配置檔案打包。會發現打包速度提高了很多。