Webpack 入门

Webpack 入门

原文:Webpack for React
翻译:野胡子

Webpack for React

Webpack 是什么?


多年来,Web 开发从简单的几乎不使用 JavaScript 的静态页面演变发展成具有复杂 JavaScript 和大型依赖树(依赖于多个其他文件的文件)的全功能 Web 应用程序。

为了帮助开发应用所面对的日益复杂问题,社区也提出了不同的方法和做法,例如:

  • JavaScript 模块化的应用,允许我们将程序划分并组织成多个文件。
  • JavaScript 预处理器(允许我们使用新的特性,而这些特性仅在将来的 JavaScript 版本中才可用)和compile-to-JavaScript 的语言(例如 CoffeeScript )

虽然这样的进步非常有帮助,但是,也给开发过程带来一些必要的额外步骤:我们需要把文件打包并且转换成浏览器能够理解的内容。而这就需要像 Webpack 这类的工具。

Webpack 是一个现代 JavaScript 应用程序的模块打包器:能够分析你的项目结构,找到 JavaScript 模块和其他的资源,然后将所有这些模块打包成少量的 bundle,由浏览器加载。

Webpack 和其他的构建工具(例如 Grunt 和 Gulp)相比有什么特性?


Webpack 和 Grunt/Gulp 是不一样的,它不仅仅是一个构建工具,Webpack 的优点使得 Webpack 能够替代 Grunt 和 Gulp。

Grunt 和 Gulp 这类构建工具的工作方式是:在配置文件中,你可以指定运行的任务和步骤,然后进行转换、组合和压缩这些文件。

51

Webpack 的工作方式是:它会分析你整个项目。指定一个入口文件,Webpack 会查看所有的项目依赖文件(通过 require 和 import 引入),使用 loaders 处理它们,最后打包成 JavaScript 文件。

52

开始


通过 npm 全局安装 Webpack

npm install -g webpack

或者安装到你的项目目录

npm install --save-dev webpack

创建示例


让我们来创建一个使用 Webpack 的示例项目。创建一个新的文件夹:例如 webpack-sample-project。在终端运行下面的命令,在webpack-sample-project 文件夹内新建 package.json 文件 ,这是一个标准的 npm 说明文件,其中包含项目的各种信息,并让开发人员指定依赖关系(可以自动下载并安装)并定义脚本任务。

npm init

init 命令会在终端询问一系列有关项目的问题(例如项目名称,描述,作者信息等),不过你不用担心,如果你不打算在 npm 中发布你的模块,按回车键默认即可。

当 package.json 文件创建完成后,在终端输入下面命令,添加 webpack 作为项目依赖关系并安装它:

npm install --save-dev webpack

Webpack 安装完成后,我们回到 webpack-sample-project 目录,里面有2个文件夹: app 文件夹和 public 文件夹,app 文件夹存放项目源代码和 JavaScript 模块,public 文件夹存放浏览器需要访问的公共文件(包括使用 webpack 打包生成的 JavaScript 文件以及一个 index.html 文件)。接下来让我们创建3个文件:index.html 新建到 public 文件夹中,main.js 和 Greeter.js 新建到 app 文件夹中。最后,项目结构如下图所示:

53

index.html 包含了一个基本的 HTML 页面,用于加载我们打包后的 JavaScript 文件(命名为 bundle.js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script src="bundle.js"></script>
  </body>
</html>

接下来,让我们回到之前创建的文件:main.js 和 Greeter.js。Greeter.js 返回一个带有问候信息的新 HTML元素的函数。 main.js 文件会将 Greeter 模块返回的 HTML 元素插入页面。

// main.js
var greeter = require('./Greeter.js');
document.getElementById('root').appendChild(greeter());


// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

运行你的第一个构建


Webpack 基本命令语法是 "webpack {entry file} {destination for bundled file}"。注意:Webpack 需要指定一个入口文件,它将自动确定所有项目的依赖关系。另外,如果你没有全局安装 webpack(使用命令 npm install -g webpack 安装),则需要在项目的 node_modules 文件夹中引用 webpack 命令。对于示例项目,webpack 命令使用方式如下:

node_modules/.bin/webpack app/main.js public/bundle.js

你应该能在终端看到如下输出:

54

提示:Webpack 将 main.js 和 Greeter.js 文件打包成了 bundle.js。如果你在浏览器中打开 index.html,结果将如下所示:

55

Webpack 配置


Webpack 有许多高级选项,允许使用 loaders 和 plugins 来对加载的模块转换。虽然我们也可以在命令行中使用这些配置,但是这很不直观且容易出错。最好的方法是使用 webpack 配置文件:一个简单的 JavaScript 文件,可以在其中放置与构建有关的所有配置信息。

在根目录下创建 webpack.config.js 文件

module.exports = {
  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  }
}

