长文慎读
版本:
- webpack 4
- clean-webpack-plugin 3.0.0
本文分为三个部分:
- 认识 Webpack
- Webpack 基础
- Webpack 进阶
- Webpack 高级
认识 Webpack
Webpack 是一个模块打包工具。
简单举个例子🌰
新建三个文件,如下:
上面的例子会报错:
因为浏览器并不支持 JS 的模块化导入。这时候 Webpack 便派上用场了。
事实上,已经有很多浏览器开始支持 JS 的模块化了,你可以在
<script>标签中加入type="module"属性 ,并且将项目运行在服务器上即可。
第一次接触 Webpack
上面的例子中,我们可以将 test.js 和 index.js 打包成一个文件 main.js ,然后在 index.html 引入 main.js。
📦 Webpack 可以帮我们生成这个 main.js 文件。
我们看一下怎么做。
首先使用 npm init -y 快速生成 package.json 。然后安装 webpack 和 webpack-cli 。
npm init -y
npm install webpack webpack-cli --save-dev --registry=https://registry.npm.taobao.org
--registry=https://registry.npm.taobao.org表示使用淘宝的镜像源,可以加速安装过程。
接着,
npx webpack index.js
便会生成 dist 目录及 main.js :
可以在 HTML 文件中引用 ./dist/main.js 。
npx会在项目中的node_modules目录中寻找webpack。
Webpack 除了可以打包 JS 文件,还可以打包 CSS 文件 、图片等。
配置文件
事实上,我们还可以指定打包生成的目录及文件名。这就用到 webpack 配置文件了。
我们在项目根目录下创建 webpack.config.js:
// webpack.config.js
const path = require('path')
module.exports = {
entry: './index.js', // 入口文件
output: {
filename: 'bundle.js', // 生成的文件名
path: path.resolve(__dirname, 'bundle') // 生成的目录
}
}
接着,
npx webpack
webpack 会根据 webpack.config.js 中的配置来进行打包,然后生成 bundle/bundle.js。
实际项目中,我们经常会在 package.json 中添加 scripts 脚本,如:
这样,我们便可以直接执行:
npm run build
效果相同。
一些补充
😶 第一点
上面的 webpack.config.js 中,entry 是一种简写:
// webpack.config.js
...
module.exports = {
entry: './index.js', // 入口文件 ⭐️
...
}
等同于:
// webpack.config.js
...
module.exports = {
entry: {
main: './index.js' // 入口文件 ⭐️
},
...
}
这种写法,在配置多入口文件时很有用,我们后面会介绍。
😯 第二点
webpack 打包有两种模式:production 和 development 。production 会对生成的代码进行压缩,development不会压缩。
默认是 production。你可以通过 mode 字段进行修改:
// webpack.config.js
...
module.exports = {
mode: 'development', // 默认是 production ⭐️
entry: {
main: './index.js'
},
...
}
😲 第三点
webpack 的默认配置文件是 webpack.config.js ,你也可以通过 webpack --config 指定配置文件。
Webpack 基础
webpack 默认是知道怎么打包 JS 文件的,但是其他类型的文件它就不知道了。
我们可以在配置文件中为各种文件类型指定 loader ,告诉 webpack 怎么打包不同的文件。
比如,使用 file-loader 来告诉 webpack 怎么打包图片:
// webpack.config.js
...
module.exports = {
...
output: {
filename: "bundle.js",
path: path.resolve(__dirname, 'bundle')
},
module: {
rules: [
{
test: /\.jpeg$/, // 正则匹配文件名以 .jpeg 结尾的文件 ⭐️
use: {
loader: 'file-loader' // 使用 file-loader ⭐️
}
}
]
}
}
file-loader 需要安装:
npm i file-loader --save-dev
我们 index.js 中引入图片:
// index.js
import avatar from './avatar.jpeg' // avatar.jpeg 是预先放在根目录下的一张图片
console.log('avatar:', avatar)
npm run build 之后,在 bundle 目录中,会生成一个文件名 b417eb0f1c79e606d72c000762bdd6b7.jpeg 图片。
控制台中会打印:
avatar: b417eb0f1c79e606d72c000762bdd6b7.jpeg
打包图片
file-loader 中可以使用 options 指定打包后的图片名。
// webpack.config.js
...
module.exports = {
...
module: {
rules: [
{
test: /\.jpeg$/,
use: {
loader: 'file-loader',
options: {
name: '[name]-[hash:7].[ext]' // 占位符 ⭐️
}
}
}
]
}
}
其中 [name] 代表图片的文件名,[hash:7] 代表 hash 值的前 7 位,[ext] 代表图片的扩展名。
[hash]代表整个项目的 hash ,只有整个项目中有文件发生变动, hash 值就会变。[contenthash]代表当前文件的 hash ,只有当前文件变了,contenthash 才会变。但是 contenthash 会与webpack.HotModuleReplacementPlugin()插件冲突,因此建议在 production 环境下使用 contenthash 。
npm run build 之后,在 bundle 目录中,会生成一个文件名 avatar-b417eb0.jpeg 图片。
除了 .jpeg ,还可以通过正则表达式匹配其他格式的图片:
// webpack.config.js
...
{
test: /\.(jpeg|jpg|png|gif)$/, // 匹配更多格式的图片 ⭐️
use: {
loader: 'file-loader',
options: {
name: '[name]-[hash:7].[ext]'
}
}
}
...
options 中除了 name ,还有 outputPath ,指定生成图片的路径。
// webpack.config.js
...
test: /\.(jpeg|jpg|png|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]-[hash:7].[ext]',
outputPath: 'images/' // 指定生成图片的路径 ⭐️
}
}
...
npm run build 之后,在 bundle 目录中,会生成一个 images 目录及 avatar-b417eb0.jpeg 图片。
除了 file-loader ,还有一个 url-loader 也可以用来打包图片。不同的是,使用 url-loader 会生成 Base64 格式的图片。你可以通过指定 limit 参数来限定阈值:小于此阈值则使用 Base64 ,否则使用图片文件的格式。
// webpack.config.js
...
{
test: /\.(jpeg|jpg|png|gif)$/,
use: {
loader: 'url-loader', // 使用 url-loader ⭐️
options: {
name: '[name]-[hash:7].[ext]',
outputPath: 'images/',
limit: 2048 // 指定生成Base64 图片的阈值 ⭐️
}
}
}
...
当然,使用 url-loader 也是需要安装的:
npm i url-loader --save-dev
推荐使用 url-loader 。
打包样式
打包 CSS 样式,需要用到两个 loader :style-loader 、 css-loader 。
// webpack.config.js
...
module.exports = {
...
module: {
rules: [
...
{
test: /\.css$/, // 正则匹配以 .css 结尾的文件 ⭐️
use: ['style-loader', 'css-loader'] // ⭐️
}
]
}
}
当然,这两个 loader 是需要我们安装的:
npm i style-loader css-loader -D
接着,我们创建两个 CSS 文件:reset.css 、 index.css ,并在 index.js 中引入 index.css ,如下图:
npm run build 之后,webpack 顺利打包。当我们在浏览器中查看元素时,会发现 <head> 标签下,多了一个 <style> 标签:
说明 CSS 样式已经成功导入了项目中。
其中,use: ['style-loader', 'css-loader'] 的顺序不能颠倒,webpack 打包的时候,是按照逆序来处理 CSS 文件的:先使用 css-loader 来处理 CSS 中的 @import 语法,再用 style-loader 将 CSS 样式插入 <style> 标签中。
🤖 css 预处理器
我们还可以使用 scss 这样的 css 预处理器。
// webpack.config.js
...
rules: [
...
{
test: /\.scss$/, // 正则匹配以 .scss 结尾的文件 ⭐️
use: ['style-loader', 'css-loader', 'sass-loader'] // ⭐️
}
]
...
还需要安装 sass-loader 、 node-sass :
npm install sass-loader node-sass --save-dev
这样,在 JS 中就可以直接引入 scss 文件了。
🔌 自动添加厂商前缀
某些特殊的 css3 属性可能需要加浏览器厂商前缀,我们可以通过 postcss-loader 中的 autoprefixer 插件自动帮我们加上:
// webpack.config.js
...
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader', // ⭐️
options: {
ident: 'postcss',
plugins: [
require('autoprefixer') // ⭐️
]
}
},
'sass-loader'
]
}
...
webpack 需要 options 中有一个唯一的标识符 —— identifier (ident) ,ident 可以随意命名。
为此我们需要安装 postcss-loader 和 autoprefixer :
npm i postcss-loader autoprefixer -D
比如我们需要指定 <input> 标签中 placeholder 的颜色,则在 css 中有:
::placeholder {
color: deepskyblue;
}
npm run build 之后,<style> 标签中会有:
::-webkit-input-placeholder{color:deepskyblue}
::-moz-placeholder{color:deepskyblue}
:-ms-input-placeholder{color:deepskyblue}
::-ms-input-placeholder{color:deepskyblue}
::placeholder{color:deepskyblue}
并不是所有的 css3 中的属性都会被加上厂商前缀,如:
transform,border-radius、transition等。css3 经过这么多年的发展,浏览器对其支持已经很好了,所以 autoprefixer 插件并不会给这些属性加上前缀。autoprefixer 会根据 Can I Use 来决定是否加上前缀。
🐖 注: css-loader 有一个 importLoaders 的选项,当你在 scss 文件中 @import 另一个 scss 文件,你希望被 import 的那个文件也能被之前的 loader 处理,则 importLoaders 可以帮到你:
module.exports = {
module: {
rules: [
{
test: /\.scss$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
// 0 => no loaders (default);
// 1 => postcss-loader;
// 2 => postcss-loader, sass-loader
}
},
'postcss-loader',
'sass-loader',
]
}
]
}
}
🧩 CSS 模块化
不同的 CSS 文件中,可能会产生冲突。
比如在 a.css 中设置.side class 的颜色为 red 🔴,而在 b.css 中设置.side class 的颜色为 blue 🔵。接着在 a.js 中引入 a.css,在 b.js 中引入 b.css 。这时冲突就会发生。根据引入位置的顺序不同, .side class 最终的颜色会为 red 或者 blue 中的一种。而不是我们最初设想的,在不同的 JS 文件中会有不同的颜色。
解决的办法就是 CSS 模块化,只需要添加 modules: true 即可:
module.exports = {
module: {
rules: [
{
test: /\.scss$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: true // ⭐️
}
},
'postcss-loader',
'sass-loader',
]
}
]
}
}
引入时,不再像之前那样 import './a.scss' ,而是 import style from './a.scss' 。webpack 会将 css 样式封装成一个对象,使用这个 css 文件中的 .side class 时,变成 style.side。如:
import style from './a.scss'
const wrap = document.getElementById('wrap')
wrap.classList.add(style.side)
npm run build 之后,会发现生成的 DOM 节点为:
<div id="wrap" class="_6VOXwYSkDQv8fWDLvRRIk"></div>
💯 打包字体文件
得益于 css3 中的 @font-face ,使得我们可以在网页中使用我们喜欢的任何字体。还有一些专门的网站(如 iconfont),可以将一些图标制作成字体文件。
如果你使用了 @font-face ,那么你就需要指定 webpack 打包这些字体文件了。同打包图片一样,打包字体文件也使用 file-loader :
// webpack.config.js
...
{
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
},
...
plugins
插件可以在 webpack 打包的某个过程中,帮我们做一些事情。
接下来,让我们遵循规范,将打包生成的目录改为 dist 。
我们通过 npm run build 生成的 dist 目录下没有 html 文件,只有一个 bundle.js 文件(可能还有你引入的一些图片和字体文件)。
🥜 HtmlWebpackPlugin
事实上,我们可以通过 webpack 中的 HtmlWebpackPlugin 插件帮我们自动生成一个 html 文件。
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin'); // ⭐️
module.exports = {
...
entry: {
...
},
output: {
...
},
module: {
...
},
plugins: [new HtmlWebpackPlugin()] // ⭐️
}
当然,首先要安装:
npm install --save-dev html-webpack-plugin
然后 npm run build 之后,我们会发现在 dist 目录下有一个 index.html 文件:
<!-- dist/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
</head>
<body>
<script type="text/javascript" src="bundle.js"></script></body>
</html>
并且,这个 index.html 文件中还自动帮我们引入了 bundle.js 。
这就是 HtmlWebpackPlugin 插件的作用。
我们还可以指定一个模板文件:
// webpack.config.js
module.exports = {
...
plugins: [new HtmlWebpackPlugin({
template: "./index.html" // ⭐️
})]
}
🧶 AddAssetHtmlWebpackPlugin
这个插件可以帮我们在生成的 html 文件中添加一些静态资源。
先安装
npm i add-asset-html-webpack-plugin --save-dev
接着:
// webpack.config.js
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin') // ⭐️
...
plugins: [
new HtmlWebpackPlugin({
template: "./index.html"
}),
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, 'assets/js/your-plugin.js') // ⭐️
})
]
...
这样,在生成的 index.html 文件中会自动帮你引入 your-plugin.js 。
🧹 CleanWebpackPlugin
每一次 npm run build 之后,都会在 dist 目录下生成一些文件。时间长了,可能会产生很多垃圾文件。我们希望每次 npm run build 的时候,webpack 都能自动帮我们清除这些文件。
这就用到了 CleanWebpackPlugin 。
先安装:
npm install clean-webpack-plugin --save-dev
接着:
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// ⭐️
...
plugins: [
new HtmlWebpackPlugin({
template: "./index.html"
}),
new CleanWebpackPlugin() // ⭐️
]
...
再说 entry 和 output
webpack 可以指定多个入口文件:
// webpack.config.js
...
entry: {
bundle: './index.js', // ⭐️
other: './other.js' // ⭐️
},
output: {
filename: "[name].[hash:7].js", // 占位符 ⭐️
path: path.resolve(__dirname, 'dist')
},
...
通过占位符 [name].[hash:7].js, webpack 会在 dist 目录下生成两个文件:
- bundle.0fbc091.js
- other.0fad032.js
并且通过 HtmlWebpackPlugin 自动生成的 dist/index.html 中也引用了这两个文件:
<!-- dist/index.html -->
<html>
<head>
<meta charset="UTF-8">
<title>HTML 模板</title>
</head>
<body>
<h1>Hello world</h1>
<script type="text/javascript" src="bundle.0fbc091.js"></script>
<script type="text/javascript" src="other.0fad032.js"></script>
</body>
</html>
output 还有一个 publicPath 属性,可以指定资源的公共域名:
// webpack.config.js
...
output: {
publicPath: "http://cdn.yourdomain.com", // ⭐️
filename: "[name].[hash:7].js",
path: path.resolve(__dirname, 'dist')
},
...
这样生成的 dist/index.html :
<!-- dist/index.html -->
<html>
<head>
<meta charset="UTF-8">
<title>HTML 模板</title>
</head>
<body>
<h1>Hello world</h1>
<script type="text/javascript" src="http://cdn.yourdomain.com/bundle.5fc8df1.js"></script>
<script type="text/javascript" src="http://cdn.yourdomain.com/other.5fc8df1.js"></script>
</body>
</html>
为了保证下面的示例正常运行,我们将 publicPath 属性删除。
source-map
如果我们在入口文件 index.js 中写错了代码:
// index.js
console.logo('hello world') // console.log 错写成了 console.logo
npm run build 之后,在浏览器打开 dist/index.html ,报错:
控制台中指示错误的地方为 bundle.83ffb3f.js 中的第 1 行。然而,这个文件是 webpack 打包生成的文件,我们希望控制台告诉我们源文件中的错误。
可以配置 devtool 字段:
// webpack.config.js
...
mode: 'production',
devtool: "source-map", // ⭐️
entry: {
bundle: './index.js'
},
output: {
...
npm run build 之后,就可以正确的显示错误来源了。
🐖 注:
- 当
mode: 'development'时建议使用devtool: 'eval-cheap-module-source-map' - 当
mode: 'production'时如有需要,建议使用devtool: 'cheap-module-source-map'
devServer
使用 WebpackDevServer 可以开启本地服务器。当我们更改代码时, 还可以自动重新打包、自动打开浏览器、自动刷新。
只需做一下更改:
// webpack.config.js
...
module.exports = {
...
entry: {
...
},
output: {
...
},
module: {
...
},
plugins: [
...
],
devServer: { // ⭐️
contentBase: './dist',
open: true,
port: 8000
}
}
其中 contentBase 指定本地服务器的根目录,open 指定是否自动打开浏览器,port 指定端口号,默认是 8080 。
然后安装:
npm install webpack-dev-server --save-dev
接着,在 package.json 中添加一个 start 脚本:
...
"scripts": {
"build": "webpack",
"start": "webpack-dev-server", // ⭐️
"test": "echo \"Error: no test specified\" && exit 1"
},
...
然后 npm start 便会开启本地服务器,并自动打开浏览器。
只有 start 和 test 可以不用
npm run。
在编辑器中更改代码,浏览器会自动刷新。
devServer 还支持代理:
// webpack.config.js
module.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
}
这样,发送到 /api/users 的请求将会被代理到 http://localhost:3000/api/users 。
HRM 热模块更替
HRM 是 Hot Module Replacement (热模块更替)的简写。
devServer 的自动刷新功能很方便,但有时候我们不想自动刷新。比如,我们填写了一大堆表单,这时候自动刷新会让表单中的内容全部消失。
HRM 可以解决这一问题。
// webpack.config.js
...
const webpack = require('webpack') // ⭐️
module.exports = {
...
entry: {
...
},
output: {
...
},
module: {
...
},
plugins: [
...
new webpack.HotModuleReplacementPlugin() // ⭐️
],
devServer: {
contentBase: './dist',
open: true,
hot: true, // ⭐️
hotOnly: true // ⭐️
}
}
HotModuleReplacementPlugin 是 webpack 自带的一个插件。hot: true 代表开启 HRM ,hotOnly: true 代表只进行 HRM ,不自动刷新。
这样,当 css 样式更改时,不刷新也能看到更改后的效果。
要想在 js 文件中实现 HRM ,可以使用 module.hot 属性:
// index.js
import Library from './library.js'
...
if (module.hot) { // 检查是否开启了 hot 功能
module.hot.accept('./library.js', function() {
// 当 library.js 发生更改时,做一些事情
});
}
之所以更改 css 文件,不用写上面这样一段代码,是因为 css-loader 帮我们写了。vue 项目中, vue-loader 也做了同样的事。
Babel
使用 Babel ,可以让我们在项目中使用 ES6 的语法。
Babel 会将 ES6 编译成 ES5 。
值得注意的是,使用 Babel ,有两种方案:
- polyfill
- runtime
二者的区别在于:polyfill 方式生成的 ES5 代码会直接作用于全局,可能会产生污染。runtime 方式生成的 ES5 则不会。
通常,我们使用 polyfill 方式就可以了,它适合于业务代码。但当你在开发第三方库的时候,使用 runtime 更好。
👹 polyfill 方式
安装:
npm install --save-dev babel-loader @babel/core @babel/preset-env
npm install --save @babel/polyfill
配置:
// webpack.config.js
...
module.exports = {
...
module: {
rules: [
...
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
['@babel/preset-env', {useBuiltIns: 'usage', corejs: 3}]
]
}
}
]
},
...
}
在入口文件中使用:
// index.js
import '@babel/polyfill' // ⭐️
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve('success')
}, 2000)
})
🐖注: 由于配置了
"useBuiltIns": "usage",所以在 index.js 中也可以不用import '@babel/polyfill'。此外,"useBuiltIns": "usage"还会使 babel 只生成项目中使用到的 ES6 API。
npm run build 之后,生成的 bundle.e8d5b05.js 中有:
// bundle.e8d5b05.js
...
var promise = new Promise(function (resolve) {
setTimeout(function () {
resolve('success');
}, 2000);
});
...
可以发现,其中的 const 变成了 var ,箭头函数变成了 function ,Promise 被重新实现了:
// bundle.e8d5b05.js
...
if (!USE_NATIVE) {
$Promise = function Promise(executor) {
...
Internal.call(this);
...
};
Internal = function Promise(executor) {
...
};
Internal.prototype = __webpack_require__(..., {
then: function then(onFulfilled, onRejected) { // ⭐️ then
...
},
'catch': function (onRejected) { // ⭐️ catch
...
}
});
...
}
...
$export(..., {
reject: function reject(r) { // ⭐️ reject
...
}
});
$export(..., {
resolve: function resolve(x) { // ⭐️ resolve
...
}
});
$export(..., {
all: function all(iterable) { // ⭐️ all
...
},
race: function race(iterable) { // ⭐️ race
...
}
});
Babel 成功的将 ES6 转成了 ES5 。
👺 runtime 方式
安装:
npm install --save-dev babel-loader @babel/core @babel/plugin-transform-runtime
npm install --save @babel/runtime-corejs2
配置:
// webpack.config.js
...
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
/*presets: [
['@babel/preset-env', {useBuiltIns: 'usage', corejs: 3}]
]*/
'plugins': [ // ⭐️
[
'@babel/plugin-transform-runtime',
{
'corejs': 2
}
]
]
}
}
...
在入口文件中使用:
// index.js
// import '@babel/polyfill' // ⭐️ 此处无需再引入 @babel/polyfill
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve('success')
}, 2000)
})
npm run build 之后,生成的 bundle.345b70e.js 中有
// bundle.345b70e.js
...
// import '@babel/polyfill'
const promise = new _babel_runtime_corejs2_core_js_promise__WEBPACK_IMPORTED_MODULE_0___default.a(resolve => {
setTimeout(() => {
resolve('success');
}, 2000);
});
...
🐖注:
上面两种方式都可以通过创建 .babelrc 来配置。在项目根目录下,创建 .babelrc 文件:
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
这样,webpack.config.js 中只需:
// webpack.config.js
...
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}
...
😈 打包 React
Babel 还可以编译 React 的 JSX 语法。
首先,安装:
npm install --save-dev babel-loader @babel/core @babel/preset-env
npm install --save-dev @babel/preset-react
npm install --save @babel/polyfill
npm install --save react react-dom
然后, 更改 html 模板文件 index.html :
<!-- index.html -->
<html>
<head>
<meta charset="UTF-8">
<title>HTML 模板</title>
</head>
<body>
<div id="app"></div> <!-- ⭐️ -->
</body>
</html>
创建 index.jsx 文件(注意,后缀名为 .jsx):
// index.jsx
// import '@babel/polyfill'
import React, {Component} from 'react'
import ReactDom from 'react-dom'
class App extends Component {
render() {
return <h1>你好,世界!</h1>
}
}
ReactDom.render(<App/>, document.getElementById('app'))
其实后缀名为 .js 也可以,只不过这样的话有些编辑器会不识别 jsx 语法。但尽管这样,仍然能正常打包编译。
配置 webpack.config.js :
// webpack.config.js
...
module.exports = {
...
entry: {
bundle: './index.jsx' // ⭐️
},
...
module: {
rules: [
...
{
test: /\.jsx$/, // ⭐️
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
...
}
配置 .babelrc :
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
"@babel/preset-react"
]
}
npm start 之后,打开浏览器:
成功打包 react 。
Webpack 进阶
Tree Shaking
开始讲解 Tree Shaking 之前,先设置:mode: 'production' 。
假设我们有一个 test.js 文件:
// test.js
function test1() {
console.log('test1')
}
function test2() {
console.log('test2')
}
export {
test1,
test2
}
在 index.js 中引入:
// index.js
import { test1 } from './test'
test1()
注意,此处我们只引入了 test1 函数,test2 没有引入。
npm run build 之后生成的 bundle.4652aa7.js 中只会包含 test1 的代码。
这就是 Tree Shaking 。它可以帮我们自动删去没有引用的模块。
🐖 注:
- 只有在
mode: 'production'时,才会真正 Tree Shaking 。若mode: 'development',也可以配置 Tree Shaking ,但仍然不会删去未引用的模块。 - 只有使用 ES6 的模块化规范,才能 Tree Shaking 。 使用 Common JS 规范会不起作用。
development 和 production
实际开发中,开发环境和生产环境的 webpack 配置通常不一样。
我们可以将开发环境和生产环境的配置分别放在 webpack.development.js 和 webpack.production.js 中,并且将二者之间的公共代码抽离出来,放在 webpack.common.js 中。
首先安装 webpack-merge 插件,用于合并两个 webpack 配置文件:
npm install webpack-merge --save-dev
再创建一个 build 目录,新建上述三个文件:
😺 webpack.common.js
// webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: {
main: './index.js' // ⭐️ 将打包生成的文件名由 bundle 改为 main
},
output: {
filename: '[name].[hash:7].js',
path: path.resolve(__dirname, '../', 'dist') // ⭐️ 注意,生成的 dist 目录 应与 build 目录平级
},
module: {
rules: [
{
test: /\.(jpeg|jpg|png|gif)$/,
exclude: /node_modules/,
use: {
loader: 'url-loader',
options: {
name: '[name]-[hash:7].[ext]',
outputPath: 'images/',
limit: 2048
}
}
},
{
test: /\.(eot|ttf|svg)$/,
exclude: /node_modules/,
use: {
loader: 'file-loader'
}
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: true
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
require('autoprefixer')
]
}
},
'sass-loader'
]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
}),
new CleanWebpackPlugin()
]
}
😸 webpack.development.js
// webpack.development.js
const webpack = require('webpack')
const merge = require('webpack-merge') // ⭐️
const common = require('./webpack.common')
const development = {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
contentBase: './dist',
// open: true,
hot: true
// hotOnly: true
}
}
module.exports = merge(common, development) // ⭐️ merge 两个 webpack 配置
😹 webpack.production.js
// webpack.production.js
const merge = require('webpack-merge') // ⭐️
const common = require('./webpack.common')
const production = {
mode: 'production',
devtool: 'cheap-module-source-map'
}
module.exports = merge(common, production) // ⭐️ merge 两个 webpack 配置
之后更改 package.json 中的 scripts :
...
"scripts": {
"build": "webpack --config ./build/webpack.production.js",
"serve": "webpack-dev-server --config ./build/webpack.development.js"
},
...
npm run build 用于生产环境,npm run serve 用于开发环境。
代码分割
webpack 默认会将所有的模块打包到一个文件中,如 main.c62b431.js 。
有时,某些第三方模块变动较少,如:jQuery、lodash 。我们可以将这些模块打包到单独的文件中。
模块导入又分为同步和异步。
import _ from 'lodash' 为同步模块,import('lodash').then(...) 为异步模块。
如,在 index.js 中:
// index.js
import {join} from 'lodash' // ⭐️ 同步模块
console.log(join(['hello', 'world'], '_'))
async function getNow() {
// ⭐️ 异步模块
const {default: $} = await import(/* webpackChunkName: "jquery" */'jquery')
return $.now()
}
getNow().then(now => {
console.log('now: ', now)
})
其中,/* webpackChunkName: "jquery" */ 称为魔法注释,它可以指定打包生成的模块的文件名。
我们可以看到,只有当 getNow 函数执行时, jquery 才会被加载。这种加载方式也称为 “ 懒加载 ”。
在 webpack.common.js 中作如下配置:
//webpack.common.js
...
module.exports = {
entry: {
main: './index.js'
},
...
plugins: [
...
],
optimization: {
splitChunks: {
chunks: 'all', // all 代表同步模块和异步模块都分割
minSize: 30000, // ⭐️ minSize 表示最小的文件大小,30000 为默认值,超过此值才进行分割
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的模块
automaticNamePrefix: 'vendors',
priority: -10
},
default: {
reuseExistingChunk: true,
automaticNamePrefix: 'default',
priority: -20
}
}
}
}
}
npm run build 之后, dist 目录下会生成:
其中,main.77fd3cc.js 是 index.js 打包生成的, vendors~jquery.77fd3cc.js 是异步加载的 jquery 模块打包生成的, 其余所有的同步模块都会被打包到 vendors~main.77fd3cc.js 中。
打包分析
安装一个插件:
npm install --save-dev webpack-bundle-analyzer
在 build 目录下新建一个 webpack.analyze.js :
// webpack.analyze.js
const merge = require('webpack-merge')
const production = require('./webpack.production')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const analyze = {
plugins: [
new BundleAnalyzerPlugin()
]
}
module.exports = merge(production, analyze)
在 package.json 中添加一个脚本:
"scripts": {
...
"analyze:build": "webpack --config ./build/webpack.analyze.js"
}
npm run analyze:build 之后,自动打开浏览器:
通过这个工具可以分析出各个包的大小。比如,从上图可以看出,lodash 和 jquery 体积过大,从而可以进一步优化。
异步模块预加载
有些模块,我们不希望在页面刚加载的时候被下载。
比如点击登录按钮,弹出一个登录弹窗,弹窗部分的 JS 代码就可以被异步加载。为实现这一点,使用前面说过的异步 import('...') 就可以了。但是如果等到点击登录按钮的时候再去下载弹窗的代码,加载时间就会很慢,用户体验不好。
这时就会用到这一个魔法注释: /* webpackPrefetch: true */ 。它会在页面初始加载完成后,自动加载异步模块。
以异步预加载 jquery 为例:
// index.js
async function getNow() {
// ⭐️
const {default: $} = await import(/* webpackPrefetch: true */ /* webpackChunkName: "jquery" */'jquery')
return $.now()
}
// ⭐️ 点击页面后再去加载 jquery
document.body.addEventListener('click', function () {
getNow().then(now => {
console.log('now: ', now)
})
})
npm run serve 之后,打开浏览器控制台:
页面性能优化的一个原则是:只在需要用到某一块代码时才去加载它。 异步 import('...') 使得这一原则得以实现,而 /* webpackPrefetch: true */ 又保证了用户体验。
还有一个魔法注释:
/* webpackPreload: true */。但是它会使异步模块与同步模块一起加载。不推荐。
Prefetch 在某些浏览器中不支持。
CSS 代码分割
使用 MiniCssExtractPlugin 可以将 CSS 代码单独放在一个文件中,而不是插入 <style> 标签里。
首先安装:
npm install --save-dev mini-css-extract-plugin
npm install --save-dev optimize-css-assets-webpack-plugin
style.scss:
body {
background: greenyellow;
}
index.js:
import './style.scss';
webpack.common.js:
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
...
module: {
rules: [
...
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader, // ⭐️ 删除 style-loader ,使用 MiniCssExtractPlugin.loader
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: true
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
require('autoprefixer')
]
}
},
'sass-loader'
]
},
...
]
},
plugins: [
...
new MiniCssExtractPlugin() // ⭐️ 使用 MiniCssExtractPlugin 插件分割 CSS 代码
],
optimization: {
...
minimizer: [new OptimizeCSSAssetsPlugin({})] // ⭐️ 使用 OptimizeCSSAssetsPlugin 插件压缩 CSS 代码
}
}
在 webpack 中使用 jQuery 插件
jQuery 的插件比较丰富,但直接在 webpack 中使用,往往会报错。
原因在于,早期的 jQuery 插件并不支持模块化,而是直接使用挂载在 window 对象下的 $ 对象,如:
$('#el').somePlugin()
但使用 webpack 打包时,window 对象下并不会有一个 $ 。故报错。
🙈 可以使用 ProvidePlugin
// webpack.common.js
const webpack = require('webpack')
...
plugins: [
...
new webpack.ProvidePlugin({ // ⭐️
$: 'jquery', // ⭐️ $ 相当于 jquery
jQuery: 'jquery',
now: ['jquery', 'now'], // ⭐️ now 相当于 jquery.now
'window.jQuery': 'jquery',
_map: ['lodash', 'map'] // ⭐️ _map 相当于 lodash.map
})
],
...
这样,在 index.js 就可以直接使用 $ ,甚至不用引入 jquery :
// index.js
// import $ from 'jquery' // ⭐️ 无需引入 jquery
$('#app').text('骄傲的使用 jQuery')
console.log(now())
实际上,ProvidePlugin 会自动帮你引入 jquery 。
🙉 还可以使用 imports-loader
imports-loader 允许你使用那些依赖于特定全局变量的模块。
安装:
npm install imports-loader
比如你有一个 jquery 插件叫 cool-plugin.js :
// cool-plugin.js
$("img").cool()
接着,我们在入口文件 index.js 中引入 cool-plugin.js :
// index.js
require("imports-loader?$=jquery!./cool-plugin.js");
实际上,webpack 会在 cool-plugin.js 前面插入 var $ = require("jquery") 。
也可以传多个值,使用 , 分隔:
// index.js
require("imports-loader?$=jquery,angular,config=>{size:50}!./file.js")
你也可以在 webpack.config.js 中配置,这样就不用手动 require("imports-loader?...) 了:
// ./webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: require.resolve("some-module"),
use: "imports-loader?this=>window" // ⭐️ 引入 some-module 时,自动将 this 指向 window
}
]
}
};
🙊 还有一个最干脆的方法
引入旧的第三方库,最鲁莽干脆的办法是直接在 index.html 模板文件中引入:
<!-- index.html -->
<html>
<head>
<meta charset="UTF-8">
<title>HTML 模板</title>
</head>
<body>
<div id="app"></div>
<script src="http://some.path.to.plugin"></script> <!-- ⭐️ -->
</body>
</html>
Webpack 高级
打包分享自己的库
入口文件:
// index.js
import * as myAwesomeLibrary from './myAwesomeLibrary'
import $ from 'jquery'
console.log($.now())
export default myAwesomeLibrary
webpack 配置:
// webpack.config.js
...
module.exports = {
externals: ['jquery'], // ⭐️
entry: {
main: './index.js'
},
output: {
filename: 'myAwesomeLibrary.js',
path: path.resolve(__dirname, '../', 'dist'),
libraryTarget: 'umd', // ⭐️
library: 'awesome' // ⭐️
},
...
}
其中 externals: ['jquery'] 表示不打包 jquery。外界在使用本模块的时候,需要额外引入 jquery 。
libraryTarget: 'umd' 表示支持 ES6 模块化、CommonJS 规范、AMD 等方式引入本模块。
library: 'awesome' 表示会在 window 下挂载一个全局变量:window.awesome 。即你可以通过<script src="./dist/myAwesomeLibrary.js"></script> 方式引入本库。
libraryTarget 和 library 可以相互配合:
libraryTarget: 'this'表示this.awesomelibraryTarget: 'window'表示window.awesomelibraryTarget: 'global'表示global.awesome
在 package.json 中:
{
"name": "webpack-test",
"version": "1.0.0",
"main": "./dist/myAwesomeLibrary.js", // ⭐️
...
}
之后就可以将项目发布到 npm 上去了。
PWA
PWA 可以实现离线上网。当用户第一次访问过你的网站,第二次访问时,即使断网,也能访问。
只有 production 环境下才需要实现 PWA 。
安装:
npm install workbox-webpack-plugin --save-dev
在 webpack.production.js 中:
// webpack.production.js
...
const WorkBoxPlugin = require('workbox-webpack-plugin') // ⭐️
const production = {
mode: 'production',
devtool: 'cheap-module-source-map',
plugins: [
new WorkBoxPlugin.GenerateSW({ // ⭐️
clientsClaim: true,
skipWaiting: true
})
]
}
module.exports = merge(common, production)
在入口文件 index.js 中:
// index.js
import $ from 'jquery'
if ('serviceWorker' in navigator) { // 检查浏览器是否支持 serviceWorker
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('serviceWorker 注册成功')
}).catch(error => {
console.log('serviceWorker 注册失败')
})
})
}
$('#app').html('<h1>PWA 成功运行</h1>')
npm run build 之后:
为了演示,我们使用 http-server ,开启一个本地服务器:
npm install http-server --save-dev
npx http-server dist
代开浏览器,输入 http://127.0.0.1:8080/ ,页面正常运行。然后,在终端按下 Ctrl + c ,终止 http-server 进程,刷新浏览器,发现页面依然正常运行。
这就实现了 PWA 。
为了避免后面开发出现问题,我们先将本地相应网站的 serviceworker 注销:
打包 TypeScript
避免冲突,我们新建一个项目。
首先安装:
npm install ts-loader typescript --save-dev
配置:
// webpack.config.js
const path = require('path')
module.exports = {
mode: "production",
entry: './index.ts',
output: {
filename: "bundle.js",
path: path.resolve(__dirname, 'bundle')
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}
}
创建入口文件:
// index.ts
let user: string = 'zhangsan'
console.log(user)
在根目录下创建 tsconfig.json :
{
"compilerOptions": {
"module": "es6",
"target": "es5",
"allowJs": true,
"sourceMap": true
},
"exclude": [
"node_modules"
]
}
npx webpack 打包之后,会在 bundle 目录下创建 bundle.js 。
打包完成。
如果需要在 typescript 中引入第三方库,需要安装该第三方库对应的类型描述文件,如引入 lodash :
首先需要安装 lodash 对应的类型文件:
npm install @types/lodash --save-dev
然后
// index.ts
import * as _ from 'lodash'
console.log(_.join(['hello', 'world'], '-'))
之后 typescript 就可以对 lodash 进行类型约束了。
你可以通过 TypeSearch 搜索 typescript 支持哪些第三方库。
本地开发代理
开发环境下,devServer 可以配置代理:
// webpack.config.js
...
devServer: {
contentBase: './dist',
// open: true,
hot: true,
hotOnly: true,
proxy: {
'/api': {
target: "https://other-server.example.com",
secure: false,
changeOrigin: true,
headers:{
// ...
}
}
}
}
...
则对 /api 的 ajax 请求会被转发到 'https://other-server.example.com'。
更多配置,参考 devServer.proxy 和 http-proxy options 。
devServer 实现单页面路由
单页面路由的功能,在 React、Vue 之类的框架中已经帮我们集成了。
但它们集成的路由功能是代码层面的,并不能阻止浏览器对路由的默认实现。比如你通过 VueRouter 配置:/home 对应 Home 组件,/about 对应 About 组件。这是代码层面的。但是浏览器的默认行为是到服务器上寻找/home、/about 页面,当然找不到,于是就会报 404 。
然而当你使用 React、Vue 的脚手架工具创建一个项目的时候,在本地开发环境下似乎从没遇到过服务器路由方面的问题,这是因为这些脚手架已经帮你默认处理好了 devServer 上的相应配置。
我们来看一下它们是怎么配置的。
非常简单,只要加上 historyApiFallback 即可
// webpack.config.js
devServer: {
contentBase: './dist',
// open: true,
hot: true,
hotOnly: true,
historyApiFallback: true // ⭐️
}
还可以传入一个对象
// webpack.config.js
...
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: '/views/landing.html' },
{ from: /^\/subpage/, to: '/views/subpage.html' },
{ from: /./, to: '/views/404.html' }
]
}
...
更多选项和信息,查看 connect-history-api-fallback 。
生产环境下,需要后端配合,更改 Apache 或者 Nginx 相应配置。
ESLint
ESLint 用于规范代码。
安装
npm install eslint --save-dev
快速生成 eslint 配置文件
npx eslint --init
根据你选择的不同,根目录下会生成不同后缀名的配置文件:
// .eslintrc.js
module.exports = {
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {}
};
通过 npx eslint src/ (或 npx eslint index.js) 便可以检查 src 目录下的文件是否符合代码规范。
这里我们更改一下 eslint 的解析器为 babel-eslint :
// .eslintrc.js
module.exports = {
...
"parser": "babel-eslint", // ⭐️
"rules": {
"semi": "error"
...
}
}
当然,要先安装:
npm install babel-eslint --save-dev
到目前为止,eslint 与 webpack 都没有多大关系,不使用 webpack ,eslint 也可以正常使用,甚至配合 vs code 或者 webstorm 这样的 IDE ,可以直接在编辑器中发现代码中的错误。
但配合 webpack ,eslint 可以更方便。
安装 loader :
npm install eslint-loader --save-dev
配置:
// webpack.config.js
...
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader']
}
...
这样,当你 npm run serve 时,便会自动进行 eslint 的检查。
你还可以在 devServer 中添加 overlay: true,让错误在浏览器中显示:
// webpack.config.js
devServer: {
overlay: true, // ⭐️
contentBase: './dist',
// open: true,
hot: true,
...
}
多页面打包
比如我们生成两个页面:index.html 和 about.html 。
则 webpack 配置:
// webpack.config.js
...
module.exports = {
entry: {
index: './index.js', // ⭐️ index 页面的入口文件
about: './about.js' // ⭐️ about 页面的入口文件
},
...
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
filename: "index.html", // ⭐️ 将生成的 html 文件名设为 index.html
chunks: ['vendors', 'default', 'index'] // ⭐️ chunks 配置需要在 index.html 引入的 js
}),
new HtmlWebpackPlugin({
template: './index.html',
filename: "about.html", // ⭐️ 将生成的 html 文件名设为 about.html
chunks: ['vendors', 'default', 'about'] // ⭐️ chunks 配置需要在 about.html 引入的 js
}),
...
],
optimization: {
splitChunks: {
...
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 目录下的模块
name: 'vendors', // ⭐️ chunk 的名字为 vendors
priority: -10
},
default: {
reuseExistingChunk: true,
name: 'default', // ⭐️ chunk 的名字为 default
priority: -20
}
}
},
minimizer: [new OptimizeCSSAssetsPlugin({})]
}
}
webpack 性能优化
当我们的项目越来越大,webpack 打包的速度就会显得重要。
这里有一些提高 webpack 打包速度的方法。
- 及时更新 webpack 版本
- 少用一些可有可无的 loader
- 合理添加
exclude: /node_modules/ - 使用官方推荐的或者社区认可的 loader 和 plugin
- 使用 DllPlugin 插件 ⭐️
- 合理使用 sourceMap
- 合理的使用 resolve
大型项目中,DllPlugin 配合 DllReferencePlugin 提高打包速度用的比较多。但是内容较多,我写烦了,不想说了。请参考其他资源吧。
关于 resolve ,补充如下
resolve.extensions
webpack 中还有一个 resolve.extensions 属性,用于指定默认的文件后缀名:
// webpack.config.js
...
entry: {
...
},
output: {
...
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.vue'] // ⭐️
},
...
这样在 index.js 中就可以
import Test from './test' // ⭐️ 无需加上后缀 .js
resolve.alias
设置别名
// webpack.config.js
...
resolve: {
extensions: ['.js', '.jsx', '.ts', '.vue'],
alias: {
'@': path.resolve(__dirname, 'src/'),
'components': path.resolve(__dirname, 'src/components')
}
},
...
这样在引用的时候,就可以直接使用 @ 和 components
// index.js
import Home from 'components/home/home.vue' // ⭐️
...
终于写完了。😑