webpack二刷之四、开发体验(devServer+sourceMap+hmr)

webpack 开发体验

webpack 增强开发体验

原始开发方式:

  1. 编写源代码
  2. webpack打包
  3. 运行应用
  4. 刷新浏览器

设想理想的开发环境:

  1. 以HTTP Server运行
    1. 不是以文件形式预览
    2. 接近生产环境的状态
    3. 类似ajax这类api,不支持文件访问形式
  2. 自动编译 + 自动刷新
  3. 提供Source Map支持
    1. 调试错误时快速定位

实现自动编译

watch监听模式:监听文件变化,自动重新运行打包任务。(类似其他构建工具的watch)

命令行使用方法:--watch参数启动监听模式

例如:yarn webpack --watch


实现编译后自动刷新浏览器

使用browser-sync模块的--files参数监听文件变化,触发浏览器刷新。

例如:browser-sync dist --files "**/*"


上面实现两个开发体验的方式的缺点

  1. 需要打开两个终端去执行命令,操作较麻烦。
  2. webpack频繁将编译后的文件写入磁盘,browser-sync从磁盘中读取文件,效率上降低了。

Webpack Dev Server 实现自动编译 + 自动刷新

webpack-dev-server 是 webpack官方的开发工具。

  1. 它提供用于开发的 HTTP Server 服务器
  2. 集成了「自动编译」和「自动刷新浏览器」等功能

安装yarn add webpack-dev-server --dev

它提供一个webpack-dev-server的命令。

webpack-dev-server为了提高开发效率,并没有将打包结果写入到磁盘当中。

它将打包结果,暂时存放在内存中。

内部的HTTP Server从内存中读取这些文件,发送给浏览器。

这样减少很多不必要的磁盘读写操作,从而大大提高构建效率。

可以添加--open参数,使启动服务后立即从浏览器打开。

Webpack Dev Server 静态资源访问

Dev Server 默认会将构建结果输出的文件,全部作为开发服务器的资源文件(即默认只会serve打包输出的文件)

也就是说,只要是webpack输出的文件,都可以直接被访问。

但是还有一些没有参与构建的静态资源也需要serve,就需要额外的告诉Webpack Dev Server

webpack配置的devServer.contentBase属性,可以额外的为开发服务器指定查找资源目录。

它可以接收表示目录的字符串或数组。

配置contentBase替代copy插件

由于webpack打包任务可能使用copy(copy-webpack-plugin)插件将静态资源文件拷贝到输出目录(Dev Server是将拷贝的内容存储在内存中),所以运行HTTP可以访问到这些静态资源。

但是,由于开发阶段修改代码会频繁重复的执行webpack打包任务。

如果拷贝的文件比较多或比较大,每次执行copy任务,打包的开销就比较大,并且会降低速度。

所以拷贝任务一般会配置在打包发布版本的阶段执行,而开发阶段使用配置额外资源的查找路径devServer.contentBase的方式去访问。

Webpack Dev Server 代理 API 服务

由于Dev Server启动了一个本地的开发服务器,默认http://localhost:8080

当请求后端发布到线上的API时,会因为跨域而请求失败。

虽然可以通过配置CORS时,API支持跨域。

但这需要后端和服务器配合,而且并不是任何情况下API都应该支持CORS。

例如:前后端同源部署,即发布后,前后端在同一个域名、协议、端口下,就没有必要开启CORS。

所以解决 「开发阶段接口跨域问题」 的最好的办法就是在开发服务器当中配置**「代理服务」**。

也就是将接口服务,代理到本地的开发服务地址。

Webpack Dev Server 支持通过配置(devServer.proxy)的方式,添加代理服务。

实现:将GitHub API 代理到开发服务器

