项目作者: liangklfangl
项目描述 :
Webpack-dev-server源码与原理深入分析,来自于[我的github文章全集](https://github.com/liangklfangl/react-article-bucket)
高级语言: JavaScript
项目地址: git://github.com/liangklfangl/webpack-dev-server.git
如果你想深入了解webpack-dev-server的内部原理,你可以查看我写的这个打包工具,通过它可以完成三种打包方式,其中devServer模式就是通过webpack-dev-server来完成的,并且支持HMR。对于webpack的HMR不了解的可以查看这里。其中也牵涉到webpack-dev-middleware中间件。如果觉得有用,记得start哦~
这篇文章来自于我的github文章全集
1.webpack-dev-server配置
1.1 ContentBase
webpack-dev-server会使用当前的路径作为请求的资源路径(所谓当前的路径
就是你运行webpack-dev-server这个命令的路径,如果你对webpack-dev-server进行了包装,比如wcf,那么当前路径指的就是运行wcf命令的路径,一般是项目的根路径),但是你可以通过指定content base来修改这个默认行为:
$ webpack-dev-server --content-base build/
这样webpack-dev-server就会使用build目录
下的资源来处理静态资源的请求,比如css/图片等
。content-base一般不要和publicPath,output.path混淆掉。其中content-base表示静态资源
的路径是什么,比如下面的例子:
<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<div id="react-content">这里要插入js内容</div>
</body>
</html>
在作为html-webpack-plugin的template以后,那么上面的index.css
路径到底是什么?是相对于谁来说?上面我已经强调了:如果在没有指定content-base的情况下就是相对于当前路径
来说的,所谓的当前路径就是在运行webpack-dev-server目录来说的,所以假如你在项目根路径运行了这个命令,那么你就要保证在项目根路径下存在该index.css资源,否则就会存在html-webpack-plugin的404报错。当然,为了解决这个问题,你可以将content-base修改为和html-webpack-plugin的html模板一样的目录。
上面讲到content-base只是和静态资源的请求有关,那么我们将其和publicPath
和output.path
做一个区分:
首先:假如你将output.path设置为build
(这里的build和content-base的build没有任何关系,请不要混淆),你要知道webpack-dev-server实际上并没有将这些打包好的bundle写到这个目录下,而是存在于内存中的,但是我们可以假设
(注意这里是假设)其是写到这个目录下的
然后:这些打包好的bundle在被请求的时候,其路径是相对于你配置的publicPath
来说的,因为我的理解publicPath相当于虚拟路径,其映射于你指定的output.path
。假如你指定的publicPath为 “/assets/“,而且output.path为”build”,那么相当于虚拟路径”/assets/“对应于”build”(前者和后者指向的是同一个位置),而如果build下有一个”index.css”,那么通过虚拟路径访问就是/assets/index.css
。
最后:如果某一个内存路径(文件写在内存中)已经存在特定的bundle,而且编译后内存中有新的资源,那么我们也会使用新的内存中的资源来处理该请求,而不是使用旧的bundle!比如我们有一个如下的配置:
module.exports = {
entry: {
app: ["./app/main.js"]
},
output: {
path: path.resolve(__dirname, "build"),
publicPath: "/assets/",
//此时相当于/assets/路径对应于build目录,是一个映射的关系
filename: "bundle.js"
}
}
那么我们要访问编译后的资源可以通过localhost:8080/assets/bundle.js来访问。如果我们在build目录下有一个html文件,那么我们可以使用下面的方式来访问js资源
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script src="assets/bundle.js"></script>
</body>
</html>
此时你会看到控制台输出如下内容:

主要关注下面两句输出:
- Webpack result is served from /assets/
- Content is served from /users/…./build
之所以是这样的输出结果是因为我们设置了contentBase为build,因为我们运行的命令为`webpack-dev-server --content-base build/`。所以,一般情况下:如果在html模板中不存在对外部相对资源的引用,我们并不需要指定content-base,但是如果存在对外部相对资源css/图片的引用,我们可以通过指定content-base来设置默认静态资源加载的路径,除非你所有的静态资源全部在`当前目录下`。但是,在wcf中,如果你指定的htmlTemplate,那么我会默认将content-base设置为htmlTemplate同样的路径,所以在htmlTemplate中你可以随意`使用相对路径`引用外部的css/图片。
我们看看webpack-dev-server中是如何处理的:
```js
contentBaseFiles: function() {
//如果contentBase是数组
if(Array.isArray(contentBase)) {
contentBase.forEach(function(item) {
app.get("*", express.static(item));
});
//如果contentBase是https/http的路径,那么重定向
} else if(/^(https?:)?\/\//.test(contentBase)) {
console.log("Using a URL as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.");
console.log('proxy: {\n\t"*": "
"\n}'); // eslint-disable-line quotes
// Redirect every request to contentBase
app.get("*", function(req, res) {
res.writeHead(302, {
"Location": contentBase + req.path + (req._parsedUrl.search || "")
});
res.end();
});
} else if(typeof contentBase === "number") {
console.log("Using a number as contentBase is deprecated and will be removed in the next major version. Please use the proxy option instead.");
console.log('proxy: {\n\t"*": "//localhost:"\n}'); // eslint-disable-line quotes
// Redirect every request to the port contentBase
app.get("*", function(req, res) {
res.writeHead(302, {
"Location": `//localhost:${contentBase}${req.path}${req._parsedUrl.search || ""}`
});
res.end();
});
} else {
// route content request
// http://www.expressjs.com.cn/starter/static-files.html
// 把静态文件的目录传递给static那么以后就可以直接访问了
app.get("*", express.static(contentBase, options.staticOptions));
}
}
```
此处不解释,因为其调用的就是express.static方法,主要用于请求静态资源。注意webpack官网的说明:
Can be used to configure the behaviour of webpack-dev-server when the webpack config is passed to webpack-dev-server CLI.
也就是说这个配置只有在命令行中有用,而不能直接传入到webpack.config.js中产生作用!
同时这个配置也会影响[serve-index的作用](https://github.com/liangklfang/serve-index)
```js
contentBaseIndex: function() {
if(Array.isArray(contentBase)) {
contentBase.forEach(function(item) {
app.get("*", serveIndex(item));
//The path is based off the req.url value, so a req.url of '/some/dir with a path of 'public' will look at 'public/some/dir'
//其中这里的path表示我们的contentBase,所以我们的请求都是在contentBase下寻找
});
} else if(!/^(https?:)?\/\//.test(contentBase) && typeof contentBase !== "number") {
app.get("*", serveIndex(contentBase));
}
}
```
注意:在webpack2中--content-base在webpack.config.js中配置也是可以生效的,建议使用一下我上面的wcf打包工具!!!!
#### 1.2 自动刷新
##### 1.2.1 iframe mode:
我们的页面被嵌套在一个iframe中,当资源改变的时候会重新加载。只需要在路径中加入webpack-dev-server就可以了,不需要其他的任何处理:
```js
http://localhost:8080/webpack-dev-server/index.html
```
从而在页面中就会产生如下的一个iframe标签并注入css/js/DOM:

这个iframe页面会请求 live.bundle.js ,其中里面会新建一个 Iframe ,你的应用就被注入到了这个 Iframe 当中。同时 live.bundle.js 中含有 socket.io 的 client 代码,这样它就能和 webpack-dev-server 建立的 http server 进行 websocket 通讯了,并根据返回的信息完成相应的动作。(`总之,因为我们的http://localhost:8080/webpack-dev-server/index.html访问的时候加载了live.bundle.js,其具有websocket的client代码,所以当websocket-dev-server服务端代码发生变化的时候会通知到这个页面,这个页面只是需要重新刷新iframe中的页面就可以了`)
该模式有如下作用:
No configuration change needed.(不需要修改配置文件)
Nice information bar on top of your app.(在app上面有information bar)
URL changes in the app are not reflected in the browser’s URL bar.(在app里面的URL改变不会反应到浏览器的地址栏中)
##### 1.2.2 inline mode
一个小的webpack-dev-server的客户端入口被添加到文件中,用于自动刷新页面。其中在cli中输入的是:
```js
webpack-dev-server --inline --content-base ./build
```
此时在页面中输出的内容中看不到插入任何的js代码:

但是在控制台中可以清楚的知道页面的重新编译等信息:

该模式有如下作用:
Config option or command line flag needed.(webpack配置或者命令行配置)
Status information in the console and (briefly) in the browser’s console log.(状态信息在浏览器的console.log中)
URL changes in the app are reflected in the browser’s URL bar(URL的改变会反应到浏览器的地址栏中).
每一个模式都是支持Hot Module Replacement的,在HMR模式下,每一个文件都会被通知内容已经改变而不是重新加载整个页面。因此,在HMR执行的时候可以加载更新的模块,从而把他们注册到运行的应用里面。
##### 1.2.3 如何在nodejs中开启inline mode:
在webpack-dev-server配置中没有inline:true去开启inline模式,`因为webpack-dev-server模块无法访问webpack的配置`。因此,用户必须添加webpack-dev-server的客户端入口文件到webpack的配置中,具体方式如下:
方式1:To do this, simply add the following to all entry points: webpack-dev-server/client?http://«path»:«port»/,也就是在entry中添加一个内容:
```js
entry: {
app: [
'webpack-dev-server/client?http://localhost:8080/',
"./app/main.js"
]
}
```
方式2:通过下面的代码来完成:
```js
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);
```
或者也可以在HTML中加入下面的文件来完成:
```html
```
#### 1.3 Hot Module Replacement
为我们的webpack-dev-server开启HMR模式只需要在命令行中添加--hot,他会将HotModuleReplacementPlugin这个插件添加到webpack的配置中去,所以开启HotModuleReplacementPlugin最简单的方式就是使用inline模式。
##### 1.3.1 inline model in ClI
你只需要在命令行中添加--inline --hot就可以自动实现。这时候webpack-dev-server就会自动添加webpack/hot/dev-server入口文件到你的配置中去,这时候你只是需要访问下面的路径就可以了http://«host»:«port»/«path»。在控制台中你可以看到如下的内容:

其中以[HMR]开头的部分来自于webpack/hot/dev-server模块,而`以[WDS]开头的部分来自于webpack-dev-server的客户端`。下面的部分来自于webpack-dev-server/client/index.js内容,其中的log都是以[WDS]开头的:
```js
function reloadApp() {
if(hot) {
log("info", "[WDS] App hot update...");
window.postMessage("webpackHotUpdate" + currentHash, "*");
} else {
log("info", "[WDS] App updated. Reloading...");
window.location.reload();
}
}
```
而在我们的webpack/hot/dev-server中的log都是以[HMR]开头的(他是来自于webpack本身的一个plugin):
```js
if(!updatedModules) {
console.warn("[HMR] Cannot find update. Need to do a full reload!");
console.warn("[HMR] (Probably because of restarting the webpack-dev-server)");
window.location.reload();
return;
}
```
注意:我们必须指定正确的output.publicPath,否则热更新的chunks不会被加载!
##### 1.3.2 Hot Module Replacement with node.js API
此时需要修改三处配置文件:
第一:添加一个webpack的入口点,也就是webpack/hot/dev-server
第二:添加一个new webpack.HotModuleReplacementPlugin()到webpack的配置中
第三:添加hot:true到webpack-dev-server配置中,从而在服务端启动HMR(可以在cli中使用webpack-dev-server --hot)
```js
if(options.inline) {
var devClient = [require.resolve("../client/") + "?" + protocol + "://" + (options.public || (options.host + ":" + options.port))];
//将webpack-dev-server的客户端入口添加到的bundle中,从而达到自动刷新
if(options.hot)
devClient.push("webpack/hot/dev-server");
//这里是webpack-dev-server中对hot配置的处理
[].concat(wpOpt).forEach(function(wpOpt) {
if(typeof wpOpt.entry === "object" && !Array.isArray(wpOpt.entry)) {
Object.keys(wpOpt.entry).forEach(function(key) {
wpOpt.entry[key] = devClient.concat(wpOpt.entry[key]);
});
} else {
wpOpt.entry = devClient.concat(wpOpt.entry);
}
});
}
```
满足上面三个条件的nodejs使用方式如下:
```js
var config = require("./webpack.config.js");
config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080/", "webpack/hot/dev-server");
//条件一(添加了webpack-dev-server的客户端和HMR的服务端)
var compiler = webpack(config);
var server = new webpackDevServer(compiler, {
hot: true //条件二(--hot配置,webpack-dev-server会自动添加HotModuleReplacementPlugin),条件三
...
});
server.listen(8080);
```
### 2.webpack-dev-server启动proxy代理
#### 2.1 代理配置
webpack-dev-server使用[http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware)去把请求代理到一个外部的服务器,配置的样例如下:
```js
proxy: {
'/api': {
target: 'https://other-server.example.com',
secure: false
}
}
// In webpack.config.js
{
devServer: {
proxy: {
'/api': {
target: 'https://other-server.example.com',
secure: false
}
}
}
}
// Multiple entry
proxy: [
{
context: ['/api-v1/**', '/api-v2/**'],
target: 'https://other-server.example.com',
secure: false
}
]
```
这种代理在很多情况下是很重要的,比如你可以把一些静态文件通过本地的服务器加载,而一些API请求全部通过一个远程的服务器来完成。还有一个情景就是在两个独立的服务器之间进行请求分割,如一个服务器负责授权而另外一个服务应用本身。下面我给出日常开发中遇到的一个例子:
(1)我有一个请求是通过相对路径来完成的,比如地址是"/msg/show.htm"。但是,在日常和生产环境下前面会加上不同的域名,比如日常是you.test.com而生产环境是you.inc.com。
(2)那么比如我现在想在本地启动一个devServer,然后通过devServer来访问日常的服务器,而且日常的服务器地址是11.160.119.131,所以我就会通过如下的配置来完成:
```js
devServer: {
port: 8000,
proxy: {
"/msg/show.htm": {
target: "http://11.160.119.131/",
// 必须注意:这里要含有http://前缀
secure: false
}
}
}
```
此时当你请求"/msg/show.htm"的时候,其实请求的真是URL地址为"http"//11.160.119.131/msg/show.htm"。
(3)在开发环境中遇到一个问题,那就是:如果我本地的devServer启动的地址为:"http://30.11.160.255:8000/" 或者常见的"http://0.0.0.0:8000/" ,那么真实的服务器会返回一个URL要求我登录,但是,将你的本地devServer启动到localhost上就不存在这个问题了(一个可能的原因在于localhost种上了后端需要的cookie,从而代理服务器,而其他的域名没有种上cookie,导致代理服务器访问日常服务器的时候没有相应的cookie,从而要求权限验证)。其中指定localhost的方式你可以通过[wcf](https://github.com/liangklfangl/wcf)来完成,因为wcf默认可以支持ip或者localhost方式来访问。当然你也可以通过添加下面的代码来完成:
```js
devServer: {
port: 8000,
host:'localhost',
proxy: {
"/msg/show.htm": {
target: "http://11.160.119.131/",
secure: false
}
}
}
```
(4)我想在此说一下webpack-dev-server的原理是什么?你可以通过查看这个[反向代理为何叫反向代理?](https://www.zhihu.com/question/24723688)这个文章来了解基础知识。其实正向代理和反向代理用一句话来概括就是:"正向代理隐藏了真实的客户端,而反向代理隐藏了真实的服务器"。而我们的webpack-dev-server其实扮演了一个代理服务器的角色,服务器之间通信不会存在前端常见的同源策略,这样当你请求我们的webpack-dev-server的时候,他会从真实的服务器中请求数据,然后将数据发送给你的浏览器。
- (1)browser => localhost:8080(webpack-dev-server无代理) => http://you.test.com
- (2)browser => localhost:8080(webpack-dev-server有代理) => http://you.test.com
上面的第一种情况就是没有代理的情况,在我们的localhost:8080的页面通过前端策略去访问http://you.test.com 会存在同源策略,即第二步是通过前端策略去访问另外一个地址的。但是对于第二种情况,我们的第二步其实是通过代理去完成的,即服务器之间的通信,不存在同源策略问题。而我们变成了直接访问代理服务器,代理服务器返回一个页面,对于*页面中*某些满足特定条件前端请求(proxy,rewrite配置)全部由代理服务器来完成,这样同源问题就通过代理服务器的方式得到了解决。
(5)上面讲述的是target是ip的情况,如果target要指定为域名的方式,可能需要绑定host。比如下面是我绑定的host:
```js
11.160.119.131 youku.min.com
```
那么我下面的proxy配置就可以采用域名了:
```js
devServer: {
port: 8000,
proxy: {
"/msg/show.htm": {
target: "http://youku.min.com/",
secure: false
}
}
}
```
这和target绑定为IP地址的效果是完全一致的。总结一句话:"target指定了满足特定URL的请求应该对应到那台主机上,即代理服务器应该访问的真实主机地址"。
#### 2.2 绕开代理
通过一个函数的返回值可以视情况的绕开一个代理。这个函数可以查看http请求和响应以及一些代理的选项。它必须返回要么是false要么是一个URL的path,这个path将会用于处理请求而不是使用原来代理的方式完成。下面的例子的配置将会忽略来自于浏览器的HTTP请求,他和historyApiFallback配置类似。浏览器请求可以像往常一样接收到HTML文件,但是API请求将会被代理到另外的服务器:
```js
proxy: {
'/some/path': {
target: 'https://other-server.example.com',
secure: false,
bypass: function(req, res, proxyOptions) {
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.');
return '/index.html';
}
}
}
}
```
#### 2.3 代理请求中重写URL
对于代理的请求可以通过提供一个函数来重写,这个函数可以查看或者改变http请求。下面的例子就会重写HTTP请求,其主要作用就是移除URL前面的/api部分。
```js
proxy: {
'/api': {
target: 'https://other-server.example.com',
pathRewrite: {'^/api' : ''}
}
}
```
其中pathRewrite配置来自于http-proxy-middleware。
#### 2.4 代理本地虚拟主机
http-proxy-middleware会预解析本地hostname成为localhost,你可以使用下面的配置来修改这种默认行为:
```js
var server = new webpackDevServer(compiler, {
quiet: false,
stats: { colors: true },
proxy: {
"/api": {
"target": {
"host": "action-js.dev",
"protocol": 'http:',
"port": 80
},
ignorePath: true,
changeOrigin: true,
secure: false
}
}
});
server.listen(8080);
```
### 3.webpack-dev-server CLI
webpack-dev-server命令行的使用如下:
```js
$ webpack-dev-server
```
所有的webpack cli配置在webpack-dev-server cli中都是存在的有效的,除了output的默认参数。
--content-base : base path for the content.
--quiet: don’t output anything to the console.
--no-info: suppress boring information.
--colors: add some colors to the output.
--no-colors: don’t use colors in the output.
--compress: use gzip compression.
--host : hostname or IP. 0.0.0.0 binds to all hosts.
--port : port.
--inline: embed the webpack-dev-server runtime into the bundle。下面是webpack-dev-server对于--inline的处理(wpOpt中最后会得到所有的入口文件)
```js
var wpOpt = require("webpack/bin/convert-argv")(optimist, argv, {
outputFilename: "/bundle.js"
});
if(options.inline) {
var devClient = [require.resolve("../client/") + "?" + protocol + "://" + (options.public || (options.host + ":" + options.port))];
if(options.hot)
devClient.push("webpack/hot/dev-server");
//添加webpack/hot/dev-server入口
[].concat(wpOpt).forEach(function(wpOpt) {
if(typeof wpOpt.entry === "object" && !Array.isArray(wpOpt.entry)) {
Object.keys(wpOpt.entry).forEach(function(key) {
wpOpt.entry[key] = devClient.concat(wpOpt.entry[key]);
});
} else {
wpOpt.entry = devClient.concat(wpOpt.entry);
}
});
}
```
--hot: adds the HotModuleReplacementPlugin and switch the server to hot mode. Note: make sure you don’t add HotModuleReplacementPlugin twice.
--hot --inline also adds the webpack/hot/dev-server entry.
--public: overrides the host and port used in --inline mode for the client (useful for a VM or Docker).
--lazy: no watching, compiles on request (cannot be combined with --hot).
```js
if(options.lazy && !options.filename) {
throw new Error("'filename' option must be set in lazy mode.");
}
```
--https: serves webpack-dev-server over HTTPS Protocol. Includes a self-signed certificate that is used when serving the requests.
--cert, --cacert, --key: Paths the certificate files.
--open: opens the url in default browser (for webpack-dev-server versions > 2.0).
--history-api-fallback: enables support for history API fallback.
--client-log-level: controls the console log messages shown in the browser. Use error, warning, info or none.
### 4.Additional configuration options
#### 4.1 webpack-dev-server配置
当使用cli的时候,可以把webpack-dev-server的配置放在一个单独的文件中,其中key是devServer。在cli中传入的参数将会覆盖我们的配置文件的内容。如下例:
```js
module.exports = {
// ...
devServer: {
hot: true
}
}
```
```js
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var fs = require("fs");
var compiler = webpack({
// configuration
});
var server = new WebpackDevServer(compiler, {
// webpack-dev-server options
contentBase: "/path/to/directory",
// Can also be an array, or: contentBase: "http://localhost/",
hot: true,
// Enable special support for Hot Module Replacement
// Page is no longer updated, but a "webpackHotUpdate" message is sent to the content
// Use "webpack/hot/dev-server" as additional module in your entry point
// Note: this does _not_ add the `HotModuleReplacementPlugin` like the CLI option does.
historyApiFallback: false,
// Set this as true if you want to access dev server from arbitrary url.
// This is handy if you are using a html5 router.
compress: true,
// Set this if you want to enable gzip compression for assets
proxy: {
"**": "http://localhost:9090"
},
// Set this if you want webpack-dev-server to delegate a single path to an arbitrary server.
// Use "**" to proxy all paths to the specified server.
// This is useful if you want to get rid of 'http://localhost:8080/' in script[src],
// and has many other use cases (see https://github.com/webpack/webpack-dev-server/pull/127 ).
setup: function(app) {
// Here you can access the Express app object and add your own custom middleware to it.
// For example, to define custom handlers for some paths:
// app.get('/some/path', function(req, res) {
// res.json({ custom: 'response' });
// });
},
// pass [static options](http://expressjs.com/en/4x/api.html#express.static) to inner express server
staticOptions: {
},
clientLogLevel: "info",
// Control the console log messages shown in the browser when using inline mode. Can be `error`, `warning`, `info` or `none`.
// webpack-dev-middleware options
quiet: false,
noInfo: false,
lazy: true,
filename: "bundle.js",
watchOptions: {
aggregateTimeout: 300,
poll: 1000
},
// It's a required option.
publicPath: "/assets/",
headers: { "X-Custom-Header": "yes" },
stats: { colors: true },
https: {
cert: fs.readFileSync("path-to-cert-file.pem"),
key: fs.readFileSync("path-to-key-file.pem"),
cacert: fs.readFileSync("path-to-cacert-file.pem")
}
});
server.listen(8080, "localhost", function() {});
// server.close();
```
其中的配置可以查看[webpack-dev-server](http://webpack.github.io/docs/webpack-dev-middleware.html)。注意:我们的webpack配置没有传入到我们的WebpackDevServer中,因此,webpack中的devServer配置并非用于这个场景。而且,在webpackDevServer中是没有inline模式的,因此如下的js必须手动插入到页面中:
```js