webpack中文网(六)迁移到新版本、使用环境变量、构建性能
文章目录
- webpack中文网(六)迁移到新版本、使用环境变量、构建性能
- 1. 迁移到新版本
- `resolve.root`, `resolve.fallback`, `resolve.modulesDirectories`
- `resolve.extensions`
- `resolve.*`
- `module.loaders` 改为 `module.rules`
- 链式 loader
- 取消「在模块名中自动添加 `-loader` 后缀」
- `json-loader` 不再需要手动添加
- 配置中的 loader 默认相对于 context 进行解析
- 移除 `module.preLoaders` 和 `module.postLoaders`
- `UglifyJsPlugin` sourceMap
- `UglifyJsPlugin` warnings
- `UglifyJsPlugin` 压缩 loaders
- 移除 `DedupePlugin`
- `BannerPlugin` - 破坏性改动
- 默认加载 `OccurrenceOrderPlugin`
- `ExtractTextWebpackPlugin` - 破坏性改动
- 全动态 require 现在默认会失败
- `require.ensure` 以及 AMD `require` 将采用异步式调用
- 通过 `options` 配置 loader
- `LoaderOptionsPlugin` context
- `debug`
- ES2015 的代码分割
- 动态表达式
- 混合使用 ES2015、AMD 和 CommonJS
- Hints
- 配置中使用 Promise
- Loader 变更
- 2. 使用环境变量
- 3. 构建性能
总结:
1. 迁移到新版本
以下各节描述从 webpack 1 到 webpack 2 的重大变化。
webpack 从 1 到 2 的变化,比从 2 到 3 要少很多,所以版本迁移起来难度应该不大。如果你遇到了问题,请查看 更新日志 以了解更多细节。
随着 webpack 2 版本已经发布一段时间,此章节的内容可能会在不久的将来被转移到博客文章中。最重要的是,webpack 3 版本最近发布,webpack 4 版本即将发布。如上所述,大家最好是参考 更新日志 去进行相应的版本迁移。
resolve.root
, resolve.fallback
, resolve.modulesDirectories
这些选项被一个单独的选项 resolve.modules
取代。更多用法请查看解析。
resolve: {
- root: path.join(__dirname, "src")
+ modules: [
+ path.join(__dirname, "src"),
+ "node_modules"
+ ]
}
resolve.extensions
此选项不再需要传一个空字符串。此行为被迁移到 resolve.enforceExtension
。更多用法请查看解析。
resolve.*
这里更改了几个 API。由于不常用,不在这里详细列出。更多用法请查看解析。
module.loaders
改为 module.rules
旧的 loader 配置被更强大的 rules 系统取代,后者允许配置 loader 以及其他更多选项。 为了兼容旧版,module.loaders
语法仍然有效,旧的属性名依然可以被解析。 新的命名约定更易于理解,并且是升级配置使用 module.rules
的好理由。
module: {
- loaders: [
+ rules: [
{
test: /\.css$/,
- loaders: [
- "style-loader",
- "css-loader?modules=true"
+ use: [
+ {
+ loader: "style-loader"
+ },
+ {
+ loader: "css-loader",
+ options: {
+ modules: true
+ }
+ }
]
},
{
test: /\.jsx$/,
loader: "babel-loader", // 这里不再使用 "use"
options: {
// ...
}
}
]
}
链式 loader
就像在 webpack 1 中,loader 可以链式调用,上一个 loader 的输出被作为输入传给下一个 loader。 使用 rule.use 配置选项,use
可以设置为一个 loader 数组。 在 webpack 1 中,loader 通常被用 !
连写。这一写法在 webpack 2 中只在使用旧的选项 module.loaders
时才有效。
module: {
- loaders: [{
+ rules: [{
test: /\.less$/,
- loader: "style-loader!css-loader!less-loader"
+ use: [
+ "style-loader",
+ "css-loader",
+ "less-loader"
+ ]
}]
}
取消「在模块名中自动添加 -loader
后缀」
在引用 loader 时,不能再省略 -loader
后缀了:
module: {
rules: [
{
use: [
- "style",
+ "style-loader",
- "css",
+ "css-loader",
- "less",
+ "less-loader",
]
}
]
}
你仍然可以通过配置 resolveLoader.moduleExtensions
配置选项,启用这一旧有行为,但是我们不推荐这么做。
+ resolveLoader: {
+ moduleExtensions: ["-loader"]
+ }
了解这一改变背后的原因,请查看 #2986。
json-loader
不再需要手动添加
如果没有为 JSON 文件配置 loader,webpack 将自动尝试通过 json-loader
加载 JSON 文件。
module: {
rules: [
- {
- test: /\.json/,
- loader: "json-loader"
- }
]
}
我们决定这么做是为了消除 webpack、 node.js 和 browserify 之间的环境差异。
配置中的 loader 默认相对于 context 进行解析
在 webpack 1 中,默认配置下 loader 解析相对于被匹配的文件。然而,在 webpack 2 中,默认配置下 loader 解析相对于 context
选项。
这解决了「在使用 npm link
或引用 context
上下文目录之外的模块时,loader 所导致的模块重复载入」的问题。
你可以移除掉那些为解决此问题的 hack 方案了:
module: {
rules: [
{
// ...
- loader: require.resolve("my-loader")
+ loader: "my-loader"
}
]
},
resolveLoader: {
- root: path.resolve(__dirname, "node_modules")
}
移除 module.preLoaders
和 module.postLoaders
module: {
- preLoaders: [
+ rules: [
{
test: /\.js$/,
+ enforce: "pre",
loader: "eslint-loader"
}
]
}
UglifyJsPlugin
sourceMap
UglifyJsPlugin
的 sourceMap
选项现在默认为 false
而不是 true
。这意味着如果你在压缩代码时启用了 source map,或者想要让 uglifyjs 的警告能够对应到正确的代码行,你需要将 UglifyJsPlugin
的 sourceMap
设为 true
。
devtool: "source-map",
plugins: [
new UglifyJsPlugin({
+ sourceMap: true
})
]
UglifyJsPlugin
warnings
UglifyJsPlugin
的 compress.warnings
选项现在默认为 false
而不是 true
。 这意味着如果你想要看到 uglifyjs 的警告信息,你需要将 compress.warnings
设为 true
。
devtool: "source-map",
plugins: [
new UglifyJsPlugin({
+ compress: {
+ warnings: true
+ }
})
]
UglifyJsPlugin
压缩 loaders
UglifyJsPlugin
不再压缩 loaders。在未来很长一段时间里,需要通过设置 minimize:true
来压缩 loaders。参考 loader 文档里的相关选项。
loaders 的压缩模式将在 webpack 3 或后续版本中取消。
为了兼容旧的 loaders,loaders 可以通过插件来切换到压缩模式:
plugins: [
+ new webpack.LoaderOptionsPlugin({
+ minimize: true
+ })
]
移除 DedupePlugin
不再需要 webpack.optimize.DedupePlugin
。请从配置中移除。
BannerPlugin
- 破坏性改动
BannerPlugin
不再接受两个参数,而是只接受单独的 options 对象。
plugins: [
- new webpack.BannerPlugin('Banner', {raw: true, entryOnly: true});
+ new webpack.BannerPlugin({banner: 'Banner', raw: true, entryOnly: true});
]
默认加载 OccurrenceOrderPlugin
OccurrenceOrderPlugin
现在默认启用,并已重命名(在 webpack 1 中为 OccurenceOrderPlugin
)。 因此,请确保从你的配置中删除该插件:
plugins: [
// webpack 1
- new webpack.optimize.OccurenceOrderPlugin()
// webpack 2
- new webpack.optimize.OccurrenceOrderPlugin()
]
ExtractTextWebpackPlugin
- 破坏性改动
ExtractTextPlugin 需要使用版本 2,才能在 webpack 2 下正常运行。
npm install --save-dev extract-text-webpack-plugin
这一插件的配置变化主要体现在语法上。
ExtractTextPlugin.extract
module: {
rules: [
{
test: /.css$/,
- loader: ExtractTextPlugin.extract("style-loader", "css-loader", { publicPath: "/dist" })
+ use: ExtractTextPlugin.extract({
+ fallback: "style-loader",
+ use: "css-loader",
+ publicPath: "/dist"
+ })
}
]
}
new ExtractTextPlugin({options})
plugins: [
- new ExtractTextPlugin("bundle.css", { allChunks: true, disable: false })
+ new ExtractTextPlugin({
+ filename: "bundle.css",
+ disable: false,
+ allChunks: true
+ })
]
全动态 require 现在默认会失败
只有一个表达式的依赖(例如 require(expr)
)将创建一个空的 context 而不是一个完整目录的 context。
这样的代码应该进行重构,因为它不能与 ES2015 模块一起使用。如果你确定不会有 ES2015 模块,你可以使用 ContextReplacementPlugin
来指示 compiler 进行正确的解析。
Link to an article about dynamic dependencies.
在 CLI 和配置中使用自定义参数
如果你之前滥用 CLI 来传自定义参数到配置中,比如:
webpack --custom-stuff
// webpack.config.js
var customStuff = process.argv.indexOf("--custom-stuff") >= 0;
/* ... */
module.exports = config;
你将会发现新版中不再允许这么做。CLI 现在更加严格了。
替代地,现在提供了一个接口来传递参数给配置。我们应该采用这种新方式,在未来许多工具将可能依赖于此。
webpack --env.customStuff
module.exports = function(env) {
var customStuff = env.customStuff;
/* ... */
return config;
};
详见 CLI。
require.ensure
以及 AMD require
将采用异步式调用
现在这些函数总是异步的,而不是当 chunk 已经加载完成的时候同步调用它们的回调函数(callback)。
require.ensure
现在依赖于原生的 Promise
。如果在不支持 Promise 的环境里使用 require.ensure
,你需要添加 polyfill。
通过 options
配置 loader
你不能再通过 webpack.config.js
的自定义属性来配置 loader。只能通过 options
来配置。下面配置的 ts
属性在 webpack 2 下不再有效:
module.exports = {
...
module: {
rules: [{
test: /\.tsx?$/,
loader: 'ts-loader'
}]
},
// 在 webpack 2 中无效
ts: { transpileOnly: false }
}
什么是 options
?
好问题。严格来说,有两种办法,都可以用来配置 webpack 的 loader。典型的 options
被称为 query
,是一个可以被添加到 loader 名之后的字符串。它比较像一个 query string,但是实际上有更强大的能力:
module.exports = {
...
module: {
rules: [{
test: /\.tsx?$/,
loader: 'ts-loader?' + JSON.stringify({ transpileOnly: false })
}]
}
}
不过它也可以分开来,写成一个单独的对象,紧跟在 loader 属性后面:
module.exports = {
...
module: {
rules: [{
test: /\.tsx?$/,
loader: 'ts-loader',
options: { transpileOnly: false }
}]
}
}
LoaderOptionsPlugin
context
有的 loader 需要从配置中读取一些 context 信息。在未来很长一段时间里,这将需要通过 loader options 传入。详见 loader 文档的相关选项。
为了保持对旧 loaders 的兼容,这些信息可以通过插件传进来:
plugins: [
+ new webpack.LoaderOptionsPlugin({
+ options: {
+ context: __dirname
+ }
+ })
]
debug
在 webpack 1 中 debug
选项可以将 loader 切换到调试模式(debug mode)。在未来很长一段时间里,这将需要通过 loader 选项传递。详见 loader 文档的相关选项。
loaders 的调试模式将在 webpack 3 或后续版本中取消。
为了保持对旧 loaders 的兼容,loader 可以通过插件来切换到调试模式:
- debug: true,
plugins: [
+ new webpack.LoaderOptionsPlugin({
+ debug: true
+ })
]
ES2015 的代码分割
在 webpack 1 中,可以使用 require.ensure
作为实现应用程序的懒加载 chunks 的一种方法:
require.ensure([], function(require) {
var foo = require("./module");
});
ES2015 模块加载规范定义了 import()
方法,可以在运行时(runtime)动态地加载 ES2015 模块。webpack 将 import()
作为分割点(split-point)并将所要请求的模块(requested module)放置到一个单独的 chunk 中。import()
接收模块名作为参数,并返回一个 Promise。
function onClick() {
import("./module").then(module => {
return module.default;
}).catch(err => {
console.log("Chunk loading failed");
});
}
好消息是:如果加载 chunk 失败,我们现在可以进行处理,因为现在它基于 Promise
。
动态表达式
可以传递部分表达式给 import()
。这与 CommonJS 对表达式的处理方式一致(webpack 为所有可能匹配的文件创建 context)。
import()
为每一个可能的模块创建独立的 chunk。
function route(path, query) {
return import(`./routes/${path}/route`)
.then(route => new route.Route(query));
}
// 上面代码为每个可能的路由创建独立的 chunk
混合使用 ES2015、AMD 和 CommonJS
你可以自由混合使用三种模块类型(甚至在同一个文件中)。在这个情况中 webpack 的行为和 babel 以及 node-eps 一致:
// CommonJS 调用 ES2015 模块
var book = require("./book");
book.currentPage;
book.readPage();
book.default === "This is a book";
// ES2015 模块调用 CommonJS
import fs from "fs"; // module.exports 映射到 default
import { readFileSync } from "fs"; // 从返回对象(returned object+)中读取命名的导出方法(named exports)
typeof fs.readFileSync === "function";
typeof readFileSync === "function";
值得注意的是,你需要让 Babel 不解析这些模块符号,从而让 webpack 可以使用它们。你可以通过设置如下配置到 .babelrc 或 babel-loader 来实现这一点。
.babelrc
{
"presets": [
["es2015", { "modules": false }]
]
}
Hints
不需要改变什么,但有机会改变。
模板字符串
webpack 现在支持表达式中的模板字符串了。这意味着你可以在 webpack 构建中使用它们:
- require("./templates/" + name);
+ require(`./templates/${name}`);
配置中使用 Promise
webpack 现在支持在配置文件中返回 Promise
了。这让你能在配置文件中做异步处理。
webpack.config.js
module.exports = function() {
return fetchLangs().then(lang => ({
entry: "...",
// ...
plugins: [
new DefinePlugin({ LANGUAGE: lang })
]
}));
};
高级 loader 匹配
webpack 现在支持对 loader 进行更多方式的匹配。
module: {
rules: [
{
resource: /filename/, // 匹配 "/path/filename.js"
resourceQuery: /^\?querystring$/, // 匹配 "?querystring"
issuer: /filename/, // 如果请求 "/path/filename.js" 则匹配 "/path/something.js"
}
]
}
更多的 CLI 参数项
你可以使用一些新的 CLI 参数项:
--define process.env.NODE_ENV="production"
见 DefinePlugin
。
--display-depth
显示每个模块到入口的距离。
--display-used-exports
显示一个模块中被使用的 exports 信息。
--display-max-modules
设置输出时显示的模块数量(默认是 15)。
-p
能够定义 process.env.NODE_ENV
为 "production"
。
Loader 变更
以下变更仅影响 loader 的开发者。
Cacheable
Loaders 现在默认可被缓存。Loaders 如果不想被缓存,需要选择不被缓存。
// 缓存 loader
module.exports = function(source) {
- this.cacheable();
return source;
}
// 不缓存 loader
module.exports = function(source) {
+ this.cacheable(false);
return source;
}
复合 options
webpack 1 只支持能够「可 JSON.stringify
的对象」作为 loader 的 options。
webpack 2 现在支持任意 JS 对象作为 loader 的 options.
webpack 2.2.1之前(即从 2.0.0 到 2.2.0),使用复合 options,需要在 options
对象上添加 ident
,允许它能够被其他 loader 引用。这在 2.2.1 中被删除,因此目前的迁移不再需要使用 ident
键。
{
test: /\.ext/
use: {
loader: '...',
options: {
- ident: 'id',
fn: () => require('./foo.js')
}
}
}
2. 使用环境变量
要在开发和生产构建之间,消除 webpack.config.js
的差异,你可能需要环境变量。
webpack 命令行环境配置中,通过设置 --env
可以使你根据需要,传入尽可能多的环境变量。在 webpack.config.js
文件中可以访问到这些环境变量。例如,--env.production
或 --env.NODE_ENV=local
(NODE_ENV
通常约定用于定义环境类型,查看这里)。
webpack --env.NODE_ENV=local --env.production --progress
如果设置
env
变量,却没有赋值,--env.production
默认将--env.production
设置为true
。还有其他可以使用的语法。有关详细信息,请查看 webpack CLI 文档。
然而,你必须对 webpack 配置进行一处修改。通常,module.exports
指向配置对象。要使用 env
变量,你必须将 module.exports
转换成一个函数:
webpack.config.js
module.exports = env => {
// Use env.<YOUR VARIABLE> here:
console.log('NODE_ENV: ', env.NODE_ENV) // 'local'
console.log('Production: ', env.production) // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
}
进一步阅读
3. 构建性能
本指南包含一些改进构建/编译性能的实用技巧。
常规
无论你正在 development 或构建 production,以下做法应该帮助到你达到最佳。
保持版本最新
使用最新的 webpack 版本。我们会经常进行性能优化。 webpack 的最新稳定版本是:
保持最新的 Node.js 也能够保证性能。除此之外,保证你的包管理工具 (例如 npm
或者 yarn
) 为最新也能保证性能。较新的版本能够建立更高效的模块树以及提高解析速度。
Loaders
将 loaders 应用于最少数的必要模块中。而不是:
{
test: /\.js$/,
loader: "babel-loader"
}
使用 include
字段仅将 loader 模块应用在实际需要用其转换的位置中:
{
test: /\.js$/,
include: path.resolve(__dirname, "src"),
loader: "babel-loader"
}
Bootstrap
每个额外的 loader/plugin 都有启动时间。尽量少使用不同的工具。
解析
以下几步可以提供解析速度:
- 尽量减少
resolve.modules
,resolve.extensions
,resolve.mainFiles
,resolve.descriptionFiles
中类目的数量,因为他们会增加文件系统调用的次数。 - 如果你不使用 symlinks ,可以设置
resolve.symlinks: false
(例如npm link
或者yarn link
). - 如果你使用自定义解析 plugins ,并且没有指定 context 信息,可以设置
resolve.cacheWithContext: false
。
Dlls
使用 DllPlugin
将更改不频繁的代码进行单独编译。这将改善引用程序的编译速度,即使它增加了构建过程的复杂性。
Smaller = Faster
减少编译的整体大小,以提高构建性能。尽量保持 chunks 小巧。
- 使用 更少/更小 的库。
- 在多页面应用程序中使用
CommonsChunksPlugin
。 - 在多页面应用程序中以
async
模式使用CommonsChunksPlugin
。 - 移除不使用的代码。
- 只编译你当前正在开发部分的代码。
Worker Pool
thread-loader
可以将非常消耗资源的 loaders 转存到 worker pool 中。
不要使用太多的 workers ,因为 Node.js 的 runtime 和 loader 有一定的启动开销。最小化 workers 和主进程间的模块传输。进程间通讯(IPC)是非常消耗资源的。
持久化缓存
使用 cache-loader
启用持久化缓存。使用 package.json
中的 "postinstall"
清除缓存目录。
自定义 plugins/loaders
这里不对它们配置的性能问题作过多赘述。
Development
下面步骤对于 development 特别有用。
增量编译
使用 webpack 的监听模式。不要使用其他工具来监听你的文件和调用 webpack 。在监听模式下构建会记录时间戳并将信息传递给编译让缓存失效。
在某些设置中,监听会回退到轮询模式。有许多监听文件会导致 CPU 大量负载。在这些情况下,你可以使用 watchOptions.poll
来增加轮询的间隔。
在内存中编译
以下几个实用工具通过在内存中进行代码的编译和资源的提供,但并不写入磁盘来提高性能:
webpack-dev-server
webpack-hot-middleware
webpack-dev-middleware
Devtool
需要注意的是不同的 devtool
的设置,会导致不同的性能差异。
"eval"
具有最好的性能,但并不能帮助你转译代码。- 如果你能接受稍差一些的 mapping 质量,可以使用
cheap-source-map
选项来提高性能 - 使用
eval-source-map
配置进行增量编译。
=> 在大多数情况下,cheap-module-eval-source-map
是最好的选择。
避免在生产环境下才会用到的工具
某些实用工具, plugins 和 loaders 都只能在构建生产环境时才有用。例如,在开发时使用 UglifyJsPlugin
来压缩和修改代码是没有意义的。以下这些工具在开发中通常被排除在外:
UglifyJsPlugin
ExtractTextPlugin
[hash]
/[chunkhash]
AggressiveSplittingPlugin
AggressiveMergingPlugin
ModuleConcatenationPlugin
最小化入口 chunk
webpack 只会在文件系统中生成已经更新的 chunk 。对于某些配置选项(HMR, [name]
/[chunkhash]
in output.chunkFilename
, [hash]
)来说,除了更新的 chunks 无效之外,入口 chunk 也不会生效。
应当在生成入口 chunk 时,尽量减少入口 chunk 的体积,以提高性能。下述代码块将只提取包含 runtime 的 chunk ,其他 chunk 都作为子模块:
new CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
})
Production
以下步骤在 production 中非常有用。
不要为了非常小的性能增益,牺牲你应用程序的质量! 请注意,优化代码质量在大多数情况下比构建性能更重要。
多个编译时
当进行多个编译时,以下工具可以帮助到你:
parallel-webpack
: 它允许编译工作在 worker 池中进行。cache-loader
: 缓存可以在多个编译时之间共享。
Source Maps
Source maps 真的很消耗资源。你真的需要他们?
工具相关问题
下列工具存在某些可能会降低构建性能的问题。
Babel
- 项目中的 preset/plugins 数量最小化。
TypeScript
- 在单独的进程中使用
fork-ts-checker-webpack-plugin
进行类型检查。 - 配置 loaders 跳过类型检查。
- 使用
ts-loader
时,设置happyPackMode: true
/transpileOnly: true
。
Sass
node-sass
中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用thread-loader
时,需要设置workerParallelJobs: 2
。