【转】Webpack 快速上手(上)
嫌啰嗦想直接看最终的配置请戳这里 webpack-workbench (https://github.com/onlymisaky/webpack-workbench)
由于文章篇幅较长,为了更好的阅读体验,本文分为上、中、下三篇:
- 上篇介绍了什么是 webpack,为什么需要 webpack,webpack 的文件输入和输出
- 中篇介绍了 webpack 在输入和输出这段中间所做的事情,也就是 loader 和 plugins
- 下篇介绍了 webpack 的优化,以及在开发环境和生产环境的不同用法
序
用两个自问自答来当作序吧:
-
Q:为什么要写这篇文章?
-
A:因为我在将自己的一个项目 AngularJS-ES6 (https://github.com/onlymisaky/AngularJS-ES6) 从 webpack3.x 升级到 4.x 的时候发现,作为一个熟练的 GitHub 搬运工,改起来还是很费力,主要是因为对其没有一个更完整的认知,因此有必要写一篇文章强化认知。
-
Q:既然是写给自己看的,那对于其他人有帮助吗?
-
A:如果你对 webpack 有少许的了解(至少知道webpack是干什么用的),那这篇文章应该还是有帮助的。
Why webpack?
一个工具的诞生,必然有其诞生的原因,也许是为了简化工作,也许是为了解决某些痛点,也可能是今年的kpi压力很大...
今天的主角 webpack 的诞生就是为了解决前端开发长久以来的痛点:模块化 ,这是也它的前辈 grunt 和 gulp 所不具备的功能。
回想一下那个前端还被称作切图仔的时代,我们是怎么组织多个 .js
文件的:
<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>
且不说这样写有多low,就单从代码维护角度来说,b.js
可能使用了 a.js
中的某个方法; c.js
同样如此,可能还用到了 b.js
中的某些方法。单看每个文件,是根本不知道这些方法是哪来的,也不清楚这三个文件之间的依赖关系的。
为了解决的这个问题,requirejs 诞生了,这是一套 AMD 的模块化实现方案。而此时 node 已经出现有些时日,其遵循的是 CommonJS ,同样是 JavaScript
模块化,却有两套实现方案,语法也不一样。于是又出现了 CMD 和其实现 seajs
,它是为了让服务端模块化和浏览器端模块化的差异能够最小化。
以上这些都是前辈们对 JavaScript 模块化的探索,虽然不是标准,但却推动了标准的发展,于是在 ES6 中,终于有了标准的、原生的模块化方案了,然鹅...
浏览器厂商:标准是标准,至于什么时候实现,fucked say (日后再议)。虽然现在大部分浏览器内核都实现了原生的模块化,但是我们不能确保用户都已将浏览器更新至最新了。
所以,在所有浏览器都实现模块化标准之前,我们还是不能够愉快的使用 import 和 export ,于是 webpack 来了,给乡亲们带了希望,让乡亲们再也不用看浏览器脸色,从此过上了没羞没臊幸福的生活了。
简单介绍
为了有更好更清晰的认识,建议读者跟着文章一起做一遍,可以先创建一个新的文件夹 learn-webpack
,在该目录中打开命令行,输入 npm init
命令初始化 package.json 文件。
安装
npm i webpack webpack-cli -D
可以全局安装,也可以本地安装,建议本地安装,因为 webpack 不同的版本之间还是有一定的差异,为了避免这个问题,我们选择本地。
在上面安装命令中,除了安装了 webpack 外,还安装了 webpack-cli 。那么这个工具是干什么用的?在 webpack4.x 之后,webpack 把命令行单独提取出来了,也就是说,我们想在命令行中执行 webpack xxx
等命令时,就需要先安装 webpack-cli 。 所以如你的使用的4.x版本的 webpack ,还需要额外安装一下 webpack-cli 。
使用
webpack 的使用还是比较简单的,并且提供了三种使用方法:
- 不使用配置文件
webpack <entry> <output>
entry:要打包的文件,可以是一个文件,也可以是一组文件。
output:打包后生成的文件。
例如将 ./src/index.js
打包到 dist/app.js
webpack ./src/index.js dist/app.js
- 使用配置文件
不使用配置文件的方式显然不够灵活多变,所以通常都是先编写 webpack 配置文件,然后根据配置文件内容进行打包。在根目录下创建 webpack.config.js
文件,然后在命令行中输入 webpack
,webpack 会自动读取 webpack.config.js
中的配置内容,然后进行打包,下文将会着重介绍如何编写配置文件。
- 在node中启动
const webpack = require('webpack');
webpack({
/* webpack配置内容 */
}, (err, stats) => {
/* 打包后回调 */
});
npm script
在使用第二种方式的时候,我们也可以将一些配置内容以参数的形式添加在命令后面,比如我们想设置环境为 production
,可以在 webpack.config.js 中将 mode
设置为 production
,也可以在命令后面添加 --mode production
:
webpack --mode production
如果还需要其他的配置参数,可有继续在后面添加。这样做的好处是可以将一些多变的参数从配置文件中抽离出来,使用起来很灵活。
但是如果参数太多,每次使用的时候又要敲好多命令,可能还会敲错,为了方便管理我们可以将这些命令全部保存在 package.json
的 scripts
属性中:
{
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
}
这样就可以通过 npm run build
命令进行打包了。
核心概念
经常看到有人抱怨 webpack 太难太复杂了,“我们万事俱备,就差一个 webpack 配置工程师了”。确实如此,相比于 gulp
简洁的 api ,webpack 确实复杂了许多。
其实仔细的梳理一下,webpack 最重要也就4个核心概念:
- entry 入口
- output 出口
- loader 模块转换器
- plugins 插件
除了这四个核心的概念,剩下的那些都是为了优化代码、让我们能有更好的开发体验而设计的。
mode
开头先讲一个 webpack4 中新增的选项:mode。可能是受 parcel 的刺激,webpack4 终于也可以零配置打包了,主要原因是 webpack 终于明白了一个道理:约定大于配置。
model 的值有三种:production
、development
、none
,分别表示不同模式。
在 production 模式下,会默认启用下面这些插件:
- process.env.NODE_ENV 的值设为 production
- FlagDependencyUsagePlugin:删除无用代码
- FlagIncludedChunksPlugin:删除无用代码
- ModuleConcatenationPlugin:作用域提升
- NoEmitOnErrorsPlugin:编译出现错误,跳过输出阶段
- OccurrenceOrderPlugin
- SideEffectsFlagPlugin
- UglifyJsPlugin:js代码压缩
在 development 模式下,会默认启用下面这些插件:
- process.env.NODE_ENV 的值设为 development
- devtool 设置为 evel
- NamedChunksPlugin
- NamedModulesPlugin
entry
既然是模块化开发,就需要有一个入口文件,相关的模块就可以根据这个入口文件形成一个树形的依赖关系。
当然 webpack 还没有智能到可以自动识别出你的模块依赖关系,所以需要咱们来告诉它,如果你不告诉它,则会默认把 src/index.js
(webpack4.x+) 当做入口文件。
入口文件可以是一个文件(string):
// webpack.config.js
module.exports = {
entry: 'src/main.js'
}
也可以是多个文件(array):
// webpack.config.js
module.exports = {
entry: ['src/login.js', 'src/logout.js']
}
甚至也可以是一个对象(object):
// webpack.config.js
module.exports = {
entry: {
login: 'src/login.js',
logout: 'src/logout.js',
}
}
这三种写法的区别是:
- 传入一个文件(string)的时候,会把所有具有依赖关系的模块打包生成一个文件;
- 传入多个文件(array)的时候,还是会打包生成一个文件,webpack 会把这些文件合并在一起,但是执行的时候会按照数组内文件的顺序依次执行;
- 传入对象的时候,则会根据对象key的个数,打包出对应数量的文件;
很显然,传入对象的方式更复杂,但也更利于扩展,同时也适合用来打包多页应用。
output
有进必有出,webpack 也需要我们指定打包后的文件存放位置,也叫做出口文件,和 entry 一样,output 也有默认值 dist/main.js
(webpack4.x+) 。
下面是 output 常见的配置项:
// webpack.config.js
module.exports = {
output: {
path: __dirname + '/dist',
filename: '[name].bundle.js',
publicPath: '/assets/'
}
}
- path
指定打包后的文件存放位置,注意这是一个 绝对路径 !上面的例子中用了 node 内置的常量 __dirname ,该常量表示当前执行文件所在的目录,所以我们打包出的文件就存放在和 webpack 配置文件同级的 dist
目录下面。
- filename
打包后的文件名称,该选项有5个可配置项:
配置项 | 作用 |
---|---|
[name] | 模块名称,对应 entry 中的 key 值,如果 entry 传入的是 string 或 array 默认为 main |
[id] | 模块id,由 webpack 生成 |
[hash] | 模块的 hash 值,当有文件修改时,这个值就会重新计算并改变 |
[chunkhash] | 这也是一个 hash 值,webpack中每打包生成一个文件,就叫一个chunk ,它是 chunk 本身的 hash ,通常用它来做文件缓存 |
补充一个小知识,如果 entry 中传入的是对象,且对象的 key 值像这种形式 "a/b"
,并且在 output.filename
中设置了 [name]
那么打包出的文件会存放在 a
文件夹下的 b.js
中(a/b.js
)。
- publicPath
关于这个配置,笔者曾经纠结了好久,知道它的作用,却总是无法理解,在网上看了很多关于 publicPath 的介绍,包括 webpack 的官网,但一直没有豁然开朗的感觉,直到后来在自己的项目中遇到了一些问题,才算是明白了为什么会有这个选项。
如果不想看下面这些内容,可以直接查看 总结 ,建议第一次阅读的时候跳过下面这一小段,等到了 devServer.publicPath 再回过来看一遍。
这里我们可以反向的分析一下。首先,在设置了 path 和 filename 这两个属性之后,便可以确定打包出的文件在本机存放的具体路径了。然后需要明确一点,打包出的代码需要上传到 Web 服务器上,这些文件中可能有 .css .js .png 等等,它们最终都要以 .html 为载体,假设这个文件是 index.html 就像这样:
注意:index.html 是通过 html-webpack-plugin 插件生成的,下文会介绍到。
<!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>webpack</title>
<link href="index.css" rel="stylesheet">
</head>
<body>
<img src="hello.png" />
<script type="text/javascript" src="index.bundle.js"></script>
</body>
</html>
从这段简单的 html 中我们可以得到一个信息,那就是 index.css
、 hello.png
、 index.bundle.js
这个三个文件都放在相对于 index.html 的 同一级别下面,就像这样:
dist
├── index.html
├── hello.png
├── index.css
└── index.bundle.js
如果我们的 webpack 设置是下面这样的:
// webpack.config.js
module.exports = {
entry: {
app: './src/main.js',
},
output: {
path: __dirname + '/dist',
filename: '[name].bundle.js'
}
}
那么打包出来的文件结构应该是和上面的一模一样的,我们按照这个结构上传到 Web 服务器,不需要修改什么就可以直接访问了。
一般公司都有专门的 cdn 服务器,那么你可以把 index.css
、 hello.png
、 index.bundle.js
这些资源传到 cdn 服务器上,假设你 cdn 地址是 https://mycdn.com
那么你可以通过 https://mycdn.com/index.css
的方式来访问相应的资源。这时候为了使我们的网站不报错,我们就需要将 index.html 中的资源引用方式改为 :
<link href="https://mycdn.com/index.css" rel="stylesheet">
<img src="https://mycdn.com/hello.png" />
<script type="text/javascript" src="https://mycdn.com/index.bundle.js"></script>
很明显,这种打包完成后还需要手动修改的方式很智障,而如果我们不想做这样修改的话,只需要将 output.publicPath 设置为 https://mycdn.com/ ,便会在打包出来的 index.html 文件内自动加上 output.publicPath 设置的值。
还有一种情况,就是笔者所遇到的情况了。假设你的项目还是传统的开发方式,并没有采用前后端分离,用的还是后端模板的方式。而你作为一个前端开发,你想要模块化开发所以引入了 webpack 来打包,后端的哥们跟你说,你把你打包出的文件放在咱们项目的 static 文件夹下面就行了。一开始都没什么问题,但是在做某一个功能的时候,你发现打包出来的文件体积有点大,而且有些代码可以通过按需加载的方式拆分一下,这时候你想到了用 import()
来动态加载,于是除了打包出了 index.js
,还有一些需要动态加载的 .js 文件,你把它们都放进了 static 下面。但是在调试的时候却发现,那些需要按需加载的资源无法加载了,全都是 404 ,咦?怎么回事小老弟!打开控制台看一下,所有 404 的资源地址都是 https://test.com/assets/xxx.js
。干!说好的 static
怎么变成了 /assets/
了?后端的哥们跟你说,这是后端框架的原因,虽然你是放在 static 下面,但是请求的时候请求的是 相对于当前页面的 /assets/
这个路径 ,总之 后端没法改,需要前端想办法解决。这个时候,我们只要把 output.publicPath 设置为 /assets/
就可以解决这个问题了。
总结:
这个选项默认是 ''
,一般情况是不需要修改的。但是在有些情况下,打包出的资源部署上线后,可能会出现 404
访问不到的情况。这个时候就需要配置一下这个选项来解决这个问题了。
如果你将打包后的资源上传到 cdn 上面,那么需要将它设置为可以通过 cdn 方式访问的地址,比如 publicPath: 'https://mycdn.com/assets/'
;
如果你的项目在服务器上面目录结构和你打包出的文件结构不一样,比如你打包出来的 .html 和 .js 是平级的,但是在服务器上却把 .js 文件都放在 /assets 下面,那你需要设置为 publicPath: /assets/'
。
所以这个值并不会影响你打包出的文件路径,它只是用来设置在线上运行的时候,所请求的资源相对于 服务 /html页面 的路径。
简单的说,在线上运行的时候,所请求的资源具体路径是 https://你的域名/publicPath/资源
或者 https://你设置的cdn地址/资源
。
output 的常用配置项就这三个,如果你想用 webpack 把你的代码打包成类库,你还需要配置一下 output.library
、 output.libraryTarget
等,不过笔者建议直接使用 rollup 打包类库。
所以如果有下面这样一份 webpack 配置文件:
// webpack.config.js
module.exports = {
entry: {
app: './src/main.js',
},
output: {
path: __dirname + '/dist',
filename: '[name].bundle.js'
}
}
会打包出如下这些文件:
project
├── src // 源代码文件夹
│ ├── main.js
│ ├── login.js
│ └── logout.js
├── dist // 打包后生成的文件夹
│ └── app.bundle.js
└── webpack.config.js // webpack 位置文件
总结:本篇介绍了前端模块化的发展历史和如何使用 webpack 打包实现模块化,那么在打包过程中 webpack 是如何多不同的文件进行解析、编译、处理的呢?在写一篇中将会详细的介绍。