注:“__dirname”是 node.js 中的一个全局变量,它指向当前执行脚本所在的目录。

你可以简单在终端运行 webpack 命令(非全局安装需使用 node_modules/.bin/webpack 命令),而不需要任何参数,由于 webpack.config 文件已存在,因此webpack 命令将根据可用的配置构建应用程序。 结果如下所示:

56

添加快捷启动


在终端输入类似这样的 `node_modules/.bin/webpack` 长命令,无聊且容易出错。还好,npm 可以通过设置 package.json 文件中的 `scripts`,只需执行一个简单命令(例如 `npm start`),就能够实现和长命令一样的效果。我们可以看看下面:

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack"
  },
  "author": "Cássio Zen",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^1.12.9"
  }
}

start 是一个特殊的脚本,你可以直接在终端使用命令 npm start 执行。 你可以创建任何其他脚本名称,和 start 不同的时,执行它们你将需要使用命令 npm run {script name}(例如 npm run build)。 可以从下面的图中看出,webpack 命令被 npm start 脚本执行了:

57

生成 source maps


Webpack 有许多的配置项,先让我们来看看其中的一个配置项:Source Maps。

把所有 JavaScript 模块打包到一个(或多个)bundle 文件,具有许多优点。一个明显的缺点是在浏览器中无法进行原代码调试:尝试调试原代码是非常有挑战性的。 然而,Webpack 可以在打包时生成 source maps: source maps 提供了将打包文件中的代码映射回其原始源文件的方式,使代码在浏览器中可读和易于调试。

要配置 Webpack 以生成指向原始文件的 source maps,需要配置 devtool,它有以下四种不同的配置选项:

devtool 选项 描述
source-map Generate a complete, full featured source map in a separate file. This option has the best quality of source map, but it does slow down the build process.
cheap-module-source-map Generate source map in a separate file without column-mappings. Stripping the column mapping favors a better build performance introducing a minor inconvenience for debugging: The browser developer tools will only be able to point to the line of the original source code, but not to a specific column (or character).
eval-source-map Bundles the source code modules using "eval", with nested, complete source map in the same file. This option does generate a full featured source map without a big impact on build time, but with performance and security drawbacks in the JavaScript execution. While it's a good option for using during development, this option should never be used in production.
cheap-module-eval-source-map The fastest way to generate a source map during build. The generated source map will be inlined with the same bundled JavaScript file, without column-mappings. As in the previous option, there are drawbacks in JavaScript execution time, so this option is not appropriate for generating production-ready bundles.

了解更多,请查看 webpack source-mapping

从描述中可以看出,由上往下构建打包时间越来越快。

对中小型项目来说,“eval-source-map”是一个很好的选择:它生成一个完整的 source map,不过,你只能在开发过程中使用它。 继续对项目中的 webpack.config.js 文件进行如下配置:

module.exports = {
  devtool: 'eval-source-map',
  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  }
}

Webpack 开发服务器


Webpack 有一个可选的服务器可用于本地开发。 它是一个小型的 node.js express app,它提供静态文件服务,并根据您的 Webpack 配置打包资源文件,缓存在内存中,并在更改源文件时自动刷新浏览器。 它是一个单独的 npm 模块,需要作为项目依赖安装:
npm install --save-dev webpack-dev-server

