共计 12480 个字符,预计需要花费 32 分钟才能阅读完成。
绝大部分生产我的项目都是基于
cli
脚手架创立一个比较完善的我的项目,从晚期的webpack
配置工程师到前面的无需配置,大大解放了前端工程建设。然而时常会遇到,不依赖成熟的脚手架,从零搭过我的项目吗,有遇到哪些问题吗?或者有理解loader
和plugin
吗?如果只是应用脚手架,作为一个深耕业务一线的工具人,什么?还要本人搭?还要写loader
, 这就过分了。
注释开始 …
前置
咱们先理解下 webpack
能干什么
webpack
是一个动态打包工具,依据入口文件构建一个依赖图,依据须要的模块组合成一个 bundle.js
或者多个bundle.js
, 用它来展现动态资源
对于 webpack
的一些外围概念,次要有以下,参考官网
entry
1、entry
入口(依赖入口文件,webpack 首先依据这个文件去做外部模块的依赖关系)
// webpack.config.js
module.exports = {entry: './src/app.js'}
// or
/*
// 是以下这种形式的简写 定义一个别名 main
module.exports = {
entry: {main: ./src/app.js'}
}
*/
也能够是一个数组
// webpack.config.js
module.exports = {entry: ['./src/app.js', './src/b.js'],
vendor: './src/vendor.js'
}
在拆散利用 app.js 与第三方包时,能够将第三方包独自打包成 vender.js
, 咱们将第三方包打包成一个独立的chunk
, 内容hash
值放弃不变,这样浏览器利用缓存加载这些第三方js
,能够缩小加载工夫,进步网站的访问速度。
不过目前 webpack4.0.0
曾经不倡议这么做,次要能够应用 optimization.splitChunks
选项,将 app
与vendor
会分成独立的文件,而不是在入口处创立独立的entry
output
2、output
输入 (把依赖的文件输入一个指定的 目录
下)
次要会依据 entry
的入口文件名输入到指定的文件名目录中, 默认会输入到 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'
}
}
*/
// 默认输入 /dist/app.bundle.js
module
3、module
配制 loader
插件,loader
能让 webpack
解决各种文件,并把文件转换为可依赖的模块,以及能够被增加到依赖图中。其中 test
是匹配对应文件类型,use
是该文件类型用什么 loader
转换,在打包前运行。
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
4、plugins
次要是在整个运行时都会作用,打包优化,资源管理,注入环境
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]
}
mode
5、mode
指定打包环境,development
与production
, 默认是production
从零开始一个我的项目搭建
新建一个目录webpack-01
, 执行npm init -y
npm init -y // 生成一个默认的 package.json
在 package.json
中配置scirpt
{
"scripts": {
"test": "echo \"Error: no test specified\"&& exit 1",
"build": "webpack",
},
}
首先咱们在在开发依赖装置 webpack
与webpack-cli
, 执行 npm i webpack webpack-cli --save-dev
在webpack5
中咱们默认新建一个 webpack
的默认配置文件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'
};
咱们在 src
目录下新建一个 app.js
并写入一段 js
代码
console.log('hello, webpack')
在终端执行 npm run build
, 这个命令我在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"
},
此时就会生成一个在 dist
文件, 并且名字就是 app.bundle.js
并且管制台上曾经胜利了
webpack
asset app.bundle.js 151 bytes [emitted] [minimized] (name: app)
./src/app.js 29 bytes [built]
webpack 5.72.1 compiled successfully in 209 ms
咱们关上一下生成的 app.bundle.js
, 咱们发现是这样的,这是在model:production
下生成的一个匿名的自定义函数。
// 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});
})();
这是生产环境输入的代码,就是在一个匿名函数中输入了后果,并且在 {}
上绑定了一个 __esModule
的对象属性,有这样一段代码 var o = exports;
次要是因为咱们在 output
中新增了 libraryTarget:commonjs
, 这个会决定js
输入的后果。
咱们再来看下如果 mode:development
那么是怎么样
// 这是在 mode: development 下生成一个 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"");
/******/
/******/ })()
;
这下面的代码就是运行 mode:development
模式下生成的, 简化一下就是
(() => {
var webpackModules = {'./src/app.js': () => evel('app.js 外部的代码')
}
weboackModules['./src/app.js']("'./src/app.js'");
})()
在开发环境就是会以文件门路为 key
, 而后通过evel
执行 app.js
的内容,并且调用这个 webpackModules
执行 evel
函数
留神咱们默认 libraryTarget
如果不设置,那么就是var
,次要有以下几种amd
、commonjs2
,commonjs
,umd
通过以上,咱们会发现咱们能够用配置不同的命令执行打包不同的脚本,在默认状况下,npm run build
与执行 npm run build:default
是等价的,咱们会看到 default
用--config webpack.config.js
指定了 webpack
打包的环境的自定义配置文件。
如果配置默认文件名就是 webpack.config.js
那么 webpack
就会依据这个文件进行打包,webpack --config xxx.js
是指定自定义文件让 webpack
依据 xxx.js
输出与输入的文件进行一系列操作。
"scripts": {
"test": "echo \"Error: no test specified\"&& exit 1",
"build": "webpack",
"build:default": "webpack --config webpack.config.js",
},
除了以上,咱们能够不应用配置 webpack --config webpack.config.js
这个命令,而是间接在命令行 -cli 间接打包指定的文件输入到对应的文件下
"scripts": {"build:o": "webpack ./src/app.js --output-path='./dist2'--output-filename='[name]_[hash].bundle.js'"
},
会创立 dist2
目录并打包进去一个默认命名的 main_ff7753e9dbb1e41a06a6.bundle.js
的文件
咱们会发现咱们配置了诸如 webpack_test_dev_config.js
或者 webpack_test_prd_config.js
z 这样的文件,通过build: test_dev
与build:test_prd
来辨别,外面文件内容仿佛大同小异,那么我可不可以复用一份文件,通过里面的环境参数来管制呢?这点在理论我的项目中会常常应用
环境参数
咱们能够通过 package.json
中指定的参数来确定, 能够用 --mode='xxx'
与--env a='xxx'
"scripts": {"build2": "webpack --mode='production'--env libraryTarget='commonjs'--config webpack.config.js"},
此时 webpack.config.js
须要改成函数的形式
第二参数 argv
能获取全副的配置的参数
// 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
运行整个我的项目
咱们曾经创立了一个 src/app.js
的入口文件,当初须要在浏览器上拜访,因而须要构建一个 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】插件
然而,当我每次批改 js
文件,我都要每次执行 npm run build
这个命令,这就有些繁琐了, 而且我本地是装置 vsode 插件的形式帮我关上页面的,这就有点坑了。
于是在 webpack
中就有一个内置 cli
watch 来监听文件的变动, 咱们只须要加上--watch
就能够了
"scripts": {"build": "webpack --watch",},
这种形式会始终监听文件的变动,当文件发生变化时,就会从新打包,页面会从新刷新。
当然还有一种形式,就是能够在 webpack.config.js
中退出watch
// webpack.config.js
{
watch: true,
entry: {app: './src/app.js'},
}
而后咱们就改回原来的,将 --watch
去掉就行。
--watch
这种形式的确晋升我本地开发效率,因为只有文件一发生变化,就会从新打包编译,联合 vscode
的插件就会从新加载最新的文件,然而随着我的项目的宏大,那么这种效率就很低了,因而除了 webpack
本身的 watch 计划,咱们须要去理解另外一个计划webpack-dev-server
webpack-dev-server
咱们须要借助一个十分弱小的插件工具来实现 本地动态服务
, 这个插件就是 webpack-dev-server,咱们经常称说为WDS
本地服务,他有热更新,并且浏览器会主动刷新页面,无需手动刷新页面
并且咱们还须要引入另一个插件 Html-webpack-plugins
这个插件,它能够主动帮咱们引入打包后的文件。当咱们启动本地服务,生地文件 js 文件会在内存中生成, 并且被 html
主动引入
咱们在 webpack.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
外部曾经有对文件监听,当文件发生变化时,能够实时更新生成在内存的那个 js
, 这个server
命令就是我装置的 webpack-dev-server
的命令
"scripts": {"server": "webpack server"},
控制台运行 npm run server
默认关上 8080 端口,曾经 ok 了
模块热更新(Hot Module Replacement)
当初当我每次批改文件时,整个文件都会从新 build, 并且是在虚拟内存中引入,如果批改的只是局部文件,全副文件从新加载就有些节约了,因而须要HMR
,热更新 devServer hot, 在运行时更新某个变动的文件模块,无需全副更新所有文件
// weboack.config.js
{
mode: argv.mode,
devServer: {hot: true},
}
当我增加完后,发现热更新还是和以前一样,没什么用,官网这里有解释 hot-module-replacement, 艰深讲就是要指定某些文件要热更新,不然默认只有文件产生更改就得全副从新编译,从而全站刷新。
写了一段测试代码
// 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('深拷贝一个对象 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) {
// 这个文件
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) {
// 这个文件
module.hot.accept('./utils/index.js', () => {});
}
这里留神一点
,指定的utils/index.js
必须是 esModule
的形式输入,要不然不会失效
,咱们会发现,当我批改utils/index.js
时,会有一个申请
当你每改这个文件都会申请一个 app.[hash].hot.update.js
这样的一个文件。
webpack-dev-server
内置了 HMR
, 咱们用webpack server
这个命令就启动动态服务了,并且还内置了HMR
, 如果我不想用命令呢,咱们能够通过 API 的形式启动dev-server
[](https://www.webpackjs.com/gui... ""), 具体示例代码如下,新建一个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);
// 设置动态资源目录
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
,能够参考官网 webpack-dev-middleware
加载 css[XHR 更新款式]
npm i style-loader css-loader --save-dev
配置加载 css 的loader
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
款式是内敛在 html
外面的,如何提取成单个文件呢?
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 文件提取了指定文件中, 此时就会发现 index.html
内敛的款式就变成一个文件加载了。
图片资源加载
咱们只晓得 css
用了 css-loader
与style-loader
, 那么图片以及非凡文件也是须要非凡 loader
能力应用,具体参考图片
首先须要装置 file-loader
执行 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
,输入的图片文件能够加hash
值后缀,当打包上传后,如果文件没有更改,图片更容易从缓存中获取
在 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: '公众号:Web 技术学苑'}
};
const result = twoSum(1, 2);
console.log(result, deepMerge(userInfo));
if (module.hot) {
// 这个文件
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
与 图片
资源都曾经 OK 了
总结
1、理解 webpack
是什么,它次要是前端构建工程化的一个工具,将一些譬如 ts
,sass
,vue
,tsx
等等一些浏览器无奈间接拜访的资源,通过 webpack
能够打包成最终浏览器能够拜访的 html
、css
、js
的文件。并且 webpack
通过一系列的插件形式,提供 loader
与plugins
这样的插件配置,达到能够编译各种文件。
2、理解 webpack
编译入口的根本配置,entry
,output
、module
、plugins
以及利用 devServer
开启热更新,并且应用 module.hot.accept('path')
实现 HMR
模块热替换性能
3、咱们理解在命令行 webpack --watch
能够做到实时监听文件的变动,每次文件变动,页面都会从新加载
4、咱们学会如何应用加载 css
以及 图片资源
,学会配置css-loader
,style-loader
、file-loader
, 以及利用min-css-extract-plugin
去提取 css
, 用html-webpack-plugin
插件实现本地 WDS
动态文件与入口文件的映射,在 html
中会主动引入实时打包的入口文件的app.bundle.js
5、相熟从 0 到 1 搭建一个前端工程化我的项目
6、本文示例 code-example
下一节会基于这个当下我的项目搭建 vue
、react
我的项目,以及我的项目的 tree-shaking
, 懒加载
、 缓存
, 自定义 loader,plugins
等