webpack 如其名,是一个前端资源打包工具,它提供了 loader 这种插件的方式来支持扩展,只需简单的一个 webpack.config.js 文件就可以完成前端开发中需要的各项功能。而且,它还可以整合到 grunt/gulp 中。与单纯地压缩编译文件之类的工具不同,webpack 通过分析一个入口(entry)文件解析其中的所有文件依赖关系,通过配置的 loader 处理并最终打包成一个文件。

一个简单的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var path = require('path');
module.exports = {
entry: { // webpack 允许提供多个入口文件,这样的话,output中就需要使用[name]来指定生成的文件名,即entry中的key
'app': './modules/app.js',
'react-app': './react-components/main.jsx'
},
output: {
path: path.join(__dirname, 'dist'),
filename: "[name].js",
chunkFilename: "[name].js"
},
// 显然,有些库文件我们并不希望每次都加到打包文件中,webpack允许使用externals来添加这些忽略项
// 这时,以后使用的时候就直接 var React = require('react'); 就好了
externals: {
'react': 'window.React',
'jquery': 'window.jQuery'
},
// resolve 指定可以被 require 的文件后缀,这样可以直接使用 require('./View') 而不是 require('./View.jsx') 了
resolve: {
extensions: ['', '.js', '.jsx']
},
module: {
// 前面提到了的babel,我们可以指定js或jsx后缀的文件使用babel来进行编译中间处理
loaders: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules|bower_components)/,
// babel?stage=0
loader: 'babel'
},
{
test: /\.scss$/,
loader: "style!css!autoprefixer?safe=true!sass?outputStyle=expanded"
}
]
}
};

具体的很多配置需求根据项目的需要,可以查询官方的文档。下面重点介绍 webpack-dev-server。

webpack-dev-server 是一个基于 webpack-dev-middleware 和 Node Express 的 webpack 打包小型 Web 服务器。同时在运行时通过 socket.io 与服务器进行通信。由服务器将打包状态发送给客户端,而客户端监听这些信息并作出相应的变化 。webpack-dev-server 提供了多种模式来达到这个目的,假定你有如下配置文件:

1
2
3
4
5
6
7
8
9
10
11
var path = require("path");
module.exports = {
entry: {
app: ["./app/main.js"]
},
output: {
path: path.resolve(__dirname, "build"),
publicPath: "/assets/",
filename: "bundle.js"
}
};

其中主目录下包含 app/main.js 文件作为 webpack 打包的根文件,生成结果文件 build/bundle.js

注:output.publicPath 用于最终生成的服务器端静态文件的目标文件夹,在 webpack-dev-server 开发中,它实际并不会写入磁盘,而是在内存中。所以浏览器也能加载到这个资源。

Content Base

webpack-dev-server 默认情况下将当前主目录作为 Web 服务器的根目录。可以使用--content-base选项进行配置:

1
webpack-dev-server --content-base public/

这样,public目录就成了web服务器的根目录。前面配置文件中,有一项publicPath,现在,你可以直接用 http://localhost:8080/assets/bundle.js来访问这个打包的文件。再次注意,这个文件是写在内存中而非每次写入硬盘的。每次源文件的变化,都会引起打包文件的变化。

假设 public 文件夹下有如下 index.html 文件:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script src="asset/bundle.js"></script>
</body>
</html>

这样,直接用 http://localhost:8080 就可以开始访问页面了。

自动刷新

webpack-dev-server还有一个很大的亮点在于它的自动刷新功能。目前webpack-dev-server提供了多种方式来进行刷新:

  • iframe模式:页面被嵌入在一个 iframe 中并响应文件的变化作刷新。默认情况下,使用的就是这种方式。我们只需要访问:http://localhost:8080/webpack-dev-server/index.html即可看到结果。
  • 内联模式:webpack-dev-server会直接向页面中加入一段代码,当监听到文件发生变化时进行刷新。
  • HTML模式:在HTML中插入一个script标签:<script src="http://localhost:8080/webpack-dev-server.js"></script>即可。

上述的这两种方式都支持模块热替换(页面不再是整体刷新,而是通过向页面注入变化的代码片断来实现无刷新的热替换)。

