webpack-dev-server的配置和使用

一、介绍

虽然webpack提供了 webpack --watch 的命令来 动态监听文件的改变并实时打包输出新bundle.js文件,这样文件多了之后打包速度会很慢,此外这样的打包的方式不能做到hot replace,即每次webpack编译之后,你还需要手动刷新浏览器。

webpack-dev-server其中部分功能就能克服上面的2个问题。webpack-dev-server主要是启动了一个使用express的Http服务器。它的作用主要是用来伺服资源文件。此外这个Http服务器和client使用了websocket通讯协议,原始文件作出改动后,webpack-dev-server会实时的编译,但是最后的编译的文件并没有输出到目标文件夹,而是为了加快打包进程是将打包后的文件放到内存中的,所以要想获取新的dist文件用于部署项目需要单独用webpack打包。

注:Webpack-dev-server十分小巧,这里的作用是用来伺服资源文件,不能替代后端的服务器,因此如果你还要进行后端开发,就要采用双服务器模式:一个后端服务器和一个资源服务器(即Webpack-dev-server)

webpack-dev-server的工作流程:

webpack-dev-server将打包后的文件存入内存,http server从内存直接读取文件:

二、安装webpack-dev-server

npm install webpack-dev-server -D

【参考】npm install XXX -D/-S/-g 区别:https://www.jianshu.com/p/2e7f3b69e51e

三、webpack-dev-server的配置和使用

? 启动webpack-dev-server有2种方式:

  1. 通过cmd line
  2. 通过Node.js API

选择使用cmd line:

webpack-dev-server为了加快打包进程是将打包后的文件放到内存中的,所以我们在项目中是看不到它打包以后生成的文件/文件夹的,但我们仍然需要配置路径output.path这个参数是配置 打包文件的保存路径 的,contentBase在webpack-dev-server中的作用就和output.path是同等的,如果不配置这个参数,webpack-dev-server就会将文件(隐藏文件)打包到项目的根路径下。

[1] 热更新:webpack-dev-sever支持两种自动刷新方式 —— Iframe mode、Inline mode

Iframe mode和Inline mode最后达到的效果都是一样的,都是监听文件的变化,然后再将编译后的文件推送到前端,完成页面的reload的。

● Iframe mode

使用iframe模式不需要配置任何东西,只需要在你启动的项目的端口号后面加上/webpack-dev-server/即可;

比如:http://localhost:8080/webpack-dev-server/

Iframe mode是在网页中嵌入了一个iframe,将我们自己的应用注入到这个iframe当中去,因此每次你修改的文件后,都是这个iframe进行了reload;

● Inline mode

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

webpack-dev-server 的常用功能之一——静态资源访问

webpack-dev-server 默认会将构建结果和输出文件全部作为开发服务器的资源文件,也就是说,只要通过 Webpack 打包能够输出的文件都可以直接被访问到。

但是如果你还有一些 没有参与打包的静态文件 也需要作为开发服务器的资源被访问,那你就需要额外通过配置“告诉” webpack-dev-server。

具体的方法就是在 webpack-dev-server 的配置对象中添加一个对应的配置。我们回到配置文件中,找到 devServer 属性,它的类型是一个对象,我们可以通过这个 devServer 对象的 contentBase 属性指定额外的静态资源路径。这个 contentBase 属性可以是一个字符串或者数组,也就是说你可以配置一个或者多个路径。

法一:可以选择在命令行中启动的时候加这个参数:

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

法二:配置contentBase:

inline: true, 指的是dev-server自动刷新模式,webpack有两种模式支持自动刷新,一种是iframe模式,一种是inline模式;

使用iframe模式是不需要在devServer进行配置的,只需使用特定的URL格式访问即可;

不过我们一般还是常用inline模式,在devServer中对inline设置为true后,当我们启动webpack-dev-server时仍要需要配置inline才能生效。

我们在package.json的script里写一个快捷方式:

1
2
// --inline 即是使用inline模式进行热更新, --open是起动服务器时打开新窗口
"dev": "webpack-dev-server --inline --open"

【注】webpack-dev-server不能直接在cmd使用,要通过npm run才能使用,因为npm run才会找到当前目录的安装的包,直接是找不到的,这也是为什么我之前需要全局安装webpack的原因,其实只是偷懒想在cmd直接跑webpack的命令。

运行npm run dev后只有bundle.js文件在页面上挂载:原因——>没有打包html,so:

将html文件移到public中并修改文件中的js、ico资源路径,favicon.ico 和 index.html 一起作为 没有参与打包的静态文件 被访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>小柿子</title>
    <link rel="shortcut icon" href="./favicon.ico" />
</head>
<body>
    <div id="box"></div>
    <script src="./bundle.js"></script>
    <script>
     //js code
    </script>
</body>
</html>

再运行 npm run dev,成功启动:

【注】output 中的 publicPath devServer 中的 contentBase publicPath 的区别:

首先明白两点:

(1)在webpack.config.js文件中,output配置只在production环境下起效,devServer只在development环境下有效。

(2)devServer运行下所编译的文件皆存在于内存中,不会改变本地文件;在服务运行中如果内存中找不到想要的文件时,devServer 会根据文件的路径尝试去本地磁盘上找,如果这样还找不到才会 404。

【参考】webpack配置中的contentBase和publicPath:https://www.jianshu.com/p/fc1341b72d47

----------------------------------------------------------------------------------------------------------------------------------------------------------------------

[2] 热替换(HMR)

当我们使用webpack-dev-server的自动刷新功能时,浏览器会整页刷新。
而热替换的区别就在于,当前端代码变动时,无需刷新整个页面,只把变化的部分替换掉

HMR(Hot Module Replacement)能够实现在页面不刷新的情况下,代码也可以及时的更新到浏览器的页面中,重新执行,避免页面状态丢失。

(1)开启 HMR 功能实现方法:

HMR 已经集成在了 webpack 模块中了,所以不需要再单独安装什么模块。

法一:使用这个特性最简单的方式就是,在运行 webpack-dev-server 命令时,通过 --hot 参数去开启这个特性。

法二:也可以在配置文件中通过添加对应的配置来开启这个功能。那我们这里打开配置文件,这里需要配置两个地方:

  1. 首先需要将 devServer 对象中的 hot / hotOnly(推荐) 属性设置为 true;
  2. 然后需要载入一个插件,这个插件是 webpack 内置的一个插件,所以我们先导入 webpack 模块,有了这个模块过后,这里使用的是一个叫作 HotModuleReplacementPlugin 的插件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js
const webpack = require('webpack')

module.exports = {
  // ...
  devServer: {
    // 开启 HMR 特性,如果资源不支持 HMR 会 fallback 到 live reloading
    hot: true
    // 只使用 HMR,不会 fallback 到 live reloading
    // hotOnly: true
  },
  plugins: [
    // ...
    // HMR 特性所需要的插件
    new webpack.HotModuleReplacementPlugin()
  ]
}

此时Webpack可以实现css的热替换;但不能实现js、图片等的热替换,此二者需要自己手动通过代码来处理。

Q1:可能你会问,为什么我们开启 HMR 过后,样式文件的修改就可以直接热更新呢?我们好像也没有手动处理样式模块的更新啊?

A1:这是因为样式文件是经过 Loader 处理的,在 style-loader 中就已经自动处理了样式文件的热更新,所以就不需要我们额外手动去处理了。

Q2:那你可能会想,凭什么样式就可以自动处理,而我们的脚本就需要自己手动处理呢?

A2:这个原因也很简单,因为样式模块更新过后,只需要把更新后的 CSS 及时替换到页面中,它就可以覆盖掉之前的样式,从而实现更新。

而我们所编写的 JavaScript 模块是没有任何规律的,你可能导出的是一个对象,也可能导出的是一个字符串,还可能导出的是一个函数,使用时也各不相同。所以 Webpack 面对这些毫无规律的 JS 模块,根本不知道该怎么处理更新后的模块,也就无法直接实现一个可以通用所有情况的模块替换方案。

那这就是为什么样式文件可以直接热更新,而 JS 文件更新后页面还是回退到自动刷新的原因。

Q3:那可能还有一些平时使用 vue-cli 或者 create-react-app 这种框架脚手架工具的人会说,“我的项目就没有手动处理,JavaScript 代码照样可以热替换,也没你说的那么麻烦”。

A3:这是因为你使用的是框架,使用框架开发时,我们项目中的每个文件就有了规律,例如 React 中要求每个模块导出的必须是一个函数或者类,那这样就可以有通用的替换办法,所以这些工具内部都已经帮你实现了通用的替换操作,自然就不需要手动处理了。

(2)图片的热替换

通过 module.hot.accept 注册这个图片模块的热替换处理函数,在这个函数中,我们只需要重新给图片元素的 src 设置更新后的图片路径就可以了。因为图片修改过后图片的文件名会发生变化,而这里我们就可以直接得到更新后的路径,所以重新设置图片的 src 就能实现图片热替换,具体代码如下:

1
2
3
4
5
6
7
8
// ./src/index.js
import logo from './icon.png'
// ... 其他代码
module.hot.accept('./icon.png', () => {
  // 当 icon.png 更新后执行
  // 重写设置 src 会触发图片元素重新加载,从而局部更新图片
  img.src = logo
})

(3)js的热替换

HMR APIs

HotModuleReplacementPlugin 为我们的 JavaScript 提供了一套用于处理 HMR 的 API,我们需要在我们自己的代码中,使用这套 API 将更新后的模块替换到正在运行的页面中。

对于开启 HMR 特性的环境中,我们可以访问到全局的 module 对象中的 hot 成员,这个成员是一个对象,这个对象就是 HMR API 的核心对象,它提供了一个 accept 方法,用于注册当某个模块更新后的处理函数。accept 方法第一个参数接收的就是所监视的依赖模块路径,第二个参数就是依赖模块更新后的处理函数

