前端开发已转向模块化方法,从而提高了代码库的封装和结构。工具成为任何项目的重要组成部分,现在有很多选择。
Webpack 因其强大的功能和可扩展性在过去几年中获得了普及,但一些开发者发现其配置过程令人困惑且难以采用。
我们将逐步从一个空的配置文件开始,到一个简单但完整的项目捆绑设置。本文假设您对 CommonJS 表示法以及模块的工作原理有基本的了解。
概念
与大多数捆绑器不同,Webpack 背后的动机是收集所有依赖项(不仅是代码,还有其他资产)并生成依赖关系图。
起初,看到一个 `.js` 文件需要一个样式表,或者一个样式表检索一个修改过的图像就像它是一个模块一样,这可能看起来很奇怪,但这些允许 Webpack 理解捆绑包中包含的内容,并帮助您转换和优化它们。
安装
让我们首先添加我们将要使用的初始包
npm install webpack webpack-dev-server --save-dev
接下来,我们在项目的根目录中创建一个 `webpack.config.js` 文件,并在我们的 `package.json` 文件中添加两个脚本,用于本地开发和生产发布。
"scripts": {
"start": "webpack-dev-server",
"build": "webpack"
}
Webpack 命令将拾取我们刚刚创建的配置文件,除非我们指示其他操作。
入口
有很多方法可以指定我们的“入口点”,它将是我们依赖关系图的根源。
最简单的方法是传递一个字符串
var baseConfig = {
entry: './src/index.js'
};
如果将来需要多个入口,我们也可以传递一个对象。
var baseConfig = {
entry: {
main: './src/index.js'
}
};
我推荐最后一个,因为它随着项目的增长而具有更好的可扩展性。
输出
Webpack 中的输出是一个对象,它保存了我们的捆绑包和资产将要放置的路径,以及入口将采用的名称。
var path = require('path');
var baseConfig = {
entry: {
main: './src/index.js'
},
output: {
filename: 'main.js',
path: path.resolve('./build')
}
};
// export configuration
module.exports = baseConfig;
如果您使用对象定义 **入口**,而不是使用字符串硬编码输出文件名,则可以执行以下操作
output: {
filename: '[name].js',
path: path.resolve('./build')
}
这样,当添加新入口时,Webpack 将拾取其键来形成文件名。
仅使用这少量配置,我们就可以运行服务器并在本地使用 `npm start` 或 `npm run build` 进行开发,以捆绑我们的代码以供发布。通过了解项目的依赖关系,webpack-dev-server 将监视它们,并在检测到其中一个依赖关系发生更改时重新加载站点。
加载器
Webpack 的目标是处理我们所有的依赖项。
// index.js file
import helpers from '/helpers/main.js';
// Hey Webpack! I will need these styles:
import 'main.css';
什么?在 JavaScript 中需要样式表?是的!但是捆绑器仅准备开箱即用地处理 JavaScript 依赖项。这就是“加载器”登场的地方。
加载器提供了一种简单的方法来拦截我们的依赖项并在它们被捆绑之前对其进行预处理。
var baseConfig = {
// ...
module: {
rules: [
{
test: /* RegEx */,
use: [
{
loader: /* loader name */,
query: /* optional config object */
}
]
}
]
}
};
为了使加载器工作,我们需要一个正则表达式来识别我们想要修改的文件,以及一个字符串或一个数组,其中包含我们想要使用的加载器。
样式
为了允许 Webpack 在需要时处理我们的样式,我们将安装 **css** 和 **style** 加载器。
npm install --save-dev css-loader style-loader
**css-loader** 将样式解释为依赖项,而 **style-loader** 将在捆绑包加载时自动在页面上包含一个 `<style>` 标签。
var baseConfig = {
entry: {
main: './src/index.js'
},
output: {
filename: '[name].js',
path: path.resolve('./build')
},
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
}
};
在此示例中,`main.css` 将首先通过 **css-loader**,然后通过 **style-loader**。
预处理器
添加对 LESS 或任何其他预处理器的支持就像安装相应的加载器并将其添加到规则一样简单。
rules: [
{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{ loader: 'less-loader' }
]
}
]
转译
JavaScript 也可以通过加载器进行转换。一个例子是使用 Babel 加载器来转译我们的脚本。
rules: [
{
test: /\.js$/,
use: [
{ loader: 'babel-loader' }
]
}
]
图像
Webpack 具有一个很棒的功能,它可以检测样式表中的 `url()` 语句,并让加载器对图像文件和 url 本身应用更改。
// index.less file
@import 'less/vars';
body {
background-color: @background-color;
color: @text-color;
}
.logo {
background-image: url('./images/logo.svg');
}
通过添加一个规则,我们可以应用 **file-loader** 来简单地复制文件,或者使用 **url-loader**,后者将图像内联为 base64 字符串,除非它超过字节限制,在这种情况下,它将用相对路径替换 url 语句,并将文件复制到输出位置。
{
test: /\.svg$/,
use: [
{
loader: 'url-loader',
query: { limit : 10000 }
}
]
}
加载器可以通过传递一个包含选项的 `query` 对象来配置,就像这里我们配置加载器在文件大小超过 10Kb 之前内联文件一样。
通过这种方式管理我们的构建过程,我们将只包含必要的资源,而不是移动一个包含大量可能或可能不会在我们的项目中使用的文件的假设资产文件夹。
如果您使用 React 或类似的库,您可以使用 **svg-inline-loader** 在组件中需要 `.svg` 文件。
插件
Webpack 包含默认行为来捆绑大多数类型的资源。当加载器不足时,我们可以使用插件来修改或添加 Webpack 的功能。
例如,Webpack 默认情况下将我们的样式包含在我们的捆绑包中,但我们可以通过引入插件来更改这一点。
提取资产
插件的常见用途是提取生成的样式表并像平时一样使用 `<link>` 标签加载它。
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var lessRules = {
use: [
{ loader: 'css-loader' },
{ loader: 'less-loader' }
]
};
var baseConfig = {
// ...
module: {
rules: [
// ...
{ test: /\.less$/, use: ExtractTextPlugin.extract(lessRules) }
]
},
plugins: [
new ExtractTextPlugin('main.css')
]
};
生成 `index.html` 文件
当构建单页面应用时,我们通常需要一个.html
文件来提供服务。
HtmlWebpackPlugin 会自动创建一个`index.html`文件,并为每个生成的 bundle 添加 script 标签。它还支持模板语法并且高度可配置。
var HTMLWebpackPlugin = require('html-webpack-plugin');
var baseConfig = {
// ...
plugins: [
new HTMLWebpackPlugin()
]
};
构建生产环境
定义环境
许多库会引入在开发时有用的警告,但在我们的生产 bundle 中没有用处,并且会增加 bundle 的大小。
Webpack 带有一个内置插件,可以在 bundle 内部设置全局常量。
var ENV = process.env.NODE_ENV;
var baseConfig = {
// ...
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(ENV)
})
]
};
现在我们需要在我们的命令中指定环境
"scripts": {
"start": "NODE_ENV=development webpack-dev-server",
"build": "NODE_ENV=production webpack"
}
process.env.NODE_ENV
将被替换为一个字符串,允许压缩器消除无法访问的开发代码分支。
这对于在代码库中引入团队的警告非常有用,并且它们不会进入生产环境。
if (process.env.NODE_ENV === 'development') {
console.warn('This warning will dissapear on production build!');
}
压缩
在生产环境中,我们需要为用户提供尽可能快的产品。通过缩小我们的代码并删除不必要的字符,这减少了 bundle 的大小并提高了加载时间。
最流行的工具之一是 UglifyJS,Webpack 带有一个内置插件来传递我们的代码。
// webpack.config.js file
var ENV = process.env.NODE_ENV;
var baseConfig = {
// ...
plugins: []
};
if (ENV === 'production') {
baseConfig.plugins.push(new webpack.optimize.UglifyJsPlugin());
}
总结
Webpack 配置文件非常有用,文件复杂度取决于你的需求。注意将其组织好,因为随着项目增长,它们可能会变得难以管理。
在本文中,我们从一个空白的配置文件开始,最终得到一个基本设置,它允许你在本地开发并发布生产代码。Webpack 还有更多内容可以探索,但这些关键部分和概念可以帮助你更好地了解它。
如果你想更深入地了解,我推荐 Webpack 官方文档,该文档已针对其第二个大版本进行了更新和改进。
我刚接触 webpack,但我相信你可以传递
-p
标志用于生产环境,它会将节点环境设置为生产环境并使用 UglifyJS(--optimize-minimize
),即使它没有包含在配置文件中。Webpack 中的
-p
标志一直存在很多混淆,这就是我在这里没有包含它的原因。我选择这种方式来展示如何在 bundle 内部设置任何常量以及如何应用插件。如果你创建一个小型示例项目,你会发现此设置与仅使用
-p
标志之间存在差异。根据我的经验,
-p
标志会缩小代码,但process.env.NODE_ENV
没有设置为生产环境。因此,为了避免任何混淆,我采用了这种更一致且详细的方式。
很棒的介绍!我想知道是否有人知道如何使用 Webpack 但同时将模块暴露给 bundle 外部的遗留代码?例如,我有一个经常在其他代码中使用的 Gbase.Utilities 模块,这些代码不会在一段时间内迁移。它像 Gbase.Utilities.IsMobile() 一样“静态”地使用,但如果我无法保留这些全局变量以供旧代码使用,我就无法过渡到 Webpack。
非常感谢!
James
ProvidePlugin?
https://webpack.js.cn/plugins/provide-plugin/
或者可能是 expose 加载器:https://github.com/webpack-contrib/expose-loader
我不知道我对这种情况是否了解得足够透彻,但 Webpack 提供了许多选项。
一种方法是通过 script 标签加载库的生产就绪版本并将其设置为外部库,Webpack 会将命名空间设置为全局变量,直到你迁移到模块/安装方法。
这是在 Webpack 中设置 externals 的参考:https://webpack.js.cn/guides/author-libraries/#add-externals
感谢阅读 :)
我正在使用 sass 和 sass-resources-loader 插件
然后我不必在每个 sass 文件中导入变量或 mixin。
我不知道是否还有类似于 sass-resources-loader 的 LESS 版本?
我认为 LESS 没有这样的东西,但肯定有人应该构建它;)
也许这超出了你的文章范围,但可能值得向人们解释为什么 Webpack 是一个合理或有用的选择,什么是依赖关系图,以及 Webpack 背后的通用概念,然后再直接跳转到定义环境。
我想我只是想知道你是否计划从基础开始构建,因为我觉得本教程假设 Webpack 新手可能不太熟悉。我所了解的大部分 webpack 知识都来自开发经验,相当于将看起来可行的东西拼凑起来,并希望它们都能正确捆绑。很想以“正确”的方式学习它。 :)
嗨,Kat。感谢你的建议。
当我开始写作时,我自己也想知道是否需要介绍捆绑器和依赖关系图,这是否适合这篇文章。但在某个时刻,我决定对于很多人来说,这将是太多的理论,而对于其他人来说则是多余的。
当然,我们每个人都不同,并且来自不同的背景,因此你可能会发现这篇文章不完整是可以理解的。我假设阅读本文的人知道什么是捆绑器,并且对它们的工作原理有所了解。
我基本上专注于扩展到一个实用的 Webpack 简单配置,以展示其基本内容。如果你想了解更多关于捆绑器及其作用的信息,这是一篇不错的文章:https://jvandemo.com/a-10-minute-primer-to-javascript-modules-module-formats-module-loaders-and-module-bundlers/
Jeremías,很棒的文章!!!!
谢谢老兄!
非常清晰,易于上手 webpack
这正是这篇文章的目的,帮助人们消除恐惧,开始使用 Webpack 配置。谢谢 Thien!
很棒的文章,不过以下是我的一些建议。至少在开头,要向读者明确脚本和 webpack 配置代码的位置(例如在 package.json 或 webpack.config.js 中)。对于初学者(我)来说,这可能并不那么明显。
此外,我相信有一段代码会导致错误。当你声明 lessRules 变量时,测试正则表达式包含在变量中,它也在规则中声明。当我尝试按照步骤操作时,它抛出了一个错误。但是,一旦我从变量中删除了 test 属性,它就可以正常编译了。
感谢你的建议和错误报告,Richard,我将尽快修复。
main.css
和main.js
应该在源文件树的哪个位置创建?我指的是
/helpers/main.js
,而不是构建目录中的main.js
。(也许这些文件名应该叫不同的名字以避免混淆。)是的,这确实可能会造成混淆,我将探索不同的命名方式。感谢阅读 :)
我以编写 Chrome 开发者工具文档为生,只是想告诉你,这是一篇很棒的入门文章。我确实从这篇文章中获得了对 Webpack 好处以及如何使用它的深刻理解。我将仔细研究这篇文章,以弄清楚它为什么如此有效;)
嘿,Kayce,谢谢!来自技术作家的赞美对我来说意义重大。如果您需要帮助,请告诉我 :)
很棒的文章!你真是知识的源泉!
哈哈,谢谢! :)