webpack 教程

陈三

说明:本文写于 webpack 1.x 年代,但最近已针对 webpack 2、webpack 3 版本调整了内容。

文中涉及的各个软件的版本号:

  1. node - 8.4.0
  2. npm - 5.4.1
  3. webpack - 3.6.0

我还清楚记得,自己刚接触 webpack 的感受:千头万绪,不知从何下手。

我是说,我不想关心 JavaScript 文件要配置哪些加载器。我只想快点搭好开发环境。总之,几经折腾,我迅速放弃 webpack。

但今天来看,webpack 其实没那么复杂。当时的复杂,只是任何领域的新手都要面对的一种心理障碍。因为我们不知道它为什么要这样,想解决什么问题,所以觉得一头雾水且无从下手。

揣着问题,我们就能找到答案;没有问题,则不会有答案。

那么,webpack 是什么?

Module bundler

这是 webpack 官网对 webpack 的描述。那么,什么是 module?我们首先会想到 ES2015/ES6 中定义的模块,又或 Node.js 里的模块。但在 webpack 领域,所有类型的文件都可以是 module,包括 CSS 文件、图片、JSON,等等。

我们甚至都不需要尝试就能知道,我们在 JavaScript 代码中 importrequire 一张图片会发生什么。但在 webpack 领域里,这一切,成为可能,这一切,归功于加载器(loader),正因为它们,我们才可以 import/require 各种文件,把它们当成模块处理 - 这正是 webpack 跨出的与众不同的一步。

字面意义介绍过了,我们来编码练习一趟。

安装 webpack

你可以选择在全局环境安装 webpack,也可以在项目目录下安装 webpack。我会推荐后者,因为 webpack 是开发环境必需的依赖,安装在全局的话,依赖就不能写入 package.json 文件,换一台电脑安装、运行项目,就会出现依赖缺失的问题。

我们来初始化一个 package.json 文件:

$ cd ~
$ mkdir webpack-demo
$ cd webpack-demo
$ npm init -y

这时,我们的项目下会生成一个 package.json 文件:

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

接着,在 webpack-demo 项目中安装 webpack

$ npm install --save-dev webpack

查看 webpack 版本

安装完成后,我们就可以在命令行下使用 webpack 命令查看我们刚才安装的 webpack 版本:

$ ./node_modules/.bin/webpack --version
3.6.0

如果你觉得这样执行 webpack 太麻烦,转身就想全局安装 webpack,那么且慢,试试 npm@5.2.0 引入的 npx:

$ npx webpack --version

前面介绍的命令中,我们查看了新安装的 webpack 版本号,那么,如果直接执行 webpack 会怎样?

$ npx webpack
No configuration file found and no output filename configured via CLI option.
A configuration file could be named 'webpack.config.js' in the current directory.
Use --help to display the CLI options.

我们需要一个配置文件 webpack.config.js

不过,为了方便,我们暂时先跳过配置文件,直接使用命令行。

我们在初始化环节生成的 package.json 文件里,有一个 main 字段,定义了我们的入口文件 index.js,让我们在目录下新建它,然后执行 webpack 命令:

$ touch index.js
$ npx webpack index.js build.min.js
Hash: d854c92bbbd4401eed3c
Version: webpack 3.6.0
Time: 35ms
       Asset     Size  Chunks             Chunk Names
build.min.js  2.47 kB       0  [emitted]  main
   [0] ./index.js 0 bytes {0} [built]

我们用一张表格来说明下前面的命令:

webpackindex.jsbuild.min.js
webpack 命令要打包的文件打包后的文件

是的,我们喂给 webpack 一个 index.js 文件,它还给我们一个 build.min.js 文件,整个过程耗时 35ms,打包出来的 build.min.js 文件大小为 2.47kB。如果你在困惑,为什么一个空空的 index.js 文件却生成一个 2.47kB 的 build.min.js,想学习一下这炼金术,那么,打开 build.min.js 文件就能看到一切。这里不展开说明。

构建完 build.min.js 后,我们还需要一个 html 文件,在目录下新建 index.html,添加内容如下:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>webpack 教程</title>
  </head>
  <body>
  </body>
</html>

浏览器下打开 index.html 页面,暂时只有一个标题。但我们马上引入脚本:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>webpack 教程</title>
  </head>
  <body>
    <script src="build.min.js"></script>
  </body>
</html>

注意,我们的 script 引用的文件是 build.min.js,而不是 index.js。这正是当下前端开发领域的一个趋势:开发文件(例子中的 index.js)与最终部署的文件(例子中的 build.min.js)是区分开的,之所以这样,是因为开发环境与用户的使用环境并不一致。比如我们可以在开发环境使用 ES2017 甚至 ES2018 的特性,而用户的浏览器不见得支持 - 这也是 webpack 等打包工具的一个意义,构建出能在目标用户浏览器上运行的代码。

