使用 Webpack 4 优化前端交付
随着最新的 Webpack 主版本(4.x 版)的发布,我们已经达到了无需配置即可开始使用的阶段。它默认进行了优化 (#0CJS!)。因此,之前需要手动添加和配置的插件(例如CommonsChunkPlugin
、UglifyjsWebpackPlugin
等)现在都由 Webpack 在后台自动实例化,让一切变得轻松便捷!
不过,我们仍然可以做一些事情来确保充分利用 Webpack。让我们一一介绍一下。
模式
Webpack 4 提供两种模式:production
和development
。使用 标志运行 webpack--mode development|production
或在配置文件中设置 会默认启用一系列优化:
选项 | 描述 |
---|---|
development |
提供process.env.NODE_ENV 价值development 。启用NamedChunksPlugin 和NamedModulesPlugin 。 |
production |
提供process.env.NODE_ENV 价值production 。启用FlagDependencyUsagePlugin 、、、、和。FlagIncludedChunksPlugin ModuleConcatenationPlugin NoEmitOnErrorsPlugin OccurrenceOrderPlugin SideEffectsFlagPlugin UglifyJsPlugin |
因此无需手动包含这些插件或NODE_ENV
使用进行设置DefinePlugin
,使用时一切都会得到处理mode
。
顺便说一句,如果您仍然想将自定义参数传递给UglifyJsPlugin
(我发现自己想要这样做),您可以通过安装它来实现:npm install uglifyjs-webpack-plugin --save-dev
然后在 Webpack 配置中指定您的自定义参数:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
if (process.env.NODE_ENV === 'production') {
config.optimization = {
minimizer: [
new UglifyJsPlugin({
parallel: true,
cache: true,
sourceMap: true,
uglifyOptions: {
compress: {
drop_console: true
}
},
}),
],
};
}
这基本上会用您自己的实例覆盖 Webpack 的默认最小化器实例,以便您可以完全控制它。
该配置将确保 uglifier 以并行模式运行,缓存输出以便在下次构建时重用,生成源映射,并在生产模式下运行时隐藏控制台中的注释。您可以在此处找到可用选项的完整列表。
哈希
默认情况下,Webpack 不会将缓存清除哈希值添加到输出文件名(例如index.7eeea311f7235e3b9a17.js
)。因此,下次发布时,您的用户可能无法获得最新代码,从而导致许多奇怪的行为和错误。
因此,为了在每次构建后刷新您的资产,您可以hash
在文件名中添加:
module.exports = {
entry: {
vendor: './src/vendor.js',
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name].[hash].js'
}
};
虽然仔细想想,这似乎有点太过分了。如果你的 没有任何变化vendor.js
,Webpack 最好能更智能一些,只更新那些发生变化的块的哈希值。这样,即使没有任何变化,客户端也不必在每次推送新版本时重新下载所有资源。
为了确保这一点,Webpack 提供了chunkhash
Chunkhash 机制。Chunkhash 是基于每个入口点的内容,而不是整个构建的。使用起来也同样简单:
module.exports = {
...
output: {
...
filename: '[name].[chunkhash].js'
}
};
这将确保我们两全其美。当新版本发布时,客户端将获取更新的文件,同时仍然使用未更改文件的缓存版本。
巴别塔
转译
由于并非所有浏览器都支持 ES6/7/Next 特性,因此在浏览器上导航哪些可行、哪些不可行很快就会变成一个雷区:
这就是 Babel 的作用所在。它提供了一些很棒的插件,通过将现代 JS 转译(转换)成可以在我们指定的每个浏览器上运行的代码,使编写现代 JavaScript 变得轻而易举。
您可以通过安装以下内容进行设置:npm install babel-core babel-loader babel-preset-env --save-dev
现在,您可以在项目文件夹的根目录中以简单的英语(使用browserslist语法)告诉 Babel 我们想要定位哪些浏览器:.babelrc
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 9"]
}
}]
]
}
这可以通过使用env 预设来实现,它会根据您指定的环境自动确定您需要的 Babel 插件。
最后,我们要让 Webpack 知道我们想要用 Babel 转换所有的 JavaScript:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
cacheDirectory: true
}
}
}
]
}
};
现在您可以放心使用所有最新的 JavaScript 语法,因为 Babel 会处理浏览器兼容性。
动态导入
因此,使用 Babel 的下一个优势与性能相关。我们可以使用它的动态导入插件,仅在需要时异步加载大型依赖项,也就是延迟加载。这会显著减少入口文件的大小,因为 Webpack 无需一次性加载整个依赖树。
您可以通过安装来设置:npm install syntax-dynamic-import --save-dev
然后将其添加到您的.babelrc
{
"presets": [
...
]
"plugins": ["syntax-dynamic-import"]
}
现在模块看起来像这样:
import foo from 'foo'
import bar from 'bar'
import baz from 'baz'
const myfun = () => {
//Do something with the modules here
}
可以转换为:
const myfun = () => {
return Promise.all([
import('foo'),
import('bar'),
import('baz'),
]).then(([foo, bar, baz]) => {
//Do something with the modules here
});
};
Webpack 会识别这些动态导入,并将它们拆分成单独的代码块。一旦在运行时调用,它们就会异步加载myfun
。这将确保我们的初始代码块大小保持较小,并且客户端无需下载甚至可能不需要的资源。
附注:如果您使用的是 Vue,则可以使用异步组件开箱即用地支持此功能,但当然,如果您要处理具有各种框架的大型应用程序,仅靠这些框架是不够的,因此您需要像这样的通用解决方案。
预载
现在我们已经实现了最佳的代码拆分,但一个缺点是客户端仍然需要在运行时加载这些依赖项,这可能会降低应用的响应速度。因此,在上面的示例中,当我们调用 时myfun
,客户端必须先加载foo
,bar
然后baz
才能执行该函数。
如果我们可以在后台预加载这些依赖项,这样当我们调用时,myfun
这些依赖项就已经可用并准备就绪了,那会怎么样?这就是预加载插件的作用所在。
它使用Preload网络标准以声明的方式让浏览器知道即将需要某个特定资源,以便它可以开始加载它。
您可以通过安装进行设置:npm install --save-dev preload-webpack-plugin html-webpack-plugin
然后将其添加到您的 Webpack 配置中:
const PreloadWebpackPlugin = require('preload-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin(),
new PreloadWebpackPlugin({
rel: 'preload',
include: 'asyncChunks'
})
]
就是这样!现在我们所有的异步代码块都会被添加到 HTML 中并预加载,如下所示:
<link rel="preload" as="script" href="chunk.31132ae6680e598f8879.js">
<link rel="preload" as="script" href="chunk.d15e7fdfc91b34bb78c4.js">
<link rel="preload" as="script" href="chunk.acd07bf4b982963ba814.js">
从 Webpack 4.6+ 开始,此功能已内置,您可以使用内联导入指令手动指定要预加载或预取的依赖项,Webpack 会自动将其输出为资源命中,而无需安装我上面提到的插件。
因此,您需要在上面的导入语句中进行以下更改:
import("foo");
import("bar")
将是这样的:
import(/* webpackPrefetch: true */ "foo");
import(/* webpackPreload: true */ "bar")
因此,这归结为一个偏好问题,您是否想使用预加载插件从整个项目的配置文件中管理您的预加载偏好,或者是否想将其留给各个开发人员并让他们决定哪些依赖项应该预加载/预取,在这种情况下无需安装任何特殊的东西。
最后,您需要仔细考虑是否要使用预取或预加载。这取决于资源和应用程序上下文。我建议您阅读Addy Osmani的这篇精彩文章,以了解两者之间的细微差别。但一般来说:
预加载您确信会在当前页面使用的资源。预取未来 跨多个导航边界
导航时可能用到的资源。
分析器
现在我们已经了解了一些优化 Webpack 设置的方法,接下来我们需要在添加更多代码和依赖项时密切关注打包过程,以确保其始终处于最佳状态。我最喜欢的两个工具是:
您可以通过安装进行设置:npm install --save-dev webpack-bundle-analyzer
然后将其添加到您的 Webpack 配置中:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
if (process.env.NODE_ENV !== 'production') {
config.plugins.push(new BundleAnalyzerPlugin())
}
下次在开发模式下启动 webpack-dev-server 时,您可以导航到http://localhost:8888来查看如上所示的 bundle 可视化效果
这是我第二喜欢的工具,它呈现与 Webpack Bundle Analyzer 相同的信息,但方式略有不同,此外还提供了一种随时间监控捆绑包历史记录的方法。
您可以通过安装进行设置:npm install --save-dev webpack-monitor
然后将其添加到您的 Webpack 配置中:
const WebpackMonitor = require('webpack-monitor');
// ...
plugins: [
new WebpackMonitor({
capture: true, // -> default 'true'
target: '../monitor/myStatsStore.json', // default -> '../monitor/stats.json'
launch: true, // -> default 'false'
port: 3030, // default -> 8081
excludeSourceMaps: true // default 'true'
}),
],
您可以像以前的插件一样在开发中运行它,或者也可以在生产版本中运行它并将输出导出到某个地方,以便您可以分析生产包随时间的变化情况。
结论
好了,就这些了!希望通过这些技巧,你能够显著减少打包体积并提升性能。欢迎告诉我进展如何。还有什么我遗漏的技巧吗?欢迎在下方留言!
这篇文章最初发表在我的博客上。如果您喜欢这篇文章,请在社交媒体上分享并在Twitter上关注我!
文章来源:https://dev.to/jesalg/optimizing-front-end-delivery-with-webpack-4-1mm4