为什么要基于webpack-dev-server起一个服务来打开打包后的文件?因为file协议打开的文件,不支持发送ajax请求;而通过起一个服务,可以通过http协议打开对应的html文件,可以更好的支持ajax请求的发送。

DevServer配置项详解

要配置DevServer,除了可以在配置文件里通过devServer传入参数,还可以通过命令行参数传入。需要注意的是: 只有在通过DevServer启动Webpack时,配置文件里的devServer才会生效,因为这些参数所对应的功能都是DevServer提供的,Webpack本身并不认识devServer配置项。

devServer.hot

hot属性配置模块热替换功能。DevServer的默认行为是:在发现源代码被更新后通过自动刷新整个页面来做到实时预览,开启模块热替换功能后,将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览。

devServer.inline

DevServer的实时预览功能依赖一个注入页面里的代理客户端去接收来自DevServer的命令并负责刷新网页的工作。inline属性用于配置是否将这个代理客户端自动注入将运行在页面中的Chunk里,默认自动注入。DevServer会根据我们是否开启inline来调整它的自动刷新策略。

  1. 开启inline:DevServer会在构建变化后的代码时通过代理客户端控制网页刷新;
  2. 关闭inline:DevServer将无法直接控制要开发的网页。这时它会通过iframe的方式去运行要开发的网页。在构建完变化后的代码时,会通过刷新iframe来实现实时预览,但这时我们需要通过http://localhost:8080/webpack-dev-server/实时预览自己的网页。

devServer.historyApiFallback

historyApiFallback属性用于方便地开发使用了HTML5 History API的单页应用。这类单页应用要求服务器在针对任何命中的路由时,都返回一个对应的HTML文件。例如:在访问http://localhost/userhttp://localhost/home时都返回index.html文件,浏览器端的javascript代码会从url里解析出当前页面的状态,显示对应的界面。

配置historyApiFallback最简单的做法是:

historyApiFallback: true
1

上述配置会导致任何请求都会返回index.html文件,这只能用于只有一个HTML文件的应用。如果应用由多个单页应用组成,则需要DevServer根据不同的请求返回不同的HTML文件,配置如下:

historyApiFallback: {
    // 使用正则匹配命中路由
    rewrites: [
        // /user开头的都返回user.html
        {from: /^\/user/, to: '/user.html'},
        // /game开头的都返回user.html
        {from: /^\/game/, to: '/game.html'},
        // 其他的都返回index.html
        {from: /./, to: '/index.html'},
    ]
}
1
2
3
4
5
6
7
8
9
10
11

devServer.contentBase

contentBase属性配置DevServer服务器的文件根目录。在默认情况下为项目根目录,所以在一般情况下不需要设置它,除非有额外的文件需要被DevServer服务。例如:想将项目根目录下的public目录设置成DevServer服务器的文件根目录,则可以这样配置:

devServer: {
    contentBase: path.join(__dirname, 'public')
}
1
2
3

也可以从多个目录提供内容:

module.exports = {
  //...
  devServer: {
    contentBase: [path.join(__dirname, 'public'), path.join(__dirname, 'assets')]
  }
};
1
2
3
4
5
6

需要注意的是:DevServer服务器通过HTTP服务暴露文件的方式分为两类:

  1. 暴露本地文件;
  2. 暴露Webpack构建出的结果,由于构建出的结果交给了DevServer,所以我们在使用DevServer时,会在本地找不到构建出的文件。

contentBase只能用来配置暴露本地文件的规则,可以通过contentBase: false来关闭暴露本地文件。

devServer.headers

headers配置项可以在HTTP响应中注入一些HTTP响应头,使用如下:

devServer: {
    headers: {
        'X-Custom-Foo': 'bar'
    }
}
1
2
3
4
5

devServer.host

host配置项用于配置DevServer服务监听的地址。默认情况下是localhost,如果想让局域网中的其他设备访问自己的本地服务,可以这样配置:

module.exports = {
  //...
  devServer: {
    host: '0.0.0.0'
  }
};
1
2
3
4
5
6

命令行使用方式:

