原文链接,点此跳转
大家好,我是 Smooth,一名大二的 SCAU 前端er自定义 Loader 内容于 2021/02/25 更新
本篇文章会带你入门 Webpack 并对基本配置以及进阶配置做比较通俗易懂的介绍!
如文章有误,恳请评论区指正,谢谢!
自定义 Plugin 内容于 2021/02/26 更新
Webpack 本教程包管理方式统一使用
npm
进行讲解学习背景 由于在平时使用 vue、react 框架进行项目开发时,vue-cli 和 create-react-app 脚手架已经为你默认配置了 webpack 的常用参数,所以没有额外需求不用另外配置 webpack。
但在一次项目开发中,突然自己想给项目增加一个优化打包速度和体积的需求,所以就开始学习起了 webpack,逐渐明白了这打包工具的强大之处,在查看了脚手架给我们默认配置好的 webpack 配置文件后,也明白了脚手架这种东西的便捷之处。
当然,系统学习后 webpack,你也可以做到去阉割脚手架的 webpack 配置文件用不到的一些配置选项,并增加一些你需要的配置,例如优化打包体积、提升打包速度等等。
此篇文章便为你进行系统地讲解 webpack 的基本使用
PS:对于 webpack 的配置文件,vue-cli 可以通过修改 vue.config.js 进行配置修改,create-react-app 需要通过 craco 覆盖,或 eject 进行暴露。
webpack介绍
webpack
是什么bundler:模块打包工具
webpack
作用对项目进行打包,明确项目入口,文件层次结构,翻译代码(将代码翻译成浏览器认识的代码,例如import/export)
webpack
环境配置webpack
安装前提:已安装 node
(node安装在此不做赘述),用指令 node -v
和 npm -v
来测试node安装有没成功npm install webpack webpack-cli --save-dev // 推荐,--save-dev结尾(或直接一个 -D),该项目内安装
npm install webpack webpack-cli -g // 不推荐,-g结尾,全局安装(如果两个项目用的两个webpack版本,会造成版本冲突)
?
安装后查询版本:
webpack -v:查找全局的 webpack 版本,非 -g 全局安装是找不到的
npx webpack -v:查找该项目下的 webpack 版本
?
其他指令:
npm init -y:初始化 npm 仓库,-y 后缀意思是创建package.json文件时默认所有选项都为yes
npm info webpack:查询 webpack 有哪些版本号
npm install webpack@版本号 webpack-cli -D:安装指定版本号的webpack
npx webpack;进行打包
webpack-cli
和 webpack
区别:webpack-cli
能让我们在命令行运行webpack
相关指令,例如 webpack
, npx webpack
等等webpack
配置文件默认配置文件:
webpack.config.js
const path = require('path');
module.exports = {
mode: "production", // 环境,默认 production 即生产环境,打包出来的文件经过压缩(可以不写),development没压缩
entry: 'index.js', // 入口文件(要写路径)
output: {// 出口位置
filename: 'bundle.js', // 出口文件名
path: path.resolve(__dirname, 'bundle'), // 出口文件打包到哪个文件夹下,参数(绝对路径根目录下,文件名)
}
}
如果想让
webpack
按其他配置文件规则进行打包,比如叫做 webpackconfig.js
npx webpack --config webpackconfig.js
小问题: 为什么使用
react
、vue
框架打包项目文件时不是输入 npx webpack
而是输入 npm start/npm run dev
等等?原因:更改
package.json
文件里的 scripts 脚本指令(该文件:项目的说明,包括所需依赖、可运行脚本、项目名、版本号等等){
"scripts": {
"bundle": "webpack" // 运行 npm run 脚本名,相当于运行 原始指令,即 `npm run bundle -> webpack`
}
}
回顾:
webpack index.js // 全局安装 webpack 后,单独对这个js文件进行打包
npx webpack index.js // 局部(项目内)安装 webpack 后,单独对这个js文件进行打包
npm run bundle -> webpack // 运行脚本,进行 webpack 打包,先在项目内查找webpack进行打包,没有再全局----前两者融合
后面开始用
npm run bundle
代替 webpack
进行打包Webpack 基本概念
webpack
的 Concepts
板块官方文档
Loader Loader 是什么?
由于 webpack 默认只认识、支持打包js文件,想要拓展其能力进行打包 css文件、图片文件等等,需要安装 Loader 进行拓展
Loader 的使用
在
webpack.config.js
的配置文件中进行配置在文件中新增
module
字段,module
中新增 rules
的数组,进行一系列规则的配置,每个规则对象有两个字段test
:匹配所有以 xxx 为后缀的文件的打包,用正则表达式进行匹配use
:指明要使用的 loader 名称 ,且要对该 loader 进行安装拓展,
use
还有 options
可选择字段,name
指明打包后的文件命名,[name].[ext]
代表打包后和打包前 命名
和 后缀
一样{
module: {
rules: [
{
test: /.jpg$/,
use: {
loader: 'file-loader',
options: {
// placeholder 占位符
name: '[name].[ext]'
}
}
}
]
}
}
在项目根目录下使用
npm install loader名字
或 yarn add loader名字
进行所需 loader 的安装常用 Loader 推荐
babel-loader
、style-loader
、css-loader
、less-loader
、sass-loader
、postcss-loader
、url-loader
、file-loader
等等图片(Images)
打包图片文件
图片静态资源,所以都对应
file-loader
,且一般项目中这些静态资源被放到 images
文件夹,通过 use
字段配置额外参数{
module: {
rules: [
{
test: /.(jpg|png|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/' // 匹配到上面后缀的文件时,都打包到的的文件夹路径
}
}
}
]
}
}
当然,对于
file-loader
,url-loader
会更具拓展性推荐用
url-loader
进行替换,因为可以设置limit
参数,当图片大于对应字节大小,会打包到指定文件夹目录,若小于,则会生成base64
(不会打包图片到文件夹下,而是生成 base64
到 output
的js
文件里 ){
module: {
rules: [
{
test: /.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/', // 匹配到上面后缀的文件时,都打包到该文件夹路径
limit: 2048 // 指定大小
}
}
}
]
}
}
样式(CSS)
打包样式文件 需要
css-loader
和 style-loader
,在 use
字段进行配置说明:设置 css 样式后,挂载到 style 的属性上,所以要两个
{
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
对于
scss
文件,除了上面两个 loader 以外,还要在 use 中额外配置 sass-loader
,然后安装两个文件npm install sass-loader node-sass webpack --save-dev
注意事项:
use
中的 loader
数组,是有打包顺序的,按从右到左,从上到下,即 scss,要先 style,然后 css,最后sass,从右到左use: ['style-loader', 'css-loader', 'sass-loader']
postcss.loader 对于样式,如果老版本的浏览器可能需要兼容,即在 css 属性中加
-webkit
等前缀,可以通过 postcss.loader
实现,在上面例子在后面加上这个 loader 并进行下载后,新建一个 postcss.config.js
文件进行该 loader 的配置即可module.exports = {
plugins: [
require('autoprefixer')
]
}
样式拓展 如何让 webpack 识别 less 文件内再引入的 less 文件,并进行打包?
如何模块化导出和使用样式?(css in js)
{
module: {
rules: [
{
test: /.scss$/,
use: ['style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2, // 允许less文件内引入less文件
modules: true // 允许模块化导入导出使用css,css in js 同理
}
},
'sass-loader,
'postcss-loader'
]
}
]
}
}
字体(Fonts)
打包字体文件(借助iconfont)
从 iconfont 网站下载对应图标的字体文件并压缩到目录后,会发现由于下载的
iconfont.css
文件内部又引入了 eot、ttf、svg
文件,webpack无法识别,引入需给这三个后缀的文件再配置打包规则,用 file-loader
即可{
module: {
rules: [
{
test: /.scss$/,
use: ['style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2, // 允许less文件内引入less文件
modules: true // 允许模块化导入导出使用css,css in js 同理
}
},
'sass-loade,
'postcss-loader'
]
},
{// 配置这个规则即可
test: /.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}
]
}
}
自定义 Loader
首先明确最基本的,编写
Loader
其实就是编写一个函数并暴露出去给 Webpack
使用例如编写一个
replaceLoader
,作用是当遇到某个字符时替换成其他字符,例如遇到 hello
字符串时,替换成 hi
// 在根目录的 loaders 文件夹下的 replaceLoader.js即路径:'./loaders/replaceLoader.js'
?
module.exports = function(source) {
return source.replace('hello', 'hi');
}
这样,一个简易的 Loader 就写好啦
注意 暴露的函数不能写成箭头函数,即不能写成如下:
// replaceLoader.js
?
module.exports = (source) => {
return source.replace('hello', 'hi');
}
由于箭头函数没有
this
指针,而 Webpack
在使用 Loader
时会做些变更,绑定一些方法到 this
上,所以会没法调用原本属于 this
的一些方法了。例如:获取传入
Loader
的参数是通过 this.query
获取当然,要用你自定义的 Loader,除了上面的编写 Loader 外,还需要对他进行使用,在
Webpack
配置文件进行相关配置// webpack.config.jsconst path = require('path');
?
module.exports = {
mode: 'development',
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /.js/,
use: [
path.resolve(__dirname, './loaders/replaceLoader.js') // 这里要书写该 js 文件的路径
]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
如果你想往你的自定义 Loader 传入一些参数,传参的方式如下:
// webpack.config.jsconst path = require('path');
?
module.exports = {
mode: 'development',
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'), // 这里要书写该 js 文件的路径
options: {
name: 'hi'
}
}
]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
这样,
Webpack
在进行打包时,会将 {name: 'hi'}
参数传入 replaceLoader.js
在
replaceLoader.js
中,参数的接收形式如下:// replaceLoader.js
?
module.exports = (source) => {
return source.replace('hello', this.query.name);
}
这样,原项目所有 js 文件中的
hello
字符串都被替换成了 hi
这样,一个简易的
Loader
就完成啦更多 loader-utils 但有时往自定义 Loader 传参时会比较诡异,例如上述例子,传入的明明是一个对象,但可能变成只有一个字符串 ,此时就需要用到
loader-utils
模块,对传入的参数进行分析,解析成正确的内容使用方法
先运行
npm install loader-utils --save-dev
安装,然后// replaceLoader.js
?
const loaderUtils = require('loader-utils');
// 引入该模块
?
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
// 使用
return source.replace('hello', options.name);
}
callback() 有时,除了用自定义 Loader 对原项目做出更改以外,如果启用了
sourceMap
,还希望 sourceMap
对应的映射也发生更改,由于该函数只返回了项目内容的更改,而没返回
sourceMap
的更改,所以要用 callback
做一些配置this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
)
通过该函数进行回调,可以返回除了项目内容更改外,还可以返回 sourceMap 、错误、meta 的更改
由于,我只需要返回项目内容以及
sourceMap
的更改,所以配置示例如下:// replaceLoader.js
?
const loaderUtils = require('loader-utils');
// 引入该模块
?
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
// 使用
const result = source.replace('hello', options.name);
this.callback(null, result, source);
}
async() 自定义 Loader 中有时会有异步操作,例如设置延时器1s后再进行打包(方便摸鱼),那如果直接 setTimeout(),设置一个延时器再返回肯定是不行的,会报错无返回内容,因为正常来说是不允许在延时器中返回内容的。
我们可以通过 async() 来解决,如下:
// replaceLoader.js
?
const loaderUtils = require('loader-utils');
// 引入该模块
?
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
// 使用
const callback = this.async();
setTimeout(() => {
const result = source.replace('hello', options.name);
callback(null, result);
// 参数同上面的 callback()
}, 1000);
}
可以看出,其实
async()
跟 callback()
很类似,只不过用于异步返回而已同时自定义多个 Loader 例如想实现一个需求:打包后项目先是将项目中的所有字符串
hello
替换成 hi
,再把 hi
替换成 Wow
那么就要编写两个 Loader,第一个将
hello
替换成 hi
,第二个将 hi
替换成 Wow
第一个
replaceLoader.js
// replaceLoader.js
?
const loaderUtils = require('loader-utils');
// 引入该模块
?
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
// 使用
const callback = this.async();
setTimeout(() => {
const result = source.replace('hello', options.name);
callback(null, result);
// 参数同上面的 callback()
}, 1000);
}
第二个
replaceLoader2.js
// replaceLoader2.js
?
module.exports = function(source) {
return source.replace('hi', 'wow');
}
同时对
webpack.config.js
进行配置// webpack.config.jsconst path = require('path');
?
module.exports = {
mode: 'development',
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader2.js')
},
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js') // 这里要书写该 js 文件的路径,
options: {
name: 'hi'
}
},
]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
要注意的地方
由于前面提到
Loader
执行顺序是从下到上,从右到左,所以要将第一个写在下面,第二个写在上面Loader 引入转换成官方的引入方式 在上面的例子中,引入
loader
时,方式都是loader: path.resolve(__dirname, './loaders/replaceLoader2.js')
太长了,太麻烦了,不美观,想更换成官方的引入方式,该怎么做呢
loader: 'replaceLoader2'
Webpack
配置文件中配置 resolveLoader
字段// webpack.config.jsconst path = require('path');
?
module.exports = {
mode: 'development',
entry: {
main: './src/index.js',
},
resolveLoader: {
modules: ['node_modules', './loaders']
},
module: {
rules: [{
test: /.js/,
use: [
{
loader: 'replaceLoader2'
},
{
loader: 'replaceLoader', // 这里要书写该 js 文件的路径,
options: {
name: 'hi'
}
},
]
}]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
参数意思:如果在
node_modules
文件夹下没找到配置的 Loader,那么就会进入同级目录下的 loaders
文件夹进行查找更多 Loader 的设计思考 推荐一些自定义的实用的 loader
- 全局异常监控,思路:给所有函数外面包裹
try{} catch(err) {console.log(err)}
语句 - style-loader
module.exports = function(source) {
const style = `
let style = document.createElement("style");
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style)
`
return style;
}
Plugins
- 使用插件让打包更快捷、多样化
- 在打包的某个生命周期,插件会帮助你做一些事情
html-webpack-plugin
作用:由于 webpack 默认打包不会生成 index.html 文件,
htmlWebpackPlugin
会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中插件运行生命周期:打包之后
参数:对象
template
指定一个模板,打包生成的 html 文件根据这个模板来生成,比如会多生成一个
new HtmlWebpackPlugin({
template: 'index.html'
})
clean-webpack-plugin
作用:打包前删除某个目录下的所有内容,主要用于删除之前的打包内容,防止重复
插件运行生命周期:打包之前
参数:数组形式
[‘要删除的文件夹名’]
new HtmlWebpackPlugin(['dist'])
自定义一个 Plugin 首先明确最基本的,编写
plugin
其实就是编写一个类并暴露出去给 Webpack
在打包的某个生命周期进行相关操作。例如编写一个
copyright-webpack-plugin
// copyright-webpack-plugin.js我定义该文件位于根目录的 plugins 文件夹下
?
class CopyrightWebpackPlugin {
constructor() {
console.log('插件被使用了')
}apply(compiler) {}
}
?
module.exports = CopyrightWebpackPlugin;
webpack.config.js
// webpack.config.js
?
const path = require('path');
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin');
?
module.exports = {
mode: 'development',
entry: {
main: './src/index.js',
},
plugins: [
new CopyRightWebpackPlugin()
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
这样,一个简易的
Plugin
就完成啦更多 往插件里传参 在 Webpack 配置文件创建插件实例时,同时传入参数就行
plugins: [
new CopyRightWebpackPlugin({
name: 'Smoothzjc'
})
],
?
这样,就可以在类的构造函数中接收到该参数了
class CopyrightWebpackPlugin {
constructor(options) {
console.log('我是', options.name)
}apply(compiler) {}
}
?
module.exports = CopyrightWebpackPlugin;
不同生命周期 前面我有提到,在打包的某个生命周期,插件会帮助你做一些事情,
所以我们可以在
Webpack
打包的不同生命周期时,写一些想让 Webpack 帮我们做的事常用生命周期:
emit
异步钩子,打包完成准备将打包内容放到生成目录前,即打包完成的最后时刻compile
同步钩子,准备进行打包前
compiler
配置的所有内容,包括打包相关的内容compilation
本次打包的所有内容如果你想在打包完成前新加一个文件到打包目录下,可以配置
compilation
的 assets
属性相关代码运行在
apply
属性中class CopyrightWebpackPlugin {
constructor(options) {
console.log('我是', options.name)
}apply(compiler) {// 同步钩子 compile
compiler.hooks.compile.tap('CopyrightWebpackPlugin', () => {
console.log('同步钩子 compile 生效');
})// 异步钩子 emit
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
compilation.assets['copyright.txt'] = {
// 文件内容配置在 source 属性中,该例子意思是文件内容是一个函数,且返回值是如下
source: funciton() {
return 'copyright write by Smoothzjc'
},
// 该文件大小
size: function() {
return 28
}
}
// 由于异步钩子,所以要运行回调函数
cb();
})
}
}
?
module.exports = CopyrightWebpackPlugin;
编写插件时进行调试 大部分调试工具都是基于 node 编写,在此我举个例子,如何在编写
plugin
时使用调试工具进行 debug
- 先添加脚本指令,通过
node
运行调试工具
// package.json
?
{
"scripts": {
"debug": node --inspect --inspect-brk node_modules/webpack/bin/webpack.js,
"build": "webpack"
}
}
- 在需要调试的地方打断点
class CopyrightWebpackPlugin {
constructor(options) {
console.log('我是', options.name)
}apply(compiler) {compiler.hooks.compile.tap('CopyrightWebpackPlugin', () => {
console.log('compiler');
})compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
debugger;
// 在此处打断点
compilation.assets['copyright.txt'] = {
// 文件内容配置在 source 属性中,该例子意思是文件内容是一个函数,且返回值是如下
source: funciton() {
return 'copyright write by Smoothzjc'
},
// 该文件大小
size: function() {
return 28
}
}
// 由于异步钩子,所以要运行回调函数
cb();
})
}
}
?
module.exports = CopyrightWebpackPlugin;
- 控制台运行
debug
指令npm run debug
后
- 打开浏览器按 F12 打开控制台,可以在开发者工具左上角看到 node 图标,点击即可进入 webpack 打包时经过的一些页面

文章图片
- 可以将鼠标放上想查看的变量的属性

文章图片
- 或在右边的
Watch
属性输入想查看的属性名称进行查看

文章图片
Entry 打包的入口文件,并指定打包后生成的 js文件名
参数:字符串 或 一个对象,默认生成的文件名是
main
,即 生成的文件名:'入口文件路径'
entry: './src/index.js'
或
entry: {
main: './src/index.js'
}
同时上面跟下面的等价
同时也可以打包成多个 js 文件,即多入口
entry:{
main: './src/index.js',
sub: './src/index.js'
}
Output 输出js文件名
参数:
filename
最后打包出来的 js 文件名,可以直接bundle.js
指定,也可以[name].js
根据 entry 指定的名字,也可以[hash].js
根据 entry 指定的哈希值chunkFilename
通过异步引入的文件的文件名
path
打包后所处文件夹和路径
publicPath
给打包出来的 js 文件的 src 引入路径都加个前缀,一般用于 cdn 配置
publicPath 详解
index.html
中引入 js 板块的代码
如果想将打包后的js 文件都放到 cdn上,减少打包后体积(此时该js就不用放在打包后的文件夹中了),例如
可配置成如下
publicPath: 'http://cdn.com.cn'
【Webpack|一份不可多得的 Webpack 学习指南(共10k字)】output配置示例
output: {
publicPath: 'http://cdn.com.cn',
filename: '[name].js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist')
}
SourceMap 打包后的文件是否开启映射关系,他知道打包后文件与打包前源代码文件的代码映射
例如:知道 dist 目录下 main.js 文件96行报错,实际上对应的是 src 目录下 index.js 文件中的第一行
通常不用开启,默认
none
关闭,因为开启后会减缓打包速度和增大打包体积参数
devtool: '参数'
?
'none' // 不开启source-map
'source-map' // 开启source-map进行映射
'inline-source-map' // 开启source-map进行映射的前提下,精确到哪一行哪一列
'cheap-source-map' // 开启source-map进行映射的前提下,只精确到哪一行
'eval' // 通过 eval 开启映射,效率最快,但不全面
推荐:
开发环境:
devtool: 'cheap-module-eval-source-map'
生产环境(线上环境):
devtool: 'cheap-module-source-map'
生产环境一般不用配置 devtool,但如果想报错时快速定位错误,可开启,建议使用上面推荐的参数
mode: 'development'
是开发环境mode: 'production'
是生产环境更多其他参数查看下表

文章图片
SourceMap 配置示例
devtool: 'cheap-module-source-map'
WebpackDevServer 开启一个本地web服务器,可提高开发效率
webpack
指令(一般直接配置第二个脚本就行)1. webpack --watch保存后自动重新打包
2. webpack-dev-server启动一个web服务器,并将对应目录资源进行打开,对应目录资源修改后保存会重新进行打包,且自动对网页进行刷新
我们下载的每个项目都经过两条指令(安装依赖 + 打包运行),下面以 create-react-app 脚手架生成的 react 项目为例
npm install
npm run start
第二步,其实就是运行
WebpackDevServer
,你会发现,start 后会直接打开浏览器,且每次保存后都会自动重新打包、重新刷新网页。WebpackDevServer
隐藏特性:打包后的资源不会生成一个 dist 文件夹,而是将打包后资源放在电脑内存中,能有效提高打包速度参数
contentBase
将哪个目录下的文件放到 web 服务器上进行打开open
是否在打包时同时打开浏览器访问项目对应预览 urlport
端口号proxy
设置代理
webpackDevServer: {
contentBase: './dist',
open: true,
port: 8080,
proxy: {
'api': 'xxxxx'
}
}
拓展内容 其实相当于自己手写一个 webpack-dev-server,但人家官方已经帮我们写好一个各配置项都齐全的一个了,自己不用手写了,只是带大家进行拓展,理解一下 webpack-dev-server 背后的源码是如何搭配 node 实现的
在 node 中使用 webpack
查看官方文档 的
Node.js API
板块在命令行中使用 webpack
查看官方文档 的
Command Line Interface
板块Hot Module Replacement 热模块更新
HMR
当内容发生更改时,只有更改的那部分发生变化,其他已加载的部分不会重新加载(例如修改css样式,只有对应样式更改,js不会改变)参数
hot
开启热模块更新hotOnly
设置为 true 后,无论热更新是否开启,都禁用webpackDevServer
的保存后自动刷新浏览器功能
webpackDevServer
里HMR
配置示例const webpack = require('webpack');
devServer: {
hot: true,
hotOnly: true
}
plugins: [
new webpack.HotModuleReplacementPlugin()
]
上面是让
HMR
生效,下面是对 HMR
进行使用// 例子:当 number.js 发生更改时,会调用函数
import number from './number';
number();
if(module.hot) {
module.hot.accept('./number', () => {
number();
})
}
但上面的 使用 一般不用写,因为其实很多地方都已经写好了,内置了
HMR
组件,例如css的话 css-loader
里面给你写好了,vue 的话 vue-loader
里写好了,react 的话 babel-preset
写好了。如果你要引入比较冷门的数据文件,没有内置
HMR
,就需要写。使用 Babel 处理 ES6 语法
babel-loader
、@babel/preset-env
、@babel/polyfill
- babel-loader 的配置选项可以单独写进
.babelrc
文件里 - 除了将 ES6 转换成 ES5 还不够,有些低版本浏览器还需要将 Promise、Array.map 注入额外代码,需要引入
@babel/polyfill
@babel/preset-env 的参数useBuiltIns: 'usage' // 对于使用的代码,才转译成 ES5 并打包至 dist 文件夹
targets: 该代码运行环境,根据环境来判定是否要做 ES6 的转化
{
chrome: '67' // 谷歌浏览器版本大于67,对 ES6 能直接正常编译,所以没必要做 ES5 的转换了
}
使用示例:
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
// 以下演示两种方案
presets: [["@babel/preset-env", {
targets: {
edge: '17',
firefox: '60',
chrome: '67',
safari: '11.1'
},
useBuiltIns: 'usage'
}]]// 以下是生成第三方库或源组件,不希望 babel 污染时才使用,可替换上面的 presets
plugins: [["babel/plugin-transform-runtime", {
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false
}]]
}
}
]
}
如果你想在 react 使用 babel 实现对 React 框架代码的打包
下载
@babel/preset-react
.babelrc 文件
{
presets: [
[
"babel/preset-env", {
targets: {
chrome: "67",
},
useBuiltIns: "usage"
}
],
"@babel/preset-react"
]
}
Webpack 高级概念
webpack
的 Guides
板块官方文档
Tree Shaking 只会对引入进行使用的代码进行打包,没引入进行使用的代码不会打包(可减少代码体积),webpack 2.0 之后默认开启该功能
- 只支持 ES Module(静态引入)
- 不支持 Common JS(动态引入)
import { } from '' // ESM支持
const xxx = require('') // Common JS不支持
development(开发环境) 默认不打开
Tree Shaking
因为如果开发环境进行调试时,如果每次重新编译打包后的代码都进行了
Tree Shaking
,那就会让 debug 时代码行数对不上,不利于调试如果你想开发环境打开
// package.json
{
"sideEffects": false 或 数组
}
?
如果是数组,则配置你不想哪些代码进行 Tree Shaking
例如:如果不想 css 文件进行 Tree Shaking,则
{
"sideEffects": ["*.css"]
}
Development 和 Production 模式的区分打包 通常来说,两个环境的 webpack 配置文件不会有变化,但如果非得区分,可以不同文件形式
webpack.dev.js
根据名字可知,是 development(开发环境)webpack.proud.js
根据名字可知,是 production(生产环境)打包脚本也要更改,如果区别开
// package.json
{
"scripts": {
"dev-build": webpack --config webpack.dev.js, // 相对路径或"proud-build": webpack --config webpack.proud.js, // 相对路径
}
}
Webpack 和 Code Splitting 为什么要进行代码分割?
如果用户一个页面要加载的 js 文件很大,足足有2MB,那么用户每次访问这个页面,都要加载完2MB的资源页面才能正常显示,但其中可能有很多代码块是当前页面不需要使用的,那么将没使用的代码分割成其他 js 文件,当要使用的时候再进行加载、当页面变更时只有那部分进行重新加载,这样可以大大加快页面加载速度。
即通过配置进行合理的代码分割,能让文件结构更清晰,项目运行更快,比如如果用到 lodash 库,就分割出来。下面通过一个例子进行解释:
假设现在有 main.js(2MB),里面含有 lodash.js(1MB)
1. 该种方式
首次访问页面时,加载 main.js(2MB)
当页面业务逻辑发生变化时,又要重新加载2MB内容
?
2. 将 lodash.js 抽离出来,即现在是 main.js(1MB) 和 lodash.js(1MB)
由于浏览器的并行机制,首次访问页面时,并行渲染两个1MB的文件是要比只渲染一个2MB的文件要快的。
其次,当页面业务逻辑发生变化时,只要重新加载 main.js(1MB) 即可。
?
同时,如果对于两个文件,如果都有用到某个模块,如果两个文件各自写一次这个模块,就会有重复,此时如果将这个公共模块抽离出来,两个文件分别去引用他,那么就会减少一次该模块的撰写(减少包体积)。
即对代码进行合理分割,还可以加快首屏加载速度,加快重新打包速度(包体积减少)代码分割:通俗解释就是将一坨代码分割成多个 js 文件
代码分割自己可以手动,例如我们平时的抽离公共组件,但为什么现在
webpack
几乎跟 Code Splitting
绑定在一起了呢?因为
webpack
中有一个插件 SplitChunksPlugin
,会让代码分割变得非常简单, 这也是 webpack
的一个强大的竞争力点// webpack.config.js
{
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
总结一下
Webpack
进行 Code Splitting
有两种方式1. 通过配置插件
SplitChunksPlugin
```
// webpack.config.js
{
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
```然后编写同步代码```
import _ from 'lodash';
// 编写业务代码
```
2. 通过异步地动态引入
function getComponent() {
return import('lodash').then(({ default: _ }) => {
let element = document.createElement('div');
element.innerHTML = _.join(['zjc', 'handsome'], '-');
return element;
})
}
?
getComponent().then(element => {
document.body.appendChild(element);
})
当然,想要支持异步地动态引入某个模块,需要先下载
babel-plugin-dynamic-import-webpack
然后
// .babelrc
{
presets: [
[
"@babel/preset-env", {
targets: {
chrome: "67"
},
useBuiltIns: 'usage'
}
],
"@babel/preset-react"
],
plugins: ["dynamic-import-webpack"]
}
SplitChunksPlugin 该板块会对该插件配置参数进行详解
重要作用是可以减少包体积,缓存组中的
reuseExistingChunk
属性:开启true后,如果要分割进该组的模块在之前已经被缓存到了某个组内,那就不会再缓存配置示例
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'all', // 只对哪些代码进行分割(all 的话全部都,async 的话只对异步代码进行分割)
minSize: 30000, // 要做代码分割的引入库的最小大小,即30000代表如果你引入的库大小超过30KB才做代码分割
minRemainingSize: 0,
minChunks: 1, // 一个库被引入至少多少次才做代码分割
maxAsyncRequests: 5, // 同时分割的库数
maxInitialRequests: 3, // 最多能分割出多少个 js 文件
automaticNameDelimiter: '~', // 代码分割出来的 js 文件名 和下面的组名 用什么符号进行连接
name: 'true' // 当为 true 时,下面组的 filename 属性才会生效
enforceSizeThreshold: 50000,
// 缓存组,当打包同步代码时,除了走完上面的设置流程,还会额外再走进下面的组设置,即代码分割进下面符合要求的各组
cacheGroups: {
vendors: {
test: /[\/]node_modules[\/]/, // 只有 node_modules 里面的库被引入时,才做代码分割到 vendor 组
priority: -10, // 优先级,越大优先级越高
reuseExistingChunk: true,
filename: 'vendors.js', // vendors 组的代码分割都分割到 filename 文件内
name: 'vendors' // 生成 vendors.chunk.js,该属性和上面的 filename 写一个就行
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true, // 开启true后,如果要分割进该组的模块在之前已经被缓存到了某个组内,那就不会再缓存
},
},
},
},
};
更多配置项请看官方文档
Lazy Loading 通过异步地动态引入某个模块,通常在路由配置页面,对引入的组件进行懒加载
function getComponent() {
return import('lodash').then(({ default: _ }) => {
let element = document.createElement('div');
element.innerHTML = _.join(['zjc', 'handsome'], '-');
return element;
})
}
?
getComponent().then(element => {
document.body.appendChild(element);
})
打包分析 应用
webpack
官方工具 Bundle Analysis
Preloading、Prefetching
Preloading
懒加载,当进行某个事件时,才会引入某个组件,例如点击某个元素时,才会 import
引入组件PreFetching
预加载,当主页面的核心功能和交互都加载完成后,如果网络空闲,那么就会预先加载某个组件,这样在某个时刻引入该组件时,就能一下子打开实现预加载:
webpack
搭配魔法注释,在引入的路径前加上 webpackPrefetch: xxx
document.addEventListener('click', () => {
import(/* webpackPrefetch: true */ './click.js').then((func) => {
func()
})
})
这也是
webpack
最为推荐的首屏加载优化手段,异步引入 + 预加载CSS 文件的代码分割 前面的
Code splitting
都是针对 js 的,将 js 文件进行代码分割,而打包出来的 css 都在 js 文件里如果想 CSS 文件也代码分割出来,可以使用
MiniCssExtractPlugin
插件,该插件由于依赖热更新,所以只能运行在线上打包环境中// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
?
module.exports = {
plugins: [ new MiniCssExtractPlugin() ],
module: {
rules: [
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};
且默认将引入的各 css 文件合并到同一个 css 文件里
如果你想代码分割出来的 css 文件做代码压缩,重复属性合并到一起,可以使用
OptimizeCSSAssetsPlugin
插件// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
?
module.exports = {
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({})
]
},
plugins: [ new MiniCssExtractPlugin() ],
module: {
rules: [
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
}
如果想多入口引入的 css文件也合并在一起,同样需要用到代码分割
// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
?
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles', // 都打包进 styles.css 的文件里
test: /.css$/,
chunks: 'all',
enforce: true // 无视默认参数,如果你在代码分割时设置过一些参数,当你对css文件进行代码分割时可以无视
}
}
},
plugins: [ new MiniCssExtractPlugin() ],
module: {
rules: [
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
}
}
Webpack 与浏览器缓存(Cache)
当浏览器加载过某项资源时会在本地进行缓存记忆,这样当用户下次再重新访问该页面加载该资源时,浏览器可以根据缓存过的文件快速加载该资源,直到该文件名发生改变,浏览器才知道该文件发生改变,需要重新渲染。浏览器该特性的作用
可以加快加载速度,当该页面某个部分发生改变时,可以只重新渲染改变的部分,做到局部渲染
问题:如何保证每次打包时只有做了更改的文件的文件名发生更改,没做更改的文件的文件名不变?
如果在
webpack
配置文件中的 output
属性设置为如下output: {
filename: '[name].js',
chunkFilename: '[name].chunk.js'
}
如果对项目中文件做了更改,而文件名没变,打包的 filename 没变,由于浏览器已经加载过该文件,缓存了这个文件名,当你该文件发生更改而文件名没改变时,浏览器不会重新渲染,为了让每次文件更改后文件名都发生改变,可以使用哈希值命名
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].chunk.js'
}
让
webpack
根据文件内容创建对应独立的一个哈希值,同时在文件内容发生改变时,由于哈希值也会改变,所以文件名也会改变拓展 在老版本
webpack
中(webpack 4.0 以下),如果在每次运行 npm run build
进行打包时,发现即使文件没变更,每次重新打包他们的哈希值都会变,可以通过配置 runtimeChunk
解决optimization: {
runtimeChunk: {
name: 'runtime'
}
}
// 同时打包后会多生成一个 runtime.hash.js 的文件,hash每次都不一样
runtimeChunk 原理 假设我通过
webpack
打包出来有两个 js文件,A文件是业务逻辑相关代码,B文件是作代码分割时的库代码(例如 lodash),由于业务逻辑中有引入库的操作,所以他们之间会有关联,而这个关联的相关代码同时存在于A和B文件(这种关联我们一般称之为 manifest
),而在每次打包时,manifest
内置的包和包的关系、js和js文件的嵌套关系会发生微小改变,所以即使A和B文件没做更改时,打包出来的哈希值还是会发生变化。通过配置
runtimeChunk
,可以将这些 manifest
相关的代码抽离出来单独放在 runtimeChunk
中,因此每次重新打包,改变的只有runtime.hash.js
,A文件只有业务逻辑,B文件只有库文件,A和B文件内都不会有任何 manifest
的代码了,这样A和B文件都不会发生改变了,因此哈希值就不会变了Shimming 打包兼容,自动引入
在你页面使用到某个库,但没进行引入时,
webpack
打包后的代码会帮你自动、"偷偷"进行引入plugins: [
new webpack.ProvidePlugin({
$: 'jquery', // 当使用 $ 时,会自动在那个页面引入 jquery 库
_: 'lodash', // 当使用 _ 时,会自动在那个页面引入 lodash 库
_join: ['lodash', 'join'] // 当输入 _join 时,会引入 lodash库的 join 方法
})
]
更多 环境变量的使用
对于开发环境和生产环境,可能有时真的需要单独写不同的
webpack
配置文件进行配置而单独写,肯定有许多属性是重复的,又不想多写,怎么办呢?
例如A和B文件是两个环境中不同的配置参数,而C文件是共同的配置文件,那么开发环境打包时希望按照 A+C 的打包规则,生产环境打包时希望按照 B+C的打包规则
可以通过
webpack-merge
配置 开发环境 和 生产环境 的 不同配置文件// webpack.common.js
?
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js');
// 假设有这个文件,且导出的是开发环境的一些配置参数
const prodConfig = require('./webpack.prod.js');
// 假设有这个文件,且导出的是生产环境的一些配置参数
const commonConfig = {
// 这里放开发和生产环境共有的一些配置参数
}
?
module.exports = (env) => {
// 如果 env 参数存在,且传进来了 production 属性,说明是生产环境
if(env && env.production) {
return merge(commonConfig, prodConfig);
} else {
return merge(commonConfig, devConfig);
}
}
?
同时
package.json
文件中修改配置{
scripts: {
"dev": "webpack-dev-server --config webpack.common.js",
"build": "webpack --env.production --config webpack.common.js" // 通过--env.production 传递参数进文件,执行线上环境的打包配置文件
}
}
当然,脚本配置时,向配置文件传入参数也可以如下方式:
"build": "webpack --env production --config webpack.common.js"
同时,
webpack.common.js
参数判断时也要改成module.exports = (env, production) => {
// 如果 env 参数存在,且传进来了 production 属性,说明是生产环境
if(env && production) {
return merge(commonConfig, prodConfig);
} else {
return merge(commonConfig, devConfig);
}
}
谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。写作不易,「点赞」+「收藏」+「转发」 谢谢支持?
我是 Smoothzjc,如果觉得写得可以的话,请点个赞吧?
我也会在今后努力产出更多好文。
感兴趣的小伙伴也可以关注我的公众号:Smooth前端成长记录,公众号同步更新
往期推荐 《都2022年了还不考虑来学React Hook吗?6k字带你从入门到吃透》
《Github + hexo 实现自己的个人博客、配置主题(超详细)》
《10分钟让你彻底理解如何配置子域名来部署多个项目》
《一文理解配置伪静态解决 部署项目刷新页面404问题
《带你3分钟掌握常见的水平垂直居中面试题》
《React实战:使用Antd+EMOJIALL 实现emoji表情符号的输入》
《【建议收藏】长达万字的git常用指令总结!!!适合小白及在工作中想要对git基本指令有所了解的人群》
《浅谈javascript的原型和原型链(新手懵懂想学会原型链?看这篇文章就足够啦!!!)》
推荐阅读
- 开发工具|小马带你认识前端开发神器WebStorm(WebStorm及Git的相关配置与使用)
- Web 性能权威指南
- 前端工程师经常上哪些网站学习最新技术?
- 融合通信常见问题3月刊 | 云信小课堂
- xterm.js+react的综合使用(onKey以及onData的区别使用导致的光标串行问题)
- 衡石BI产品预置明道云数据连接器
- 前端实现多文件编译器
- 前端|VScode 主题和打字特效配置,让你的VScode活“”起来
- 前端|2018再见! | 掘金年度征文