webpack-dev-server 可以由 webpack.config.js 文件中的 devserver 进行配置,相关的配置如下:

devserver 选项 描述
contentBase 默认情况下, webpack-dev-server 为根文件夹提供本地服务器,如果想为其他目录下的文件提供本地服务器,应该在这里设置其所在目录(例如我们的示例项目设置为 “public”)
port 设置默认监听端口,如果省略,默认为”8080“
inline 设置为 true,当源文件改变时会自动刷新页面
colors 设置终端输出字体的颜色
historyApiFallback 开发单页应用时非常有用,它依赖于 HTML5 history API,如果设置为 true,所有的跳转将指向 / ,也就是 index.html 文件

把这些命令都加到 webpack 的配置文件中,现在的 webpack.config.js 文件配置如下所示:

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  devServer: {
    contentBase: "./public",
    colors: true,
    historyApiFallback: true,
    inline: true
  } 
}

现在你可以执行 webpack-dev-server 启动服务器:

node_modules/.bin/webpack-dev-server

为了方便起见,您可以在项目的 package.json 文件中的 “scripts" 添加如下命令,通过调用 “npm start” 来运行服务器,如下所示(在脚本内无需填写 “node_modules / .bin” 这样的完整路径):

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack-dev-server --progress"
  },
  "author": "Cássio Zen",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^1.12.9",
    "webpack-dev-server": "^1.14.0"
  }
}

Loaders(加载器)


Webpack 最令人激动的功能之一就是 loaders。 通过使用 loaders,Webpack 可以调用外部的脚本和工具来处理源文件。 例如将 JSON 文件解析为纯 JavaScript,或将下一代的 JavaScript(ES6,ES7) 代码转换为当前浏览器可以理解的常规 JavaScript 代码。loaders 对于 React 开发也是必不可少的,因为它们可将 React 的 JSX 转换为 JavaScript。

Loaders 需要单独安装,并在 webpack.config.js 中的 “modules” 选项下进行配置。 Loaders 配置包括:

  • test:一个用以匹配 loader 所处理文件的拓展名的正则表达式(必须)
  • loader:loader 的名称(必须)
  • include/exclude: 手动添加必须处理的文件(文件夹)或忽略不需要处理的文件(文件夹)(可选);
  • query:为 loader 提供额外的设置选项(可选)

用示例查看更清晰,更改示例应用程序,并将问候语文本移到单独的 Json 配置文件。 首先安装 Webpack 的 json-loader 模块:

npm install --save-dev json-loader

接下来,在 webpack.config.js 文件中添加 json-loader:

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      }
    ]
  },

  devServer: {
    contentBase: "./public",
    colors: true,
    historyApiFallback: true,
    inline: true
  }
}

最后,创建一个 config.json 文件,并在 Greeter.js 中 require。 新的 config.json 文件的源代码如下所示:

{
  "greetText": "Hi there and greetings from JSON!"
}

更新后的 Greeter.js 如下所示:

var config = require('./config.json');

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = config.greetText;
  return greet;
};

Babel


Babel 是一个编译 JavaScript 的平台,它非常强大,强大到可以让你:

  • 使用下一代的 JavaScript 编程(ES6 / ES2015,ES7 / ES2016 等等),即使这些标准现在还未被当前的浏览器完全的支持;
  • 使用 JavaScript 的拓展语言,例如 React 的 JSX。

Babel 是一个独立的工具,但是也可以和 Webpack 配合使用。

安装和配置 Babel


Babel 是若干个 npm 模块包,它的核心功能由 babel-core 包提供。如果你在代码中希望使用不同的功能和扩展,则需要分别安装单独的包(最常见的是分别是用于编译 ES6 和 React 的 JSX 的 babel-preset-es2015 和 babel-preset-react 包),使用下面的命令安装:

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react

和其他的 loader 一样,你可以在示例项目中的 webpack.config.js 文件中配置 babel,详细的配置请看下面:

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: {
          presets: ['es2015','react']
        }
      }
    ]
  },

  devServer: {
    contentBase: "./public",
    colors: true,
    historyApiFallback: true,
    inline: true
  }
}

