原始开发方式:
设想理想的开发环境:
实现自动编译
watch监听模式:监听文件变化,自动重新运行打包任务。(类似其他构建工具的watch)
命令行使用方法:--watch
参数启动监听模式
例如:yarn webpack --watch
实现编译后自动刷新浏览器
使用browser-sync模块的--files
参数监听文件变化,触发浏览器刷新。
例如:browser-sync dist --files "**/*"
上面实现两个开发体验的方式的缺点:
webpack-dev-server 是 webpack官方的开发工具。
安装yarn add webpack-dev-server --dev
它提供一个webpack-dev-server
的命令。
webpack-dev-server为了提高开发效率,并没有将打包结果写入到磁盘当中。
它将打包结果,暂时存放在内存中。
内部的HTTP Server从内存中读取这些文件,发送给浏览器。
这样减少很多不必要的磁盘读写操作,从而大大提高构建效率。
可以添加--open
参数,使启动服务后立即从浏览器打开。
Dev Server 默认会将构建结果输出的文件,全部作为开发服务器的资源文件(即默认只会serve打包输出的文件)
也就是说,只要是webpack输出的文件,都可以直接被访问。
但是还有一些没有参与构建的静态资源也需要serve,就需要额外的告诉Webpack Dev Server
webpack配置的devServer.contentBase
属性,可以额外的为开发服务器指定查找资源目录。
它可以接收表示目录的字符串或数组。
由于webpack打包任务可能使用copy(copy-webpack-plugin)插件将静态资源文件拷贝到输出目录(Dev Server是将拷贝的内容存储在内存中),所以运行HTTP可以访问到这些静态资源。
但是,由于开发阶段修改代码会频繁重复的执行webpack打包任务。
如果拷贝的文件比较多或比较大,每次执行copy任务,打包的开销就比较大,并且会降低速度。
所以拷贝任务一般会配置在打包发布版本的阶段执行,而开发阶段使用配置额外资源的查找路径devServer.contentBase
的方式去访问。
由于Dev Server启动了一个本地的开发服务器,默认http://localhost:8080
。
当请求后端发布到线上的API时,会因为跨域而请求失败。
虽然可以通过配置CORS时,API支持跨域。
但这需要后端和服务器配合,而且并不是任何情况下API都应该支持CORS。
例如:前后端同源部署,即发布后,前后端在同一个域名、协议、端口下,就没有必要开启CORS。
所以解决 「开发阶段接口跨域问题」 的最好的办法就是在开发服务器当中配置**「代理服务」**。
也就是将接口服务,代理到本地的开发服务地址。
Webpack Dev Server 支持通过配置(devServer.proxy
)的方式,添加代理服务。
目标:将API(https://api.github.com/
)代理到本地开发服务器。
github接口的Endpoint一般都是在根目录下。
例如 https://api.github.com/users
Endpoint 可以理解为 接口端点/入口
webpack通过devServer.proxy
对象配置代理服务。
对象中的每个属性,都是一个代理规则的配置。
key
)就是需要代理的请求路径的前缀,例如'/api'
。value
)是为这个前缀匹配的代理规则配置。
target
:代理目标,即访问key
相当于访问target/key
,他会将key
添加到后面,可通过pathRewrite
实现代理路径的重写。pathRewrite
:重写代理路径。它接收一个对象,key是正则匹配的路径字符串,value是要替换的内容。
https://api.github.com/api/users
修改的是/api/users
。changeOrigin
:设置为true
。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
。
配置changeOrigin
为true
,就会以实际发生代理请求的「host」(api.github.com
)作为发起请求的「host」。
这样就不用关心,最终会把它代理成了什么样。
通过构建编译,可以将开发环境的源代码转化为能在生产环境运行的代码。
这使得 运行代码 完全不同于 源代码。
由于调试和报错都是基于运行代码。如果需要调试应用,或运行应用时报出了错误,就无法定位。
Source Map(源代码地图) 就是解决这类问题最好的办法。
它用来映射 转换后的代码(compiled) 与 源代码(source) 之间的关系。
转换后的代码,通过转换过程中生成的 Source Map 解析,就可以逆向得到源代码。
目前很多第三方的库在打包后都会生成一个.map
后缀的Source Map文件。
它是一个 json 格式的文件,主要包含以下属性:
{
"version": 3。
"sources": ["jquery.js"],
"names": [...],
"mappings": "Base64 VLQ编码字符串"
}
可以在转换后的文件中通过添加注释的方式引入source map文件。例如:
// jquery.min.js
// ...转换后的代码
//# sourceMappingURL=jquery.min.map
引入后,如果在浏览器中打开开发人员工具,开发人员工具在加载到这个js文件时发现有这个注释,它就会自动去请求这个source map文件。
然后根据这个文件的内容,逆向解析对应的源代码,以便于调试。(在开发人员工具的sources面板就会多出一个解析后的源文件)
同时因为有了映射的关系,如果源代码中出现了错误,也能很容易定位到源代码中对应的位置。
source map文件主要用于调试和定位错误,所以它对生产环境没有太大的意义,所以生产环境一般不需要生成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文件,并以常规方式在打包文件最后添加sourceMappingsURL注释
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模式类似,但它查看的代码内容,是编译前的内容,所以它能定位到具体的行和列的信息。
原因是它生成了一个 Data URLs 地址的 source map。
// eval执行的字符串
// ...执行代码
//# sourceURL=[module]
//# sourceMappingURL=data:application/json;charset=utf-8;base64,[base64内容]
cheap 表示会生成 廉价(阉割版) 的source-map。
效果:
由于少了一些效果,所以生成速度比 eval-source-map快很多。
与eval-cheap-source-map的区别是,查看的源码与实际源文件一样(loader转换前)。
但同样无法定位列。
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是否包含行信息。
module
:解析loader处理之前的源代码。
cheap-
后,所以同样无法定位到列。根据它们的定义可以理解以下规则:
inline/hidden/nosources/cheap
需要与source-map
一起使用module
需要与cheap
一起使用使用建议:
[eval/inline]-source-map
会将source map以Data URLs方式嵌入到打包文件中,会使文件变大很多,一般不建议使用。
webpack dev server 主要为使用webpack构建的项目,提供友好的开发环境,和一个用于调试的开发服务器。
它可以监视到代码的变化,自动打包,最后通过 自动刷新页面 的方式同步到浏览器以便于即时预览。
缺点: 自动刷新浏览器 会导致页面状态丢失。
期望:页面不刷新的前提下,模块也可以及时更新。
HMR(Hot Module Replacement):模块热替换 / 模块热更新
计算机行业常见名词「热拔插」:在一个正在运行的机器上随时插拔设备。
例如电脑上的USB端口就是可以热拔插的。
「模块热替换」 中的「热」与「热拔插」中的「热」是一个道理,它们都是在运行过程中的即时变化。
模块热替换 就是 应用运行过程中实时替换某个模块,应用运行状态不受影响。
相对于自动刷新页面丢失页面状态,热替换只将修改的模块实时替换至应用中,不必完全刷新应用。
HMR可以实时更新包括CSS、JS 以及 静态资源的所有模块。
HMR是webpack中最强大、最受欢迎的功能之一。它极大程度的提高了开发者的工作效率。
webpack-dev-server 已经集成了 HMR。
webpack 或 webpack-dev-server 可以通过在运行命令时添加--hot
参数去开启这个特性。
也可以通过在配置文件中配置devServer.hot
为true
开启。
注意:
HotModuleReplacementPlugin
才能完全启用HMR--hot
启用,则会自动添加此插件,而不需要将其添加到webpack.config.js。通过上述启用HMR后发现,修改css文件确实实现了热替换,而修改js文件依然会刷新页面。
这是由于webpack中的HMR并不像其他特性一样开箱即用。
它还需要进行一些额外的操作,才能正常工作。
webpack中的HMR需要通过代码手动处理 模块热替换逻辑 ( 当模块更新后,如何把更新过的模块替换到运行页面中 )。
如果没有手动处理,就会触发自动刷新页面,反之就不会触发自动刷新页面。
因为样式文件是通过loader处理的,上例(代码目录08-hmr)中样式文件在style-loader中就已经自动处理了样式文件的热更新。
可通过在开发这工具中查看样式文件的source map,其中使用了处理热替换逻辑的代码:
if (module.hot) {
// ...
module.hot.accept(/*...*/)
// ...
}
因为样式文件变更后,只需要将样式文件的内容替换到页面中,就可以实现样式的即时更新。
而Javascript模块是没有任何规律的:模块可能导出的是一个对象,一个字符串,或者一个函数。
开发中对这些导出的使用方式也是不同的。
所以webpack面对这些毫无规律的JS模块,不知道如何处理当前更新后的模块。也就没有办法实现一个可以通用所有情况的模块替换方案。
这是因为项目使用了框架,框架提供了统一的规则,框架下的开发,每种文件都是有规律的。
例如在react中要求每个文件必须导出一个函数或一个类。
有了规律,就可能有一个通用的替换方案。
例如如果每个文件都导出一个函数,就把这个函数拿过来再次执行一次,实现热替换。
另一方面,通过脚手架创建的项目内部已经集成并使用了通用的HMR方案,所以不需要手动处理。
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 模块更新了,需要这里手动处理热替换逻辑')
})
}
devServer.hotOnly:true
启用不刷新页面的热模块替换,代替devServer.hot:true
。--hot-only
module.hot.accept
),但是并没有配置完全启用HMR。执行时就会报错:Cannot read property 'accept' of undefined
module.hot
就是undefined
if (module.hot)
if(module.hot)
确认,所以生产环境打包后,处理热替换的代码就会编译为if (false) {}
。代码全部清空。