webpack 是什么?
简单来说,webpack 是一个 JavaScript 模块打包器。然而,自从它发布以来,它已经发展成为前端代码的管理器(无论是出于意愿还是社区的意愿)。
传统的任务运行器方式:你的标记、样式和 JavaScript 都是独立的。你必须单独管理它们,并且你的工作是确保所有内容都能正确地传递到生产环境。
例如 Gulp 这样的任务运行器可以处理许多不同的预处理器和转换器,但在所有情况下,它都会将源代码作为输入,并将其压缩为编译后的输出。但是,它在没有关注整个系统的情况下对每个文件都这样做。这是开发人员的负担:在任务运行器完成的工作基础上,找到所有这些移动部分在生产环境中相互配合的正确方式。
webpack 试图通过提出一个大胆的问题来减轻开发人员的负担:如果有一部分开发过程可以自行处理依赖关系会怎么样?如果我们可以仅以最终需要的内容为基础编写代码,那么构建过程将会自行管理?
webpack 的方式:如果 webpack 知道它,它只会将你实际使用的内容打包到生产环境中。
如果你过去几年一直是 Web 社区的一部分,你就已经知道了解决问题的首选方法:用 JavaScript 构建。因此,webpack 试图通过通过 JavaScript 传递依赖项来简化构建过程。但它的设计真正的力量不仅仅在于代码管理部分;它的管理层是 100% 有效的 JavaScript(具有 Node 特性)。webpack 使你能够编写有效的 JavaScript,它可以更好地了解整个系统。
换句话说:你不是为 webpack 编写代码,而是为你的项目编写代码。webpack 会跟上(当然需要一些配置)。
简而言之,如果你曾经遇到过以下任何问题:
- 按顺序加载依赖项
- 在生产环境中包含未使用的 CSS 或 JS
- 意外重复加载(或三重加载)库
- 遇到 CSS 和 JavaScript 的作用域问题
- 找到一个好的使用 NPM 包的系统,或者依靠疯狂的后端配置来充分利用 NPM
- 需要更好地优化资源交付,但担心会出现问题
那么你可以从 webpack 中受益。它通过让 JavaScript 处理依赖关系和加载顺序,而不是开发人员的大脑,轻松处理所有这些问题。最好的部分?webpack 是提前运行的,因此你仍然可以构建渐进式 Web 应用程序,或者不需要 JavaScript 运行的应用程序。
第一步
在本教程中,我们将使用 Yarn(brew install yarn
)而不是 npm
,但完全取决于你;它们都是一样的。从我们的项目文件夹中,在终端窗口中运行以下命令将 webpack 添加到我们的本地项目中:
yarn add webpack webpack-cli --dev
注意:推荐使用 NPM 脚本,但是为了简单起见,我们将在此博客文章中手动运行命令。请确保长期使用 NPM 脚本的自动化!
然后我们将在项目目录的根目录中使用 webpack.config.js
文件声明一个 webpack 配置:
// webpack.config.js
module.exports = {
entry: './src/app.js',
output: {
filename: 'app.bundle.js',
path: __dirname + '/dist'
}
};
注意:__dirname
指的是这个 webpack.config.js
所在的目录,在本博客文章中是项目根目录。
记住 webpack “知道”你的项目中的内容吗?它通过读取你的代码来了解它。webpack 基本上会执行以下操作:
- 从
context
文件夹开始,... - ... 它查找
entry
文件名... - ... 并读取内容。每个
import
(ES6)或require()
(Node)依赖项在解析代码时被发现时,它都会为最终构建打包。然后搜索这些依赖项及其依赖项,直到到达“树”的末端,只打包所需内容,不打包其他内容。 - 从那里,webpack 将所有内容打包到
output.path
文件夹中,并使用output.filename
命名模板进行命名([name]
将被entry
中的对象键替换)。
因此,如果我们的 src/app.js
文件看起来像这样(假设我们之前运行了 yarn add moment
):
import moment from 'moment';
console.log('Hello, world!');
console.log(moment().startOf('day').fromNow());
从终端运行:
node_modules/.bin/webpack --mode production src/app.js -o dist/app.bundle.js -p
注意:-p
标志是“生产”模式,它会压缩/缩小输出。当我们运行 node_modules/.bin/
时,我们在这个项目中运行本地版本的 webpack。这是首选的,因为随着时间和跨项目,我们可能安装了不同的 webpack 版本,这样可以确保不会出现任何问题。
它将输出一个 dist/app.bundle.js
文件,并将当前日期和时间记录到控制台。webpack自动知道 'moment'
指的是我们安装的NPM包。
提示:只需该字符串 'moment'
即可工作,因为在它的 package.json 中,main: 指向了核心库。如果没有这个,我们必须明确声明我们想要的库,例如:'moment/moment'
(我们可以省略末尾的 .js
)。有时候检查NPM包可能是值得的,因为它们可能包含可节省时间或文件大小的替代库,如果你不需要整个库的话。
使用多个文件
你可以通过修改 entry
对象来指定任意数量的入口/输出点。
多个文件打包在一起
const path = require('path');
const webpack = require('webpack');module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
app: ['./home.js', './events.js', './vendor.js'],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
};
所有这些文件将按数组顺序捆绑在一起成为一个 dist/app.bundle.js
文件。
多个文件,多个输出
const path = require('path');
const webpack = require('webpack');module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
home: './home.js',
events: './events.js',
contact: './contact.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
};
或者,你可以选择打包多个JS文件以拆分应用程序的各个部分。这将被打包成3个文件:dist/home.bundle.js
、dist/events.bundle.js
和dist/contact.bundle.js
。
厂商缓存
如果你想将你的厂商库分离到它们自己的包中,这样用户就不必在每次你进行小型应用程序更新时重新下载第三方依赖项,你可以很容易地做到这一点,感谢webpack内置的Commons Chunk插件:
const webpack = require('webpack');module.exports = {
entry: {
index: './index.js',
vendor: ['react', 'react-dom'],
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
}),
],
}
注意:确保CommonsChunkPlugin中的 vendor
与上面的 'vendor'
入口名称匹配,它可以是任何东西,只要它与一个 entry
键匹配即可。
在这里,你明确告诉webpack将你的 vendor
包作为通用块使用,为整个应用程序包含你的 react
和 react-dom
节点模块。在优化关键的大型应用程序中,如果你像这样限制了你的供应商包,这可能会产生更好的结果。
请注意,通过这样做,你应该在你的模板中在app之前加载 vendor
。webpack通常会发出类似于这样的东西:
Asset Size Chunks Chunk Names
vendor.bundle.js 230 kB 0 [emitted] vendor
app.bundle.js 173 kB 1 [emitted] [big] index
看到webpack有一个 0
块和一个 1
块吗?它告诉你它希望它们加载的顺序。如果你编写自己的HTML模板,你需要按正确的顺序包含 <script>
标签,或者你可以使用类似于HTML webpack Plugin这样的工具来处理这个问题。
编辑者注:本文先前的版本列出了第二个例子,以自动提取跨包重复模块,使用Commons Chunk插件。该示例已被删除,因为它对大多数初学者来说没有用处,除非你知道你在做什么,否则它可能会减慢你的应用程序。如果你想了解更多关于Commons Chunk插件的(许多)“隐藏”功能,请查看webpack的文档。
开发
webpack实际上有自己的开发服务器,所以无论你是开发静态站点还是只是原型化你的前端,它都非常适合。要运行它,只需将 devServer
对象添加到 webpack.config.js
:
module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
app: './app.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist', 'assets'),
publicPath: '/assets', // New
},
devServer: {
contentBase: path.resolve(__dirname, 'src'), // New
},
};
现在创建一个 src/index.html
文件,其中包含:
<script src="/assets/app.bundle.js"></script>
…然后从终端运行:
node_modules/.bin/webpack-dev-server
你的服务器现在在 localhost:8080
上运行。请注意,<em>/assets</em>
在脚本标记中与 <em>output.publicPath</em>
匹配,这样就可以从任何需要的地方加载资源(如果你使用CDN,这非常有用)。
webpack将热加载任何JavaScript更改,而无需刷新浏览器。但是,任何对 webpack.config.js
文件的更改都需要重新启动服务器才能生效。
全局访问方法
需要从全局命名空间使用一些函数吗?只需在 webpack.config.js
中设置 output.library
:
module.exports = {
output: {
library: 'myClassName',
}
};
…它将把你的包附加到一个 window.myClassName
实例上。因此,使用该名称空间,你可以调用该入口点可用的方法(你可以在文档中阅读更多关于此设置的内容)。
加载器
到目前为止,我们只涉及了JavaScript的工作。从JavaScript的角度来看,这是很重要的,因为这是webpack所支持的唯一语言。我们可以使用几乎任何文件类型,只要我们将其传递到JavaScript中。我们可以使用_加载器_来做到这一点。
加载器可以是诸如Sass之类的预处理器,也可以是诸如Babel之类的转换器。在NPM上,它们通常被命名为 *-loader
,例如 sass-loader
或 babel-loader
。
Babel + ES6
如果我们想在项目中通过Babel使用ES6,我们首先要在本地安装适当的加载器:
yarn add --dev babel-loader babel-core babel-preset-env
…然后将其添加到 webpack.config.js
中,这样webpack就知道在哪里使用它。
module.exports = {
// … module: {
rules: [
{
test: /\.js$/i,
exclude: [/node_modules/],
use: [{
loader: 'babel-loader',
options: { presets: ['env'] },
}],
},
// Loaders for other file types can go here ],
}, // …
};
webpack 1.x 用户请注意:_Loaders 的核心概念保持不变,但语法已经改进了。_这里使用了 /\.js$/
正则表达式来查找以 .js
结尾的文件,并通过 Babel 加载。webpack 依靠正则表达式测试来给你完全的控制权,不会限制你只使用文件扩展名或者假设你的代码必须按照某种特定的方式组织。
如果你发现某个加载器在破坏文件或者处理不该处理的内容,你可以指定一个 exclude
选项来跳过某些文件。在这里,我们排除了 node_modules
文件夹的 Babel 处理,因为我们不需要它。但是我们也可以将其应用到我们自己的项目文件中,例如如果我们有一个 my_legacy_code
文件夹。这并不会阻止你加载这些文件;相反,你只是让 webpack 知道可以按原样导入它们而不是处理它们。
CSS + Style Loader
如果我们只想在应用程序需要的时候加载 CSS,我们也可以这样做。假设我们有一个 index.js
文件,我们会从那里导入它:
import styles from './assets/stylesheets/application.css';
我们会得到以下错误:You may need an appropriate loader to handle this file type
。请记住,webpack 只能理解 JavaScript,所以我们需要安装适当的加载器:
yarn add --dev css-loader style-loader
然后在 webpack.config.js
中添加规则:
module.exports = {
// … module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}, // …
],
},
};
加载器按照 相反的数组顺序 处理。这意味着 <em>css-loader</em>
将在 <em>style-loader</em>
之前运行。
你可能会注意到,即使在生产构建中,这实际上也将你的 CSS 与捆绑的 JavaScript 一起打包,并且 style-loader
手动将你的样式写入到了 <head>
中。乍一看,它可能看起来有点奇怪,但是越想它就越有意义。你节省了一个头部请求——在某些连接上节省了宝贵的时间——如果你正在使用 JavaScript 加载 DOM,这本质上消除了 FOUC(未样式化内容的闪烁)。
你还会注意到,webpack 默认情况下已自动解析了所有的 @import
查询,将这些文件打包在一起(而不是依赖于 CSS 的默认导入,这可能会导致不必要的头部请求和缓慢加载的资源)。
从 JS 加载 CSS 真的很棒,因为现在你可以以强大的新方式将你的 CSS 模块化。假设你只通过 button.js
加载了 button.css
。这意味着如果 button.js
从未被使用,它的 CSS 将不会膨胀我们的生产构建。如果你遵循基于组件的 CSS 实践,例如 SMACSS 或 BEM,你会发现将 CSS 与你的标记和 JavaScript 更紧密地配对的价值。
CSS + Node Modules
我们可以使用 webpack 利用 Node 的 ~
前缀导入 Node 模块。如果我们运行 yarn add normalize.css
,我们可以使用:
@import "~normalize.css";
… 并充分利用 NPM 为我们管理第三方样式——包括版本控制——而不需要我们复制+粘贴。此外,让 webpack 为我们捆绑 CSS 相对于使用 CSS 的默认导入有明显的优势,可以节省客户端不必要的头部请求和缓慢的加载时间。
更新:本节和下一节已经更新以获得准确性,不再混淆使用 CSS Modules 来简单地导入 Node Modules。感谢
提供的帮助!
CSS Modules
你可能听说过 CSS Modules,它从 CSS 中去掉了 C。它通常只在你使用 JavaScript 构建 DOM 时才能发挥最好的作用,但本质上,它会将你的 CSS 类神奇地限定在加载它的 JavaScript 文件中(在这里了解更多)。如果你计划使用它,CSS Modules 就已经打包在 css-loader
中了(yarn add --dev css-loader
):
module.exports = {
// … module: {
rules: [
{
test: /\.css$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: { modules: true },
},
],
}, // …
],
},
};
注意:对于 css-loader
,我们现在使用了 扩展的对象语法 来传递选项。你可以使用字符串作为缩写来使用默认选项,就像我们仍然使用 <em>style-loader</em>
一样。
值得注意的是,你实际上可以在启用 CSS Modules 的情况下省略导入 Node Modules 时的 ~
(例如:@import "normalize.css";
)。但是,如果你导入自己的 CSS 时遇到“找不到___”错误,尝试在 webpack.config.js
中添加一个 resolve
对象,以便让 webpack 更好地理解你的预期模块顺序。
module.exports = {
//… resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules']
},
};
我们首先指定了我们的源目录,然后是 node_modules
。所以 webpack 将更好地处理解析,首先查找我们的源目录,然后是安装的 Node 模块,按照这个顺序(将 "src"
和 "node_modules"
替换为你的源和 Node 模块目录)。
Sass
需要使用 Sass 吗?没问题。安装:
yarn add --dev sass-loader node-sass
并添加另一个规则:
module.exports = {
// … module: {
rules: [
{
test: /\.(sass|scss)$/i,
use: [
'style-loader',
'css-loader',
'sass-loader',
]
} // …
],
},
};
然后当你的 JavaScript 调用 .scss
或 .sass
文件的 import
时,webpack 将做它的事情。请记住:use
的顺序是反向的,所以我们首先加载 Sass,然后是我们的 CSS 解析器,最后是 Style Loader,将我们解析后的 CSS 加载到页面的 <head>
中。
单独捆绑 CSS
也许你正在处理渐进增强;也许你需要一个单独的 CSS 文件以满足其他某些原因。我们可以在不更改任何代码的情况下在配置中将 style-loader
替换为 extract-text-webpack-plugin
。以我们的示例 app.js
文件为例:
import styles from './assets/stylesheets/application.css';
让我们在本地安装插件...
yarn add --dev extract-text-webpack-plugin
...并添加到 webpack.config.js
:
const ExtractTextPlugin = require('extract-text-webpack-plugin');module.exports = {
// … module: {
rules: [
{
test: /\.css$/i,
use: ExtractTextPlugin.extract({
use: [{
loader: 'css-loader',
options: { importLoaders: 1 },
}],
}),
},
// …
]
},
plugins: [
new ExtractTextPlugin({
filename: '[name].bundle.css',
allChunks: true,
}),
],
};
现在,当运行 node_modules/.bin/webpack -p
时,你还会注意到一个 app.bundle.css
文件出现在你的 output
目录中。只需在 HTML 中添加一个 <link>
标签即可正常使用。## HTML
你可能已经猜到了,webpack 也有一个用于加载 HTML 的 <a class="af nz" href="https://github.com/webpack/html-loader" rel="noopener ugc nofollow" target="_blank">html-loader</a>
插件。然而,当我们开始使用 JavaScript 加载 HTML 时,我们就要分支到各种不同的方法中去了。我无法想出一个单一的例子能为你做好接下来要做的任何计划。通常情况下,你会加载 HTML 以使用类似于 JSX、Mustache 或 Handlebars 的 JavaScript 风格标记语言,这些标记语言将被用于更大的系统,例如 React、Vue 或 Angular。或者你正在使用一个预处理器,例如 Pug(之前的 Jade)。或者你只是从你的源目录直接推送相同的 HTML 到你的构建目录。无论你在做什么,我都不会假设。
因此,在此结束本教程:使用 webpack 加载标记是_极力鼓励_的,也是可行的,但到这一步,你将自己决定你的架构,既不是我,也不是 webpack 能为你做出的决定。但使用上面的示例作为参考,并在 NPM 上搜索正确的加载器应该足以让你开始。
模块化思维
为了充分利用 webpack,你必须思考模块化——小型、可重用、自包含的过程,只做一件事情,并且做好一件事情。这意味着将这样的东西:
└── js/
└── application.js // 300KB of spaghetti code
...并将其转换为这样:
└── js/
├── components/
│ ├── button.js
│ ├── calendar.js
│ ├── comment.js
│ ├── modal.js
│ ├── tab.js
│ ├── timer.js
│ ├── video.js
│ └── wysiwyg.js
│
└── index.js // ~ 1KB of code; imports from ./components/
结果是干净、可重用的代码。每个单独的组件都依赖于 import
它自己的依赖项,并将其想要公开给其他模块的内容 export
。与 Babel + ES6 配合使用,你可以利用 JavaScript 类 实现良好的模块化,并且自带工作的_不用思考_的作用域。
有关模块的更多信息,请参见 Preethi Kasireddy 的这篇优秀文章。
升级到 3
根据 Sean T. Larkin 在 发布博客文章 中所说:“从 webpack 2 迁移到 3,应该只需要在终端中运行升级命令,不需要做任何努力。 我们将其标记为主要更改,因为内部的破坏性更改可能会影响某些插件。”
版本 3 还添加了一个漂亮的功能:作用域提升。 这是一个特殊的名称,用于描述一种将所有模块捆绑在一起的新方法。最好的情况是:它可以将你的包大小减少近一半(来源)。最坏的情况是:作用域提升对你的特殊包不起作用,并且输出几乎与 2.x 版本相同的代码。
要添加它,只需将以下行添加到 plugins
中:
module.exports = {
//… plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
], //…
};
评论(0)