现在,webpack 的配置可以让我们在项目中使用 ES6 模块和语法,以及 JSX。接下来,我们修改示例代码。让我们先安装 React 和 React-DOM:

npm install --save react react-dom

接下来让我们修改 Greeter.js ,使用 ES6 语法定义并返回一个 React 组件,代码如下:

import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

最后,让我们更新 main.js 文件,使用 ES6 语法渲染 Greeter.js 组件:

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

render(<Greeter />, document.getElementById('root'));

Babel 的配置文件


Babel 可以完全在 webpack.config.js 文件中配置,但是,Babel 本身就有许多配置项,如果全部都放在 webpack.config.js 文件中,webpack.config.js 会很臃肿,所以,许多开发人员会单独创建一个文件用于配置 Babel:即 .babelrc(请注意,名称前的小数点)

让我们将 babel 的配置提取出来,分为两个配置文件进行配置,如下所示:

// webpack.config.js
module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      }
    ]
  },

  devServer: {...} // Omitted for brevity
}

// .babelrc
{
  "presets": ["react", "es2015"]
}

Beyond JavaScript


Webpack 最独特的特点之一就是将每个文件视为一个模块。通过对应的 loaders,对 JavaScript 代码,CSS,字体等进行处理。 Webpack 可以根据 @import 和 CSS 中的 URL 将所有有依赖关系的文件构建,预处理和打包。

样式


Webpack 提供两种 loaders 来处理样式:`css-loader` 和 `style-loader`。两者处理的任务不同,`css-loader` 负责查找 @import 和 url(...) 并处理它们, `style-loader`将所有的计算后的样式加入页面中。两者结合使用,能够把样式嵌入到 webpack 打包后的 JS 文件中。

接下来,我们安装 css-loaderstyle-loader 和更新 webpack.config.js 文件:

npm install --save-dev style-loader css-loader
// webpack.config.js
module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/build",
    filename: "bundle.js"
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.css$/,
        loader: 'style!css'
      }
    ]
  },

  devServer: {...}
}

Note: The exclamation point ("!") can be used in a loader configuration to chain different loaders to the same file types.

接着,在项目中新建 main.css 文件,对应用的一些元素设置默认样式,写入如下内容:

html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}

Webpack 是通过诸如 import, require, url 等方式与入口文件建立依赖关系,这意味着我们创建的 main.css 文件也必须在应用程序中的某个地方导入,以便 webpack “找到”它。在示例项目中,我们从 main.js 入口文件导入 main.css

import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';    // //使用import导入css文件

render(<Greeter />, document.getElementById('root'));

CSS Modules


在过去的几年里,JavaScript 通过新的语言特性、更好的工具和更好的实践方法(例如模块化)发展得非常迅速。

模块让开发人员将复杂的代码分解成具有依赖关系的小型、干净和独立的单元。由优化工具、依赖关系管理和 load 管理可以自动完成整合。

但是大多数样式表仍然非常庞大,全局的声明使得开发和维护变得非常困难。

最近一个名为 CSS modules 的项目旨在将所有这些优势引入 CSS。通过 CSS modules,所有的类名,动画名称默认都只作用于当前模块。Webpack 从一开始包含对 CSS modules 的支持,它内置在 CSS loader 中,你所需要做的就是通过传递 “modules” 查询字符串来激活它。然后就可以直接把 CSS 的类名传递到组件的代码中。(这样做只对当前组件有效,不必担心在不同的模块中使用相同的类名造成冲突)

编辑 webpack.config.js 以启用 CSS modules(如下所示)

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {...},

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.css$/,
        loader: 'style!css?modules' // 启用 css modules
      }
    ]
  },

  devServer: {...}
}

接下来,在 app 文件夹下创建一个 greeter.css 文件:

.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}

接着更新 Greeter.js 文件:

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css'; // 导入 css

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

CSS modules 是一个很大的主题。更深入的 CSS modules 知识已经超出本文的范围,但你可以在 GitHub 的 官方文档 中了解更多信息。

CSS 预处理器


