问:这篇文章适合哪些人?
答:适合没接触过Webpack或者了解不全面的人。
问:这篇文章的目录怎么安排的?
答:先介绍背景,由背景引入Webpack的概念,进一步介绍Webpack基础、核心和一些常用配置案例、优化手段,Webpack的plugin和loader确实非常多,短短2w多字还只是覆盖其中一小部分。
问:这篇文章的出处?
答:此篇文章知识来自付费视频(链接在文章末尾),文章由自己独立撰写,已获得讲师授权并首发于掘金。
下一篇:从今天开始,学习Webpack,减少对脚手架的依赖(下)
如果你觉得写的不错,请给我点一个star,原博客地址:原文地址
Webpack
注意,本篇博客 Webpack 版本是4.0+,请确保你安装了Node.js最新版本。
Webpack 的核心概念是一个 模块打包工具,它的主要目标是将js
文件打包在一起,打包后的文件用于在浏览器中使用,但它也能胜任 转换(transform
) 、打包(bundle
) 或 包裹(package
) 任何其他资源。
追本溯源
在学习 Webpack 之前,我们有必要来了解一下前端领域的开发历程,只有明白了这些开发历程,才能更加清楚 Webpack 是怎么应运而生的,又能给我们解决什么样的问题。
面向过程开发
特征: 一锅乱炖
在早期 js
能力还非常有限的时候,我们通过面向过程的方式把代码写在同一个.js
文件中,一个面向过程的开发模式可能如下所示:
<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./index.js"></script>
复制代码
// index.js代码
var root = document.getElementById('root');
// header模块
var header = document.createElement('div');
header.innerText = 'header';
root.appendChild(header);
// sidebar模块
var sidebar = document.createElement('div');
sidebar.innerText = 'sidebar';
root.appendChild(sidebar);
// content模块
var content = document.createElement('div');
content.innerText = 'content';
root.appendChild(content);
复制代码
面向对象开发
特征: 面向对象开发模式便于代码维护,深入人心。
随着 js
的不断发展,它所能解决的问题也越来越多,如果再像面向过程那样把所有代码写在同一个.js
文件中,那么代码将变得非常难以理解和维护,此时面向对象开发模式便出现了,一个面向对象开发模式可能如下所示:
在index.html
中引入不同的模块:
<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./src/header.js"></script>
<script src="./src/sidebar.js"></script>
<script src="./src/content.js"></script>
<script src="./index.js"></script>
复制代码
// header.js代码
function Header() {
var header = document.createElement('div');
header.innerText = 'header';
root.appendChild(header);
}
复制代码
// sidebar.js代码
function Sidebar() {
var sidebar = document.createElement('div');
sidebar.innerText = 'sidebar';
root.appendChild(sidebar);
}
复制代码
// content.js代码
function Content() {
var content = document.createElement('div');
content.innerText = 'content';
root.appendChild(content);
}
复制代码
// index.js代码
var root = document.getElementById('root');
new Header();
new Sidebar();
new Content();
复制代码
不足: 以上的代码示例中,虽然使用面向对象开发模式解决了面向过程开发模式中的一些问题,但似乎又引入了一些新的问题。
- 每一个模块都需要引入一个
.js
文件,随着模块的增多,这会影响页面性能 - 在
index.js
文件中,并不能直接看出模块的逻辑关系,必须去页面才能找到 - 在
index.html
页面中,文件的引入顺序必须严格按顺序来引入,例如:index.js
必须放在最后引入,如果把header.js
文件放在index.js
文件后引入,那么代码会报错
现代开发模式
特征: 模块化加载方案让前端开发进一步工程化
根据面向对象开发模式中的一系列问题,随后各种模块化加载的方案如雨后春笋,例如:ES Module
、AMD
、CMD
以及CommonJS
等,一个ES Module
模块化加载方案可能如下所示:
<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./index.js"></script>
复制代码
// header.js
export default function Header() {
var root = document.getElementById('root');
var header = document.createElement('div');
header.innerText = 'header';
root.appendChild(header);
}
复制代码
// sidebar.js
export default function Sidebar() {
var root = document.getElementById('root');
var sidebar = document.createElement('div');
sidebar.innerText = 'sidebar';
root.appendChild(sidebar);
}
复制代码
// content.js代码
export default function Content() {
var root = document.getElementById('root');
var content = document.createElement('div');
content.innerText = 'content';
root.appendChild(content);
}
复制代码
// index.js代码
import Header from './src/header.js';
import Sidebar from './src/sidebar.js';
import Content from './src/content.js';
new Header();
new Sidebar();
new Content();
复制代码
注意: 以上代码并不能直接在浏览器上执行,因为浏览器并不能直接识别ES Module
代码,需要借助其他工具来进行翻译,此时 Webpack 就粉墨登场了。
Webpack初体验
不建议跟随此小结一起安装,此次示例仅仅作为一个例子,详细学习步骤请直接阅读下一章节
生成package.json文件
-y参数表示直接生成默认配置项的package.json文件,不加此参数需要一步步按需进行配置。
$ npm init -y
复制代码
生成的package.json
文件:
{
"name": "webpack-vuepress",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
复制代码
安装Webpack
-D参数代表在本项目下安装 Webpack ,它是--save-dev的简写
$ npm install webpack webpack-cli -D
复制代码
修改代码
Webpack默认打包路径到dist文件夹,打包后的js文件名字叫main.js
其他代码不动,将index.html
中的.js
文件改成如下引用方式(引用打包后的文件):
<!-- index.html代码 -->
<p>这里是我们网页的内容</p>
<div id="root"></div>
<script src="./dist/main.js"></script>
复制代码
Webpack打包
参数说明
- npx webpack代表在本项目下寻找 Webpack 打包命令,它区别npm命令
- index.js参数代表本次打包的入口是index.js
$ npx webpack index.js
复制代码
打包结果:
正如上面你所看到的那样,网页正确显示了我们期待的结果,这也是 Webpack 能为我们解决问题的一小部分能力,下面将正式开始介绍 Webpack 。
安装
全局安装
如果你只是想做一个 Webpack 的 Demo案例,那么全局安装方法可能会比较适合你。如果你是在实际生产开发中使用,那么推荐你使用本地安装方法。
全局安装命令
Webpack4.0+的版本,必须安装webpack-cli,-g命令代表全局安装的意思
$ npm install webpack webpack-cli -g
复制代码
卸载
通过npm install安装的模块,对应的可通过npm uninstall进行卸载
$ npm uninstall webpack webpack-cli -g
复制代码
本地安装(推荐)
本地安装的 Webpack 意思是,只在你当前项目下有效。而通过全局安装的Webpack,如果两个项目的 Webpack 主版本不一致,则可能会造成其中一个项目无法正常打包。本地安装方式也是实际开发中推荐的一种 Webpack 安装方式。
$ npm install webpack webpack-cli -D 或者 npm install webpack webpack-cli --save-dev
复制代码
版本号安装
如果你对Webpack的具体版本有严格要求,那么可以先去github的Webpack仓库查看历史版本记录或者使用npm view webpack versions查看Webpack的npm历史版本记录
// 查看webpack的历史版本记录
$ npm view webpack versions
// 按版本号安装
$ npm install webpack@4.25.0 -D
复制代码
起步
创建项目结构
现在我们来创建基本的项目结构,它可能是下面这样
|-- webpack-vuepress
| |-- index.html
| |-- index.js
| |-- package.json
复制代码
其中package.json
是利用下面的命令自动生成的配置文件
$ npm init -y
复制代码
添加基础代码
在创建了基本的项目结构以后,我们需要为我们创建的文件添加一些代码
index.html
页面中的代码:
<p>这是最原始的网页内容</p>
<div id="root"></div>
<!-- 引用打包后的js文件 -->
<script src="./dist/main.js"></script>
复制代码
index.js
文件中的代码:
console.log('hello,world');
复制代码
安装Webpack
运行如下命令安装webpack4.0+
和webpack-cli
:
$ npm install webpack webpack-cli -D
复制代码
添加配置文件
使用如下命令添加 Webpack 配置文件:
$ touch webpack.config.js
复制代码
使用此命令,变更后的项目结构大概如下所示:
|-- webpack-vuepress
| |-- index.html
| |-- index.js
| |-- webpack.config.js
| |-- package.json
复制代码
至此我们的基础目录已创建完毕,接下来需要改写webpack.config.js
文件,它的代码如下:
// path为Node的核心模块
const path = require('path');
module.exports = {
entry: './index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
}
}
复制代码
配置参数说明:
entry
配置项说明了webpack
打包的入口。output
配置项说明了webpack
输出配置,其中filename
配置了打包后的文件叫main.js
path
配置了打包后的输出目录为dist
文件夹下
改写package.json文件
改写说明:
- 添加
private
属性并设置为true
,此属性能让我们的项目为私有的,防止意外发布代码 - 移除
main
属性,我们的项目并不需要对外暴露一个入口文件 - 添加
scripts
命令,即我们的打包命令
改写后的package.json
文件如下所示:
{
"name": "webpack-vuepress",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"bundle": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^4.31.0",
"webpack-cli": "^3.3.2"
}
}
复制代码
第一次打包
npm run代表运行一个脚本命令,而bundle就是我们配置的打包命令,即npm run bundle就是我们配置的webpack打包命令。
运行如下命令进行项目打包:
$ npm run bundle
复制代码
打包后的效果如下所示:
打包后的项目目录如下所示,可以看到我们多出了一个叫dist
的目录,它里面有一个main.js
文件
|-- dist
| |-- main.js
|-- index.html
|-- index.js
|-- webpack.config.js
|-- package.json
复制代码
打包成功后,我们需要在浏览器中运行index.html
,它的运行结果如下图所示
理解webpack打包输出
在上一节中,我们第一次运行了一个打包命令,它在控制台上有一些输出内容,这一节我们详细来介绍这些输出是什么意思
- Hash:
hash
代表本次打包的唯一hash
值,每一次打包此值都是不一样的 - Version: 详细展示了我们使用
webpack
的版本号 - Time: 代表我们本次打包的耗时
- Asset: 代表我们打包出的文件名称
- Size: 代表我们打包出的文件的大小
- Chunks: 代表打包后的
.js
文件对应的id
,id
从0
开始,依次往后+1
- Chunks Names: 代表我们打包后的
.js
文件的名字,至于为何是main
,而不是其他的内容,这是因为在我们的webpack.config.js
中,entry:'./index.js'
是对如下方式的简写形式:
// path为Node的核心模块
const path = require('path');
module.exports = {
// entry: './index.js',
entry: {
main: './index.js'
}
// 其它配置
}
复制代码
- Entrypoint main = bundle.js: 代表我们打包的入口为
main
- warning in configuration: 提示警告,意思是我们没有给
webpack.config.js
设置mode
属性,mode
属性有三个值:development
代表开发环境、production
代表生产环境、none
代表既不是开发环境也不是生产环境。如果不写的话,默认是生产环境,可在配置文件中配置此项,配置后再次打包将不会再出现此警告。
// path为Node的核心模块
const path = require('path');
module.exports = {
// 其它配置
mode: 'development'
}
复制代码
打包静态资源
什么是loader?
loader是一种打包规则,它告诉了 Webpack 在遇到非js文件时,应该如何处理这些文件
loader
有如下几种固定的运用规则:
- 使用
test
正则来匹配相应的文件 - 使用
use
来添加文件对应的loader
- 对于多个
loader
而言,从 右到左 依次调用
使用loader打包图片
打包图片需要用到file-loader或者url-loader,需使用npm install进行安装
$ npm install file-loader -D 或者 npm install url-loader -D
复制代码
一点小改动
在打包图片之前,让我们把index.html
移动到上一节打包后的dist
目录下,index.html
中相应的.js
引入也需要修改一下,像下面这样
// index.html的改动部分
<script src="./main.js"></script>
复制代码
添加打包图片规则
对于打包图片,我们需要在webpack.config.js
中进行相应的配置,它可以像下面这样:
// path为Node的核心模块
const path = require('path');
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader'
}
}
]
}
}
复制代码
改写index.js
import avatar from './avatar.jpg'
var root = document.getElementById('root');
var img = document.createElement('img');
img.src = avatar
root.appendChild(img)
复制代码
打包后的项目目录
|-- dist
| |-- bd7a45571e4b5ccb8e7c33b7ce27070a.jpg
| |-- main.js
| |-- index.html
|-- index.js
|-- avatar.jpg
|-- package.json
|-- webpack.config.js
复制代码
打包结果
运用占位符
在以上打包图片的过程中,我们发现打包生成的图片好像名字是一串乱码,如果我们要原样输出原图片的名字的话,又该如何进行配置呢?这个问题,可以使用 占位符 进行解决。
文件占位符它有一些固定的规则,像下面这样:
[name]
代表原本文件的名字[ext]
代表原本文件的后缀[hash]
代表一个唯一编码
根据占位符的规则再次改写webpack.config.js
文件,
// path为Node的核心模块
const path = require('path');
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]'
}
}
}
]
}
}
复制代码
根据上面占位符的运用,打包生成的图片,它的名字如下
|-- dist
| |-- avatar_bd7a45571e4b5ccb8e7c33b7ce27070a.jpg
复制代码
使用loader打包CSS
样式文件分为几种情况,每一种都需要不同的loader
来处理:
- 普通
.css
文件,使用style-loader
和css-loader
来处理 .less
文件,使用less-loader
来处理.sass或者.scss
文件,需要使用sass-loader
来处理.styl
文件,需要使用stylus-loader
来处理
打包css文件
首先安装style-loader
和css-loader
$ npm install style-loader css-loader -D
复制代码
改写webpack配置文件:
// path为Node的核心模块
const path = require('path');
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]'
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'] // 从右到左的顺序调用,所以顺序不能错
}
]
}
}
复制代码
根目录下创建index.css
.avatar{
width: 150px;
height: 150px;
}
复制代码
改写index.js
文件
import avatar from './avatar.jpg';
import './index.css';
var root = document.getElementById('root');
var img = new Image();
img.src = avatar;
img.classList.add('avatar');
root.appendChild(img);
复制代码
打包结果
打包Sass文件
需要安装sass-loader
和node-sass
$ npm install sass-loader node-sass -D
复制代码
改写webpack.config.js
文件
// path为Node的核心模块
const path = require('path');
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]'
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(sass|scss)$/,
use: ['style-loader','css-loader','sass-loader']
}
]
}
}
复制代码
根目录下添加index-sass.sass
文件
body{
.avatar-sass{
width: 150px;
height: 150px;
}
}
复制代码
改写index.js
import avatar from './avatar.jpg';
import './index.css';
import './index-sass.sass';
var img = new Image();
img.src = avatar;
img.classList.add('avatar-sass');
var root = document.getElementById('root');
root.appendChild(img);
复制代码
根据上面的配置和代码改写后,再次打包,打包的结果会是下面这个样子
自动添加CSS厂商前缀
当我们在css
文件中写一些需要处理兼容性的样式的时候,需要我们分别对于不同的浏览器书添加不同的厂商前缀,使用postcss-loader
可以帮我们在webpack
打包的时候自动添加这些厂商前缀。 自动添加厂商前缀需要npm install
安装postcss-loader
和autoprefixer
npm install postcss-loader autoprefixer -D
复制代码
修改index-sass.sass
.avatar-sass {
width: 150px;
height: 150px;
transform: translate(50px,50px);
}
复制代码
在修改sass
文件代码后,我们需要对webpack.config.js
// path为Node的核心模块
const path = require('path');
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]'
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(sass|scss)$/,
use: ['style-loader','css-loader','sass-loader','postcss-loader'] // 顺序不能变
}
]
}
}
复制代码
根目录下添加postcss.config.js
,并添加代码
module.exports = {
plugins: [require('autoprefixer')]
}
复制代码
根据上面的配置,我们再次打包运行,在浏览器中运行index.html
,它的结果如下图所示
模块化打包CSS文件
CSS的模块化打包的理解是:除非我主动引用你的样式,否则你打包的样式不能影响到我。
根目录下添加createAvatar.js
文件,并填写下面这段代码
import avatar from './avatar.jpg';
export default function CreateAvatar() {
var img = new Image();
img.src = avatar;
img.classList.add('avatar-sass');
var root = document.getElementById('root');
root.appendChild(img);
}
复制代码
改写index.js
,引入createAvatar.js
并调用
import avatar from './avatar.jpg';
import createAvatar from './createAvatar';
import './index.css';
import './index-sass.sass';
createAvatar();
var img = new Image();
img.src = avatar;
img.classList.add('avatar-sass');
var root = document.getElementById('root');
root.appendChild(img);
复制代码
打包运行
我们可以看到,在createAvatar.js
中,我们写的img
标签的样式,它受index-sass.sass
样式文件的影响,如果要消除这种影响,需要我们开启对css
样式文件的模块化打包。
进一步改写webpack.config.js
// path为Node的核心模块
const path = require('path');
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]'
}
}
},
{
test: /\.(sass|scss)$/,
use: ['style-loader', {
loader: 'css-loader',
options: {
modules: true
}
}, 'sass-loader', 'postcss-loader']
}
]
}
}
复制代码
开启css
模块化打包后,我们需要在index.js
中做一点小小的改动,像下面这样子
import avatar from './avatar.jpg';
import createAvatar from './createAvatar';
import './index.css';
import style from './index-sass.sass';
createAvatar();
var img = new Image();
img.src = avatar;
img.classList.add(style['avatar-sass']);
var root = document.getElementById('root');
root.appendChild(img);
复制代码
打包运行后,我们发现使用createAvatar.js
创建出来的img
没有受到样式文件的影响,证明我们的css
模块化配置已经生效,下图是css
模块化打包的结果:
Webpack核心
使用WebpackPlugin
plugin的理解是:当 Webpack 运行到某一个阶段时,可以使用plugin来帮我们做一些事情。
在使用plugin
之前,我们先来改造一下我们的代码,首先删掉无用的文件,随后在根目录下新建一个src
文件夹,并把index.js
移动到src
文件夹下,移动后你的目录看起来应该是下面这样子的
|-- dist
| |-- index.html
|-- src
| |-- index.js
|-- postcss.config.js
|-- webpack.config.js
|-- package.json
复制代码
接下来再来处理一下index.js
文件的代码,写成下面这样
// src/index.js
var root = document.getElementById('root');
var dom = document.createElement('div');
dom.innerHTML = 'hello,world';
root.appendChild(dom);
复制代码
最后我们来处理一下我们的webpack.config.js
文件,它的改动有下面这些
- 因为
index.js
文件的位置变动了,我们需要改动一下entry
- 删除掉我们配置的所有
loader
规则 按照上面的改动后,webpack.config.js
中的代码看起来是下面这样的
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist')
}
}
复制代码
html-webpack-plugin
html-webpack-plugin
可以让我们使用固定的模板,在每次打包的时候 自动生成 一个.html
文件,并且它会 自动 帮我们引入我们打包后的.js
文件
使用如下命令安装html-webpack-plugin
$ npm install html-webpack-plugin -D
复制代码
在src
目录下创建index.html
模板文件,它的代码可以写成下面这样子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Html 模板</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
复制代码
因为我们要使用html-webpack-plugin
插件,所以我们需要再次改写webpack.config.js
文件(具体改动部分见高亮部分掘金无高亮)
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [
new htmlWebpackPlugin({
template: 'src/index.html'
})
],
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist')
}
}
复制代码
在完成上面的配置后,我们使用npm run bundle
命令来打包一下测试一下,在打包完毕后,我们能在dist
目录下面看到index.html
中的代码变成下面这样子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HTML模板</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
复制代码
我们发现,以上index.html
的结构,正是我们在src
目录下index.html
模板的结构,并且还能发现,在打包完成后,还自动帮我们引入了打包输出的.js
文件,这正是html-webpack-plugin
的基本功能,当然它还有其它更多的功能,我们将在后面进行详细的说明。
clean-webpack-plugin
clean-webpack-plugin
它能帮我们在打包之前 自动删除dist
打包目录及其目录下所有文件,不用我们手动进行删除。
我们使用如下命令来安装clean-webpack-plugin
$ npm install clean-webpack-plugin -D
复制代码
安装完毕以后,我们同样需要在webpack.config.js
中进行配置(改动部分参考高亮代码块掘金无高亮)
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [
new htmlWebpackPlugin({
template: 'src/index.html'
}),
new cleanWebpackPlugin()
],
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist')
}
}
复制代码
在完成以上配置后,我们使用npm run bundle
打包命令进行打包,它的打包结果请自行在你的项目下观看自动清理dist
目录的实时效果。
在使用WebpackPlugin
小节,我们只介绍了两种常用的plugin
,更多plugin
的用法我们将在后续进行讲解,你也可以点击Webpack Plugins来学习更多官网推荐的plugin
用法。
配置SourceMap
SourceMap的理解:它是一种映射关系,它映射了打包后的代码和源代码之间的对应关系,一般通过devtool来配置。
以下是官方提供的devtool
各个属性的解释以及打包速度对比图:
通过上图我们可以看出,良好的source-map
配置不仅能帮助我们提高打包速度,同时在代码维护和调错方面也能有很大的帮助,一般来说,source-map
的最佳实践是下面这样的:
- 开发环境下(
development
):推荐将devtool
设置成cheap-module-eval-source-map
- 生产环境下(
production
):推荐将devtool
设置成cheap-module-source-map
使用WebpackDevServer
webpack-dev-server的理解:它能帮助我们在源代码更改的情况下,自动*帮我们打包我们的代码并启动一个小型的服务器。如果与热更新一起使用,它能帮助我们高效的开发。
自动打包的方案,通常来说有如下几种:
watch
参数自动打包:它是在打包命令后面跟了一个--watch
参数,它虽然能帮我们自动打包,但我们任然需要手动刷新浏览器,同时它不能帮我们在本地启动一个小型服务器,一些http
请求不能通过。webpack-dev-server
插件打包(推荐):它是我们推荐的一种自动打包方案,在开发环境下使用尤其能帮我们高效的开发,它能解决watch
参数打包中的问题,如果我们与热更新(HMR
)一起使用,我们将拥有非常良好的开发体验。webpack-dev-middleware
自编码启动小型服务器(不讲述)
watch参数自动打包
使用watch
参数进行打包,我们需要在package.json
中新增一个watch
打包命令,它的配置如下
{
// 其它配置
"scripts": {
"bundle": "webpack",
"watch": "webpack --watch"
}
}
复制代码
在配置好上面的打包命令后,我们使用npm run watch
命令进行打包,然后在浏览器中运行dist
目录下的index.html
,运行后,我们尝试修改src/index.js
中的代码,例如把hello,world
改成hello,dell-lee
,改动完毕后,我们刷新一下浏览器,会发现浏览器成功输出hello,dell-lee
,这也证明了watch
参数确实能自动帮我们进行打包。
webpack-dev-server打包
要使用webpack-dev-server
,我们需要使用如下命令进行安装
$ npm install webpack-dev-server -D
复制代码
安装完毕后,我们和watch
参数配置打包命令一样,也需要新增一个打包命令,在package.json
中做如下改动:
// 其它配置
"scripts": {
"bundle": "webpack",
"watch": "webpack --watch",
"dev": "webpack-dev-server'
}
复制代码
配置完打包命令后,我们最后需要对webpack.config.js
做一下处理:
module.exports = {
// 其它配置
devServer: {
// 以dist文件为基础启动一个服务器,服务器运行在4200端口上,每次启动时自动打开浏览器
contentBase: 'dist',
open: true,
port: 4200
}
}
复制代码
在以上都配置完毕后,我们使用npm run dev
命令进行打包,它会自动帮我们打开浏览器,现在你可以在src/index.js
修改代码,再在浏览器中查看效果,它会有惊喜的哦,ღ( ´・ᴗ・` )比心
这一小节主要介绍了如何让工具自动帮我们打包,下一节我们将讲解模块热更新(HMR)。
模块热更新(HMR)
模块热更新(HMR)的理解:它能够让我们在不刷新浏览器(或自动刷新)的前提下,在运行时帮我们更新最新的代码。
模块热更新(HMR)已内置到 Webpack ,我们只需要在webpack.config.js
中像下面这样简单的配置即可,无需安装别的东西。
const webpack = require('webpack');
module.exports = {
// 其它配置
devServer: {
contentBase: 'dist',
open: true,
port: 3000,
hot: true, // 启用模块热更新
hotOnly: true // 模块热更新启动失败时,重新刷新浏览器
},
plugins: [
// 其它插件
new webpack.HotModuleReplacementPlugin()
]
}
复制代码
在模块热更新(HMR)配置完毕后,我们现在来想一下,什么样的代码是我们希望能够热更新的,我们发现大多数情况下,我们似乎只需要关心两部分内容:CSS
文件和.js
文件,根据这两部分,我们将分别来进行介绍。
CSS中的模块热更新
首先我们在src
目录下新建一个style.css
样式文件,它的代码可以这样下:
div:nth-of-type(odd) {
background-color: yellow;
}
复制代码
随后我们改写一下src
目录下的index.js
中的代码,像下面这样子:
import './style.css';
var btn = document.createElement('button');
btn.innerHTML = '新增';
document.body.appendChild(btn);
btn.onclick = function() {
var dom = document.createElement('div');
dom.innerHTML = 'item';
document.body.appendChild(dom);
}
复制代码
由于我们需要处理CSS
文件,所以我们需要保留处理CSS
文件的loader
规则,像下面这样
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
复制代码
在以上代码添加和配置完毕后,我们使用npm run dev
进行打包,我们点击按钮后,它会出现如下的情况
理解: 由于item
是动态生成的,当我们要将yellow
颜色改变成red
时,模块热更新能帮我们在不刷新浏览器的情况下,替换掉样式的内容。直白来说:自动生成的item
依然存在,只是颜色变了。
在js中的模块热更新
在介绍完CSS
中的模块热更新后,我们接下来介绍在js
中的模块热更新。
首先,我们在src
目录下创建两个.js
文件,分别叫counter.js
和number.js
,它的代码可以写成下面这样:
// counter.js代码
export default function counter() {
var dom = document.createElement('div');
dom.setAttribute('id', 'counter');
dom.innerHTML = 1;
dom.onclick = function() {
dom.innerHTML = parseInt(dom.innerHTML,10)+1;
}
document.body.appendChild(dom);
}
复制代码
number.js
中的代码是下面这样的:
// number.js代码
export default function number() {
var dom = document.createElement('div');
dom.setAttribute('id','number');
dom.innerHTML = '1000';
document.body.appendChild(dom);
}
复制代码
添加完以上两个.js
文件后,我们再来对index.js
文件做一下小小的改动:
// index.js代码
import counter from './counter';
import number from './number';
counter();
number();
复制代码
在以上都改动完毕后,我们使用npm run dev
进行打包,在页面上点击数字1
,让它不断的累计到你喜欢的一个数值(记住这个数值),这个时候我们再去修改number.js
中的代码,将1000
修改为3000
,也就是下面这样修改:
// number.js代码
export default function number() {
var dom = document.createElement('div');
dom.setAttribute('id','number');
dom.innerHTML = '3000';
document.body.appendChild(dom);
}
复制代码
我们发现,虽然1000
成功变成了3000
,但我们累计的数值却重置到了1
,这个时候你可能会问,我们不是配置了模块热更新了吗,为什么不像CSS
一样,直接替换即可?
回答:这是因为CSS
文件,我们是使用了loader
来进行处理,有些loader
已经帮我们写好了模块热更新的代码,我们直接使用即可(类似的还有.vue
文件,vue-loader
也帮我们处理好了模块热更新)。而对于js
代码,还需要我们写一点点额外的代码,像下面这样子:
import counter from './counter';
import number from './number';
counter();
number();
// 额外的模块HMR配置
if(module.hot) {
module.hot.accept('./number.js', () => {
document.body.removeChild(document.getElementById('number'));
number();
})
}
复制代码
写完上面的额外代码后,我们再在浏览器中重复我们刚才的操作,即:
- 累加数字
1
带你喜欢的一个值 - 修改
number.js
中的1000
为你喜欢的一个值
以下截图是我的测试结果,同时我们也可以在控制台console
上,看到模块热更新第二次启动时,已经成功帮我们把number.js
中的代码输出到了浏览器。
小结:在更改CSS
样式文件时,我们不用书写module.hot
,这是因为各种CSS
的loader
已经帮我们处理了,相同的道理还有.vue
文件的vue-loader
,它也帮我们处理了模块热更新,但在.js
文件中,我们还是需要根据实际的业务来书写一点module.hot
代码的。
处理ES6语法
我们在项目中书写的ES6
代码,由于考虑到低版本浏览器的兼容性问题,需要把ES6
代码转换成低版本浏览器能够识别的ES5
代码。使用babel-loader
和@babel/core
来进行ES6
和ES5
之间的链接,使用@babel/preset-env
来进行ES6
转ES5
在处理ES6
代码之前,我们先来清理一下前面小节的中的代码,我们需要删除counter.js
、number.js
和style.css
这个三个文件,删除后的文件目录大概是下面这样子的:
|-- dist
| |-- index.html
| |-- main.js
|-- src
| |-- index.html
| |-- index.js
|-- package.json
|-- webpack.config.js
复制代码
要处理ES6
代码,需要我们安装几个npm
包,可以使用如下的命令去安装
// 安装 babel-loader @babel/core
$ npm install babel-loader @babel/core --save-dev
// 安装 @babel/preset-env
$ npm install @babel/preset-env --save-dev
// 安装 @babel/polyfill进行ES5代码补丁
$ npm install @babel/polyfill --save-dev
复制代码
安装完毕后,我们需要改写src/index.js
中的代码,可以是下面这个样子:
import '@babel/polyfill';
const arr = [
new Promise(() => {}),
new Promise(() => {}),
new Promise(() => {})
]
arr.map(item => {
console.log(item);
})
复制代码
处理ES6
代码,需要我们使用loader
,所以需要在webpack.config.js
中添加如下的代码:
module.exports = {
// 其它配置
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
}
复制代码
@babel/preset-env
需要在根目录下有一个.babelrc
文件,所以我们新建一个.babelrc
文件,它的代码如下:
{
"presets": ["@babel/preset-env"]
}
复制代码
为了让我们的打包变得更加清晰,我们需要在webpack.config.js
中把source-map
配置成none
,像下面这样:
module.exports = {
// 其他配置
mode: 'development',
devtool: 'none'
}
复制代码
本次打包,我们需要使用npx webpack
,打包的结果如下图所示:
在以上的打包中,我们可以发现:
- 箭头函数被转成了普通的函数形式
- 如果你仔细观察这次打包输出的话,你会发现打包体积会非常大,有几百K,这是因为我们将
@babel/polyfill
中的代码全部都打包进了我们的代码中
针对以上最后一个问题,我们希望,我们使用了哪些ES6
代码,就引入它对应的polyfill
包,达到一种按需引入的目的,要实现这样一个效果,我们需要在.babelrc
文件中做一下小小的改动,像下面这样:
{
"presets": [["@babel/preset-env", {
"corejs": 2,
"useBuiltIns": "usage"
}]]
}
复制代码
同时需要注意的时,我们使用了useBuiltIns:"usage"
后,在index.js
中就不用使用import '@babel/polyfill'
这样的写法了,因为它已经帮我们自动这样做了。
在以上配置完毕后,我们再次使用npx webpack
进行打包,如下图,可以看到此次打包后,main.js
的大小明显变小了。
Webpack进阶
Tree Shaking
Tree Shaking是一个术语,通常用于描述移除项目中未使用的代码,Tree Shaking 只适用于ES Module语法(既通过export导出,import引入),因为它依赖于ES Module的静态结构特性。
在正式介绍Tree Shaking
之前,我们需要现在src
目录下新建一个math.js
文件,它的代码如下:
export function add(a, b) {
console.log(a + b);
}
export function minus(a, b) {
console.log(a - b);
}
复制代码
接下来我们对index.js
做一下处理,它的代码像下面这样,从math.js
中引用add
方法并调用:
import { add } from './math'
add(1, 4);
复制代码
在上面的.js
改动完毕后,我们最后需要对webpack.config.js
做一下配置,让它支持Tree Shaking
,它的改动如下:
const path = require('path');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: {
main: './src/index.js'
},
optimization: {
usedExports: true
},
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist')
}
}
复制代码
在以上webpack.config.js
配置完毕后,我们需要使用npx webpack
进行打包,它的打包结果如下:
// dist/main.js
"use strict";
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "a", function() { return add; });
/* unused harmony export minus */
function add(a, b) {
console.log(a + b);
}
function minus(a, b) {
console.log(a - b);
}
复制代码
打包结果分析:虽然我们配置了 Tree Shaking
,但在开发环境下,我们依然能够看到未使用过的minus
方法,以上注释也清晰了说明了这一点,这个时候你可能会问:为什么我们配置了Tree Shaking
,minus
方法也没有被使用,但依然还是被打包进了main.js
中?
其实这个原因很简单,这是因为我们处于开发环境下打包,当我们处于开发环境下时,由于source-map
等相关因素的影响,如果我们不把没有使用的代码一起打包进来的话,source-map
就不是很准确,这会影响我们本地开发的效率。
看完以上本地开发Tree Shaking
的结果,我们也知道了本地开发Tree Shaking
相对来说是不起作用的,那么在生产环境下打包时,Tree Shaking
的表现又如何呢?
在生产环境下打包,需要我们对webpack.config.js
中的mode
属性,需要由development
改为production
,它的改动如下:
const path = require('path');
module.exports = {
mode: 'production',
devtool: 'source-map',
entry: {
main: './src/index.js'
},
optimization: {
usedExports: true
},
output: {
filename: 'main.js',
path: path.resolve(__dirname,'dist')
}
}
复制代码
配置完毕后,我们依然使用npx webpack
进行打包,可以看到,它的打包结果如下所示:
// dist/main.js
([function(e,n,r){
"use strict";
var t,o;
r.r(n),
t=1,
o=4,
console.log(t+o)
}]);
复制代码
打包代码分析:以上代码是一段被压缩过后的代码,我们可以看到,上面只有add
方法,未使用的minus
方法并没有被打包进来,这说明在生产环境下我们的Tree Shaking
才能真正起作用。
SideEffects
由于Tree Shaking
作用于所有通过import
引入的文件,如果我们引入第三方库,例如:import _ from 'lodash'
或者.css
文件,例如import './style.css'
时,如果我们不 做限制的话,Tree Shaking将起副作用,SideEffects
属性能帮我们解决这个问题:它告诉webpack
,我们可以对哪些文件不做 Tree Shaking
// 修改package.json
// 如果不希望对任何文件进行此配置,可以设置sideEffects属性值为false
// *.css 表示 对所有css文件不做 Tree Shaking
// @babael/polyfill 表示 对@babel/polyfill不做 Tree Shaking
"sideEffects": [
"*.css",
"@babel/polyfill"
],
复制代码
小结:对于Tree Shaking
的争议比较多,推荐看你的Tree Shaking并没有什么卵用,看完你会发现我们对Tree Shaking
的了解真是太浅薄了。
区分开发模式和生产模式
像上一节那样,如果我们要区分Tree Shaking
的开发环境和生产环境,那么我们每次打包的都要去更改webpack.config.js
文件,有没有什么办法能让我们少改一点代码呢? 答案是有的!
区分开发环境和生产环境,最好的办法是把公用配置提取到一个配置文件,生产环境和开发环境只写自己需要的配置,在打包的时候再进行合并即可,webpack-merge 可以帮我们做到这个事情。
首先,我们效仿各大框架的脚手架的形式,把 Webpack 相关的配置都放在根目录下的build
文件夹下,所以我们需要新建一个build
文件夹,随后我们要在此文件夹下新建三个.js
文件和删除webpack.config.js
,它们分别是:
webpack.common.js
:Webpack 公用配置文件webpack.dev.js
:开发环境下的 Webpack 配置文件webpack.prod.js
:生产环境下的 Webpack 配置文件webpack.config.js
:删除根目录下的此文件
新建完webpack.common.js
文件后,我们需要把公用配置提取出来,它的代码看起来应该是下面这样子的:
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}
]
},
plugins: [
new htmlWebpackPlugin({
template: 'src/index.html'
}),
new cleanWebpackPlugin()
],
output: {
filename: '[name].js',
path: path.resolve(__dirname,'dist')
}
}
复制代码
提取完 Webpack 公用配置文件后,我们开发环境下的配置,也就是webpack.dev.js
中的代码,将剩下下面这些:
const webpack = require('webpack');
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: 'dist',
open: true,
port: 3000,
hot: true,
hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
复制代码
而生产环境下的配置,也就是webpack.prod.js
中的代码,可能是下面这样子的:
module.exports = {
mode: 'production',
devtool: 'cheap-module-source-map',
optimization: {
usedExports: true
}
}
复制代码
在处理完以上三个.js
文件后,我们需要做一件事情:
- 当处于开发环境下时,把
webpack.common.js
中的配置和webpack.dev.js
中的配置合并在一起 - 当处于开发环境下时,把
webpack.common.js
中的配置和webpack.prod.js
中的配置合并在一起
针对以上问题,我们可以使用webpack-merge
进行合并,在使用之前,我们需要使用如下命令进行安装:
$ npm install webpack-merge -D
复制代码
安装完毕后,我们需要对webpack.dev.js
和webpack.prod.js
做一下手脚,其中webpack.dev.js
中的改动如下(代码高亮部分掘金无高亮):
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: 'dist',
open: true,
port: 3000,
hot: true,
hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
module.exports = merge(commonConfig, devConfig);
复制代码
相同的代码,webpack.prod.js
中的改动部分如下:
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common');
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
optimization: {
usedExports: true
}
}
module.exports = merge(commonConfig, prodConfig);
复制代码
聪明的你一定想到了,因为上面我们已经删除了webpack.config.js
文件,所以我们需要重新在package.json
中配置一下我们的打包命令,它们是这样子写的:
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
},
复制代码
配置完打包命令,心急的你可能会马上开始尝试进行打包,你的打包目录可能长成下面这个样子:
|-- build
| |-- dist
| | |-- index.html
| | |-- main.js
| | |-- main.js.map
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- src
| |-- index.html
| |-- index.js
| |-- math.js
|-- .babelrc
|-- postcss.config.js
|-- package.json
复制代码
问题分析:当我们运行npm run build
时,dist
目录打包到了build
文件夹下了,这是因为我们把Webpack 相关的配置放到了build
文件夹下后,并没有做其他配置,Webpack 会认为build
文件夹会是根目录,要解决这个问题,需要我们在webpack.common.js
中修改output
属性,具体改动的部分如下所示:
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist')
}
复制代码
那么解决完上面这个问题,赶紧使用你的打包命令测试一下吧,我的打包目录是下面这样子,如果你按上面的配置后,你的应该跟此目录类似
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- dist
| |-- index.html
| |-- main.js
| |-- main.js.map
|-- src
| |-- index.html
| |-- index.js
| |-- math.js
|-- .babelrc
|-- postcss.config.js
|-- package.json
复制代码
代码分离(CodeSplitting)
Code Splitting 的核心是把很大的文件,分离成更小的块,让浏览器进行并行加载。
常见的代码分割有三种形式:
- 手动进行分割:例如项目如果用到
lodash
,则把lodash
单独打包成一个文件。 - 同步导入的代码:使用 Webpack 配置进行代码分割。
- 异步导入的代码:通过模块中的内联函数调用来分割代码。
手动进行分割
手动进行分割的意思是在entry
上配置多个入口,例如像下面这样:
module.exports = {
entry: {
main: './src/index.js',
lodash: 'lodash'
}
}
复制代码
这样配置后,我们使用npm run build
打包命令,它的打包输出结果为:
Asset Size Chunks Chunk Names
index.html 462 bytes [emitted]
lodash.js 1.46 KiB 1 [emitted] lodash
lodash.js.map 5.31 KiB 1 [emitted] lodash
main.js 1.56 KiB 2 [emitted] main
main.js.map 5.31 KiB 2 [emitted] main
复制代码
它输出了两个模块,也能在一定程度上进行代码分割,不过这种分割是十分脆弱的,如果两个模块共同引用了第三个模块,那么第三个模块会被同时打包进这两个入口文件中,而不是分离出来。
所以我们常见的做法是关心最后两种代码分割方法,无论是同步代码还是异步代码,都需要在webpack.common.js
中配置splitChunks
属性,像下面这样子:
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
复制代码
你可能已经看到了其中有一个chunks
属性,它告诉 Webpack 应该对哪些模式进行打包,它的参数有三种:
async
:此值为默认值,只有异步导入的代码才会进行代码分割。initial
:与async
相对,只有同步引入的代码才会进行代码分割。all
:表示无论是同步代码还是异步代码都会进行代码分割。
同步代码分割
在完成上面的配置后,让我们来安装一个相对大一点的包,例如:lodash
,然后对index.js
中的代码做一些手脚,像下面这样:
import _ from 'lodash'
console.log(_.join(['Dell','Lee'], ' '));
复制代码
就像上面提到的那样,同步代码分割,我们只需要在webpack.common.js
配置chunks
属性值为initial
即可:
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'initial'
}
}
}
复制代码
在webpack.common.js
配置完毕后,我们使用npm run build
来进行打包, 你的打包dist
目录看起来应该像下面这样子:
|-- dist
| |-- index.html
| |-- main.js
| |-- main.js.map
| |-- vendors~main.js
| |-- vendors~main.js.map
复制代码
打包分析:main.js
使我们的业务代码,vendors~main.js
是第三方模块的代码,在此案例中也就是lodash
中的代码。
异步代码分割
由于chunks
属性的默认值为async
,如果我们只需要针对异步代码进行代码分割的话,我们只需要进行异步导入,Webpack会自动帮我们进行代码分割,异步代码分割它的配置如下:
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'async'
}
}
}
复制代码
注意:由于异步导入语法目前并没有得到全面支持,需要通过 npm 安装 @babel/plugin-syntax-dynamic-import
插件来进行转译
$ npm install @babel/plugin-syntax-dynamic-import -D
复制代码
安装完毕后,我们需要在根目录下的.babelrc
文件做一下改动,像下面这样子:
{
"presets": [["@babel/preset-env", {
"corejs": 2,
"useBuiltIns": "usage"
}]],
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
复制代码
配置完毕后,我们需要对index.js
做一下代码改动,让它使用异步导入代码块:
// 点击页面,异步导入lodash模块
document.addEventListener('click', () => {
getComponent().then((element) => {
document.getElementById('root').appendChild(element)
})
})
function getComponent () {
return import(/* webpackChunkName: 'lodash' */'lodash').then(({ default: _ }) => {
var element = document.createElement('div');
element.innerHTML = _.join(['Dell', 'lee'], ' ')
return element;
})
}
复制代码
上面import里面的注释内容是plugin-syntax-dynamic-import插件支持的注释内容,俗称为"魔法注释",它的含义是告诉 Webpack 我们的异步模块的名字叫lodash,在后续preloading和prefetch也使用了相同的"魔法注释"方法。
写好以上代码后,我们同样使用npm run build
进行打包,dist
打包目录的输出结果如下:
|-- dist
| |-- 1.js
| |-- 1.js.map
| |-- index.html
| |-- main.js
| |-- main.js.map
复制代码
我们在浏览器中运行dist
目录下的index.html
,切换到network
面板时,我们可以发现只加载了main.js
,如下图:
当我们点击页面时,才 真正开始加载 第三方模块,如下图(1.js
):
SplitChunksPlugin配置参数详解
在上一节中,我们配置了splitChunks
属性,它能让我们进行代码分割,其实这是因为 Webpack 底层使用了 splitChunksPlugin
插件。这个插件有很多可以配置的属性,它也有一些默认的配置参数,它的默认配置参数如下所示,我们将在下面为一些常用的配置项做一些说明。
module.exports = {
// 其它配置项
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
复制代码
chunks参数
此参数的含义在上一节中已详细说明,同时也配置了相应的案例,就不再次累述。
minSize 和 maxSize
minSize
默认值是30000,也就是30kb,当代码超过30kb时,才开始进行代码分割,小于30kb的则不会进行代码分割;与minSize
相对的,maxSize
默认值为0,为0表示不限制打包后文件的大小,一般这个属性不推荐设置,一定要设置的话,它的意思是:打包后的文件最大不能超过设定的值,超过的话就会进行代码分割。
为了测试以上两个属性,我们来写一个小小的例子,在src
目录下新建一个math.js
文件,它的代码如下:
export function add(a, b) {
return a + b;
}
复制代码
新建完毕后,在index.js
中引入math.js
:
import { add } from './math.js'
console.log(add(1, 2));
复制代码
打包分析:因为我们写的math.js
文件的大小非常小,如果应用默认值,它是不会进行代码分割的,如果你要进一步测试minSize
和maxSize
,请自行修改后打包测试。
minChunks
默认值为1,表示某个模块复用的次数大于或等于一次,就进行代码分割。
如果将其设置大于1,例如:minChunks:2
,在不考虑其他模块的情况下,以下代码不会进行代码分割:
// 配置了minChunks: 2,以下lodash不会进行代码分割,因为只使用了一次
import _ from 'lodash';
console.log(_.join(['Dell', 'Lee'], '-'));
复制代码
maxAsyncRequests 和 maxInitialRequests
maxAsyncRequests
:它的默认值是5,代表在进行异步代码分割时,前五个会进行代码分割,超过五个的不再进行代码分割。maxInitialRequests
:它的默认值是3,代表在进行同步代码分割时,前三个会进行代码分割,超过三个的不再进行代码分割。
automaticNameDelimiter
这是一个连接符,左边是代码分割的缓存组,右边是打包的入口文件的项,例如vendors~main.js
cacheGroups
在进行代码分割时,会把符合条件的放在一组,然后把一组中的所有文件打包在一起,默认配置项中有两个分组,一个是vendors和default
vendors组: 以下代码的含义是,将所有通过引用node_modules
文件夹下的都放在vendors
组中
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
复制代码
default组: 默认组,意思是,不符合vendors
的分组都将分配在default
组中,如果一个文件即满足vendors
分组,又满足default
分组,那么通过priority
的值进行取舍,值最大优先级越高。
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
复制代码
reuseExistingChunk: 中文解释是复用已存在的文件。意思是,如果有一个a.js
文件,它里面引用了b.js
,但我们其他模块又有引用b.js
的地方。开启这个配置项后,在打包时会分析b.js
已经打包过了,直接可以复用不用再次打包。
// a.js
import b from 'b.js';
console.log('a.js');
// c.js
import b from 'b.js';
console.log('c.js');
复制代码
Lazy Loading懒加载
Lazy Loading懒加载的理解是:通过异步引入代码,它说的异步,并不是在页面一开始就加载,而是在合适的时机进行加载。
Lazy Loading
懒加载的实际案例我们已经在上一小节书写了一个例子,不过我们依然可以做一下小小的改动,让它使用async/await
进行异步加载,它的代码如下:
// 页面点击的时候才加载lodash模块
document.addEventListener('click', () => {
getComponet().then(element => {
document.body.appendChild(element);
})
})
async function getComponet() {
const { default: _ } = await import(/* webpackChunkName: 'lodash' */ 'lodash');
var element = document.createElement('div');
element.innerHTML = _.join(['1', '2', '3'], '**')
return element;
}
复制代码
以上懒加载的结果与上一小节的结果类似,就不在此展示,你可以在你本地的项目中打包后自行测试和查看。
PreLoading 和Prefetching
在以上Lazy Loading
的例子中,只有当我们在页面点击时才会加载lodash
,也有一些模块虽然是异步导入的,但我们希望能提前进行加载,PreLoading
和Prefetching
可以帮助我们实现这一点,它们的用法类似,但它们还是有区别的:Prefetching
不会跟随主进程一些下载,而是等到主进程加载完毕,带宽释放后才进行加载,PreLoading
会随主进程一起加载。
实现PreLoading
或者Prefetching
非常简单,我们只需要在上一节的例子中加一点点代码即可:
// 页面点击的时候才加载lodash模块
document.addEventListener('click', () => {
getComponet().then(element => {
document.body.appendChild(element);
})
})
async function getComponet() {
const { default: _ } = await import(/* webpackPrefetch: true */ 'lodash');
var element = document.createElement('div');
element.innerHTML = _.join(['1', '2', '3'], '**')
return element;
}
复制代码
改写完毕后,我们使用npm run dev
或者npm run build
进行打包,在浏览器中点击页面,我们将在network
面板看到如下图所示:
相信聪明的你一定看到了0.js
,它是from disk cache
,那为什么?原因在于,Prefetching
的代码它会在head
头部,添加像这样的一段内容:
<link rel="prefetch" as="script" href="0.js">
复制代码
这样一段内容追加到head
头部后,指示浏览器在空闲时间里去加载0.js
,这正是Prefetching
它所能帮我们做到的事情,而PreLoading
的用法于此类似,请自行测试。
CSS代码分割
当我们在使用style-loader
和css-loader
打包.css
文件时会直接把CSS文件打包进.js
文件中,然后直接把样式通过<style></style>
的方式写在页面,如果我们要把CSS单独打包在一起,然后通过link
标签引入,那么可以使用mini-css-extract-plugin
插件进行打包。
截止到写此文档时,此插件还未支持HMR,意味着我们要使用这个插件进行打包CSS时,为了开发效率,我们需要配置在生产环境下,开发环境依然还是使用。style-loader
进行打包
此插件的最新版已支持HMR。
在配置之前,我们需要使用npm install
进行安装此插件:
$ npm install mini-css-extract-plugin -D
复制代码
安装完毕后,由于此插件已支持HMR
,那我们可以把配置写在webpack.common.js
中(以下配置为完整配置,改动参考高亮代码块掘金无高亮):
const path = require('path');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: miniCssExtractPlugin.loader,
options: {
hmr: true,
reloadAll: true
}
},
'css-loader'
]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
}
]
},
plugins: [
new htmlWebpackPlugin({
template: 'src/index.html'
}),
new cleanWebpackPlugin(),
new miniCssExtractPlugin({
filename: '[name].css'
})
],
optimization: {
splitChunks: {
chunks: 'all'
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname,'../dist')
}
}
复制代码
配置完毕以后,我们来在src
目录下新建一个style.css
文件,它的代码如下:
body {
color: green;
}
复制代码
接下来,我们改动一下index.js
文件,让它引入style.css
,它的代码可以这样写:
import './style.css';
var root = document.getElementById('root');
root.innerHTML = 'Hello,world'
复制代码
使用npm run build
进行打包,dist
打包目录如下所示:
|-- dist
| |-- index.html
| |-- main.css
| |-- main.css.map
| |-- main.js
| |-- main.js.map
复制代码
如果发现并没有打包生成main.css文件,可能是Tree Shaking的副作用,应该在package.json中添加属性sideEffects:['*.css']
CSS压缩
CSS压缩的理解是:当我们有两个相同的样式分开写的时候,我们可以把它们合并在一起;为了减`CSS文件的体积,我们需要像压缩JS文件一样,压缩一下CSS文件。
我们再在src
目录下新建style1.css
文件,内容如下:
body{
line-height: 100px;
}
复制代码
在index.js
文件中引入此CSS文件
import './style.css';
import './style1.css';
var root = document.getElementById('root');
root.innerHTML = 'Hello,world'
复制代码
使用打包npm run build
打包命令,我们发现虽然插件帮我们把CSS打包在了一个文件,但并没有合并压缩。
body {
color: green;
}
body{
line-height: 100px;
}
复制代码
要实现CSS
的压缩,我们需要再安装一个插件:
$ npm install optimize-css-assets-webpack-plugin -D
复制代码
安装完毕后我们需要再一次改写webpack.common.js
的配置,如下:
const optimizaCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
// 其它配置
optimization: {
splitChunks: {
chunks: 'all'
},
minimizer: [
new optimizaCssAssetsWebpackPlugin()
]
}
}
复制代码
配置完毕以后,我们再次使用npm run build
进行打包,打包结果如下所示,可以看见,两个CSS文件的代码已经压缩合并了。
body{color:red;line-height:100px}
复制代码
Webpack和浏览器缓存(Caching)
在讲这一小节之前,让我们清理下项目目录,改写下我们的index.js
,删除掉一些没用的文件:
import _ from 'lodash';
var dom = document.createElement('div');
dom.innerHTML = _.join(['Dell', 'Lee'], '---');
document.body.append(dom);
复制代码
清理后的项目目录可能是这样的:
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- src
|-- index.html
|-- index.js
|-- postcss.config.js
|-- package.json
复制代码
我们使用npm run build
打包命令,打包我们的代码,可能会生成如下的文件:
|-- build
| |-- webpack.common.js
| |-- webpack.dev.js
| |-- webpack.prod.js
|-- dist
| |-- index.html
| |-- main.js
| |-- main.js.map
| |-- vendors~main.js
| |-- vendors~main.js.map
|-- src
|-- index.html
|-- index.js
|-- package.json
|-- postcss.config.js
复制代码
我们可以看到,打包生成的dist
目录下,文件名是main.js
和vendors~main.js
,如果我们把dist
目录放在服务器部署的话,当用户第一次访问页面时,浏览器会自动把这两个.js
文件缓存起来,下一次非强制性刷新页面时,会直接使用缓存起来的文件。
假如,我们在用户第一次刷新页面和第二次刷新页面之间,我们修改了我们的代码,并再一次部署,这个时候由于浏览器缓存了这两个.js
文件,所以用户界面无法获取最新的代码。
那么,我们有办法能解决这个问题呢,答案是[contenthash]
占位符,它能根据文件的内容,在每一次打包时生成一个唯一的hash值,只要我们文件发生了变动,就重新生成一个hash值,没有改动的话,[contenthash]
则不会发生变动,可以在output
中进行配置,如下所示:
// 开发环境下的output配置还是原来的那样,也就是webpack.common.js中的output配置
// 因为开发环境下,我们不用考虑缓存问题
// webpack.prod.js中添加output配置
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
复制代码
使用npm run build
进行打包,dist
打包目录的结果如下所示,可以看到每一个.js
文件都有一个唯一的hash
值,这样配置后就能有效解决浏览器缓存的问题。
|-- dist
| |-- index.html
| |-- main.8bef05e11ca1dc804836.js
| |-- main.8bef05e11ca1dc804836.js.map
| |-- vendors~main.4b711ce6ccdc861de436.js
| |-- vendors~main.4b711ce6ccdc861de436.js.map
复制代码
Shimming
有时候我们在引入第三方库的时候,不得不处理一些全局变量的问题,例如jQuery的$
,lodash的_
,但由于一些老的第三方库不能直接修改它的代码,这时我们能不能定义一个全局变量,当文件中存在$
或者_
的时候自动的帮他们引入对应的包。
这个问题,可以使用ProvidePlugin插件来解决,这个插件已经被 Webpack 内置,无需安装,直接使用即可。
在src
目录下新建jquery.ui.js
文件,代码如下所示,它使用了jQuery
的$
符号,创建这个文件目的是为了来模仿第三方库。
export function UI() {
$('body').css('background','green');
}
复制代码
创建完毕后,我们修改一下index.js
文件, 让它使用刚才我们创建的文件:
import _ from 'lodash';
import $ from 'jquery';
import { UI } from './jquery.ui';
UI();
var dom = $(`<div>${_.join(['Dell', 'Lee'], '---')}</div>`);
$('#root').append(dom);
复制代码
接下来我们使用npm run dev
进行打包,它的结果如下:
问题: 我们发现,根本运行不起来,报错$ is not defined
解答: 这是因为虽然我们在index.js
中引入的jquery
文件,但$
符号只能在index.js
才有效,在jquery.ui.js
无效,报错是因为jquery.ui.js
中$
符号找不到引起的。
以上场景完美再现了我们最开始提到的问题,那么我们接下来就通过配置解决,首先在webpack.common.js
文件中使用ProvidePlugin
插件:
配置$:'jquery',只要我们文件中使用了$符号,它就会自动帮我们引入jquery,相当于import $ from 'jquery'
const webpack = require('webpack');
module.exports = {
// 其它配置
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
_: 'lodash'
})
]
}
复制代码
打包结果: 使用npm run dev
进行打包,打包结果如下,可以发现,项目已经可以正确运行了。
处理全局this指向问题
我们现在来思考一个问题,一个模块中的this
到底指向什么,是模块自身还是全局的window
对象
// index.js代码,在浏览器中输出:false
console.log(this===window);
复制代码
如上所示,如果我们使用npm run dev
运行项目,运行index.html
时,会在浏览器的console
面板输出false
,证明在模块中this
指向模块自身,而不是全局的window
对象,那么我们有什么办法来解决这个问题呢?可以安装使用imports-loader
来解决这个问题!
$ npm install imports-loader -D
复制代码
安装完毕后,我们在webpack.common.js
加一点配置,在.js
的loader处理中,添加imports-loader
module.exports = {
// ... 其它配置
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader'
},
{
loader: 'imports-loader?this=>window'
}
]
}
]
}
}
复制代码
配置完毕后使用npm run dev
来进行打包,查看console
控制台输出true
,证明this
这个时候已经指向了全局window
对象,问题解决。
本篇博客由慕课网视频从基础到实战手把手带你掌握新版Webpack4.0阅读整理而来,观看视频请支持正版。