webpack 配置文件

在前面,我们通过命令行打包 js 文件,因为只是演示,所以并不会觉得麻烦,但真正项目中,因为要配置的很多,不可能每个都通过命令行传递,所以我们要把这些配置集中到一个文件中,即 webpack.config.js

首先,我们在项目中新建一个 webpack.config.js 文件:

$ touch webpack.config.js

然后我们通过 entry 字段配置入口文件,即 webpack 要打包的文件:

module.exports = {
  entry: './index.js',
}

然后我们再通过 output 配置一个输出位置:

module.exports = {
  entry: './index.js',
  output: {
    filename: 'build.min.js'
  }
}

这时再执行 npx webpack,不再提示 webpack 配置文件缺失的问题,而会直接输出打包结果:

$ npx webpack
Hash: d854c92bbbd4401eed3c
Version: webpack 3.6.0
Time: 35ms
       Asset     Size  Chunks             Chunk Names
build.min.js  2.47 kB       0  [emitted]  main
   [0] ./index.js 0 bytes {0} [built

实时刷新

index.html 文件中引用 bundle.min.js 文件之后,我们有几个问题需要解决。

  1. 入口文件 index.js 的变化,包括它所引用的其它模块的变化如何通知给 webpack,以便重新构建 bundle.min.js 文件?
  2. bundle.min.js 文件更新后,浏览器中打开的页面该如何自动刷新?

监控文件

第一个问题,webpack 有好几个解决办法,其中 watch 选项最直观,我们可以让 webpack 监控文件变化,一旦文件有变化,就重新构建。但默认情况下,watch 选项是禁用的。

我们可以在命令行下启用它:

$ npx webpack --watch
Webpack is watching the files…

Hash: d854c92bbbd4401eed3c
Version: webpack 3.6.0
Time: 37ms
       Asset     Size  Chunks             Chunk Names
build.min.js  2.47 kB       0  [emitted]  main
   [0] ./index.js 0 bytes {0} [built]

当然也可以在配置文件 webpack.config.js 文件中启用它:

module.exports = {
  watch: true,
  entry: './index.js',
  output: {
    filename: 'build.min.js'
  }
}

index.js 文件中添加一行 console.log('hello js'),保存,我们就会看到命令行下的变化:

npx webpack --watch
Webpack is watching the files…

Hash: d854c92bbbd4401eed3c
Version: webpack 3.6.0
Time: 37ms
       Asset     Size  Chunks             Chunk Names
build.min.js  2.47 kB       0  [emitted]  main
   [0] ./index.js 0 bytes {0} [built]
Hash: 915673b7e187314044b2
Version: webpack 3.6.0
Time: 14ms
       Asset    Size  Chunks             Chunk Names
build.min.js  2.5 kB       0  [emitted]  main
   [0] ./index.js 23 bytes {0} [built]

从日志中我们看到,webpack 再次打包 build.min.js,耗时 14ms,最终文件大小 2.5 kB。

刷新浏览器

第二个问题,webpack 提供了 webpack-dev-server 来解决,它是一个简单的 web 开发服务器,提供了实时刷新的功能。

webpack-dev-server

我们先在项目下安装 webpack-dev-server

$ npm install --save-dev webpack-dev-server

然后在命令行下执行 webpack-dev-server

$ npx webpack-dev-server
Project is running at http://localhost:8080/
webpack output is served from /
Hash: 85013f0dc4ae684285bb
Version: webpack 3.6.0
Time: 361ms
       Asset    Size  Chunks                    Chunk Names
build.min.js  327 kB       0  [emitted]  [big]  main
  [35] multi (webpack)-dev-server/client?http://localhost:8080 ./index.js 40 bytes {0} [built]
  [36] (webpack)-dev-server/client?http://localhost:8080 7.27 kB {0} [built]
  [37] ./node_modules/url/url.js 23.3 kB {0} [built]
  [39] ./node_modules/url/util.js 314 bytes {0} [built]
  [40] ./node_modules/querystring-es3/index.js 127 bytes {0} [built]
  [43] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
  [45] ./node_modules/loglevel/lib/loglevel.js 7.74 kB {0} [built]
  [46] (webpack)-dev-server/client/socket.js 1.04 kB {0} [built]
  [78] (webpack)-dev-server/client/overlay.js 3.71 kB {0} [built]
  [80] ./node_modules/html-entities/index.js 231 bytes {0} [built]
  [83] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
  [84] (webpack)/hot/log.js 1.04 kB {0} [optional] [built]
  [85] (webpack)/hot/emitter.js 77 bytes {0} [built]
  [86] ./node_modules/events/events.js 8.33 kB {0} [built]
  [87] ./index.js 23 bytes {0} [built]
    + 73 hidden modules
webpack: Compiled successfully.

Ooops,让人晕眩的输出结果。

不过且忽视它们,我们现在可以在 http://localhost:8080/ 访问我们的项目。

让我们在入口文件 index.js 里添加一行代码验证下实时刷新功能:

console.log('webpack live reload is working')

我们可以在 chrome 浏览器的控制台看到如下内容:

[WDS] App updated. Reloading...
Navigated to http://localhost:8080/
hello js
webpack live reload is working

瞧,页面不仅刷新,连 index.js 都重新打包了 - webpack-dev-server 一举解决我们前面提出的两个问题。

截止现在,我们才只是搭起脚手架,没真正开始编码。接下来,让我们写一个简单的 React.js 单页面应用。在整个过程中,你可以看到 webpack 都发挥了什么作用。

安装第三方包

首先,使用 npm 来安装 reactreact-dom

$ npm install react react-dom

当然,你也可以使用 yarn

加载 React

让我们在 index.html 里添加一个 div

<div id='app'></div>

接着清空之前添加到 index.js 中的内容,新增内容如下:

import React from 'react'
import ReactDOM from 'react-dom'
console.log(React, ReactDOM)

保存文件后,我们马上就能在命令行下看到输出了。噫!webpack 竟支持原生的 ES2015 的 import - 不不,这是 webpack 2 新增的特性,在 webpack 1.x 年代是没有的,那时我们还需要 babel

好了,让我们去掉 index.js 里的 console.log(React, ReactDOM),写点实际的:

import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
  render () {
    return <div>hello webpack</div>
  }
}
ReactDOM.render(<App />, document.getElementById('app'))