假设原来的index.js:

1
2
3
4
5
6
7
8
9
10
import createEditor from './editor'
import logo from './icon.png'
import './global.css'

const img = new Image()
img.src = logo
document.body.appendChild(img)

const editor = createEditor()
document.body.appendChild(editor)

在index.js使用HMR APIs :

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
import createEditor from './editor'
import logo from './icon.png'
import './global.css'

const img = new Image()
img.src = logo
document.body.appendChild(img)

const editor = createEditor()
document.body.appendChild(editor)


// HMR -----------------------------------
let lastEditor = editor

if (module.hot) {
    module.hot.accept('./editor', () => {  // 当 editor.js 更新,自动执行此函数

      // 临时记录更新前编辑器内容
      const value = lastEditor.innerHTML

      // 移除更新前的元素
      document.body.removeChild(lastEditor)

      // 创建新的编辑器 ( 此时 createEditor 已经是更新过后的函数了 )
      lastEditor = createEditor()

      // 还原编辑器内容
      lastEditor.innerHTML = value

      // 追加到页面
      document.body.appendChild(lastEditor)
    })
}

至此,对于 editor 模块的热替换逻辑就算是全部实现了。我们可以发现,为什么 Webpack 需要我们自己处理 JS 模块的热更新了:因为不同的模块有不同的情况,不同的情况,在这里处理时肯定也是不同的。就好像,我们这里是一个文本编辑器应用,所以需要保留状态,如果不是这种类型那就不需要这样做。所以说 Webpack 没法提供一个通用的 JS 模块替换方案。

关于框架的 HMR,因为在大多数情况下是开箱即用的,所以这里不做过多介绍,详细可以参考:

React HMR 方案:https://github.com/gaearon/react-hot-loader (maybe将被React Fast Refresh取代)

Vue.js HMR 方案:https://vue-loader.vuejs.org/guide/hot-reload.html

------------------------------------------------------------------------------------------------------------------------------------------------------------

[3] webpack解决生成环境和开发环境因server不同产生的跨域问题:proxy 代理

  1. 如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。
  2. 解决开发环境的跨域问题(不用在去配置nginx和host)

由于 webpack-dev-server 是一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost 的一个端口上,而后端服务又是运行在另外一个地址上。但是最终上线过后,我们的应用一般又会和后端服务部署到同源地址下。

那这样就会出现一个非常常见的问题:在实际生产环境中能够直接访问的 API,回到我们的开发环境后,再次访问这些 API 就会产生跨域请求问题。

可能有人会说,我们可以用跨域资源共享(CORS)解决这个问题。确实如此,如果我们请求的后端 API 支持 CORS,那这个问题就不成立了。但是并不是每种情况下服务端的 API 都支持 CORS。如果前后端应用是同源部署,也就是协议 / 域名 / 端口一致,那这种情况下,根本没必要开启 CORS,所以跨域请求的问题仍然是不可避免的。

那解决这种开发阶段跨域请求问题最好的办法,就是在开发服务器中配置一个后端 API 的代理服务,也就是把后端接口服务代理到本地的开发服务地址。

webpack-dev-server 就支持直接通过配置的方式,添加代理服务。接下来,我们来看一下它的具体用法。

比如我们假定 GitHub 的 API 就是我们应用的后端服务,那我们的目标就是将 GitHub API 代理到本地开发服务器中,我们可以先在浏览器中尝试访问其中的一个接口: https://api.github.com/users

得到如下结果:

GitHub API 的 Endpoint 都是在根目录下,也就是说不同的 Endpoint 只是 URL 中的路径部分不同,例如 https://api.github.com/users 和 https://api.github.com/events。

知道 API 地址的规则过后,我们回到配置文件中,在 devServer 配置属性中添加一个 proxy 属性,这个属性值需要是一个对象,对象中的每个属性就是一个代理规则配置。

属性的名称是需要被代理的请求路径前缀,一般为了辨别,我都会设置为 /api。值是所对应的代理规则配置,我们将代理目标地址设置为 https://api.github.com,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
// ./webpack.config.js
module.exports = {
  // ...
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.github.com'
      }
    }
  }
}

那此时我们请求 http://localhost:8080/api/users ,就相当于请求了 https://api.github.com/api/users。

而我们真正希望请求的地址是 https://api.github.com/users,所以对于代理路径开头的 /api 我们要重写掉。我们可以添加一个 pathRewrite 属性来实现代理路径重写,重写规则就是把路径中开头的 /api 替换为空,pathRewrite 最终会以正则的方式来替换请求路径。

以上实现了:(见下)

【注】 没有 "secure:lase" 此处运行会报错:

1
2
3
Error occurred while trying to proxy request /users from localhost:8081
to https://api.github.com (ECONNRESET)
(https://nodejs.org/api/errors.html#errors_common_system_errors)