目标:将API(https://api.github.com/)代理到本地开发服务器。

github接口的Endpoint一般都是在根目录下。

例如 https://api.github.com/users

Endpoint 可以理解为 接口端点/入口

webpack通过devServer.proxy对象配置代理服务。

对象中的每个属性,都是一个代理规则的配置。

  1. 属性的名称(key)就是需要代理的请求路径的前缀,例如'/api'
  2. 属性的值(value)是为这个前缀匹配的代理规则配置。
    1. target:代理目标,即访问key相当于访问target/key,他会将key添加到后面,可通过pathRewrite实现代理路径的重写。
    2. pathRewrite:重写代理路径。它接收一个对象,key是正则匹配的路径字符串,value是要替换的内容。
      1. 它修改的是path路径(参考location.pathname),例如https://api.github.com/api/users修改的是/api/users
    3. changeOrigin:设置为true

Host 和 changeOrigin

HTTP请求头(Request Headers)中必须包含一个 「host」 头字段

「host」 请求头指明了 服务器的域名 和 以及(可选的)端口号。(也有说是 指明了主机名 和 端口号)

如果没有给定 端口号,会自动使用被请求服务的默认端口。

例如:请求https://api.github.com/api/users时,请求头的 「host」 为api.github.com(默认80端口)

「host」的意义:一般情况下,服务器会配置多个网站,服务器端需要根据 「host」 判断当前请求是哪个网站,从而把这个请求指派到对应的网站。


Webpack Dev Server 在客户端对代理后的地址发起请求时,请求的地址是http://localhost:8080/api/users,所以请求头的 「host」 为localhost:8080

代理背后又去请求被代理的地址https://api.github.com/users,请求的过程中同样会带一个 「host」,而代理服务默认使用用户在客户端发起请求的 「host」,即localhost:8080

localhost:8080并不是GitHub配置的网站。请求头应为实际请求地址的「host」,即api.github.com

配置changeOrigintrue,就会以实际发生代理请求的「host」(api.github.com)作为发起请求的「host」。

这样就不用关心,最终会把它代理成了什么样。

Source Map

通过构建编译,可以将开发环境的源代码转化为能在生产环境运行的代码。

这使得 运行代码 完全不同于 源代码。

由于调试和报错都是基于运行代码。如果需要调试应用,或运行应用时报出了错误,就无法定位。

Source Map(源代码地图) 就是解决这类问题最好的办法。

它用来映射 转换后的代码(compiled) 与 源代码(source) 之间的关系。

转换后的代码,通过转换过程中生成的 Source Map 解析,就可以逆向得到源代码。

Source Map 文件

目前很多第三方的库在打包后都会生成一个.map后缀的Source Map文件。

它是一个 json 格式的文件,主要包含以下属性:

  1. version:表示当前文件所使用source map标准的版本
  2. sources:记录转换之前源文件的名称
    1. 可能是多个文件合并转换成一个文件,所以它是数组形式
  3. names:记录源代码中使用的成员名称
    1. 压缩代码时,会将开发阶段编写的有意义的变量名替换为简短的字符,从而去压缩整体代码的体积
    2. names记录的就是原始对应的名称
  4. mappings:记录转换后的代码当中的字符,与转换前所对应的映射关系。
    1. 它是整个source map的核心属性。
    2. 他是一个 Base64 VLQ 编码的字符串
{
	"version": 3"sources": ["jquery.js"],
	"names": [...],
	"mappings": "Base64 VLQ编码字符串"
}

Source Map 文件使用

可以在转换后的文件中通过添加注释的方式引入source map文件。例如:

// jquery.min.js
// ...转换后的代码

//# sourceMappingURL=jquery.min.map

引入后,如果在浏览器中打开开发人员工具,开发人员工具在加载到这个js文件时发现有这个注释,它就会自动去请求这个source map文件。

然后根据这个文件的内容,逆向解析对应的源代码,以便于调试。(在开发人员工具的sources面板就会多出一个解析后的源文件)

同时因为有了映射的关系,如果源代码中出现了错误,也能很容易定位到源代码中对应的位置。

source map文件主要用于调试和定位错误,所以它对生产环境没有太大的意义,所以生产环境一般不需要生成source map文件。

Source Map 总结

解决了在前端方向引入了构建编译之类的概念之后,导致前端编写的代码与运行的代码之间不一样所产生的调试的问题。

webpack 配置 Source Map

webpack支持对打包后的结果生成对应的source map文件。

可通过devtool属性配置指定一个生成方式。

例如:devtool: 'source-map'

webpack 基于对source map不同风格的支持,提供了12种不同的模式(实现方式)。

每种方式的 效率 和 效果 各不相同。

简单表现为:效果越少的,生成速度越快。

webpack官方文档 提供了一个 devtool不 同模式对比表。

分别从 初次构建(打包)速度「build」、监视模式重新打包速度「rebuild」、是否适合在生产环境中使用「production」 以及 所生成的 source map 的质量「quality」4个维度对比了不同方式之间的差异。

webpack期望设置devtool时,使用特定的顺序(eval (none)除外):

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

webpack的配置文件,一般返回一个配置对象,也可以返回由多个配置对象组成的数组,从而实现一次构建执行多个配置任务。

以下使用这种方式查看不同devtool模式的差异。

source-map

会生成对应的source map文件,并以常规方式在打包文件最后添加sourceMappingsURL注释

eval

eval即js当中的eval函数。

eval('console.log(123)')会讲js代码默认运行在一个虚拟机环境中,在开发者环境中执行这条语句,可以看到它的来源指向VM**,点击可跳转到sources面板查看它的源代码,tab名即虚拟机环境名称VM**

可以通过 sourceURL 修改它的运行环境的 名称/所属文件路径

它修改的只是个标识而已,代码依然在虚拟机上运行。

执行eval('console.log(123)' //# sourceURL=./foo/bar.js),它的来源就会指向./foo/bar.js


使用 eval 模式 ,会在打包文件中将要执行的代码放到eval()方法中执行,并且在eval函数执行的字符串最后,通过sourceURL去说明所对应的模块文件路径。

eval 模式 只指明了对应模块的文件路径,并没有指定source map路径(实际上也没有生成source map)。

如此,浏览器在通过eval执行这段代码时,就知道所对应的源代码文件。查看源代码时,只能看到对应的模块打包后的代码。

  • eval模式只能正确定位到代码所属的模块文件(路径)
  • 这种模式不会生成 source map,也就是和 source map 没有太大关系。
  • 构建速度最快:不需要生成 source.map
  • 效果最差:只能定位源代码文件的路径,而不知道具体的行列信息

eval-source-map

与eval模式类似,但它查看的代码内容,是编译前的内容,所以它能定位到具体的行和列的信息。

原因是它生成了一个 Data URLs 地址的 source map。

// eval执行的字符串
// ...执行代码
//# sourceURL=[module]
//# sourceMappingURL=data:application/json;charset=utf-8;base64,[base64内容]

eval-cheap-source-map

cheap 表示会生成 廉价(阉割版) 的source-map。

效果:

  1. 查看的源码是经过loader转换后的代码(如果配置了对应的loader),导致定位到的行与实际源代码不一致。
  2. 无法定位到列。
    1. 表现为通过开发人员工具跳转到源代码时,光标只会定位到代码的行,不会定位到代码的列。

由于少了一些效果,所以生成速度比 eval-source-map快很多。

eval-cheap-module-source-map

与eval-cheap-source-map的区别是,查看的源码与实际源文件一样(loader转换前)。

但同样无法定位列。

devtool 总结

devtool是将几种配置拼接在一起使用,webpack期望设置devtool时,使用特定的顺序(eval (none)除外):

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

拆解介绍:

  • eval:是否使用eval执行模块代码。

  • inline:指定source map以Data URLs方式嵌入到打包文件。

  • hidden:指定不会在打包文件中,通过注释引入source map文件。

    • 一般用于开发第三方包时使用。
  • nosources:在开发人员工具中会看到行列信息,但无法看到源码(报错:Could not load content for xxxx)。

    • 用于在生产环境避免其他人看到源码的同时,定位错误。
    • 可能是通过未定义sourcesContent实现。
  • source-map:表示会生成source map,eval/inline模式会以 Data URLs 形式嵌入到打包文件中,其他模式以物理文件(.map)形式生成

  • cheap:source map是否包含行信息。

    • 会解析生成阉割版的source map,即经过loader加工后的代码,并且无法定位到列。
  • module:解析loader处理之前的源代码。

    • 会解析完整的source map,即没有经过loader加工的与源代码一致的代码,因它需要配置在cheap-后,所以同样无法定位到列。

根据它们的定义可以理解以下规则:

  • inline/hidden/nosources/cheap需要与source-map一起使用
  • module需要与cheap一起使用

使用建议:

[eval/inline]-source-map会将source map以Data URLs方式嵌入到打包文件中,会使文件变大很多,一般不建议使用。

Source Map 模式选择建议

  • 开发环境:eval-cheap-module-source-map
    • cheap:每行代码不会太长,只需要定位到行位置即可。
    • module:项目中一般都使用了loader,需要查看加工前的代码。
    • 通过官方对比表可以看到,这个模式首次启动打包速度慢,但是重写打包速度快,开发中一般使用dev server实现自动编译,所以首次启动打包速度慢无所谓。
  • 生产环境:none
    • source map会暴露源代码
    • 调试是开发阶段的事情
    • 如果没有信息预防生产环境报错的情况,建议使用 nosources-source-map,以定位位置又不至于暴露源代码内容。

webpack 自动刷新

webpack dev server 主要为使用webpack构建的项目,提供友好的开发环境,和一个用于调试的开发服务器。

它可以监视到代码的变化,自动打包,最后通过 自动刷新页面 的方式同步到浏览器以便于即时预览。

缺点: 自动刷新浏览器 会导致页面状态丢失。

期望:页面不刷新的前提下,模块也可以及时更新。

webpack HMR 热替换

HMR(Hot Module Replacement):模块热替换 / 模块热更新

计算机行业常见名词「热拔插」:在一个正在运行的机器上随时插拔设备。

  • 机器的运行状态不会受插拔设备的影响。
  • 插上的设备可以立即开始工作。

例如电脑上的USB端口就是可以热拔插的。

「模块热替换」 中的「热」与「热拔插」中的「热」是一个道理,它们都是在运行过程中的即时变化。

模块热替换 就是 应用运行过程中实时替换某个模块,应用运行状态不受影响。

相对于自动刷新页面丢失页面状态,热替换只将修改的模块实时替换至应用中,不必完全刷新应用。

HMR可以实时更新包括CSS、JS 以及 静态资源的所有模块。

HMR是webpack中最强大、最受欢迎的功能之一。它极大程度的提高了开发者的工作效率。

开启HMR

webpack-dev-server 已经集成了 HMR。

webpack 或 webpack-dev-server 可以通过在运行命令时添加--hot参数去开启这个特性。

也可以通过在配置文件中配置devServer.hottrue开启。

注意:

  • 如果通过配置文件启用,则需要配合webpack内置的热替换插件HotModuleReplacementPlugin才能完全启用HMR
  • 如果通过命令行参数--hot启用,则会自动添加此插件,而不需要将其添加到webpack.config.js。

HMR 疑问

通过上述启用HMR后发现,修改css文件确实实现了热替换,而修改js文件依然会刷新页面。

这是由于webpack中的HMR并不像其他特性一样开箱即用。

它还需要进行一些额外的操作,才能正常工作。

webpack中的HMR需要通过代码手动处理 模块热替换逻辑 ( 当模块更新后,如何把更新过的模块替换到运行页面中 )。

如果没有手动处理,就会触发自动刷新页面,反之就不会触发自动刷新页面。

Q1. 为什么样式文件的热更新开箱即用?

因为样式文件是通过loader处理的,上例(代码目录08-hmr)中样式文件在style-loader中就已经自动处理了样式文件的热更新。

可通过在开发这工具中查看样式文件的source map,其中使用了处理热替换逻辑的代码:

if (module.hot) {
	// ...
	module.hot.accept(/*...*/)
  // ...
}

Q2. 为什么样式文件可以自动处理,而脚本文件需要手动处理?

因为样式文件变更后,只需要将样式文件的内容替换到页面中,就可以实现样式的即时更新。

而Javascript模块是没有任何规律的:模块可能导出的是一个对象,一个字符串,或者一个函数。

开发中对这些导出的使用方式也是不同的。

所以webpack面对这些毫无规律的JS模块,不知道如何处理当前更新后的模块。也就没有办法实现一个可以通用所有情况的模块替换方案。

Q3. 使用vue-cli或create-react-app创建的项目,没有手动处理,JS照样可以热替换

这是因为项目使用了框架,框架提供了统一的规则,框架下的开发,每种文件都是有规律的。

例如在react中要求每个文件必须导出一个函数或一个类。

有了规律,就可能有一个通用的替换方案。

例如如果每个文件都导出一个函数,就把这个函数拿过来再次执行一次,实现热替换。

另一方面,通过脚手架创建的项目内部已经集成并使用了通用的HMR方案,所以不需要手动处理。

HMR APIs

HotModuleReplacementPlugin 为JS提供了一套用于处理HMR的API。

开发者需要在自己的代码中使用这套API,以处理当某个模块更新后,应该如何替换到当前正在运行的页面中。

module.hot是HMR API的核心对象。

module.hot.accept(arg1, arg2)用于注册,当某个模块更新后的处理函数。

arg1接收一个依赖模块的路径。

arg2就是依赖模块更新后的处理函数。

if (module.hot) {
  module.hot.accept('./editor', () => {
    console.log('editor 模块更新了,需要这里手动处理热替换逻辑')
  })
}

HMR 注意事项

  1. 手动处理HMR时,如果处理逻辑的代码中报错导致失败,就会回退到自动刷新页面的方式实现替换。由于自动刷新,处理逻辑代码中的报错信息就不会展示。
    1. 解决办法:配置devServer.hotOnly:true启用不刷新页面的热模块替换,代替devServer.hot:true
    2. 命令行使用:--hot-only
  2. 项目中使用了HMR APIs(module.hot.accept),但是并没有配置完全启用HMR。执行时就会报错:Cannot read property 'accept' of undefined
    1. 这是由于module.hot是内置插件HotModuleReplacementPlugin提供的,未启用HMR(也就是未使用这个插件),module.hot就是undefined
    2. 解决办法:在使用API前先确认下hot是否开启,使用if (module.hot)
  3. 代码中写了很多与业务无关的代码(处理热替换的逻辑代码)
    1. 解决办法:由于生产环境不需要启用HMR,并且在调用HMR APIs前进行了if(module.hot)确认,所以生产环境打包后,处理热替换的代码就会编译为if (false) {}。代码全部清空。

你可能感兴趣的:(webpack)