保存代码,我们马上就能在命令行下见到错误:

webpack: Compiling...
Hash: 5d99695ec47189150820
Version: webpack 3.6.0
Time: 14ms
       Asset    Size  Chunks                    Chunk Names
build.min.js  327 kB       0  [emitted]  [big]  main
  [83] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
  [87] ./index.js 266 bytes {0} [built] [failed] [1 error]
    + 86 hidden modules

ERROR in ./index.js
Module parse failed: /Users/sam/webpack-demo/index.js Unexpected token (5:11)
You may need an appropriate loader to handle this file type.
| class App extends React.Component {
|   render () {
|     return <div>hello webpack</div>
|   }
| }
 @ multi (webpack)-dev-server/client?http://localhost:8080 ./index.js
webpack: Failed to compile.

在前面的代码中,我们写了 JSX 语法,而 webpack 默认并不认识它 - 此时,我们需要一个加载器。这个加载器叫 babel-loader

  1. 安装 babel-loader

    $ npm install --save-dev babel-loader babel-core babel-preset-react
    
  2. webpack.config.js 中新增配置

    新增的代码如下:

    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /(node_modules|bower_components)/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['react']
            }
          }
        }
      ]
    }
    

完成以上两个步骤后,为什么还是报错?因为我们修改了 webpack.config.js,要使修改生效,需要重启 webpack-dev-server。

重启试试:

$ npx webpack-dev-server
Project is running at http://localhost:8080/
webpack output is served from /
Hash: 9f94918539dcd82525f1
Version: webpack 3.6.0
Time: 924ms
       Asset     Size  Chunks                    Chunk Names
build.min.js  1.07 MB       0  [emitted]  [big]  main
 [115] multi (webpack)-dev-server/client?http://localhost:8080 ./index.js 40 bytes {0} [built]
 [116] (webpack)-dev-server/client?http://localhost:8080 7.27 kB {0} [built]
 [117] ./node_modules/url/url.js 23.3 kB {0} [built]
 [120] ./node_modules/querystring-es3/index.js 127 bytes {0} [built]
 [123] ./node_modules/strip-ansi/index.js 161 bytes {0} [built]
 [125] ./node_modules/loglevel/lib/loglevel.js 7.74 kB {0} [built]
 [126] (webpack)-dev-server/client/socket.js 1.04 kB {0} [built]
 [158] (webpack)-dev-server/client/overlay.js 3.71 kB {0} [built]
 [159] ./node_modules/ansi-html/index.js 4.26 kB {0} [built]
 [163] (webpack)/hot nonrecursive ^\.\/log$ 170 bytes {0} [built]
 [165] (webpack)/hot/emitter.js 77 bytes {0} [built]
 [167] ./index.js 282 bytes {0} [built]
 [168] ./node_modules/react/react.js 56 bytes {0} [built]
 [184] ./node_modules/react-dom/index.js 59 bytes {0} [built]
 [185] ./node_modules/react-dom/lib/ReactDOM.js 5.17 kB {0} [built]
    + 255 hidden modules
