Webpack已经成为现代web开发中最重要的工具之一。它主要是一个用于JavaScript的模块打包器,可以转换所有前端资源,比如HTML、CSS,image,可以有效地减少HTTP请求数,并可以使用其他资源类型(如Pug、Sass和ES8)。Webpack还允许您轻松地使用npm上的模块。

这篇文章针对的是Webpack的新手,将介绍初始配置、模块、加载器、插件、代码分割和热模块替换。

本文所有代码可在GitHub找到

安装

创建webpack-demo, 并安装webpack and 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')
  }
}

为package.json 增加npm script脚本:

{
    ...
    "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以在生产模式下编译我们的bundle。

Asset      Size       Chunks           Chunk Names
bundle.js  647 bytes  main  [emitted]  main
注意bundle.js 从2.92 KiB 减少到 647 bytes

查看dist/bundle.js,你会看到一堆丑陋的代码。webpack已经用UglifyJS缩小了:代码运行结果与压缩之前一致。

  • ——--mode development 优化构建速度和调试
  • ——--mode production 优化运行时的执行速度和输出文件大小

Modules - 模块

使用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来启动Webpack和并刷新页面。您应该看到按manager分组的人员数组打印到页面上。

注意控制台中,我们的bundle.js大小已增加到1.41 MiB! 不用担心。使用npm run build在生产模式下进行编译,可以从bundle中删除lodash-es中所有未使用的lodash模块。这个删除未使用导入的过程称为tree-shaking,这是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

Loaders - 加载器

Loaders允许您在导入文件时运行预处理。这允许您将静态资源绑定到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将导入和导出语句转换到ES5中,并支持动态导入——我们将在关于代码分割的一节稍后介绍动态导入。

我们现在可以自由地使用现代语言特性,它们将被编译成可以在所有浏览器中运行的ES5。

Sass

加载器可以链接到一系列转换中。一个很好的方法来演示如何从我们的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'
+         }]
+       }
      ]
    }
  }

这些loader是按照相反的顺序处理的:

  • sass-loader 将sass转换为css
  • css-loader 将CSS解析为JavaScript并解决任何依赖关系。
  • style-loader 将我们的CSS输出到文档中的<style>

可以把这些看作函数调用。一个加载器的输出feed作为下一个的输入:

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'

运行npm run develop , 刷新页面就能看到样式生效。

CSS in JS

我们从JavaScript中导入了一个Sass文件作为模块。

打开dist /bundle.js和查找“pre{”。事实上,我们的Sass已经被编译成一个CSS字符串,并保存为bundle中的一个模块。当我们将这个模块导入JavaScript时,style-loader将该字符串输出到一个嵌入的<style>标签中。

为什么要这么做?

一些需要考虑的原因:

  • 您可能希望在项目中包含的JavaScript组件可能依赖于其他资源来正常工作(HTML、CSS、图像、SVG)。如果这些都可以打包在一起,那么导入和使用起来就容易得多。
  • 移除无用代码:当JS组件不再使用时,CSS也将不再被导入。生成的包将只包含实际使用的代码。
  • CSS模块: CSS的全局命名空间使我们很难相信对CSS的更改不会有任何副作用。CSS模块改变了这一点,默认情况下将CSS设置为局部作用域,并公开惟一的类名,您可以在JavaScript中引用这些类名。
  • 以便捷的方式打包/拆分代码来减少HTTP请求的数量。

Images

我们将看到的最后一个加载器示例是使用file-loader处理图像。

在标准HTML文档中,当浏览器解析到具有background-image属性的img标记或元素时,将加载图片。使用Webpack,您可以通过将图像源作为字符串存储在JavaScript中来优化小图像。这样,你就可以预加载它们,以后浏览器就不必用单独的请求来获取它们了:

npm install --save-dev file-loader

webpack.config.js

module.exports = {
    ...
    module: {
      rules: [
        ...
+       {
+         test: /\.(png|svg|jpg|gif)$/,
+         use: [
+           {
+             loader: 'file-loader'
+           }
+         ]
+       }
      ]
    }
  }

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="data:image/png;base64,iVBO..." 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;
  }