内联模式

内联模式不能在webpack.config.js文件中开启,为开启它,可以使用 webpack-dev-server 命令时要加参数 --inline,然后直接访问http://localhost:8080/index.html即可:

1
webpack-dev-server --inline --content-base public/

在使用 Node 手工调用 webpack-dev-server 时,可以使用下面的方式开启内联模式:

1
2
3
4
5
var config = require("./webpack.config.js");
config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080");
var compiler = webpack(config);
var server = new webpackDevServer(compiler, {...});
server.listen(8080);

模块热替换

为开启模块热替换,只需要简单的一步,加 --hot 选项即可。本质上,这会向配置中注入 HotModuleReplacementPlugin 插件。

命令行下,配合热替换的最简单的方式是使用内联模式:

1
webpack-dev-server --content-base public/ --inline --hot

成功运行后,你会看到浏览器 console 中出现以下文字:

1
2
[HMR] Waiting for update signal from WDS...
[WDS] Hot Module Replacement enabled.

其中以 [HMR] 打头的信息来自 webpack/hot/dev-server 模块,[WDS]打头的则来自 webpack-dev-server 客户端。

使用 Node 直接调用 API 开启模块热替换也很方便,只需要:

  1. 向配置的 entry 选项中加入 webpack/hot/dev-server
  2. 加入插件 new webpack.HotModuleReplacementPlugin()
  3. webpack-dev-server配置中加入hot: true选项
1
2
3
4
5
6
7
var config = require("./webpack.config.js");
config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080", "webpack/hot/dev-server");
var compiler = webpack(config);
var server = new webpackDevServer(compiler, {
hot: true
});
server.listen(8080);

代理支持

webpack-dev-server尽管提供的是一个本地开发环境,但需求中难免会有使用外部接口的情况(如认证、数据 Mock 等)。webpack-dev-server底层使用[node-http-proxy]提供请求代理的转发,一个简单的配置如下:

1
2
3
4
5
6
7
8
9
10
{
devServer: {
proxy: {
'/some/path*': {
target: 'https://other-server.example.com',
secure: false,
},
},
},
}

具体的选项配置与 node-http-proxy 保持一致。与此同时,webpack-dev-server还提供了绕过代理的一种途径,这往往用于正则作代理匹配的时候出现的一些白名单路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
devServer: {
proxy: {
'/some/path*': {
target: 'https://other-server.example.com',
secure: false,
byPass: (req, res, proxyOption) => { // 或者返回 false
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.');
return '/index.html';
}
}
},
},
},
}

关于webpack-dev-server CLI的一些使用,这里不作赘述,命令选项可以参考http://webpack.github.io/docs/webpack-dev-server.html#webpack-dev-server-cli。对于 Node 调用的 API,其实前面也已经提到了,

1
2
3
4
5
6
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var compiler = webpack(require('./webpack.config'));
var server = new WebpackDevServer(compiler, {});
server.listen(8080, '0.0.0.0', callback);

集成已存在的 Web 服务器

很多情况下,由于业务的原因,我们并非会直接使用 webpack-dev-server,而是使用如 PHP 或其它方式搭建一个面向业务的本地化 Web 服务器,此时,webpack-dev-server 只能是用来服务静态文件。那么如何集成进来呢?

可以运行两个服务:webpack-dev-server服务及业务后端服务。

这种情况下,我们只需要将所有 webpack 生成的文件请求(包括业务后端服务生成的 HTML 中所包含的请求)全转发到 webpack-dev-server 服务上。另外,还需要将业务后端服务生成 HTML 中包含的静态资源路由指向 webpack-dev-server。也就是说,需要建立两个服务之间联系以便重新编译文件时能够触发重新加载。

以一个简单的例子来说明上述过程:

webpack-dev-server 监听端口 8080,后端服务监听端口 9090,配置后端服务生成的 HTML 中包含 <script src="http://localhost:8080/assets/bundle.js">,同时,配置 webpack 的 output publicPath 项为 output.publicPath = "http://localhost:8080/assets/"。当生产环境下编译文件时,使用 -output-public-path /assets/