CSS 预处理器(如Sass 和 Less)是对原生 CSS 的拓展,它们允许你使用 CSS 中不存在的特性(如 variablesnestingmixinsinheritance等)编写 CSS,CSS 预处理器可以把这些特殊类型的语句转化为浏览器可识别的普通 CSS。

你可能已经熟悉了,在 webpack 里使用 loaders 进行配置就可以使用了,下面是常用的 CSS loaders:

  • Less loader
  • Sass loader
  • Stylus loader

此外还有一个用于 CSS 转换的工具:PostCSS,它可以帮助 CSS 实现更多功能,更多信息可以访问官方文档

举例说明,我们使用 PostCSS loader 的 autoprefixer 插件(给 CSS 自动添加前缀),给 CSS 代码添加适应不同浏览器的前缀。首先安装 PostCSS 和使用 autoprefixer 插件:

npm install --save-dev postcss-loader autoprefixer

接下来,在 webpack 配置文件中添加 postcss-loader,配置需要使用到的哪些插件(这里我们只使用 autoprefixer):

module.exports = {
  devtool: 'eval-source-map',
  entry: __dirname + "/app/main.js",
  output: {...},

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.css$/,
        loader: 'style!css?modules!postcss'
      }
    ]
  },

  postcss: [
    require('autoprefixer')  // 添加 autoprefixer 插件
  ],

  devServer: {...}
}

Plugins(插件)


Webpack 可以通过插件扩展功能。在 Webpack 中,插件可以将自己注入到构建过程中以执行相关特定的任务。

loadersplugins(插件) 常常被混淆,但它们是完全不同的东西。大概来说,loaders 会处理每个源文件(例如 JSX、LESS),一次一个,而 plugins 不对单个源文件进行操作:它们作用于整个构建过程。

Webpack 有许多内置插件,但也有很多第三方插件可用。

在该节中,我们将在开发中体验一些最常用的插件。

开始使用插件


使用插件前,使用 npm 安装它。安装完成后,在 webpack.config.js 配置文件中的 plugins 数组内添加该插件对象的一个实例。

接着继续我们的示例,添加一个给打包后代码添加版权声明的插件:bannerPlugin

webpack.config.js 详细的配置如下:

var webpack = require('webpack');  // 导入 webpack

module.exports = {
  devtool: 'eval-source-map',
  entry:  __dirname + "/app/main.js",
  output: {...},

  module: {
    loaders: [
      { test: /\.json$/, loader: "json" },
      { test: /\.js$/, exclude: /node_modules/, loader: 'babel' },
      { test: /\.css$/, loader: 'style!css?modules!postcss' }
    ]
  },
  postcss: [
    require('autoprefixer')
  ],

  plugins: [
    new webpack.BannerPlugin("Copyright Flying Unicorns inc.")  // 添加版权插件
  ],

  devServer: {...}
}

打包后的 JS 文件会添加如下的版权信息:

58

下面是一些常用插件

HtmlWebpackPlugin


在第三方 Webpack 插件中,其中最有用的莫过于 HtmlWebpackPlugin 插件。

这个插件的作用是根据一个 index.html 模板,生成一个自动引用你打包后的 JS 文件的新 的 HTML5 文件。

使用下面的命令安装 HtmlWebpackPlugin:

npm install --save-dev html-webpack-plugin

接下来,按照下面步骤对项目结构进行一些修改:

1:移除 public 文件夹:因为利用 HtmlWebpackPlugin 插件能够自动生成 HTML5 页面,因此你可以删除在 public 文件夹中手动创建的 index.html 文件。同时前面 CSS 的配置已经将 CSS 打包进 JS 文件中,所以整个 public 文件夹将不再需要,可以完全删除。

2:在 app 目录下,创建一个 HTML 模板文件 index.tmpl.html ,该模板包含 title 等元素,在构建过程中,html-webpack-plugin 插件会依据此模板生成最终的 html 页面,会自动添加所依赖的 css, js,manifest,favicon 等文件,index.tmpl.html 模板源代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

