概念
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。
它会递归地构建一个依赖关系图(dependency graph),包含应用程序需要的每个模块,然后将这些模块打包成一个或多个 bundle。
一、入口(entry)
- 口起点(entry point)指示 webpack 应该使用哪个模块,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。每个依赖项随即被处理,最后输出到称之为 bundles 的文件中,
- 可以通过在 webpack 配置中配置
entry
属性,来指定一个入口起点(或多个入口起点)。默认值为./src
。
webpack.config.js
module.exports = {
entry: './path/to/my/entry/file.js'
};
entry 属性的单个入口语法,是下面的简写:
const config = {
entry: {
main: './path/to/my/entry/file.js'
}
};
分离 应用程序(app) 和 第三方库(vendor) 入口
const config = {
entry: {
app: './src/app.js',
vendors: './src/vendors.js'
}
};
这种方式比较常见于,只有一个入口起点(不包括 vendor)的单页应用程序中。
多页面应用程序
const config = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
};
为每个页面间的应用程序共享代码创建 bundle。由于入口起点增多,多页应用能够复用入口起点之间的大量代码/模块
根据经验:每个 HTML 文档只使用一个入口起点。
二、出口(output)
- output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。
- 基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。
- 即使可以存在多个入口起点,但只指定一个输出配置。
- 你可以通过在配置中指定一个 output 字段,来配置这些处理过程:
webpack.config.js
const config = {
output: {
filename: 'bundle.js',
path: '/home/proj/public/assets'
}
};
module.exports = config;
在 webpack 中配置 output 属性的最低要求是,将它的值设置为一个对象,包括以下两点:
- filename 用于输出文件的文件名。
- 目标输出目录 path 的绝对路径。
多个入口起点
如果配置创建了多个单独的 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用占位符来确保每个文件具有唯一的名称。
{
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
}
// 写入到硬盘:./dist/app.js, ./dist/search.js
三、模式
- 提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。
- 通过选择 development 或 production 之中的一个。
module.exports = {
mode: 'production'
};
- 或者从 [CLI]参数中传递:
webpack --mode=production
四、loader
- loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
- loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能
- 在 webpack 的配置中 loader 有两个目标:
1、test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
2、use 属性,表示进行转换时,应该使用哪个 loader。 - 你可以使用 loader 告诉 webpack 加载 CSS 文件,或者将 TypeScript 转为 JavaScript。为此,首先安装相对应的 loader:
npm install --save-dev css-loader
npm install --save-dev ts-loader
然后指示 webpack 对每个 .css
使用 css-loader
,以及对所有 .ts
文件使用 `ts-loadert
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
{ test: /\.ts$/, use: 'ts-loader' }
]
}
};
在 webpack 配置中定义 loader 时,要定义在 module.rules 中
module.rules
允许你在 webpack 配置中指定多个 loader。
在你的应用程序中,有三种使用 loader 的方式:
- 配置(推荐):在 webpack.config.js 文件中指定 loader。
- 内联:在每个
import
语句中显式指定 loader。 - CLI:在 shell 命令中指定它们。
可以在 import
语句或任何等效于 "import" 的方式中指定 loader。
使用 !
将资源中的 loader 分开。分开的每个部分都相对于当前目录解析。
import Styles from 'style-loader!css-loader?modules!./styles.css';
通过前置所有规则及使用 !,可以对应覆盖到配置中的任意 loader。
尽可能使用 module.rules,因为这样可以减少源码中的代码量,并且可以在出错时,更快地调试和定位 loader 中的问题。
loader 特性
- loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。
- loader 可以是同步的,也可以是异步的。
- loader 接收查询参数。用于对 loader 传递配置。
- loader 也能够使用
options
对象进行配置。 - 除了使用
package.json
常见的main
属性,还可以将普通的 npm 模块导出为 loader,做法是在package.json
里定义一个loader
字段。 - 插件(plugin)可以为 loader 带来更多特性。
- loader 能够产生额外的任意文件。
- loader 通常被命名为 xxx-loader(例如 json-loader)。
五、插件(plugins)
- 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。
- 想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。
- 你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。
module.exports = {
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
webpack 插件是一个具有 apply
属性的 JavaScript 对象。apply
属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。
即便使用 Node API,用户也应该在配置中传入 plugins 属性。compiler.apply 并不是推荐的使用方式。
六、配置(configuration)
因为 webpack 配置是标准的 Node.js CommonJS 模块,你可以做到以下事情:
- 通过 require(...) 导入其他文件
- 通过 require(...) 使用 npm 的工具函数
- 使用 JavaScript 控制流表达式,例如 ?: 操作符
- 对常用值使用常量或变量
- 编写并执行函数来生成部分配置
虽然技术上可行,但应避免以下做法:
- 导出不确定的值(调用 webpack 两次应该产生同样的输出文件)
- 编写很长的配置(应该将配置拆分为多个文件)
你可能已经注意到,很少有 webpack 配置看起来很完全相同。这是因为 webpack 的配置文件,是导出一个对象的 JavaScript 文件。此对象,由 webpack 根据对象定义的属性进行解析。
七、模块(modules)
- 在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。
- 每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。
什么是 webpack 模块
对比 Node.js 模块,webpack 模块能够以各种方式表达它们的依赖关系,几个例子如下:
- ES2015 import 语句
- CommonJS require() 语句
- AMD define 和 require 语句
- css/sass/less 文件中的 @import 语句。
- 样式(url(...))或 HTML 文件()中的图片链接(image url)
支持的模块类型
webpack 通过 loader 可以支持各种语言和预处理器编写模块。 webpack 社区已经为各种流行语言和语言处理器构建了 loader,包括:
- CoffeeScript
- TypeScript
- ESNext (Babel)
- Sass
- Less
- Stylus
总的来说,webpack 提供了可定制的、强大和丰富的 API,允许任何技术栈使用 webpack,保持了在你的开发、测试和生成流程中无侵入性(non-opinionated)。
八、模块解析(module resolution)
使用 enhanced-resolve,webpack 能够解析三种文件路径:
绝对路径
import "/home/me/file";
import "C:\\Users\\me\\file";
- 由于我们已经取得文件的绝对路径,因此不需要进一步再做解析。
相对路径
import "../src/file1";
import "./file2";
- 在这种情况下,使用 import 或 require 的资源文件(resource file)所- 在的目录被认为是上下文目录(context directory)。
- 在 import/require 中给定的相对路径,会添加此上下文路径(context path),以产生模块的绝对路径(absolute path)。
模块路径
import "module";
import "module/lib/file";
模块将在 resolve.modules 中指定的所有目录内搜索。 你可以替换初始模块路径,此替换路径通过使用 resolve.alias 配置选项来创建一个别名。
根据上述规则解析路径后,解析器(resolver)将检查路径是否指向文件或目录。
如果路径指向一个文件:
- 如果路径具有文件扩展名,则被直接将文件打包。
- 否则,将使用 [resolve.extensions] 选项作为文件扩展名来解析,此选项告诉解析器在解析中能够接受哪些扩展名(例如 .js, .jsx)。
如果路径指向一个文件夹,则采取以下步骤找到具有正确扩展名的正确文件:
- 如果文件夹中包含
package.json
文件,则按照顺序查找resolve.mainFields
配置选项中指定的字段。并且package.json
中的第一个这样的字段确定文件路径。 - 如果
package.json
文件不存在或者package.json
文件中的 main 字段没有返回一个有效路径,则按照顺序查找 [resolve.mainFiles
]配置选项中指定的文件名,看是否能在 import/require 目录下匹配到一个存在的文件名。 - 文件扩展名通过
resolve.extensions
选项采用类似的方法进行解析。
九、构建目标(targets)
因为服务器和浏览器代码都可以用 JavaScript 编写,所以 webpack 提供了多种构建目标(target),你可以在你的 webpack 配置中设置。
要设置 target 属性,只需要在你的 webpack 配置中设置 target 的值。
module.exports = {
target: 'node'
};
在上面例子中,使用 node webpack 会编译为用于「类 Node.js」环境(使用 Node.js 的 require ,而不是使用任意内置模块(如 fs 或 path)来加载 chunk)。
多个 Target
尽管 webpack 不支持向 target 传入多个字符串,你可以通过打包两份分离的配置来创建同构的库:
var path = require('path');
var serverConfig = {
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'lib.node.js'
}
//…
};
var clientConfig = {
target: 'web', // <=== 默认是 'web',可省略
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'lib.js'
}
//…
};
module.exports = [ serverConfig, clientConfig ];
上面的例子将在你的 dist 文件夹下创建 lib.js 和 lib.node.js 文件。
十、模块热替换(hot module replacement)
模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块而无需重新加载整个页面。
主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面时丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。
在应用程序中
通过以下步骤,可以做到在应用程序中置换(swap in and out)模块:
- 应用程序代码要求 HMR runtime 检查更新。
- HMR runtime(异步)下载更新,然后通知应用程序代码。
- 应用程序代码要求 HMR runtime 应用更新。
- HMR runtime(同步)应用更新。
你可以设置 HMR,以使此进程自动触发更新,或者你可以选择要求在用户交互时进行更新。
在编译器中
除了普通资源,编译器(compiler)需要发出 "update",以允许更新之前的版本到新的版本。"update" 由两部分组成:
- 更新后的 manifest(JSON)
- 一个或多个更新后的 chunk (JavaScript)
manifest 包括新的编译 hash 和所有的待更新 chunk 目录。每个更新 chunk 都含有对应于此 chunk 的全部更新模块(或一个 flag 用于表明此模块要被移除)的代码。
在模块中
- HMR 是可选功能,只会影响包含 HMR 代码的模块。
举个例子,通过style-loader
为 style 样式追加补丁。为了运行追加补丁,style-loader
实现了 HMR 接口;当它通过 HMR 接收到更新,它会使用新的样式替换旧的样式。 - 如果一个模块没有 HMR 处理函数,更新就会冒泡(bubble up)。这意味着一个简单的处理函数能够对整个模块树(complete module tree)进行更新。
- 如果在这个模块树中,一个单独的模块被更新,那么整组依赖模块都会被重新加载。