本文在我们的《 现代JavaScript工具和技能》 一书中有介绍 。 熟悉支持现代JavaScript开发的基本工具。
Webpack 4文档指出:
Webpack是一个模块捆绑器。 它的主要目的是捆绑JavaScript文件以供在浏览器中使用,但它也能够转换,捆绑或打包几乎任何资源或资产。
Webpack已成为现代Web开发的最重要工具之一。 它主要是JavaScript的模块捆绑器,但是可以教它转换所有前端资产,例如HTML,CSS甚至图像。 它可以让您更好地控制应用程序发出的HTTP请求的数量,并允许您使用这些资产的其他形式(例如,Pug,Sass和ES8)。 Webpack还允许您轻松使用npm中的软件包。
本文针对的是Webpack的新手,他们将介绍初始设置和配置,模块,加载程序,插件,代码拆分和热模块替换。 如果您发现视频教程很有用,我可以强烈推荐《 第一原理》中的Glen Maddern的Webpack作为起点,以了解使Webpack特别的原因。 现在有点旧了,但原理仍然相同,并且是很棒的介绍。
要在家中继续学习,您需要安装Node.js。 您还可以从我们的GitHub存储库下载演示应用程序 。
设定
让我们使用npm初始化一个新项目,并安装webpack
和webpack-cli
:
mkdir webpack-demo && cd webpack-demo
npm init -y
npm install --save-dev webpack webpack-cli
接下来,我们将创建以下目录结构和内容:
webpack-demo
|- package.json
+ |- webpack.config.js
+ |- /src
+ |- index.js
+ |- /dist
+ |- index.html
dist / index.html
<!doctype html>
<html>
<head>
<title>Hello Webpack</title>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>
src / index.js
const root = document.createElement("div")
root.innerHTML = `<p>Hello Webpack.</p>`
document.body.appendChild(root)
webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
这告诉Webpack在我们的入口点src/index.js
编译代码,并在/dist/bundle.js
输出一个包。 让我们添加一个npm脚本来运行Webpack。
package.json
{
...
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "develop": "webpack --mode development --watch",
+ "build": "webpack --mode production"
},
...
}
使用npm run develop
命令,我们可以创建我们的第一个捆绑包!
Asset Size Chunks Chunk Names
bundle.js 2.92 KiB main [emitted] main
现在,您应该能够在浏览器中加载dist/index.html
并受到“ Hello Webpack”的欢迎。
打开dist/bundle.js
以查看Webpack所做的事情。 顶部是Webpack的模块引导代码,底部是我们的模块。 您可能还没有对颜色留下深刻的印象,但是如果您走了这么远,现在就可以开始使用ES模块了,Webpack将能够为所有浏览器使用的产品制作捆绑包。
使用Ctrl + C重新启动构建,然后运行npm run build
以生产模式编译我们的捆绑软件。
Asset Size Chunks Chunk Names
bundle.js 647 bytes main [emitted] main
请注意,包大小已从2.92 KiB减少到647字节 。
再看一下dist/bundle.js
,您会看到一堆难看的代码。 我们的捆绑软件已使用UglifyJS进行了精简:代码将完全相同地运行,但是它以最小的文件大小完成。
-
--mode development
优化构建速度和调试 -
--mode production
针对运行时的执行速度和输出文件大小进行了优化。
模组
使用ES模块,您可以将大型程序拆分为许多小型的独立程序。
Webpack开箱即用,知道如何使用import
和export
语句使用ES模块。 作为示例,让我们现在通过安装lodash-es并添加第二个模块来进行尝试:
npm install --save-dev lodash-es
src / index.js
import { groupBy } from "lodash-es"
import people from "./people"
const managerGroups = groupBy(people, "manager")
const root = document.createElement("div")
root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`
document.body.appendChild(root)
src / people.js
const people = [
{
manager: "Jen",
name: "Bob"
},
{
manager: "Jen",
name: "Sue"
},
{
manager: "Bob",
name: "Shirley"
}
]
export default people
运行npm run develop
rundevelop来启动Webpack并刷新index.html
。 您应该看到按管理员分组的一组人员被打印到屏幕上。
注意:没有相对路径(例如'es-lodash'
是从npm安装到/node_modules
模块。 您自己的模块将始终需要一个相对路径,例如'./people'
,因为这是区分它们的方式。
注意,在控制台中,我们的捆绑包大小已增加到1.41 MiB ! 值得关注的是,尽管在这种情况下无需担心。 使用npm run build
在生产模式下进行编译,会将来自lodash-es的所有未使用的lodash模块从包中删除。 删除未使用的导入的过程称为“ 摇树” ,这是Webpack免费提供的。
> npm run develop
Asset Size Chunks Chunk Names
bundle.js 1.41 MiB main [emitted] [big] main
> npm run build
Asset Size Chunks Chunk Names
bundle.js 16.7 KiB 0 [emitted] main
装载机
加载程序使您可以在导入文件时对文件运行预处理器。 这使您可以捆绑JavaScript以外的静态资源,但让我们看一下首先加载.js
模块时可以做什么。
让我们通过下一代JavaScript编译器Babel运行所有.js
文件,使代码保持现代性:
npm install --save-dev "babel-loader@^8.0.0-beta" @babel/core @babel/preset-env
webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components)/,
+ use: {
+ loader: 'babel-loader',
+ }
+ }
+ ]
+ }
}
.babelrc
{
"presets": [
["@babel/env", {
"modules": false
}]
],
"plugins": ["syntax-dynamic-import"]
}
此配置可防止Babel将import
和export
语句转换为ES5,并启用动态导入-我们将在稍后的代码拆分部分中进行介绍。
现在,我们可以自由使用现代语言功能,并将它们编译为可在所有浏览器中运行的ES5。
萨斯
加载程序可以链接在一起,形成一系列转换。 演示如何工作的一个好方法是从我们的JavaScript导入Sass:
npm install --save-dev style-loader css-loader sass-loader node-sass
webpack.config.js
module.exports = {
...
module: {
rules: [
...
+ {
+ test: /\.scss$/,
+ use: [{
+ loader: 'style-loader'
+ }, {
+ loader: 'css-loader'
+ }, {
+ loader: 'sass-loader'
+ }]
+ }
]
}
}
这些装载机的处理顺序相反:
-
sass-loader
将Sass转换为CSS。 -
css-loader
将CSS解析为JavaScript,并解决所有依赖关系。 -
style-loader
我们的CSS输出到文档中的<style>
标记中。
您可以将它们视为函数调用。 一个加载器的输出作为输入输入到另一个:
styleLoader(cssLoader(sassLoader("source")))
让我们添加一个Sass源文件,导入是一个模块。
src / style.scss
$bluegrey: #2b3a42;
pre {
padding: 8px 16px;
background: $bluegrey;
color: #e1e6e9;
font-family: Menlo, Courier, monospace;
font-size: 13px;
line-height: 1.5;
text-shadow: 0 1px 0 rgba(23, 31, 35, 0.5);
border-radius: 3px;
}
src / index.js
import { groupBy } from 'lodash-es'
import people from './people'
+ import './style.scss'
...
用Ctrl + C重新启动构建,然后npm run develop
。 在浏览器中刷新index.html
,您应该会看到一些样式。
JS中的CSS
我们只是从JavaScript导入了一个Sass文件作为模块。
打开dist/bundle.js
并搜索“ pre {”。 实际上,我们的Sass已被编译为CSS字符串,并保存为捆绑软件中的模块。 当我们将此模块导入JavaScript时, style-loader
会将字符串输出到嵌入式<style>
标记中。
你为什么要这样做?
在这里,我不会深入探讨这个主题,但是有一些需要考虑的原因:
- 您可能希望包含在项目中的JavaScript组件可能依赖于其他资产才能正常运行(HTML,CSS,图像,SVG)。 如果这些都可以捆绑在一起,则导入和使用起来会容易得多。
- 消除无效代码:当您的代码不再导入JS组件时,CSS也将不再导入。 生成的捆绑软件将仅包含执行某些操作的代码。
- CSS模块:CSS的全局名称空间使您很难确定对CSS的更改不会产生任何副作用。 CSS模块通过默认情况下将CSS设置为本地并公开可在JavaScript中引用的唯一类名称来更改此设置。
- 通过巧妙地捆绑/拆分代码来减少HTTP请求的数量。
图片
我们将要看到的最后一个加载器示例是使用file-loader
处理图像。
在标准HTML文档中,当浏览器遇到img
标签或具有background-image
属性的元素时,将提取background-image
。 使用Webpack,您可以在图像较小的情况下,通过将图像源作为字符串存储在JavaScript中来优化此效果。 这样,您就可以预加载它们,并且浏览器以后不必再通过单独的请求来获取它们:
npm install --save-dev file-loader
webpack.config.js
module.exports = {
...
module: {
rules: [
...
+ {
+ test: /\.(png|svg|jpg|gif)$/,
+ use: [
+ {
+ loader: 'file-loader'
+ }
+ ]
+ }
]
}
}
使用以下命令下载测试图像 :
curl https://raw.githubusercontent.com/sitepoint-editors/webpack-demo/master/src/code.png --output src/code.png
使用Ctrl + C重新启动构建,然后npm run develop
,您现在就可以将图像导入为模块了!
src / index.js
import { groupBy } from 'lodash-es'
import people from './people'
import './style.scss'
+ import './image-example'
...
src / image-example.js
import codeURL from "./code.png"
const img = document.createElement("img")
img.src = codeURL
img.style = "background: #2B3A42; padding: 20px"
img.width = 32
document.body.appendChild(img)
这将包括一个图像,其中src
属性包含该图像本身的数据URI :
<img src="https://img-blog.csdnimg.cn/2022010617344249088.png" style="background: #2B3A42; padding: 20px" width="32">
CSS中的背景图片也由file-loader
。
src / style.scss
$bluegrey: #2b3a42;
pre {
padding: 8px 16px;
- background: $bluegrey;
+ background: $bluegrey url("code.png") no-repeat center center / 32px 32px;
color: #e1e6e9;
font-family: Menlo, Courier, monospace;
font-size: 13px;
line-height: 1.5;
text-shadow: 0 1px 0 rgba(23, 31, 35, 0.5);
border-radius: 3px;
}
在文档中查看加载程序的更多示例:
依赖图
现在,您应该能够看到装载程序如何帮助您在资产之间建立依赖关系树。 这就是Webpack主页上显示的图像。
尽管JavaScript是切入点,但Webpack赞赏您的其他资产类型(如HTML,CSS和SVG)各自具有自己的依赖性,应将其视为构建过程的一部分。
代码分割
从Webpack文档 :
代码拆分是Webpack最引人注目的功能之一。 此功能使您可以将代码分成多个捆绑包,然后可以按需或并行加载。 它可用于实现较小的捆绑包并控制资源负载优先级,如果正确使用,则会对负载时间产生重大影响。
到目前为止,我们只看到了一个入口点src/index.js
和一个输出包dist/bundle.js
。 当您的应用增长时,您需要对其进行拆分,以免一开始就不会下载整个代码库。 一个好的方法是使用代码拆分和延迟加载来按需获取内容,因为代码路径需要它们。
让我们通过添加一个“聊天”模块来演示该模块,当有人与之交互时会获取并初始化该模块。 我们将创建一个新的入口点并为其命名,并且还将输出的文件名动态化,以便每个块都不同。
webpack.config.js
const path = require('path')
module.exports = {
- entry: './src/index.js',
+ entry: {
+ app: './src/app.js'
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
...
}
src / app.js
import './app.scss'
const button = document.createElement("button")
button.textContent = 'Open chat'
document.body.appendChild(button)
button.onclick = () => {
import(/* webpackChunkName: "chat" */ "./chat").then(chat => {
chat.init()
})
}
src / chat.js
import people from "./people"
export function init() {
const root = document.createElement("div")
root.innerHTML = `<p>There are ${people.length} people in the room.</p>`
document.body.appendChild(root)
}
src / app.scss
button {
padding: 10px;
background: #24b47e;
border: 1px solid rgba(#000, .1);
border-width: 1px 1px 3px;
border-radius: 3px;
font: inherit;
color: #fff;
cursor: pointer;
text-shadow: 0 1px 0 rgba(#000, .3), 0 1px 1px rgba(#000, .2);
}
注意:尽管使用/* webpackChunkName */
注释为捆绑软件提供了名称,但是此语法 并非特定于Webpack。 这是动态导入的建议语法,旨在直接在浏览器中支持。
让我们运行npm run build
看看会产生什么:
Asset Size Chunks Chunk Names
chat.bundle.js 377 bytes 0 [emitted] chat
app.bundle.js 7.65 KiB 1 [emitted] app
由于条目包已更改,因此我们也需要更新其路径。
dist / index.html
<!doctype html>
<html>
<head>
<title>Hello Webpack</title>
</head>
<body>
- <script src="bundle.js"></script>
+ <script src="app.bundle.js"></script>
</body>
</html>
让我们从dist目录启动服务器,以查看运行情况:
cd dist
npx serve
在浏览器中打开http:// localhost:5000,看看会发生什么。 最初仅获取bundle.js
。 单击该按钮时,将导入并初始化聊天模块。
只需很少的努力,我们就为应用程序添加了动态代码拆分和模块的延迟加载。 这是构建高性能Web应用程序的一个很好的起点。
外挂程式
加载程序在单个文件上进行转换时, 插件在更大的代码块上进行操作。
现在,我们将代码,外部模块和静态资产捆绑在一起,我们的捆绑包将迅速增长。 插件可以帮助我们以巧妙的方式拆分代码并优化生产环境。
不知不觉中,我们实际上已经使用了许多默认的Webpack插件和“模式”
发展
- 为
process.env.NODE_ENV
提供值“开发” - 命名模块插件
生产
- 为
process.env.NODE_ENV
提供值“生产” - UglifyJsPlugin
- ModuleConcatenationPlugin
- NoEmitOnErrorsPlugin
生产
在添加其他插件之前,我们将首先拆分配置,以便我们可以应用特定于每种环境的插件。
将webpack.config.js
重命名为webpack.common.js
并添加用于开发和生产的配置文件。
- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js
我们将使用webpack-merge
将我们的通用配置与特定于环境的配置结合起来:
npm install --save-dev webpack-merge
webpack.dev.js
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development'
})
webpack.prod.js
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production'
})
package.json
"scripts": {
- "develop": "webpack --watch --mode development",
- "build": "webpack --mode production"
+ "develop": "webpack --watch --config webpack.dev.js",
+ "build": "webpack --config webpack.prod.js"
},
现在,我们可以在webpack.dev.js
添加特定于开发的插件,并在webpack.prod.js
添加特定于生产的webpack.prod.js
。
拆分CSS
在使用ExtractTextWebpackPlugin进行生产捆绑时,将CSS与JavaScript分离是一种最佳实践。
目前.scss
装载机是完美的发展,因此,我们将继续前进来自webpack.common.js
到webpack.dev.js
并添加ExtractTextWebpackPlugin
到webpack.prod.js
只。
npm install --save-dev extract-text-webpack-plugin@4.0.0-beta.0
webpack.common.js
...
module.exports = {
...
module: {
rules: [
...
- {
- test: /\.scss$/,
- use: [
- {
- loader: 'style-loader'
- }, {
- loader: 'css-loader'
- }, {
- loader: 'sass-loader'
- }
- ]
- },
...
]
}
}
webpack.dev.js
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
+ module: {
+ rules: [
+ {
+ test: /\.scss$/,
+ use: [
+ {
+ loader: 'style-loader'
+ }, {
+ loader: 'css-loader'
+ }, {
+ loader: 'sass-loader'
+ }
+ ]
+ }
+ ]
+ }
})
webpack.prod.js
const merge = require('webpack-merge')
+ const ExtractTextPlugin = require('extract-text-webpack-plugin')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production',
+ module: {
+ rules: [
+ {
+ test: /\.scss$/,
+ use: ExtractTextPlugin.extract({
+ fallback: 'style-loader',
+ use: ['css-loader', 'sass-loader']
+ })
+ }
+ ]
+ },
+ plugins: [
+ new ExtractTextPlugin('style.css')
+ ]
})
让我们比较两个构建脚本的输出:
> npm run develop
Asset Size Chunks Chunk Names
app.bundle.js 28.5 KiB app [emitted] app
chat.bundle.js 1.4 KiB chat [emitted] chat
> npm run build
Asset Size Chunks Chunk Names
chat.bundle.js 375 bytes 0 [emitted] chat
app.bundle.js 1.82 KiB 1 [emitted] app
style.css 424 bytes 1 [emitted] app
现在,我们的CSS已从JavaScript包中提取出来进行生产,我们需要从HTML <link>
到它。
dist / index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Code Splitting</title>
+ <link href="style.css" rel="stylesheet">
</head>
<body>
<script type="text/javascript" src="app.bundle.js"></script>
</body>
</html>
这允许在浏览器中并行下载CSS和JavaScript,因此将比单个捆绑包更快地加载。 它还允许在JavaScript完成下载之前显示样式。
产生HTML
每当我们的输出更改时,我们就必须不断更新index.html
来引用新的文件路径。 这正是创建html-webpack-plugin
自动为我们完成的工作。
我们还可以同时添加clean-webpack-plugin
,以在每次构建之前清除我们的/dist
目录。
npm install --save-dev html-webpack-plugin clean-webpack-plugin
webpack.common.js
const path = require('path')
+ const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
+ plugins: [
+ new CleanWebpackPlugin(['dist']),
+ new HtmlWebpackPlugin({
+ title: 'My killer app'
+ })
+ ]
}
现在,每次构建时,dist将被清除。 现在,我们还将看到index.html
输出,以及指向条目捆绑包的正确路径。
运行npm run develop
会产生以下结果:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My killer app</title>
</head>
<body>
<script type="text/javascript" src="app.bundle.js"></script>
</body>
</html>
而npm run build
会产生以下结果:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My killer app</title>
<link href="style.css" rel="stylesheet">
</head>
<body>
<script type="text/javascript" src="app.bundle.js"></script>
</body>
</html>
发展历程
webpack-dev-server为您提供了一个简单的Web服务器,并提供了实时重载 ,因此您无需手动刷新页面即可查看更改。
npm install --save-dev webpack-dev-server
package.json
{
...
"scripts": {
- "develop": "webpack --watch --config webpack.dev.js",
+ "develop": "webpack-dev-server --config webpack.dev.js",
}
...
}
> npm run develop
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
在浏览器中打开http:// localhost:8080 /并更改其中一个JavaScript或CSS文件。 您应该看到它会自动生成并刷新。
HotModuleReplacement
HotModuleReplacement
插件比Live HotModuleReplacement
前进了一步,并且在运行时交换模块而无需刷新 。 如果配置正确,则可以在开发单页应用程序时节省大量时间。 在页面上有很多状态的位置,您可以对组件进行增量更改,并且仅替换和更新已更改的模块。
webpack.dev.js
+ const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
+ devServer: {
+ hot: true
+ },
+ plugins: [
+ new webpack.HotModuleReplacementPlugin()
+ ],
...
}
现在,我们需要接受代码中已更改的模块,以重新初始化事物。
src / app.js
+ if (module.hot) {
+ module.hot.accept()
+ }
...
注意:启用“热模块替换”时, module.hot
设置为true
进行开发, false
进行生产,因此将它们从捆绑中剥离。
重新启动构建,看看执行以下操作会发生什么:
- 点击打开聊天
- 向
people.js
模块添加一个新人员 - 再次单击“ 打开聊天” 。
这是正在发生的事情:
- 单击“ 打开聊天”后 ,将获取并初始化
chat.js
模块。 - HMR检测到
people.js
被修改 -
index.js
module.hot.accept()
导致替换此条目块加载的所有模块 - 再次单击“ 打开聊天”时,使用更新模块中的代码运行
chat.init()
。
CSS替换
让我们将按钮颜色更改为红色,看看会发生什么:
src / app.scss
button {
...
- background: #24b47e;
+ background: red;
...
}
现在我们可以看到样式的即时更新,而不会丢失任何状态。 这是一个大大改善的开发人员体验! 感觉就像是未来。
HTTP / 2
使用像Webpack这样的模块捆绑器的主要好处之一是,它可以让您控制如何构建资产然后在客户端上提取资产,从而帮助您提高性能。 多年来,人们一直认为最佳做法是串联文件以减少需要在客户端上进行的请求数量。 这仍然有效,但是HTTP / 2现在允许在单个请求中传递多个文件 ,因此串联不再是灵丹妙药。 实际上,您的应用可能会受益于单独缓存许多小文件。 然后,客户端可以获取单个已更改的模块,而不必再次获取大部分内容相同的整个包。
Webpack的创建者Tobias Koppers撰写了一篇内容丰富的文章,解释了为什么即使在HTTP / 2时代,捆绑仍然很重要。
在Webpack和HTTP / 2上了解有关此内容的更多信息。
交给你
我希望您对Webpack的介绍很有帮助,并能够开始使用它以取得巨大的效果。 花费一些时间来了解Webpack的配置,加载程序和插件,但是学习此工具的工作原理将会有所收获。
Webpack 4的文档目前正在开发中,但是组合得很好。 我强烈建议您通读《 概念和指南》以获取更多信息。 您可能对以下其他主题感兴趣:
Webpack 4是您选择的模块捆绑器吗? 在下面的评论中让我知道。
From: https://www.sitepoint.com/beginners-guide-webpack-module-bundling/