webpack從0到1構(gòu)建
絕大部分生產(chǎn)項(xiàng)目都是基于cli腳手架創(chuàng)建一個(gè)比較完善的項(xiàng)目,從早期的webpack配置工程師到后面的無需配置,大大解放了前端工程建設(shè)。但是時(shí)常會(huì)遇到,不依賴成熟的腳手架,從零搭過項(xiàng)目嗎,有遇到哪些問題嗎?或者有了解loader和plugin嗎?如果只是使用腳手架,作為一個(gè)深耕業(yè)務(wù)一線的工具人,什么?還要自己搭?還要寫loader,這就過分了。
正文開始...
前置
我們先了解下webpack能干什么
webpack是一個(gè)靜態(tài)打包工具,根據(jù)入口文件構(gòu)建一個(gè)依賴圖,根據(jù)需要的模塊組合成一個(gè)bundle.js或者多個(gè)bundle.js,用它來展示靜態(tài)資源
關(guān)于webpack的一些核心概念,主要有以下,參考官網(wǎng)
entry
1、entry入口(依賴入口文件,webpack首先根據(jù)這個(gè)文件去做內(nèi)部模塊的依賴關(guān)系)
// webpack.config.js
module.exports = {
entry: './src/app.js'
}
// or
/*
// 是以下這種方式的簡寫 定義一個(gè)別名main
module.exports = {
entry: {
main: ./src/app.js'
}
}
*/
也可以是一個(gè)數(shù)組
// webpack.config.js
module.exports = {
entry: ['./src/app.js', './src/b.js'],
vendor: './src/vendor.js'
}
在分離應(yīng)用app.js與第三方包時(shí),可以將第三方包單獨(dú)打包成vender.js,我們將第三方包打包成一個(gè)獨(dú)立的chunk,內(nèi)容hash值保持不變,這樣瀏覽器利用緩存加載這些第三方j(luò)s,可以減少加載時(shí)間,提高網(wǎng)站的訪問速度。
不過目前webpack4.0.0已經(jīng)不建議這么做,主要可以使用optimization.splitChunks選項(xiàng),將app與vendor會(huì)分成獨(dú)立的文件,而不是在入口處創(chuàng)建獨(dú)立的entry
output
2、output輸出(把依賴的文件輸出一個(gè)指定的目錄下)
主要會(huì)根據(jù)entry的入口文件名輸出到指定的文件名目錄中,默認(rèn)會(huì)輸出到dist文件中
const path = require('path');
// webpack.config.js
module.exports = {
entry: {
app: './src/app.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
}
}
/*
module.exports = {
entry: './src/app.js',
output: {
filename: '[name].bundle.js'
}
}
*/
// 默認(rèn)輸出 /dist/app.bundle.js
mode
3、mode模式,主要是開發(fā)模式和生產(chǎn)模式兩種模式,在生產(chǎn)模式webapck打包會(huì)默認(rèn)壓縮
module
4、module 配制loader插件,loader能讓webpack處理各種文件,并把文件轉(zhuǎn)換為可依賴的模塊,以及可以被添加到依賴圖中。其中test是匹配對(duì)應(yīng)文件類型,use是該文件類型用什么loader轉(zhuǎn)換,在打包前運(yùn)行。
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: 'less-loader'
},
{
test: /\.ts$/,
use: 'ts-loader'
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: true
}
},
{
loader: 'sass-loader'
}
]
}
]
}
}
plugins
5、plugins主要是在整個(gè)運(yùn)行時(shí)都會(huì)作用,打包優(yōu)化,資源管理,注入環(huán)境
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]
}
mode
6、mode指定打包環(huán)境,development與production,默認(rèn)是production
從零開始一個(gè)項(xiàng)目搭建
新建一個(gè)目錄webpack-01,執(zhí)行npm init -y
npm init -y // 生成一個(gè)默認(rèn)的package.json
在package.json中配置scirpt
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
},
}
首先我們?cè)谠陂_發(fā)依賴安裝webpack與webpack-cli,執(zhí)行npm i webpack webpack-cli --save-dev在webpack5中我們默認(rèn)新建一個(gè)webpack的默認(rèn)配置文件webpack.config.js
const path = require('path');
module.exports = {
entry: {
app: './src/app.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs'
},
mode: 'production'
};
我們?cè)趕rc目錄下新建一個(gè)app.js并寫入一段js代碼
console.log('hello, webpack')
在終端執(zhí)行npm run build,這個(gè)命令我在package.json的script中配置
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"build:test_dev": "webpack --config webpack_test_dev_config.js",
"build:test_prd": "webpack --config webpack_test_prd_config.js",
"build:default": "webpack --config webpack.config.js",
"build:o": "webpack ./src/app.js -o dist/app.js"
},
此時(shí)就會(huì)生成一個(gè)在dist文件,并且名字就是app.bundle.js
并且控制臺(tái)上已經(jīng)成功了
> webpack
asset app.bundle.js 151 bytes [emitted] [minimized] (name: app)
./src/app.js 29 bytes [built] [code generated]
webpack 5.72.1 compiled successfully in 209 ms
我們打開一下生成的app.bundle.js,我們發(fā)現(xiàn)是這樣的,這是在mode:production下生成的一個(gè)匿名的自定義函數(shù)。
// app.bundle.js
(() => {
var e = {};
console.log(3), console.log('hello, webpack');
var o = exports;
for (var l in e) o[l] = e[l];
e.__esModule && Object.defineProperty(o, '__esModule', { value: !0 });
})();
這是生產(chǎn)環(huán)境輸出的代碼,就是在一個(gè)匿名函數(shù)中輸出了結(jié)果,并且在{}上綁定了一個(gè)__esModule的對(duì)象屬性,有這樣一段代碼var o = exports;主要是因?yàn)槲覀冊(cè)趏utput中新增了libraryTarget:commonjs,這個(gè)會(huì)決定js輸出的結(jié)果。
我們?cè)賮砜聪氯绻鹠ode:development那么是怎么樣
// 這是在mode: development下生成一個(gè)bundle.js
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/app.js":
/*!********************!*\
!*** ./src/app.js ***!
\********************/
/***/ (() => {
eval("\nfunction twoSum(a, b) {\n return a+b\n}\nconst result = twoSum(1,2);\nconsole.log(result);\nconsole.log('hello, webpack');\n\n//# sourceURL=webpack://webpack-01/./src/app.js?");
/***/ })
/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/app.js"](""./src/app.js"" ""./src/app.js"");
/******/
/******/ })()
;
這上面的代碼就是運(yùn)行mode:development模式下生成的,簡化一下就是
(() => {
var webpackModules = {
'./src/app.js': () => evel('app.js內(nèi)部的代碼')
}
weboackModules['./src/app.js']( "'./src/app.js'");
})()
在開發(fā)環(huán)境就是會(huì)以文件路徑為key,然后通過evel執(zhí)行app.js的內(nèi)容,并且調(diào)用這個(gè)webpackModules執(zhí)行evel函數(shù)
注意我們默認(rèn)libraryTarget如果不設(shè)置,那么就是var,主要有以下幾種amd、commonjs2,commonjs,umd
通過以上,我們會(huì)發(fā)現(xiàn)我們可以用配置不同的命令執(zhí)行打包不同的腳本,在默認(rèn)情況下,npm run build與執(zhí)行npm run build:default是等價(jià)的,我們會(huì)看到default用--config webpack.config.js指定了webpack打包的環(huán)境的自定義配置文件。
如果配置默認(rèn)文件名就是webpack.config.js那么webpack就會(huì)根據(jù)這個(gè)文件進(jìn)行打包,webpack --config xxx.js是指定自定義文件讓webpack根據(jù)xxx.js輸入與輸出的文件進(jìn)行一系列操作。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"build:default": "webpack --config webpack.config.js",
},
除了以上,我們可以不使用配置webpack --config webpack.config.js這個(gè)命令,而是直接在命令行-cli[1]直接打包指定的文件輸出到對(duì)應(yīng)的文件下
"scripts": {
"build:o": "webpack ./src/app.js --output-path='./dist2' --output-filename='[name]_[hash].bundle.js'"
},
會(huì)創(chuàng)建dist2目錄并打包出來一個(gè)默認(rèn)命名的main_ff7753e9dbb1e41a06a6.bundle.js的文件
我們會(huì)發(fā)現(xiàn)我們配置了諸如webpack_test_dev_config.js或者webpack_test_prd_config.js這樣的文件,通過build: test_dev與build:test_prd來區(qū)分,里面文件內(nèi)容似乎大同小異,那么我可不可以復(fù)用一份文件,通過外面的環(huán)境參數(shù)來控制呢?這點(diǎn)在實(shí)際項(xiàng)目中會(huì)經(jīng)常使用
環(huán)境參數(shù)
我們可以通過package.json中指定的參數(shù)來確定,可以用--mode='xxx'與--env a='xxx'
"scripts": {
"build2": "webpack --mode='production' --env libraryTarget='commonjs' --config webpack.config.js"
},
此時(shí)webpack.config.js需要改成函數(shù)的方式 第二參數(shù)argv能獲取全部的配置的參數(shù)
// webpack.config.js
const path = require('path');
module.exports = function (env, argv) {
console.log(env, argv);
return {
entry: {
app: './src/app.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
library: 'MyTest',
libraryTarget: argv.libraryTarget
},
mode: argv.mode
};
};
因此我們就可以通過修改package.json里面的變量,從而控制webpack.config.js
運(yùn)行整個(gè)項(xiàng)目
我們已經(jīng)創(chuàng)建了一個(gè)src/app.js的入口文件,現(xiàn)在需要在瀏覽器上訪問,因此需要構(gòu)建一個(gè)index.html,在根目錄中新建public/index.html,并且引入我剛打包的js文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hello-webpack</title>
</head>
<body>
<div id="app"></div>
<script src="../dist/app.bundle.js"></script>
</body>
</html>
終于大功告成,我打開瀏覽器,打開頁面終于可以訪問了,【我本地裝了live server】插件
但是,當(dāng)我每次修改js文件,我都要每次執(zhí)行npm run build這個(gè)命令,這就有些繁瑣了,而且我本地是安裝vsode插件的方式幫我打開頁面的,這就有點(diǎn)坑了。
于是在webpack中就有一個(gè)內(nèi)置cliwatch來監(jiān)聽文件的變化,我們只需要加上`--watch`[2]就可以了
"scripts": {
"build": "webpack --watch",
},
這種方式會(huì)一直監(jiān)聽文件的變化,當(dāng)文件發(fā)生變化時(shí),就會(huì)重新打包,頁面會(huì)重新刷新。
當(dāng)然還有一種方式,就是可以在webpack.config.js中加入watch
// webpack.config.js
{
watch: true,
entry: {
app: './src/app.js'
},
}
然后我們就改回原來的,將--watch去掉就行。
--watch這種方式確實(shí)提升我本地開發(fā)效率,因?yàn)橹灰募话l(fā)生變化,就會(huì)重新打包編譯,結(jié)合vscode的插件就會(huì)重新加載最新的文件,但是隨著項(xiàng)目的龐大,那么這種效率就很低了,因此除了webpack自身的watch方案,我們需要去了解另外一個(gè)方案webpack-dev-server
webpack-dev-server
我們需要借助一個(gè)非常強(qiáng)大的插件工具來實(shí)現(xiàn)本地靜態(tài)服務(wù),這個(gè)插件就是`webpack-dev-server`[3],我們常常稱呼為WDS本地服務(wù),他有熱更新,并且瀏覽器會(huì)自動(dòng)刷新頁面,無需手動(dòng)刷新頁面
并且我們還需要引入另一個(gè)插件Html-webpack-plugins這個(gè)插件,它可以自動(dòng)幫我們引入打包后的文件。當(dāng)我們啟動(dòng)本地服務(wù),生地文件js文件會(huì)在內(nèi)存中生成,并且被html自動(dòng)引入
我們?cè)趙ebpack.config.js中引入html-webpack-plugin
const path = require('path');
// 引入html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = function (env, argv) {
console.log(env);
console.log(argv);
return {
entry: {
app: './src/app.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
library: 'MyTest',
libraryTarget: argv.libraryTarget
},
mode: argv.mode,
plugins: [new HtmlWebpackPlugin({
template: './public/index.html'
})]
};
};
并且在package.json中增加server命令,注意我們加了server,webpack-dev-server內(nèi)部已經(jīng)有對(duì)文件監(jiān)聽,當(dāng)文件發(fā)生變化時(shí),可以實(shí)時(shí)更新生成在內(nèi)存的那個(gè)js,這個(gè)server命令就是我安裝的webpack-dev-server的命令
"scripts": {
"server": "webpack server"
},
控制臺(tái)運(yùn)行npm run server默認(rèn)打開8080端口,已經(jīng)ok了
模塊熱更新(Hot Module Replacement)
現(xiàn)在當(dāng)我每次修改文件時(shí),整個(gè)文件都會(huì)重新build,并且是在虛擬內(nèi)存中引入,如果修改的只是部分文件,全部文件重新加載就有些浪費(fèi)了,因此需要HMR,模塊熱更新devServer hot[4],在運(yùn)行時(shí)更新某個(gè)變化的文件模塊,無需全部更新所有文件
// weboack.config.js
{
mode: argv.mode,
devServer: {
hot: true
},
}
當(dāng)我添加完后,發(fā)現(xiàn)熱更新還是和以前一樣,沒什么用,官方這里有解釋hot-module-replacement[5],通俗講就是要指定某些文件要熱更新,不然默認(rèn)只要文件發(fā)生更改就得全部重新編譯,從而全站刷新。
寫了一段測試代碼
// utils/index
var str = '123';
function deepMerge(target) {
console.log(target, '=22==');
if (Array.isArray(target)) {
return target;
}
const result = {};
for (var key in target) {
if (Reflect.has(target, key)) {
if (Object.prototype.toString.call(target[key]) === '[object Object]') {
result[key] = deepMerge(target[key]);
} else {
result[key] = target[key];
}
}
}
return result;
}
console.log('深拷貝一個(gè)對(duì)象555', str);
export default deepMerge;
// module.exports = {
// deepMerge
// };
在app.js中引入
import deepMerge from './utils/index';
// const { deepMerge } = require('./utils/index.js');
function twoSum(a, b) {
return a + b;
}
const userInfo = {
name: 'Maic',
age: 18,
test: {
book: 'webpack'
}
};
const result = twoSum(1, 2);
console.log(result, deepMerge(userInfo));
if (module.hot) {
// 這個(gè)文件
module.hot.accept('./utils/index.js', () => {});
}
const str = 'hello, webpack322266666';
console.log(str);
const app = document.getElementById('app');
app.innerHTML = str;
注意我們加了一段代碼判斷指定模塊是否HMR
if (module.hot) {
// 這個(gè)文件
module.hot.accept('./utils/index.js', () => {});
}
這里注意一點(diǎn),指定的utils/index.js必須是esModule的方式輸出,要不然不會(huì)生效 ,我們會(huì)發(fā)現(xiàn),當(dāng)我修改utils/index.js時(shí),會(huì)有一個(gè)請(qǐng)求
當(dāng)你每改這個(gè)文件都會(huì)請(qǐng)求一個(gè)app.[hash].hot.update.js這樣的一個(gè)文件。
webpack-dev-server內(nèi)置了HMR,我們用webpack server這個(gè)命令就啟動(dòng)靜態(tài)服務(wù)了,并且還內(nèi)置了HMR,如果我不想用命令呢,我們可以通過API的方式啟動(dòng)dev-server(https://www.webpackjs.com/guides/hot-module-replacement/#%E5%90%AF%E7%94%A8-hmr "" "")
具體示例代碼如下,新建一個(gè)config/server.js
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');
const config = require('../webpack.config.js');
const options = { hot: true, contentBase: '../dist', host: 'localhost' };
// 只能用V2版本https://github.com/webpack/webpack-dev-server/blob/v2
webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);
const PORT = '9000';
server.listen(PORT, 'localhost', () => {
console.log('server is start' + PORT);
});
webpack-dev-middleware代替webpack-dev-server
// config/server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('../webpack_test_dev_config');
const compiler = webpack(config);
// 設(shè)置靜態(tài)資源目錄
app.use(express.static('dist'));
app.use(webpackDevMiddleware(compiler, {}));
const PORT = 8000;
app.listen(PORT, () => {
console.log('server is start' + PORT);
});
然后命令行配置node config/server.js,可以參考官網(wǎng)webpack-dev-middleware[6]
加載css[XHR更新樣式]
npm i style-loader css-loader --save-dev
配置加載css的loader
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
樣式是內(nèi)聯(lián)在html里面的,如何提取成單個(gè)文件呢?
mini-css-extract-plugin 提取css
// webpack.config.js
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = function (env, argv) {
return {
module: {
rules: [
{
test: /\.css$/,
// use: ['style-loader', 'css-loader']
use: [
miniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: 'css/[name].css'
})
]
}
}
我們把style-loader去掉了,并且換成了miniCssExtractPlugin.loader,并且在plugins中加入插件,將css文件提取了指定文件中,此時(shí)就會(huì)發(fā)現(xiàn)index.html內(nèi)聯(lián)樣式就變成一個(gè)文件加載了。
圖片資源加載
我們只知道css用了css-loader與style-loader,那么圖片以及特殊文件也是需要特殊loader才能使用,具體參考圖片[7]
首先需要安裝file-loader執(zhí)行npm i file-loader --save-dev
// webpack.config.js
{
...
module: {
rules: [
{
test: /\.css$/,
use: [miniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(png|svg|jpg|gif|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
outputPath: 'assets',
name: '[name].[ext]'
}
}
]
}
]
}
}
可以參考`file-loader`[8],輸出的圖片文件可以加hash值后綴,當(dāng)打包上傳后,如果文件沒有更改,圖片更容易從緩存中獲取
在app.js中加入引入圖片
import deepMerge from './utils/index';
import '../assets/css/app.css';
import image1 from '../assets/images/1.png';
import image2 from '../assets/images/2.jpg';
// const { deepMerge } = require('./utils/index.js');
function twoSum(a, b) {
return a + b;
}
const userInfo = {
name: 'Maic',
age: 18,
test: {
book: '公眾號(hào):Web技術(shù)學(xué)苑'
}
};
const result = twoSum(1, 2);
console.log(result, deepMerge(userInfo));
if (module.hot) {
// 這個(gè)文件
module.hot.accept('./utils/index.js', () => {});
}
const str = `<div>
<h5>hello, webpack</h5>
<div>
<img src=${image1} />
</div>
<div>
<img src=${image2} />
</div>
</div>`;
console.log(str);
const app = document.getElementById('app');
app.innerHTML = str;
看下引入的圖片頁面
大功告成,css與圖片資源都已經(jīng)OK了
總結(jié)
1、了解webpack是什么,它主要是前端構(gòu)建工程化的一個(gè)工具,將一些譬如ts,sass,vue,tsx等等一些瀏覽器無法直接訪問的資源,通過webpack可以打包成最終瀏覽器可以訪問的html、css、js的文件。并且webpack通過一系列的插件方式,提供loader與plugins這樣的插件配置,達(dá)到可以編譯各種文件。
2、了解webpack編譯入口的基本配置,entry、output、module、plugins以及利用devServer開啟熱更新,并且使用module.hot.accept('path')實(shí)現(xiàn)HMR模塊熱替換功能
3、我們了解在命令行webpack --watch可以做到實(shí)時(shí)監(jiān)聽文件的變化,每次文件變化,頁面都會(huì)重新加載
4、我們學(xué)會(huì)如何使用加載css以及圖片資源,學(xué)會(huì)配置css-loader、style-loader、file-loader以及利用min-css-extract-plugin去提取css,用html-webpack-plugin插件實(shí)現(xiàn)本地WDS靜態(tài)文件與入口文件的映射,在html中會(huì)自動(dòng)引入實(shí)時(shí)打包的入口文件的app.bundle.js
5、熟悉從0到1搭建一個(gè)前端工程化項(xiàng)目
6、本文示例code-example[9]
下一節(jié)會(huì)基于當(dāng)下項(xiàng)目搭建vue、react項(xiàng)目,以及項(xiàng)目的tree-shaking,懶加載,緩存,自定義loader,plugins等
參考資料
[1]
命令行-cli: https://www.webpackjs.com/api/cli/
[2]
--watch: https://www.webpackjs.com/api/cli/#watch-%E9%85%8D%E7%BD%AE
[3]
webpack-dev-server: https://www.webpackjs.com/guides/development/#%E4%BD%BF%E7%94%A8-webpack-dev-server
[4]
devServer hot: https://www.webpackjs.com/configuration/dev-server/#devserver-hot
[5]
hot-module-replacement: https://www.webpackjs.com/api/hot-module-replacement/
[6]
webpack-dev-middleware: https://www.webpackjs.com/guides/development/#%E4%BD%BF%E7%94%A8-webpack-dev-middleware
[7]
圖片: https://www.webpackjs.com/guides/asset-management/#加載圖片
[8]
file-loader: https://www.webpackjs.com/loaders/file-loader/
[9]
code-example: https://github.com/maicFir/lessonNote/tree/master/webpack/webpack-01
作者:Maic
歡迎關(guān)注微信公眾號(hào) :web技術(shù)學(xué)苑