webpack: Compiled successfully.

成了:不再报错,查看 HTML 页面的话,也有内容。

从 JSX 的加载器一例我们看到,webpack 在处理它不认识的模块时,需要加载器做预处理 - 这正是我最初学习 webpack 觉得麻烦的地方,也恰恰是 webpack 强大的地方,借着这些加载器,它现在几乎要成为前端开发的标准打包工具。

图片加载器

说完 JSX,我们再来看看 webpack 是怎么处理图片的。

传统的 HTML 里,我们通常会有一个 assetsimages 目录,用于存放图片,然后代码中使用相对路径引用。

但在单页面应用中,我们不再使用相对路径来引用图片,因为开发目录的结构与最终打包的结构并不会一致。另外,我们不打算手动拷贝图片目录 - 我们希望,构建工具帮我们处理这一切,包括图片的拷贝、图片大小的优化等。

在 webpack 里,负责图片预处理的是 file-loader

$ npm install --save-dev file-loader

接着是配置 webpack.config.js

{
  test: /\.(png|jpg|gif)$/,
  use: [
    {
      loader: 'file-loader',
      options: {}  
    }
  ]
}

然后我们就可以在 JavaScript 模块中引用图片模块

import icon from './img/icon.png'
ReactDOM.render(<img src={icon} />, document.getElementById('app))

webpack 在最终构建时,会自动将模块中引用的图片拷贝到相应目录 - 谢天谢地,再也不用手动拷贝。

加载 CSS 文件

CSS 文件同样可以是一种模块,自然也有相应的加载器:

  1. css-loader - 预处理 CSS 文件
  2. style-loader - 将 CSS 插入到 DOM 中的 style 标签

注意,我们如果只使用了 css-loader,则 webpack 只是将 CSS 文件预处理成模块然后打包到构建文件中,并不会插入到页面 - 这就是 style-loader 的作用。

我们先来安装它们:

$ npm install --save-dev css-loader style-loader

然后修改 webpack.config.js 配置:

,
{
  test: /\.css$/,
  use: [
    { loader: 'style-loader' },
    { loader: 'css-loader' }
  ]
}

请注意,loader 的顺序很重要,比如上面新增的这一节,如果把 style-loader 放到 css-loader 后面,我们就会撞到错误:

ERROR in ./index.css
Module build failed: Unknown word (5:1)

  3 | // load the styles
  4 | var content = require("!!./index.css");
> 5 | if(typeof content === 'string') content = [[module.id, content, '']];
    | ^
  6 | // Prepare cssTransformation
  7 | var transform;
  8 |

 @ ./index.js 3:0-21
 @ multi (webpack)-dev-server/client?http://localhost:8080 ./index.js
webpack: Failed to compile

因为 style-loader 无法理解 CSS 文件,需要先经 css-loader 预处理 - 是的,加载器的执行顺序是从后往前的。

接着,我们在 index.js 里可以这样引用样式文件:

import './index.css'

注意,这里的 CSS 仍然是全局的,等效于我们平常使用 <link href="" /> 引用的 CSS。但 webpack 还提供了 CSS Modules 的配置,可以将样式模块化。

当然,除了 CSS Modules 外,我们还有很多 CSS in js 可选,它们允许我们将样式跟 HTML、JS 放到一起 - 如果你写过 Vue.js 的单文件模块,可能会很喜欢。

脚手架

前面的步骤里,我们几乎是一步、一步手动配置每个类型文件的加载器,一次添加一小节,然后重启 webpack-dev-server,没人愿意这样干活。所以市面上有非常多的 boilerplates、presets 等,其中比较出名的有:

  1. create-react-app react 官方出品的一套,只适用开发 react.js
  2. neutrino.js 这是 Mozilla 出品的一套解决方案,Web、React、Node.js 等方案均有。

构建与优化

最后,我们需要输出文件给生产环境部署,只要执行:

$ npx webpack -p

即可。-p 参数等效于 --optimize-minimize --define process.env.NODE_ENV="'production'",具体的优化内容可以看 webpack 文档

多数时候,我们会额外再定义一个 webpack.production.config.js 文件,针对生产环境做更细致的调整,比如分离 vendors 文件等。当然,如果你用 creat-react-appneutrino.js 一类工具,通常都已经配置好了,非常方便。