-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
聊一聊webpack-dev-server和其中socket,HMR的实现 #5
Comments
6啊 |
大哥,我能转载吗? |
@proYang 转转转 就是写的有点水 |
@879479119 膜拜大佬!大佬有没有遇到过 |
1、文章很深刻,学到了很多 |
学习了 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
我们写这篇博客是有一个目标的,就是想着把dev-server应用到rollup上面重新实现一次,不过碍于二者的打包方式以及输出资源的方式都有所不同,这里我们就先看看dev-server源代码的执行方式,看搞清楚他们的原理之后会不会有方法将他们组合起来
webpack-dev-server
简写为DS
webpack-dev-middleware
简写为DM
webpack-hot-middleware
简写为HM
EventEmitter
简写为EE
目标
先理一理执行过程
收到socket链接发送过来的hash值,更新了自己目前的hash值,不过并没有下载json文件
(其实我想问这里真的能够保证两次的顺序吗?)收到ok,或者error等,这里只讨论ok
通知其他的iframe和worker等,发送OK消息并清除错误显示屏overlay,之后重新加载reloadApp
利用hotEmitter的共享实例,发送出一个『webpackHotUpdate』的事件,注意这里在客户端是由webpack的polyfill实现的
发出的事件会被dev-server中的代码接收到,执行check操作,取回并检查我们的json文件
初次进入到这里,或者是没有完全搞明白webpack的目录结构的同学可能会很懵逼,这个
module.hot.check
方法是哪里那进来的啊混蛋?!其实要知道这个首先得知道我们的module不能当成一个普通的对象来看待,她和require一样都是webpack和我们文件沟通的桥梁,很多时候webpack会在她上面动一些手脚这里就是通过上面的方法,在module对象上面添加上我们的对象和参数等,我们看到这里的hot被设置成为执行一个函数的返回值,发现这个函数在HotModuleReplacement.runtime.js中
这里面有我们的check,decline,accept等方法,也即是我们在代码中执行的那些方法的实际实现
仔细看看hotCheck方法的实现,把状态变成check,并执行hotDownloadManifest去取我们的描述json文件,返回一个Promise
这个下载方法在不同环境又有不同实现方式,我们现在心里只有浏览器!所以只看浏览器的!
不过也只是自己发了一个request请求而已,拿到那一段json,从这里可以看出,其实现在已经没有必要管古代的浏览器了,直接使用的XMLHttprequest,然后去服务端拿数据
[hash].hot-update.json
的请求,进行回复进入dev-server收到请求,但是交由dev-middleware进行处理
对于这一次请求我们得到的路径是
/Users/rocksama/project-name/public/9c531a0d5c8a256697b3.hot-update.json
,可以确定是我们在那个目录下是没有那个文件的在memory-js寻寻觅觅,终于找到了我们的文件,读出来,并把它返回给我们
这个json文件是什么时候写入的呢?是在additional-chunk-assets阶段,在内存中存储了一个json文件,c代表的是变化的chunk是哪个,h代表的是hash值,而经常还会看到l代表的是是否需要重新加载,比如只是改了个空格肯定就不会有变化啦
那么这个c中的值是囊个计算出来的呢?之前也说过我们的文件变化过后,会导致重新编译,(重新编译不一定hash变化,这个hash值是AST相关,不是纯粹的与文件内容相关联的)这个时候会拿之前编译后留下的record中记录的hash和我们现在模块的hash,进行对比看哪些module发生变化
知道module后就好办了,往上面找到引用了他的父chunk,就能得到哪些是需要进行更新的了
把我们做好的json串放到虚拟文件系统中,等着前端来请求,美滋滋(不过好像没看到删除操作)
客户端拿到json文件,对其中的
c
字段进行检查,对于需要改变的chunk请求对应的新的js注意这里重新请求的是chunk,为什么不是module呢?
我知道个屁在不同环境下利用hotDownloadUpdateChunk下载新的chunk文件
添加一个新的hot-update.js的文件script到head里面进行下载工作
我们的hot-chunk又是在哪里生成的呢?之前不是compiler一直都watch着吗,这一轮的complication执行下来得到的hot资源就是通过Jsonp的插件在render的时候进行的格式化然后插入到我们的内存中的
webpackJsonp
,但是在现在这里是用的是webpackHotUpdate
,并且这两个东西你无法直接在源码中找到对应名字的函数,他们是挂在window上面的,所以在中途还被改了个名字hotUpdateDownloaded方法开始执行,标记现在的状态为ready,并把之前下载manifest的deffer给置为null清空了;忽略特殊情况开始处理apply的事件
用getAffectedStuff处理后,从更改的节点往上找到拿到被影响的模块,最终返回几个列表,这样就能开始我们的替换工作啦,同时方法执行最后会返回一个对象
这次处理由于只有这么一个模块发生了修改,而且没有把本次的热加载往上冒泡(可能是因为dva的作用,我们这个页面实际上没有hot相关配置),所以直接得到了accepted的许可,如果有设置则执行
onAccepted
方法。继续往后把doApply设置为true接着往下执行当doApply为true时会往outdatedModules中添加不重复的module元素id,用于一会儿的移除并更新操作
改变标记位,进入dispose阶段,进入对过期模块的清理工作,但是只是从installedMoudules的列表中把他们delete掉了,和node还需要清除cache不一样,简单删除掉就好了;当然除了把自身卸载掉还有之前说的dependency也需要处理,我不是很清楚就不再赘述了
删除完成进入apply阶段,将新的代码模块都应用上去,这里终于发现我们的dependency其实是设置好了
module.hot.accept
的模块,拥有这个配置的模块会将回调函数放到_acceptedDependencies里面存好,过后边开始执行hot中的操作,比如利用ReactDOM重新挂载,或者重新加载模块等,具体操作以dva为例,请前往dva部分查看处理完更新过后,有错误就触发fail标识,不然直接进入idle的空闲状态
调用栈一直退出直到check中,进行收尾工作打印操作正确与否的日志
webpack-dev-server
本是同根生,所以webpack还是使用了yargs进行参数的处理,其他还有optimist和commander等等,大家都大同小异,而且诶个人使用下来还是yargs舒服而且相对活跃,那这就是用他的原因吗?
其实我倒觉得是因为两个包都用一样的yargs会保证npm下载的时候更快更稳啦~
之后会对里面拿到的参数进行格式化(convert)处理,这里有个选项直接会导致他的输出统一变成bundle.js,当然这也是为了方便DS读取固定且单一的路径
processOptions
对设置的参数进行处理,如果设置了stdin的参数的话会打开输入流,是用来后面直接读取资源数据?方便管道处理?
之后还有其他初始化,比如这个读取证书的操作,DS实际上是支持https的,不过少有用到,跟着研究一下
DS是真的皮,如果-p参数带来的端口刚好是默认端口号的话,他反而会先尝试去使用在文件中设置的值
如果我们没有在任何地方设置好服务启动端口的话,那就会通过portfinder从8080一直往上面找,直到找到一个可以用的端口,但是如果指定了某一个值就没那么好玩了,不行的话直接GG
addDevServerEntrypoints
听名字就知道不是什么好事,他会在我们webpack的配置中添加上两个entry,其中一个是我们必要的DS客户端,就是干响应新数据啊,发起链接这些事情的
但是另一个就要分两种情况了,但是都是和热加载有关的文件,可能是only-dev-server也可能是dev-server。有个什么区别呢?区别在于
webpack/hot/dev-server
在 HMR 更新失败之后会刷新整个页面,如果你想查看错误自己刷新页面, 可以改用webpack/hot/only-dev-server
那么我们就很愉快的拿到了两个入口,看看这次测试是什么样子
/Users/you/project/node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:8080
,后面的query字符串会在webpack处理之后变成我们的熟人__resourceQuerywebpack/hot/dev-server
,一般都是用hot,所以相当于默认的就是这个相关:github-issue
startDevServer
创建一个新的webpack实例,拿到我们的compiler
创建一个新的Server,美滋滋,就是简单的express服务器加上我们的相关中间件
设置好相关的插件,分别在compile,invalid,done阶段向客户端的socket连接发送信息告知详情,done的时候会整个发送stat过去,就是我们的分析数据摘要,包含了请求资源json和补丁js文件的hash值等等,另外这几个里面只有process的过程是按需的,根据process的设置启动
创建express服务器,拿到所有的请求,先把host过滤一遍,不知道意义何在;添加上我们的中间件DM
他会把我们的文件存到虚拟系统里,你别无选择,如果input的时候是虚拟系统就直接用那个,没有就新建一个memory-fs的实例拿来存取东西
给done,invalid,watch-run,run阶段添加上一些管理函数
开始调用compiler的watch方法对文件进行监视
创建一个Watcher对象,进行初始化,由于fs其实还是扫描文件是否发生变化,所有有一个时间间隔,这里默认的值是200ms
试图读取record的缓存记录,但是很可惜,什么都没有那么直接执行_go,跟我们之前所说的的compile差不多,不过是里面和正统的webpack打包对比起来,有一些生命周期发生了变化,比如不会有什么资源存储,本来的run变身为watch-run等
首先会进行一次编译操作,然后回到我们的onCompiled函数,这样构成了一个闭环,不断的做递归,每次检查我们的资源有没有变化
在_done方法的回调中,进行相关的状态发送,不只是hash,还可能出现still-ok,errors的情况,浏览器端会给出相应的反映
本来以为会在某个插件中卡住一直等待文件状态刷新,但是其实这个东西是webpack自己的方法watch,如果发现我们的文件发改变进入invalidate方法,把之前的watcher扔掉(watcher实际上一个Watchpack对象,这个对象还是webpack的大佬们定制的,毫无疑问继承自EventEmitter)
给线程绑定上两个信号的监听,虽然只有这么几种信号,但是姑且也算是一种通信方式吧
SIGINT
——程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。SIGTERM
——程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。SIGKILL
——用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。(所以理所当然这里没有监听他)我们现在已经有了一个express的服务器,不过还没有启动(只是一个实例,没有监听端口),那剩下来要做的就是创建我们的socket服务,实现双向数据传输
如果我们配置了socket,那就直接使用我们配置的socket而不是重新起一个sockjs的实例来处理,因为是继承自EventEmitter,所以监听error事件,如果出现了端口占用的情况,则创建一个新的socket连接,要是再拒绝了链接那就再重新尝试连接一次,然后抛出错误,真是坚韧不拔啊
app这个属性中存储的是我们的express服务器,用于接收来自我们页面的所有http请求,用作分发处理,中间代理等等
listeningApp中存储的是一个新的httpServer,如果没有启用https的话会很简单直接是把之前的express服务器拿过来启动就好,但是如果是https的话会用到
spdy
这个库进行创建执行server的listen方法,处理监听操作
listening
事件,并添加once的监听放入我们设置的回调函数sockjs
承接上文的利用sockjs创建一个新的socket服务器,我们探究一下其中的原理
看一下默认的配置呢,优先使用websocket咯,完全没毛病;jsessionid是什么?socket连接怎么会能用这种浪费流量的东西?好吧其实是拿给域名服务商看的
还有个sockjs_url这个就比较有意思了,这里的默认值是sockjs挂在线上CDN的sockjs-client库,我们会对他进行替换,只是换成本地的资源,没有一丁点变化
那么我们现在要看sockjs的启动得去哪里找呢?hey,还记得之前在每个entry中添加的两个文件吗,现在就去看看他们做了什么
index.js
这里用到的sockjs不是之前直接设置的打包好的sockjs,而是在这里重新做的一次引用
require('sockjs')
,就目前看来这样会导致我们引入多余的js文件,我们一会儿看看打包出来的文件是什么样子。没准儿最后有什么奇妙的方法修复了对象里面定义了我们响应socket信息时可能会接受到的所有信号,看看都坐了些啥
这里面的sendMsg等方法都是用来通知其他页面的,利用的就是postMessage,其他页面只需要监听message事件并做出响应就好了,如果当前的工作环境是在WebWorker中那就不发消息通知。
但是这样发送消息会发送到所有的页面上去,也就是说我们如果有两个应用同时在调试的话,那么其中的iframe都会收到这些个消息并且打印到控制台
除了发送消息展示结果,更重要的就是刷新我们的引用了,reloadApp方法就是拿来做此事的。
拿到资源还是广昭天下,给大家说说这次拿到的hash值,可以新添加上JSONP去请求新的资源,请求的时候是怎么请求的呢?这里就要详细的说一说了
资源异步加载
都知道我们的webpack不止能用于browser中,也可能存在于worker或者是直接的node环境中使用,如果我们是一套同构代码的话,那么也会碰上不同环境下异步模块的处理问题,这里来看下不同环境是怎么做到的
为了方便大家找到,直接在webpack目录里面找到这几个文件就行了,直接搜索hotDownloadUpdateChunk这个方法找到相关线索
这里的几个文件都存在着这个函数的不同实现方式,而且他们都有一个runtime的中间名,实际上这一点代表了他们是会被作为插入的模块打包到我们的运行时环境中的,而不是在打包的时候执行的代码,那就来看看看看每一种实现
异步主要逻辑
逻辑部分,很多是HotReplacement的插件和插入进去的runtime做的,我们现在观察一下这两个文件
HotReplacementPlugin
该插件做了几件事
引入我们的runtime文件,并且把里面的代码拿出来进行一些必要的代码替换(会被替换的字段写到了文件中最上方的global注释里面,可以参考看一看)
就像这样子,其中以
$$
框起来的变量会在本次处理的时候被替换掉,具体的逻辑是发现在这个过程里面还有很多变量没有被替换是怎么回事?他们是在哪里定义的?installedModules这种,还有其他相关的函数
dva-hmr
我们这里是用的是dva的babel-plugin,至于为什么没有使用在loader和plugin中添加操作,可以看看redux作者,同时也是react-hot-loader的作者写的一篇博客,详细的阐述了判断一个东西是不是组件有多少困难
最终选择了到babel的层面来注入代码也是有原因的,对于dva来说他是把hmr的兼容操作进行了一层封装放到了整个系统内部作为一个插件,将onHmr的入口留给webpack进行hash的注入
紧接上文,我们提到把存在
module.hot.accept
的模块称之为dependency,对应到dva里面就是我们在上面展示的这一段代码了,他会出现在我们使用app.router的地方,把每一个路由中的内容做了一个热处理这里的render实际上。。。只是重新挂载了一次到dom上面,是我设置有错吗??这个东西应该做到的其实是保留当前的store中的状态,重新挂载更新的组件啊!
我们来看看performance中记录的是怎样的情景(部分,过深处的调用栈也是差不多),看看这么整个重新挂载dom树会有哪些动作
从上图中可以发现这样重新挂载到DOM节点上面实际上是会导致我们的项目整个重新来过;我们逐个山峰进行分析
react-hot-loader
看了上面的dva-hmr是不是有点沮丧。这也能叫热加载?感觉就比刷新页面少了个加载其他资源的步骤,其实要求也不要那么高,这只是dva顺手做的一个功能而已,我们具体来看看 Dan Abramov是怎么做的吧,如何才能做到保留我们组件的状态和store的状态
ReactDOM.render原来不是我想的那样,把所有组件卸载了然后重新挂载,根据官方解释,其实是做一次更新,那前面dva怎么搞成了这样
为了便于于dva做一个对比,我们看一下他的函数调用图谱(部分)是怎么样的
可以看到和之前的全部unmount再进行mount不一样,这里都是在update我们的组件了,这个操作是和我们文档中的ReactDOM是完全相符合的,算下来其实是dva有点奇葩,她底层也是使用的
ReactDOM.render
,不过我们的react-hot-loader是利用了AppContainer在我们的组件最外层包裹上了一层,才达到这样的效果根据AppContainer的源码,其主要是有一个展示编码错误的功能,还有就是手动对所有子元素进行深度强制更新(forceUpdate),当他的props发生改变的时候便会触发这一操作,而这一动作的触发想必也一定是和我们的热更新资源有关了
关于react-hot-loader我之前翻译了一篇相关文章,请跳转继续阅读
不知道你看完有没有想到我上面所写的目标的答案?欢迎下方留言~
The text was updated successfully, but these errors were encountered: