开篇词 🤗
这是一个系统学习webpack的系列文章,在多年的编程学习之后,我知道:这个系列注定不会一帆风顺🙄,同样我也知道在这些失败的背后隐藏着通往美好未来的秘密,星光不问赶路人,时光不负有心人,出发吧。
友情提示 🤣
本系列开始之前,请调整好心态,我们要做好长(总)期(有)作(报)战(错)的准备😌。经过我的的第二次折腾webpack,得出一个重要结论:
当你依赖多个包时,一定要小心不同版本之间的细微的语法差别。尤其是webpack这种考验配置管理的打包器,这个结论适用于其他所有需要借助配置项、存在多版本交叉依赖的工具使用 🙃
什么是webpack 📦
一言以蔽之,webpack把所有的外部依赖都视为文件,统一处理成web最终能够处理的js css jpg等静态资源
总之,是一个🐄的打包器,可以说是web工程化的道路上的最强利器之一,现代化的web开发必备工具,再多溢美之词详见webpack。
起步 ✍️
巧妇难为无米之炊,当然要先安装webpack相关工具链。本系列的webpack版本是基于webpack5.8 webpack-cli 4.2
,理论上webpack4以上都可以适用,同时node版本要求10.13.0(LTS)及以上,我选择采用yarn安装,也可以选择npm安装,两者细微的区别:
- npm i === yarn add
- npm uninstall === yarn remove
- 其它的命令基本上把npm 换成 yarn即可
极简项目搭建的初始化脚本如下所示:
mkdir webpack-demo
cd webpack-demo
yarn init -y
yarn add webpack webpack-cli -D
配置文件 📖
项目初始化成功后,我们可以书写es6、sass 、less
等其它浏览器不能直接解析的代码了,但是要做到上述功能,我们还需要各一个配置文件webpack.config.js
,这是一个最基本的配置文件,后面我会拆分成开发、生产两套配置文件。
//webpack.config.js
"use strict";
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
//在当前目录下的dist目录
path: path.join(__dirname, "dist"),
filename: "bundle.js",
},
mode: "production"
};
借助安装的webpack-cli,此时我们已经可以在命令行使用webpack进行打包了,这里为了方便的使用webpack,修改package.json
的scripts
{
//...
"script": {
"build": "webpack"
},
//...
}
项目编译查看效果是直接yarn run build
,此时会通过在.node_modules/.bin/webpack
的软连接编译项目。
核心基础概念
webpack的配置中有很多项,其中有五个最核心的概念:
entry
入口,有单入口和多入口。output
输出,根据单入口 多入口有两种写法。mode
模式,三个值production(默认值) development none
loader
plugin
entry
这是webpack编译项目的入口文件,通常可以有两种写法:
- 单文件入口写法,如上面的基本配置所示。
- 多入口文件写法,采用对象的形式,对应output则变为使用占位符
[name]
//webpack.config.js
module.exports = {
//...
entry: {
'index': './src/index.js',
'search': './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js' //这里的name即上面entry对象的key值
},
//...
}
ouput
项目编译构建成功后的输出目录和输出文件的名字,有两种写法,分别对应单入口和多入口写法
//webpack.config.js
module.exports = {
//...
output: {
//单入口文件对应的写法
path: path.join(__dirname, "dist"),
filename: "bundle.js",
//or 多入口文件对应的写法
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
//...
}
可以看出来,entry
和output
这两个概念是配合着使用。
mode
启用不同的mode,会开启不同的内置函数进行优化,并没有特别需要注意的要点。
development |
会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development . 为模块和 chunk 启用有效的名。 |
---|---|
production |
会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production 。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin ,FlagIncludedChunksPlugin ,ModuleConcatenationPlugin ,NoEmitOnErrorsPlugin 和 TerserPlugin 。 |
none |
不使用任何默认优化选项 |
对于剩下的两个概念,极其重要,需要仔细研究一下🤓
Loaders
loader
这个概念是对依赖的文件进行预处理,对于需要打包的资源选用不同的loader进行处理,会起到事半功倍的效果😋。
通常来说,工程开发中(目前我暂时涉猎的)用到以下几种的loader
:
loader
的简单分类
文件相关的loader:
url-loader
,可以用来预处理图片、文字等静态资源。file-loader
,基本上与url-loader
作用一样,但是可以通过options配置项启用base64编码来处理上述静态资源。
语法转换相关的:
babel-loader
,大名鼎鼎的babel
转换,把es6等高阶语法降级成基础js语法,提高老版本项目的兼容性。
样式相关的:
style-loader
,将导出的css文件插入到style标签
里 再插入到head标签
内,实现html
的加载css代码
。css-loader
,加载其它css预处理器处理后的css文件并解析import的css文件less-loader or sass-loader or stylus-loader
,加载并编译less sass文件
框架相关的:
vue-loader
,加载并编译vue单文件组件。
上面是几种常用的loader的分类,下面就使用一下它们。
babel-loader
的使用
在使用babel-loder
的时候需要配合另外的几个依赖,一起食用才会更加美味😋。
yarn add @babel/core @babel/preset-env babel-loader -D
,安装成功后修改配置文件
//webpack.config.js
module.exports = {
//...
//4、loader的使用
module: {
rules: [
{
//4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
test: /.js$/,
use: 'babel-loader'
}
]
}
}
loader
的写法稍微有一点点小麻烦,同时babel-loader
还需要配置.babelrc
//项目根目录下新建.babelrc
{
"presets": [
"@babel/preset-env", //es6的预设
]
}
css
相关loader
的使用
本文使用less
做css预处理器
,所以配合使用的依赖有四个:
yarn add less less-loader css-loader style-loader -D
。
对应的配置文件修改如下:
//webpack.config.js
module.exports = {
//...
//4、loader的使用
module: {
rules: [
{
//4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
test: /.js$/,
use: 'babel-loader'
},
{
//4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
test: /.css$/,
use: [
'style-loader', //把css放到style标签里面插进head标签里面
'css-loader', //加载css文件,并转换成commonjs对象
]
},
{
//4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
test: /.less$/,
use: [
'style-loader', //把css放到style标签里面插进head标签里面
'css-loader',
'less-loader', //把less转换成css文件
]
},
]
}
}
多个loader加载时注意顺序:链式调用多个loader,从右往左,所以一定要注意加载的先后顺序。
图片、字体等静态资源相关loader
的使用
对于图片、字体这些依赖来说,虽然我们看来不是一种类型,但是在打包器看来都是静态资源,直接打包就完事了🤓
上面的简单分类里面说过,一般有两种loader
-file-loader
和url-loader
,对于具体采用哪种,视实际情况而定。
先安装yarn add file-loader url-loader -D
,同样的,修改配置文件。
//webpack.config.js
module.exports = {
//...
//4、loader的使用
module: {
rules: [
{
//4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
test: /.js$/,
use: 'babel-loader'
},
{
//4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
test: /.css$/,
use: [
'style-loader', //把css放到style标签里面插进head标签里面
'css-loader', //加载css文件,并转换成commonjs对象
]
},
{
//4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
test: /.less$/,
use: [
'style-loader', //把css放到style标签里面插进head标签里面
'css-loader',
'less-loader', //把less转换成css文件
]
},
{
//4.4 解析图片文件,需要file-loader
test: /.(png|img|jpg|jpeg|gif)$/,
use: "file-loader",
},
{
//4.5 解析字体文件,需要file-loader
test: /.(woff|woff2|eot|ttf|otf)$/,
use: "file-loader",
},
{
//4.6 解析图片、字体文件,也可以url-loader
//TODO 字体文件22m打包有问题,需要解决
test: /.(png|img|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: "url-loader",
options: {
limit: 1024 * 10,
//小于这个字节数会直接base64处理,不生成单独的图片字体文件,直接打进对应的js文件中,对应的那个js文件会变大
},
},
],
},
]
}
}
这里有一个问题,字体文件太大了,需要压缩,现在直接打包,比较费劲😶,同时此时编译后的静态文件并没有单独的文件名称,这个问题需要用到后续的优化插件。
开启webpack的监听功能
在开发中,经常需要更新页面,而我们如果不想每次重新编译,则可以开启这个选项,修改后的配置文件如下:
//webpack.config.js
module.exports = {
//...
//附加:开启监听
watch: true, //开启监听 等同于在package.json中增加 webpack --watch命令
// 只有开启监听后下面配置才有用
watchOptions: {
//使用正则去忽略某些文件,能提高监听的性能,默认为空
ignored: /node_modules/,
//聚合等待时间,不会立刻执行
aggregateTimeout: 200,
//轮询时间,默认一秒轮询1000次
poll: 1000
},
}
这个功能通常来说并不是特别重要,因为后面有其它操作来替换-也就是热更新服务器。
Plugins
有一些功能loader
不方便完成,所以此时需要一种补充功能的东西,这些东西称之为Plugin
,webpack
变得更加灵活了。
上一节结尾处提到了一种热更新机制-开发环境时,本地代码更新后需要即时编译结果,我们需要借助插件来完成,但是这个热更新插件(专业名词叫HMR-HotModuleReplacementPlugin
)自己还不能完成全部热更新机制,我们还需要一个热更新服务器(WDS-webpack-dev-server
,它的后台使用了WDM-web-server-middleware
)去把项目直接在浏览器里起起来。
热更新
首先要明确的一点是,热更新机制只有在开发环境下才有意义。
安装依赖,yarn add webpack-dev-server -D
,修改配置文件,这次改动较大,要细心👀
//webpack.config.js
const webpack = require("webpack")
module.exports = {
//...
mode: "development",
//5、plugin的使用。
// 以webpack内置的HMR-HotModuleReplacementPlugin(热模块替换,只替换更新的)为例
// WDS-webpack-dev-server的热更新服务器需要这个插件配合使用。
//同样实现热更新的还有WDM-webpack-dev-middleware,它将webpack输出文件传输给服务器,更加灵活
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
//开启内置的webpack-dev-server热更新的配置哦
//注意:老版本的直接在package.json的scripts使用webpack-dev-server --open,新版本后使用webpack serve
devServer: {
contentBase: path.join(__dirname, 'dist'), //基础路径
hot: true
}
}
webpack-dev-server
3版本的命令变了,package.json的scripts中要用webpack serve
命令,如果你是老版本的话,继续使用webpack-dev-server --open
即可。
版本迭代的一点小优化
随着我们一直在改良项目的编译流程,项目的版本自然而然也会越来越多,多版本必然带来一些问题,每次更新版本后生产环境里需要替换哪些内容呢,全替换还是有针对性的替换新的,如果只替换修改后的,那么如何保证找到更新的文件呢?🤔,上面的一系列文件带来了一个新的理念-文件指纹。
简而言之,文件指纹
就是给文件打上标签,用以区分不同版本。
关于指纹的分类,一般有3种,都是md5值:
hash
,图片、字体使用8位的普通hash并加上后缀,使之可以单独编译成原来格式的文件,不丢失后缀。chunkhash
,js文件使用8位的chunkhash做指纹contenthash
,css文件一般用8位的contenthash
拆分webpack.config.js
既然我们已经涉及到了版本管理,同时也要区分开发环境和生产环境了,陪伴我们很久的老朋友webpack.config.js
也要离开历史的舞台了,它的两个孩子webpack.prod.js
和webpack.dev.js
将会代替他陪我们走完剩下的路👬
至此,package.json
的脚本也可以告一段落了:
{
"scripts": {
"build": "webpack --config webpack.prod.js",
"watch": "webpack --watch",
"dev": "webpack serve --config webpack.dev.js"
},
}
拆分后的开发环境配置文件 webpack.dev.js更新如下:
"use strict";
const path = require("path");
const webpack = require("webpack")
module.exports = {
//1、单入口文件写法如下:
// entry: "./src/index.js",
// output: {
// path: path.join(__dirname, "dist"),
// filename: "bundle.js",
// },
//2、多入口文件写法如下:entry 使用对象的写法,output只能使用占位符[name]
entry: {
index: "./src/index.js",
search: "./src/search.js",
},
output: {
path: path.join(__dirname, "dist"),
filename: "[name].js",
},
//3、mode有三个值:production(默认值)、development、none,开启之后会启动相应的默认配置函数
mode: "development",
//附加:开启监听
watch: true, //开启监听 等同于在package.json中增加 webpack --watch命令
// 只有开启监听后下面配置才有用
watchOptions: {
//使用正则去忽略某些文件,能提高监听的性能,默认为空。它是会输出到硬盘中。
ignored: /node_modules/,
//聚合等待时间,不会立刻执行
aggregateTimeout: 200,
//轮询时间,默认一秒轮询1000次
poll: 1000
},
//4、loader的使用
module: {
rules: [
{
//4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
test: /.js$/,
use: "babel-loader",
},
{
//4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
test: /.css$/,
use: [
"style-loader", //把css放到style标签里面插进head标签里面
"css-loader", //加载css文件,并转换成commonjs对象
],
},
{
//4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
test: /.less$/,
use: [
"style-loader", //把css放到style标签里面插进head标签里面
"css-loader",
"less-loader", //把less转换成css文件
],
},
// {
// //4.4 解析图片文件,需要file-loader
// test: /.(png|img|jpg|jpeg|gif)$/,
// use: "file-loader",
// },
// {
// //4.5 解析字体文件,需要file-loader
// test: /.(woff|woff2|eot|ttf|otf)$/,
// use: "file-loader",
// },
{
//4.6 解析图片、字体文件,也可以url-loader
//TODO 字体文件22m打包有问题,需要解决
test: /.(png|img|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: "url-loader",
options: {
limit: 1024 * 10, //小于这个字节数会直接base64处理,不生成单独的图片字体文件,直接打进对应的js文件中,对应的那个js文件会变大
},
},
],
},
],
},
//5、plugin的使用。
// 以webpack内置的HMR-HotModuleReplacementPlugin(热模块替换,只替换更新的)为例
// WDS-webpack-dev-server的热更新服务器需要这个插件配合使用。
//同样实现热更新的还有WDM-webpack-dev-middleware,它将webpack输出文件传输给服务器,更加灵活
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
//开启内置的webpack-dev-server热更新的配置哦
//注意:老版本的直接在package.json的scripts使用webpack-dev-server --open,新版本后使用webpack serve
devServer: {
contentBase: path.join(__dirname, 'dist'), //基础路径
hot: true
}
};
拆分后的生产环境的配置文件 webpack.prod.js
加上指纹后的更新如下:
"use strict";
const path = require("path");
module.exports = {
//1、单入口文件写法如下:
// entry: "./src/index.js",
// output: {
// path: path.join(__dirname, "dist"),
// filename: "bundle.js",
// },
//2、多入口文件写法如下:entry 使用对象的写法,output只能使用占位符[name]
entry: {
index: "./src/index.js",
search: "./src/search.js",
},
output: {
path: path.join(__dirname, "dist"),
filename: "[name]_[chunkhash:8].js", //js文件使用8位的chunkhash做指纹
},
//3、mode有三个值:production(默认值)、development、none,开启之后会启动相应的默认配置函数
mode: "production",
//4、loader的使用
module: {
rules: [
{
//4.1 解析js文件,需要安装@babel/core @babel/preset-env babel-loader 配置.babelrc
test: /.js$/,
use: "babel-loader",
},
{
//4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
test: /.css$/,
use: [
"style-loader", //把css放到style标签里面插进head标签里面
"css-loader", //加载css文件,并转换成commonjs对象
],
},
{
//4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
test: /.less$/,
use: [
"style-loader", //把css放到style标签里面插进head标签里面
"css-loader",
"less-loader", //把less转换成css文件
],
},
{
//4.4 解析图片文件,需要file-loader
test: /.(png|img|jpg|jpeg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'//图片使用8位的普通hash并加上后缀
}
}
],
},
{
//4.5 解析字体文件,需要file-loader
test: /.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}
],
},
// {
// //4.6 解析图片、字体文件,也可以url-loader
// //TODO 字体文件22m打包有问题,需要解决
// test: /.(png|img|jpg|jpeg|gif|woff|woff2|eot|ttf|otf)$/,
// use: [
// {
// loader: "url-loader",
// options: {
// limit: 1024 * 10, //小于这个字节数会直接base64处理,不生成单独的图片字体文件,直接打进对应的js文件中,对应的那个js文件会变大
// },
// },
// ],
// },
],
},
};
抽离css
并加指纹
前面我们的编译流程结果,细心地你会发现并没有单独的css文件,这是因为它被打到了一起,并动态插入到了HTML的head标签的style中,我们需要借助一个插件-mini-css-extract-plugin
把它们抽离出来。
yarn add mini-css-extract-plugin -D
,有了这个插件之后我们就不需要style-loader
,借助这个插件自带的loader即可。
更新后的开发环境的配置文件webpack.prod.js
如下:
//webpack.prod.js
"use strict";
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
module: {
rules: [
{
//4.2 解析.css文件,需要style-loader css-loader,链式调用,从右往左,必须先解析.css文件
test: /.css$/,
use: [
MiniCssExtractPlugin.loader, //提取成带指纹的单个css文件,不能与style-loader共存
"css-loader", //加载css文件,并转换成commonjs对象
],
},
{
//4.3 解析.css文件,需要style-loader css-loader less-loader, 链式调用,从右往左,必须先解析.less文件
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"less-loader", //把less转换成css文件
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
})
]
};