一、介绍
虽然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种方式:
- 通过cmd line
- 通过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模式不需要配置任何东西,只需要在你启动的项目的端口号后面加上
比如: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 参数去开启这个特性。
法二:也可以在配置文件中通过添加对应的配置来开启这个功能。那我们这里打开配置文件,这里需要配置两个地方:
- 首先需要将 devServer 对象中的 hot / hotOnly(推荐) 属性设置为 true;
- 然后需要载入一个插件,这个插件是 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 代理
- 如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。
- 解决开发环境的跨域问题(不用在去配置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) |