3:更新 webpack 配置,添加 HTMLWebpackPlugin 插件到 webpack 的 plugins 中,同时新建一个 build 文件夹,用于接受构建打包后的输出文件。

// webpack.config.js
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');  // 导入 html-webpack-plugin

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/build",     // 构建后的文件导出到build目录
    filename: "bundle.js"
  },

  module: {
    loaders: [
      { test: /\.json$/, loader: "json" },
      { test: /\.js$/, exclude: /node_modules/, loader: 'babel' },
      { test: /\.css$/, loader: 'style!css?modules!postcss' }
    ]
  },
  postcss: [
    require('autoprefixer')
  ],

  plugins: [
    new HtmlWebpackPlugin({
      template: __dirname + "/app/index.tmpl.html"  // 配置 HtmlWebpackPlugin
    })
  ],

  devServer: {
    colors: true,
    historyApiFallback: true,
    inline: true
  }
}

模块热替换


Webpack 著名的一个特性是模块热替换:即 Hot Module Replacement(HMR),它允许你修改组件代码后,浏览器自动刷新实时预览修改后的效果。

在 Webpack 中启用 HMR 很简单,你需要进行两个配置:
1. 将 HotModuleReplacementPlugin 添加到 webpack 中。
2. 将 hot 参数添加到 Webpack Dev Server 中。

不过,配置完这些后,JS 模块还不能自动热加载,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用。还有一个比较实用的方法:使用我们熟悉的 Babel。

如你所见,Babel 和 Webpack 可以一起工作,它们都能够转换 JavaScript 文件。在我们的示例里,配置了 Webpack 和 Babel 将 JSX 和 ES6 代码转换成浏览器可以理解的普通 JavaScript。使用 Babel 插件,可以使 Webpack 进行额外的转换,让你所有的组件代码支持 HMR。

让我们重新整理下有些混乱的思路:

  • Webpack 和 Babel 是独立的工具
  • 两者可以一起工作
  • 两者都可以通过插件扩展功能
  • HMR 是一个 Webpack 插件,当你更改代码后,它可以让你在浏览器看到实时刷新后的结果。但需要你做一些额外相关配置工作,才能使 HMR 工作。
  • Babel 有一个名为 react-transform-hmr 的插件,可以在所有的 React 组件中自动插入所需的 HMR 代码(即让 HMR 生效)。

让我们更新示例,以启用热加载。从 Webpack 配置开始修改,如下所示:

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  devtool: 'eval-source-map',
  entry: __dirname + "/app/main.js",
  output: {
    path: __dirname + "/build",
    filename: "bundle.js"
  },

  module: {
    loaders: [
      { test: /\.json$/, loader: "json" },
      { test: /\.js$/, exclude: /node_modules/, loader: 'babel' },
      { test: /\.css$/, loader: 'style!css?modules!postcss' }
    ]
  },
  postcss: [
    require('autoprefixer')
  ],

  plugins: [
    new HtmlWebpackPlugin({
      template: __dirname + "/app/index.tmpl.html"
    }),
    new webpack.HotModuleReplacementPlugin()  // 启用热加载插件
  ],

  devServer: {
    colors: true,
    historyApiFallback: true,
    inline: true,
    hot: true  // hot 参数
  }
}

接下来安装 Babel 所需的插件:

npm install --save-dev babel-plugin-react-transform react-transform-hmr

然后更新根目录下的 .babelrc 配置文件,添加我们安装的插件:

{
  "presets": ["react", "es2015"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         // if you use React Native, pass "react-native" instead:
         "imports": ["react"],
         // this is important for Webpack HMR:
         "locals": ["module"]
       }]
       // note: you can put more transforms into array
       // this is just one of them!
     }]]
    }
  }
}

npm start 启动服务器,并尝试修改 Greeter.js 文件中的内容,浏览器应该会实时显示更新后的内容了。

生产环境构建


到目前为止,我们已经使用 webpack 构建了一个完整的开发环境。但是在生产环境,可能还需要对打包后的文件进行额外处理,例如压缩,缓存以及分离 CSS 和 JavaScript。

