请注意:本文编写于 2019-03-05,其中某些信息可能已经失去时效性。
导读
写这篇文章是为了让自己在自学 webpack
的过程中有所产出,于是边读 webpack 中文文档 边写下了这篇文章,里面的很多实例都是直接挪用的文档中的实例,但在一些概念的理解上我加入了自己的想法「未必精确」,所以读的时候要抱着「怀疑的态度」。
文章内容不仅仅是简单的「概念堆叠」,还有一些「重点」概念的「深入理解」,不过篇幅有限我不希望这篇文章变成一份冗长的伪文档,所以全部的内容都是围绕 webpack
的 4个 核心概念延展开来的,每个配置后面我都会尽量跟上一个实例以更加形象的展示配置的具体作用。
站在我的角度上,读完这篇文章并不能让你精通 webpack 但是理解 webpack 中的重要概念,自己编写一个 webpack.config.js 配置文件还是可以的。
webpack 简介
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler)。在 webpack 处理应用程序时,它会在内部创建一个依赖图(dependency graph),用于映射到项目需要的每个模块,然后将所有这些依赖生成到一个或多个bundle。 来自 webpack 中文文档
目前都是使用一些成熟的 CLI
工具,一般都内置 webpack
所以我对 webpack
的认知一直比较少,只是大概的了解它是用来管理项目中的 .js
文件依赖,然后打包整个项目的。
核心概念
1. 入口(entry)
对应属性:entry
默认值:./src/index.js
作用说明:
用来规定 webpack
应该使用哪个模块作为构建内部依赖图的起点。
webpack
会找出所有「入口模块」(直接或间接)依赖的「模块」和 [library]。
代码示例:
代码语言:javascript复制// weboack.config.js
module.exports = {
entry: './path/to/entry/file.js'
}
2. 出口(output)
对应属性:output
主输出文件默路径:./dist/main.js
其他文件默认路径:./dist/<filename>
作用说明:
用来规定 webpack
在那里输出 bundles
以及如何命名这些文件。
// weboack.config.js
const path = require('path') // Node.js 核心模块,用于操作文件路径
代码示例:
module.exports = {
entry: './path/to/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '<WhateverYouLike>.js'
}
}
3. 处理器(loader)
对应属性:module->rules
作用说明:
作为开箱即用的自带特性,webpack
自身只支持处理 JavaScript
文件。而 loader
能够让 webpack
处理那些非 JavaScript
文件,并且先将它们转换为有效「模块」,然后添加到「依赖图」中,提供给应用程序使用。
属性特征:
test
: 利用「正则表达式」规定loader
用于哪些或哪个文件。use
: 规定运行时使用哪个loader
。
代码示例:
代码语言:javascript复制// webpack.config.js
const path = require('path')
module.exports = {
...
module: {
rules: [
{
test: /.txt$/,
use: 'raw-loader'
}
]
}
}
代码作用: 当运行包含 .txt 文件的 require() 或 import 语句时,在它打包之前,先使用 raw-loader 转换。
4. 插件(plugins)
对应属性:plugings
作用说明: 打包优化、资源管理和注入环境变量。
代码实例:
代码语言:javascript复制// webpack.config.js
const HtmlWebpackPlugn = require('html-webpack-plugin') // 提前通过 npm 安装
const webpack = require('webpack') //用于访问内置插件
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
}
核心概念解析及拓展
1. 入口(entry)
单入口及其简写
代码语言:javascript复制// webpack.config.js
module.exports = {
entry: {
main: './path/to/entry/file.js'
}
}
// 可简写为如下形式
module.exports = {
enrty: './path/to/enrty/file.js'
}
/*
* 当你需要为只有一个入口的应用程序或工具(library)快速设置 webpack 配置时,
* 简写会是个很不错的选择。然而,使用此语法在扩展配置时有失灵活性。
*/
思考:当你向 entry
传入一个数组时会发生什么?
解释:向 entry
传入「文件路径数组」将创建「多个主入口」。在你想要多个依赖文件一起注入,并且将它们的依赖导向到一个 chunk
时,传入数组的方式就很有用。
对象语法
用法:entry: {<enrtyChunkName: String>: <Path: String | Array>}
// webpack.config.js
module.exports = {
entry: {
app: './src/app.js',
vendors: './src/vendors.js'
}
}
// 对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式。
常见场景
1. 分离应用程序主体和第三方库
代码语言:javascript复制// webpack.config.js
module.exports = {
entry: {
app: './src/app.js'
vendors: './src/vendors.js'
}
}
/*
* webpack 从 app.js 和 vendors.js 开始创建依赖图。
* 这些依赖图是彼此完全分离、互相独立的(每个 bundle 中都有一个 webpack 引导)。
* 这种方式比较常见于,只有一个入口起点(不包括 vendor)的单页应用程序中。
*
* 此设置允许你使用 CommonsChunkPlugin 从应用程序依赖图中提取 vendor 到 vendor 依赖图,并把引用 vendor 的部分替换为 __webpack_require__() 调用。
* 如果应用程序依赖图中没有 vendor 代码,那么你可以在 webpack 中实现被称为长效缓存的通用模式。
* 说实话,目前看不懂上面这段话,所以也不晓得怎么通俗的表述。
*/
2. 多页面应用程序
代码语言:javascript复制// webpack.config.js
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
}
/*
* webpack 分离 3 个的依赖图
*
* 在多页应用中,每当页面跳转时服务器将为你获取一个新的 HTML 文档。
* 页面重新加载新文档,并且资源被重新下载。这给了我们特殊的机会去做很多事:
* 使用 CommonsChunkPlugin 使所有页面的应用程序共享代码创建依赖图,
* 入口增多,多页应用能够复用不同入口的大量重复代码/模块。
*/
2. 出口(output)
注意,即使可以存在多个入口,但只配置一个出口设置。
用法
在 webpack
中配置 output
的最低要求是,将它的值是一个包括以下两点的对象:
filename
: 输出文件的文件名。path
: 输出目录的绝对路径。
// webpack.config.js
module.exports = {
output: {
filename: '<WhateverYouLike>.js',
path: '/path/to/project'
}
}
// 此配置将一个单独的 .js 文件输出到 /path/to/project 目录中。
配合多个入口设置
如果配置创建了多个单独的入口,则应该使用 占位符 来确保每个文件具有唯一的名称。
代码语言:javascript复制// webpack.config.js
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js',
path: __dirname '/dist'
}
};
// 写入到硬盘:./dist/app.js, ./dist/search.js
常用占位符
内部ID:[id]
入口名称:[name]
基于构建的hash(每次构建都会改变):[hash]
基于内容的hash(文件内容改变才会改变):[chunkhash]
高级进阶
官网所谓高级进阶其实就是利用哈希占位符构建随版本迭代的文件命名方式这里不展示了。
比较有用的是如何动态设置 publicPath
:
首先,何为 publicPath,以及周边概念
output.publicPath
: 所有资源的基础路径,它被称为公共路径,以/
结束,示例:
// webpack.config.js
module.exports = {
...
output: {
publicPath: '/assets/',
chunkFilename: '[id].chunk.js'
}
};
/*
* HTML loader 输入出:<link href="/assets/spinner.gif" />
* CSS:background-image: url(/assets/spinner.gif);
* 静态资源最终访问路径 = output.publicPath loader 或插件等配置路径
*/
devServer.publicPath
: 确定从哪里提供 bundle
假设服务器运行在 http://localhost:8080
并且 output.filename
被设置为 bundle.js
。默认 publicPath
是 /
,所以你的包可以通过 http://localhost:8080/bundle.js
访问。
可以修改通过 devServer.publicPath
来修改请求资源时的服务器前缀,示例:
// webpack.config.js
module.exports = {
...
devServer: {
publicPath: '/assets/'
}
};
/*
* 现在可以通过 http://localhost:8080/assets/bundle.js 访问 bundle。
* 确保 publicPath 总是以斜杠(/)开头和结尾。
* devServer.publicPath 也可以是一个完整的 URL。
* 一般情况下都要保证 devServer.publicPath 与 output.publicPath 保持一致。
*/
devServer.contentBase
: 告诉服务器从哪里提供内容,只有在提供静态文件时才需要
默认情况下,将使用当前工作目录作为提供内容的目录,但是你可以修改为其他目录,示例:
代码语言:javascript复制// webpack.config.js
module.exports = {
...
devServer: {
// 推荐使用绝对路径。
contentBase: path.join(__dirname, 'public')
}
};
// 也可以从多个目录提供内容
module.exports = {
...
devServer: {
contentBase: [path.join(__dirname, 'public'), path.join(__dirname, 'assets')]
}
};
// 具体作用不详,官网并没有给出说明也懒得查了
其次,如何动态设置 publicPath
代码语言:javascript复制// webpack.config.js
...
const BASE_URL = process.env.NODE_ENV === 'production'
? '/'
: '/'
module.exports = {
...
publicPath: BASE_URL,
...
}
// 方法来自 iview-admin vue.config.js
// 我不知道我理解的动态设置对不对,不过官网给的 __webpack_public_path__ 我没看明白
3. 处理器(loader)
loader
用于对模块的源代码进行转换,可以使你在「载入」模块时预处理文件。
loader
类似于其他构建工具中「任务(task
)」,提供了处理前端构建步骤的方法。
loader
可以将文件从不同的语言(如 TypeScript
)转换为 JavaScript
,或将内联图像转换为 data URL
。允许你直接在 JavaScript
模块中 import CSS
文件。
示例
配置 loader
使 webpack
加载 CSS
文件,或者将 TypeScript
转为 JavaScript
。
首先安装相对应的 loader
:
npm install --save-dev css-loader
npm install --save-dev ts-loader
然后配置 webpack
对每个 .css
使用 css-loader
,所有 .ts
文件使用 ts-loader
:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{ test: /.css$/, use: 'css-loader' },
{ test: /.ts$/, use: 'ts-loader' }
]
}
}
使用 loader 的三种方式
- 配置:在
webpack.config.js
文件中指定loader
。(推荐)
前面展示过了,这里就不重复了。
- 内联:在每个
import
语句中显式指定loader
。
可以在 import
语句或任何等效于 import
的方式中指定 loader
。使用 !
将资源中的 loader
分开。分开的每个部分都相对于当前目录解析,示例:
import Styles from 'style-loader!css-loader?modules!./styles.css';
- CLI:利用
shell
命令指定loader
。
webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
loader 特性
loader
支持链式传递。loader
链中每个loader
,都对前一个loader
处理后的资源进行转换。loader
链会按照相反的顺序执行。第一个loader
将(应用转换后的资源作为)返回结果传递给下一个loader
,依次这样执行下去。最终,在链中最后一个loader
,返回webpack
所预期的JavaScript
。loader
可以是同步的,也可以是异步的。loader
运行在Node.js
中,并且能够执行任何可能的操作。loader
接收查询参数,用于对loader
传递配置。loader
也能够使用 options 对象进行配置。- 除了使用
package.json
常见的main
属性,还可以将普通的npm
模块导出为loader
,做法是在package.json
里定义一个loader
字段。 - 插件可以为
loader
带来更多特性。 loader
能够产生额外的任意文件。
解析 loader
loader
遵循标准的 模块解析。多数情况下,loader
将从模块路径(通常将模块路径认为是 node_modules
)解析。
loader
模块需要导出为一个函数,并且使用 Node.js
兼容的 JavaScript
编写。通常使用 npm
进行管理,但是也可以将自定义 loader
作为应用程序中的文件。按照约定,loader
通常被命名为 xxx-loader
(例如 json-loader
)。有关详细信息,请查看 如何编写 loader?。
4. 插件(plugins)
插件是 webpack
的支柱功能。webpack
自身也构建于插件系统之上。
插件目的在于解决 loader
无法实现的其他事。
剖析
webpack
插件是一个具有 apply
方法的 JavaScript
对象。apply
属性会被 webpack compiler
调用,并且 compiler
对象可在整个编译生命周期访问。
// ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
// compiler hook 的 tap 方法的第一个参数,应该是驼峰式命名的插件名称。
// 建议为此使用一个常量,以便它可以在所有 hook 中复用。
compiler.hooks.run.tap(pluginName, compilation => {
console.log('webpack 构建过程开始!');
});
}
}
// 插件编写属于比较深入的内容,这里不过多探讨,目前仅需要知道实现原理即可
用法
由于插件可以携带参数/选项,你必须在 webpack
配置中,向 plugins
属性传入 new
实例。
配置写法
代码语言:javascript复制// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin') //通过 npm 安装
const webpack = require('webpack') //访问内置的插件
const path = require('path')
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
filename: 'my-first-webpack.bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.(js|jsx)$/,
use: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
}
了解更多
1. 模式(mode)
对应属性:mode | String
作用说明:
通过将 mode
参数设置为 development
, production
或 none
,可以启用对应环境下 webpack
内置的优化。默认值为 production
。
用法
- 在配置文件中设置
// webpack.config.js
module.exports = {
...
mode: 'production'
};
- 通过 CLI 参数设置
webpack --mode=production
支持模式
选项 | 描述 |
---|---|
development | 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。 |
production | 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin. |
None | 不选用任何默认优化选项 |
根据 mode 改变编译行为
代码语言:javascript复制// webpack.config.js
var config = {
entry: './app.js'
...
}
module.exports = (env, argv) => {
if (argv.mode === 'development') {
config.devtool = 'source-map';
}
if (argv.mode === 'production') {
...
}
return config
}
2. 模块(modules)
在模块化编程中,开发者将程序分解成离散功能块,并称之为「模块」。
每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。 精心编写的「模块」提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。
webpack
将「模块」的概念应用于项目中的任何文件。
什么是 webpack 模块
对比 Node.js
模块,webpack
「模块」能够以各种方式表达它们的依赖关系,几个例子如下:
样式:(url(...))
ES2015
: import
CommonJS
: require()
HTML
: <img src=...>
AMD
: define | require
css/sass/less
: @import
支持的模块类型
webpack
通过 loader
可以支持各种语言和预处理器编写模块。loader
描述了 webpack
如何处理「非 JavaScript(non-JavaScript) 模块」,并且在 bundle
中引入这些「依赖」。
目前 webpack
已经但不限于支持以下语言的 loader
:
- CoffeeScript
- TypeScript
- ESNext (Babel)
- Sass
- Less
- Stylus
3. 模块解析(module resolution)
resolver
是一个库,用于帮助找到模块的绝对路径。 它帮助 webpack
从每个如 require/import
语句中,找到需要引入到 bundle
中的模块代码。 当打包模块时,webpack
使用 enhanced-resolve
来解析文件路径。
webpack 中的解析规则
使用 enhanced-resolve
,webpack
能够解析三种文件路径:
1. 绝对路径
代码语言:javascript复制// 已经取得文件的绝对路径,因此不需要进一步再做解析。
import '/home/me/file';
import 'C:\Users\me\file';
2. 相对路径
代码语言:javascript复制// 在这种情况下,使用 import 或 require 的资源文件所在的目录,被认为是上下文目录。
// 在 import/require 中给定的相对路径,会拼接此上下文路径,以产生模块的绝对路径。
import '../src/file1';
import './file2';
3. 模块路径
代码语言:javascript复制import 'module';
import 'module/lib/file';
// 解释很啰嗦,感兴趣可以自己去看一下文档
缓存
每次文件系统访问都会被缓存,以便更快触发对同一文件的多个并行或串行请求。在 观察模式下,只有修改过的文件会从缓存中摘出。如果关闭观察模式,会在每次编译前清理缓存。
4. 依赖图(dependency graph)
任何时候,一个文件依赖于另一个文件,webpack
就把此视为文件之间有「依赖关系」。这使得 webpack
可以接收非代码资源(例如 images
或 web fonts
),并且可以把它们作为「依赖」提供给你的应用程序。
webpack
从命令行或配置文件中定义的「入口」开始,递归地构建一个依赖图,这个依赖图包含着应用程序所需的每个模块,然后将所有这些模块打包为少量可由浏览器加载的 bundle
(通常只有一个)。
5. 浏览器兼容性
webpack
支持所有 ES5
兼容(IE8
及以下不提供支持)的浏览器。webpack
的 import()
和 require.ensure()
需要环境中有 Promise
。如果你想要支持旧版本浏览器,你应该在使用这些 webpack
提供的表达式之前,先 加载一个 polyfill。
总结
通过整理这篇文档我已经对 webpack
有了一个初步的认识和了解了。
当然如果你要真正的在项目中投入使用 webpack
仅仅阅读这一篇文章是不够的,你还需要去深入地阅读了解文档里的各种配置参数和其他常用的前端构建工具或预处理器配合 webpack
进行调试使用。
前路漫漫,与君共勉。
参考
- webpack 中文文档
- 掘金:Webpack中publicPath详解-Mello_Z
- segmentfault:webpack output.publicPath 如何动态配置地址
- GitHub: iview-admin 源码