webpack-dev-server --host 0.0.0.0
1

devServer.port

port配置项用于配置DevServer服务监听的端口,默认使用8080端口。

devServer.allowedHosts

allowedHosts配置项用于配置一个白名单列表,只有HTTP请求的host在列表里才正常返回,配置如下:

module.exports = {
  //...
  devServer: {
    allowedHosts: [
      // 匹配单个域名
      'host.com',
      'subdomain.host.com',
      'subdomain2.host.com',
      'host2.com'
    ]
  }
};
1
2
3
4
5
6
7
8
9
10
11
12

模仿django的ALLOWED_HOSTS,以.开头的值可以用作子域通配符。.host.com将会匹配host.com, www.host.com 和host.com的任何其他子域名。

module.exports = {
  // 和上述配置等价
  devServer: {
    allowedHosts: [
      '.host.com',
      'host2.com'
    ]
  }
};
1
2
3
4
5
6
7
8
9

devServer.disableHostCheck

devServer.disableHostCheck配置项用于配置是否关闭用于DNS重新绑定的HTTP请求的host检查。DevServer默认只接收来自本地的请求,关闭后可以接收来自任意HOST的请求。通常用于搭配--host 0.0.0.0使用,因为想让其他设备访问自己的本地服务,但访问时是直接通过ip地址访问而不是通过host访问,所以需要关闭host检查。

配置局域网内访问:

host: '0.0.0.0',
disableHostCheck: false // 关闭host检查
1
2

devServer.https

DevServer默认使用HTTP服务,也可以使用HTTPS服务。配置如下:

module.exports = {
  devServer: {
    https: true
  }
};
1
2
3
4
5

DevServer会自动为我们生成一份HTTPS证书,如果想使用自己的证书,可以这样配置:

module.exports = {
  devServer: {
    https: {
      key: fs.readFileSync('/path/to/server.key'),
      cert: fs.readFileSync('/path/to/server.crt'),
      ca: fs.readFileSync('/path/to/ca.pem'),
    }
  }
};
1
2
3
4
5
6
7
8
9

devServer.clientLogLevel

clientLogLevel属性配置客户端的日志等级,这会影响我们在浏览器开发者工具控制台里看到的日志内容。clientLogLevel是枚举类型,可取如下值之一:none、error、warning、info。默认为info级别,即输出所有类型的日志,设置为none时可以不输出任何日志。

devServer.compress

compress属性配置是否启用Gzip压缩,默认为false。

devServer.open

open属性用于在DevServer启动且第一次构建完成时,自动用我们系统的默认浏览器去打开要开发的网页。还可以使用devServer.openPage配置项来打开指定URL的网页。

devServer.openPage

指定打开浏览器时的导航页面。

devServer.overlay

overlay属性配置当出现编译器错误或警告时,在浏览器中显示全屏覆盖层,默认禁用。如果只想显示编译器错误:

module.exports = {
  //...
  devServer: {
    overlay: true,
    // errors: true // 显示警告信息
  }
};
1
2
3
4
5
6
7

devServer.before

在服务内部的所有其他中间件之前,提供执行自定义中间件的功能,这可以用来配置自定义处理程序。

devServer.index

指定被作为索引文件的文件名,devServer.index的值需要与HtmlWebpackPlugin中的filename保持一致。