对于完整或复杂的项目,将 Webpack 配置分割成多个文件是保持一切更有条理的好习惯。 在示例项目根目录下,创建一个名为 webpack.production.config.js 的新文件,并填写一些基本设置。

webpack.production.config.js 包含项目所需的基本配置,它和我们的 webpack.config.js 文件非常相似,具体如下:

// webpack.production.config.js
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: __dirname + "/app/main.js",
  output: {
    path: __dirname + "/build",
    filename: "bundle.js"
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.css$/,
        loader: 'style!css?modules!postcss'
      }
    ]
  },
  postcss: [
    require('autoprefixer')
  ],

  plugins: [
    new HtmlWebpackPlugin({
      template: __dirname + "/app/index.tmpl.html"
    }),
  ],

}

接着我们编辑 package.json 文件,添加一个新的构建任务,该任务在生产环境中运行Webpack,并分配新创建的配置文件:

// package.json
{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack-dev-server --progress",
    "build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"  // 添加生产环境构建任务
  },
  "author": "Cássio Zen",
  "license": "ISC",
  "devDependencies": {...},
  "dependencies": {...}
}

优化插件


Webpack 提供了一些在发布阶段非常有用的优化插件,大多来自于 webpack 社区,可以通过 npm 安装,可以通过使用以下 Webpack 插件来实现上述生产构建的所有期望特征(如压缩,缓存以及分离 CSS):

  • OccurenceOrderPlugin:Webpack提供了识别模块的ID。使用此插件,Webpack 将分析并优先处理分配最小ID的常用模块。
  • UglifyJsPlugin:压缩 JavaScript 代码。
  • ExtractTextPlugin:CSS 单独导出,分离 CSS 和 JS(样式不再打包到 JavaScript 文件中)。

让我们开始添加这些插件到 webpack.production.config.js 文件中,OccurenceOrderUglifyJS 插件是内置的,我们只需要安装 ExtractTextPlugin 即可:

npm install --save-dev extract-text-webpack-plugin

webpack.production.config.js 文件配置如下:

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: __dirname + "/app/main.js",
  output: {
    path: __dirname + "/build",
    filename: "bundle.js"
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract('style', 'css?modules!postcss')
      }
    ]
  },
  postcss: [
    require('autoprefixer')
  ],

  plugins: [
    new HtmlWebpackPlugin({
      template: __dirname + "/app/index.tmpl.html"
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin(),
    new ExtractTextPlugin("style.css")
  ]
}

终端运行 npm run build 命令,在 build 目录中查看打包后的文件,代码应该都被压缩了。

缓存


缓存无处不在(CDNSs, ISPs, 网络设备, Web 浏览器...),一种简单而有效的方式使用缓存就是确保你的文件名是独一无二的(即如果文件内容发生变化,文件名也应该更改)

Webpack 可以为打包后的文件名添加哈希值,只需将[name],[id]和[hash]的特殊字符串组合添加到输出文件名配置中即可,详细配置如下:

// webpack.production.config.js
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: __dirname + "/app/main.js",
  output: {
    path: __dirname + "/build",
    filename: "[name]-[hash].js"  // 文件名添加哈希值
  },

  module: {
    loaders: [
      {
        test: /\.json$/,
        loader: "json"
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract('style', 'css?modules!postcss')
      }
    ]
  },
  postcss: [
    require('autoprefixer')
  ],

  plugins: [
    new HtmlWebpackPlugin({
      template: __dirname + "/app/index.tmpl.html"
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin(),
    new ExtractTextPlugin("[name]-[hash].css")  
  ]
}

总结


Webpack是一个用于处理和打包你所有项目模块的神奇工具。在本文中,你学习了如何正确配置Webpack以及如何使用Webpack中的loaders和plugins来创建更好的开发体验。

可能还喜欢下面的内容

JavaScript 中有趣的特性

JavaScript 中有趣的特性

帮你节省旅行开销的几点方法

帮你节省旅行开销的几点方法

深入了解 Webpack 4

深入了解 Webpack 4

ins@heyrock