依赖关系图

现在您应该看到loader如何在您的资产之间构建依赖关系树。这就是Webpack主页上的图片所展示的。

虽然JavaScript是入口文件,但Webpack知道您的其他资源类型——比如HTML、CSS和SVG——都有它们自己的依赖项,应该将它们视为构建过程的一部分。

Code Splitting - 代码分片

代码分片是Webpack最引人注目的特性之一。这个特性允许您将代码分割成不同的包,然后可以按需或并行加载这些包。它可以用于实现更小的包和控制资源负载优先级,如果使用正确,可以对负载时间产生重大影响。

目前,我们只看到一个入口文件——src/index.js 和一个输出 dist/bundle .js。随着应用的增长,您需要将其拆分,以便在开始时不会下载整个库。一种好的方法是使用Code SplittingLazy Loading来按需加载代码。

让我们添加一个“chat”模块来演示这一点,这个模块是在有人与它交互时获取和初始化的。我们将创建一个新的入口文件,我们还将使输出的文件名是动态的,因此每个块的文件名都是不同的。

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>

在浏览器中打开http://localhost:5000只包。首先获取bundle.js 。当点击按钮时,将导入并初始化chat模块。

我们只花了很少的精力,就将动态代码分割和模块延迟加载添加到了我们的应用程序中。这是构建高性能web应用程序的一个很好的起点。

Plugins - 插件

loaders在单个文件上操作转换,而插件可以再更大的代码块上操作。

现在我们将代码、外部模块和静态资源打包在一起,我们的打包文件将快速增长。插件可以帮助我们以聪明的方式分割代码并优化产品。

在不知道的情况下,我们已经使用了很多默认的Webpack插件“mode”

development

  • process.env.NODE_ENV 指定为 “development”
  • NamedModulesPlugin

production

  • process.env.NODE_ENV 指定为 “production”
  • UglifyJsPlugin
  • ModuleConcatenationPlugin
  • NoEmitOnErrorsPlugin

Production

在添加额外的插件之前,我们首先将配置拆分,以便能够应用特定于每个环境的插件。

重命名webpack.config。并添加分别创建一个用于development和production的配置文件。

- |- webpack.config.js
+ |- webpack.common.js
+ |- webpack.dev.js
+ |- webpack.prod.js

我们将使用webpackage -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"
},

现在我们可以将特定于开发环境的插件添加到webpackage .dev.js中,并在webpackage .prod.js中添加特定于生产环境的插件。

Split CSS

在使用ExtractTextWebpackPlugin进行产品绑定时,最好将CSS与JavaScript分离。

当前的.scss加载器非常适合开发,所以我们将把它们从webpackage.common.js移到webpackage.dev.js中,并将ExtractTextWebpackPlugin添加到webpackage.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- webpackage -plugin的作用。

我们也可以在每次构建之前同时添加clean- webpackage -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'
+     })
+   ]
  }

npm run develop 生成dist/index.html如下:

<!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 生成dist/index.html如下:

<!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>

Development - 开发模式

webpackage -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插件比热加载更进一步,在运行时无需刷新即可交换模块。如果配置正确,这将在开发单个页面应用时节省大量时间。在页面中有很多状态的地方,可以对组件进行增量更改,只有更改的模块才会被替换和更新。

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()
+ }

  ...

HTTP/2

使用Webpack之类的模块打包的主要好处之一是,它可以通过控制如何构建资源并在客户端上获取资源来提高性能。多年来,将文件连接起来以减少需要在客户端上发出的请求数量一直被认为是最佳实践。但是HTTP/2现在允许在一个请求中交付多个文件。您的应用程序实际上可以从单独缓存许多小文件中获益。然后,客户机可以获取单个更改的模块,而不必再次获取几乎内容相同的整个包。

剩下靠你了

您可能需要花一些时间来熟悉Webpack的配置、加载器和插件,但是学习这个工具如何工作将会给你的工作带来更大的帮助。

Webpack 4的文档目前正在编写中。我强烈建议您通读这些概念和指南以获得更多信息。

下面是一些你可能感兴趣的话题:

enjoy learning!