devServer: {
  index: 'test.html',
  port: 3000,
  progress: true, // 显示打包进度条
  contentBase: './dist', // 指定服务地址
  open: true, // 自动打开浏览器
  compress: true // 压缩
},
plugins: [
  new HtmlWebpackPlugin({
     filename: 'test.html',
     template: './src/index.html',
     title: '自动生成html',
     inject: 'head',
     minify: {
       removeAttributeQuotes: true, // 移除属性的双引号
       collapseWhitespace: true // 折叠空行变成一行
     },
     hash: true // 给插入到模板中的静态资源文件后面加一个hash值
  })
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

devServer.proxy

proxy一般用于反向代理来解决跨域问题。假设前端服务运行在http://localhost:8080,访问http://localhost:8080/api/users将会被代理到http://localhost:8080/users(这里启用了路径重写,/api会被干掉)。

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://www.baidu.com/',
        pathRewrite: {'^/api' : ''},
        changeOrigin: true, // target是域名的话,需要这个参数,
        secure: false, // 设置支持https协议的代理,
        headers: {
           'Host': 'www.baidu.com',
           'Origin': 'http://www.baidu.com',
           'Referer': 'http://www.baidu.com'
       }
      },
      '/v1': {
          ...
      }
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • /api:请求路径匹配,如果请求以/api开头就开始匹配代理,比如api请求/api/users,会被代理到请求http://www.baidu.com/users;
  • target:代理服务器地址,就是需要跨域的服务器地址。地址可以是域名,如:http://www.baidu.com;也可以是ip地址:http://localhost:3000。如果是域名需要额外添加一个参数changeOrigin: true,否则会代理失败;
  • pathRewrite:路径重写,也就是说会修改最终请求的url路径。比如访问的api路径:/api/users,设置pathRewrite: {'^/api' : ''},后,最终代理访问的路径:http://www.baidu.com/users;
  • changeOrigin:这个参数可以让target参数可以是域名;
  • secure:secure: false,表示不检查安全问题。设置后,可以接受运行在HTTPS上,可以使用无效证书的后端服务器。
  • headers: 有些后端对请求中的Origin做了校验,需要重写请求headers中的Origin等字段。

webpack-dev-server使用的是http-proxy-middleware来实现跨域代理的。

devServer.publicPath

指定相关路径下的打包文件可在浏览器中访问。假设服务器运行在http://localhost:3000,并且output.filename被设置为bundle.js。默认devServer.publicPath是 '/',所以bundle.js可以通过http://localhost:3000/bundle.js访问。 修改devServer.publicPath,将bundle放在指定目录下:

module.exports = {
  devServer: {
     index: 'test.html',
     port: 3000,
     progress: true, // 显示打包进度条
     contentBase: './dist', // 指定服务地址
     open: true, // 自动打开浏览器
     compress: true, // 压缩,
     publicPath: '/assets/'
 }
};
1
2
3
4
5
6
7
8
9
10
11

现在可以通过http://localhost:3000/assets/bundle.js访问bundle.js。

配置实战

watch

webpack提供了webpack --watch的命令来动态监听文件的改变并实时打包,输出新的bundle.js文件,这样文件多了之后打包速度会很慢,此外这样的打包的方式不能做到HMR(Hot Module Replacement),就是每次webpack编译完成之后,我们还需要手动刷新浏览器。 webpack --watch

const path = require('path');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'bundle.js',
        // publicPath: '/assets/'
    },
    // 只有在开启监听模式时,watchOptions才有意义
    // 默认为false,也就是不开启
    watch: true,
    // 监听模式运行时的参数
    // 在开启监听模式时才有意义
    watchOptions: {
        // 不监听的文件或文件夹,支持正则匹配
        // 默认为空
        ignored: /node_modules/,
        // 监听到变化发生后等300ms再去执行动作,截流
        // 防止文件更新太快而导致重新编译频率太快。默认为300ms
        aggregateTimeout: 300,
        // 判断文件是否发生变化是通过不停地询问系统指定文件有没有变化实现的
        // 默认每秒询问1000次
        poll: 1000
    }
}
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

webpack-dev-server提供的功能就能解决上面watch的2个问题。webpack-dev-server通过启动一个基于express的HTTP服务器。它的作用主要是用来伺服资源文件。 此外,这个HTTP服务器和client使用了websocket通讯协议,原始文件作出改动后,webpack-dev-server会实时的编译,需要注意的是:实时编译后的文件都保存到了内存当中,也就是说最后编译的文件并没有输出到目标文件夹

content-base

设定webpack-dev-server伺服的目录。如果不进行设定的话,默认是在当前目录下。

需要注意:入口文件index.html所在目录必须要与webpack-dev-server伺服的目录保持一致,否则访问不成功。当前文件目录结构如下:

const path = require('path');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'bundle.js',
        // publicPath: '/assets/'
    },
    devServer: {
        // contentBase: path.join(__dirname, 'dist'),
        compress: true,
        port: 9000,
        host: '0.0.0.0',
        disableHostCheck: false,
        open: true,
        overlay: true,
        noInfo: true
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

此时没有设置contentBase,webpack-dev-server伺服的目录为当前项目的根目录,与入口文件index.html所在目录一致,因此可以正常访问。

假如将入口文件index.html放到dist目录下,就需要配置:

contentBase: path.join(__dirname, 'dist')
1

配置了output的publicPath

output: {
     path: path.resolve(__dirname, './dist'),
     filename: 'bundle.js',
     publicPath: '/assets/'
}
1
2
3
4
5
<body>
    <button id="btn">我是按钮</button>
    <script src="bundle.js"></script>
</body>
1
2
3
4

配置了output的publicPath为'/assets/',而index.html文件中bundle.js的引用路径没有改变,则会报文件找不到(404)。

<body>
    <button id="btn">我是按钮</button>
    <script src="/assets/bundle.js"></script>
</body>
1
2
3
4

所以,如果配置了output的publicPath字段的值,在index.html文件里面也应该做出调整。因为webpack-dev-server伺服的文件是相对于publicPath这个路径的

自动刷新浏览器

监听到文件更新后的下一步是刷新浏览器,Webpack模块负责监听文件变化,webpack-dev-server模块负责刷新浏览器。在使用webpack-dev-serve模块去启动webpack模块时,webpack模块的监听模式默认会被开启。webpack模块会在文件发生变化时通知webpack-dev-server模块。 webpack-dev-server支持2种自动刷新的方式:

  • iframe mode
  • inline mode(默认方式)

这2种模式配置的方式和访问的路径稍微有点区别,最主要的区别还是iframe mode是在网页中嵌入了一个iframe,将我们自己的应用注入到这个iframe当中去,因此每次你修改的文件后,都是这个iframe进行了reload。

默认情况下,应用程序启用内联模式(inline mode)。这意味着一段处理实时重载的脚本被插入到你的包(bundle)中,并且构建消息将会出现在浏览器控制台。

iframe mode

当然也可以使用iframe模式,它在通知栏下面使用<iframe>标签,包含了关于构建的消息。切换到iframe 模式:

devServer: {
    inline: false
    ...
}
1
2
3
4

Usage via the CLI

webpack-dev-server --inline=false
1

Iframe mode下,浏览器访问的路径是: localhost:8080/webpack-dev-server/index.html。 这个时候这个页面的header部分会出现整个reload消息的状态。当时改变源文件的时候,即可以完成自动编译打包,页面自动刷新的功能。

在Iframe mode下,请求/webpack-dev-server/index.html路径时,会返回client/live.html文件,这个文件的内容如下:

<!DOCTYPE html><html><head><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta charset="utf-8"/><meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"/</script></head><body></body></html>
1

这个页面会请求client目录下的live.bundle.js,其中里面会新建一个iframe,我们的应用就被注入到了这个iframe当中。同时live.bundle.js中含有socket.io的client代码,这样它就能和webpack-dev-server建立的http server进行websocket通讯了。并根据返回的信息完成相应的动作。

inline mode

Usage via the CLI:

webpack-dev-server --inline
1

这个时候访问的路径是:

localhost:8080/index.html
1

也能完成自动编译打包,页面自动刷新的功能。但是没有的header部分的reload消息的显示,不过在控制台中会显示reload的状态。

## 在Node中使用webpack 可以基于webpack-dev-middleware和express实现一个类似于webpack-dev-server的服务器。下面的实现比较简单,还需要手动刷新页面才能看到效果。 ```bash yarn add webpack-dev-middleware express -D ``` ```js const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const config = require('./webpack.config'); const complier = webpack(config); const app = express();

app.use(webpackDevMiddleware(complier, { publicPath: config.output.publicPath }));

app.listen(8088, () => { console.log('server is run 8088'); });

## 参考文档
1. [webpack-dev-server使用方法,看完还不会的来找我~](https://segmentfault.com/a/1190000006670084#articleHeader4)
1
2