您的位置首页  散文日记

热更新?热更新原理?墙裂推荐

戳蓝字「前端技术优选」关注我们哦! style-loader 支持热更新 回到我们之前写的 style-lo

热更新?热更新原理?墙裂推荐

 

戳蓝字「前端技术优选」关注我们哦! style-loader 支持热更新 回到我们之前写的 style-loader,当时为了简单并没有支持热更新,这里我们为他加上热更新功能因为 style-loader。

其实会处理 css-loader 传过来的 locals,也就是 css modules 的class映射那么根据有没有启用 css modules,其实 style-loader 的热更新会分为两种情况。

第一种情况,启用了 css modules如果我们已经在项目中配好了 HMR的代码,那么style-loader不作任何修改,就能默认支持热更新为什么呢?因为在webpack中,如果一个模块没有处理热更新的事件,那么会自动冒泡到他的父元素,直到被处理或者最终刷新浏览器。

那么如果 style-loader 没有处理热更新的事件,会自动冒泡上去以一个 React 项目为例,最终会冒泡到 react-hot-loader,他会重新渲染 Root 组件,然后我们的 import styles from ./styles.css。

就会被重新执行一遍,所以CSS样式就被更新了当然,因为我们只负责插入 style 标签,而没有删除它,所以 style 标签会越来越多为了保证性能,我们还是需要增加一行删除旧 style 标签的代码:。

if (module.hot) {module.hot.dispose(/**此处删除我们的旧style标签**/)}需要说明的是,在启用 css modules 的时候,style-loader 不处理热更新事件并不是投机取巧,而是必须不能处理。

为什么呢?因为如果 style-loader 自己处理了,比如把 style 标签的内容更新下,那么热更新就到此停止,父元素不会被更新,而且其实父元素依赖的 className 的列表已经变了,这样会导致样式出现错误。

所以必须不能处理而如果没有启用  css modules,则父组件不会有直接依赖,这个时候只要 style-loader 自己更新下样式就好了第二种情况,如果没有启用 css modules其实不作任何修改也可以!道理和上面的一样。

然而,由于不作任何修改会冒泡到React根节点上,而其实只需要把 style 更新一下就好了,完全不用 React 组件做任何修改所以在没有启用 css modules时,我们的 style-loader

就自己处理热更新,只需要在热更新的时候,把style的内容更新一下就好我们加上完整的热更新的代码如下:/** * style loader will insert css into DOM */module。

.exports.pitch = function (request) {var result = [var content=require( + loaderUtils.stringifyRequest(

this, !! + request) + );,var style = require( + loaderUtils.stringifyRequest(this, ! + path.join(__dirname, 

"add-style.js")) + )(content);,if (module.hot) {,  if (content.locals) {, // 未启用 css modules, 则可以直接更新 style内容即可。

如果启用了,因为还需要父组件更新,所以这里就不作处理,直接冒泡到父组件处理(style-loader被父组件重新调用了一次)        module.hot.accept( + loaderUtils.stringifyRequest(

this, !! + request) + , function() {,     console.log("update new style"),     style.innerHTML = require(

 + loaderUtils.stringifyRequest(this, !! + request) + );,   }),  },  module.hot.dispose(function () { console.log(style);style.remove() })

,  // 无论如何,当dispose的时候记得把之前创建的 style 标签删除掉},if(content.locals) module.exports = content.locals  ]return

 result.join(;)}事实上,这也就是官方的 style-loader 的做法:只有在启用 cssmodules 的时候才处理热更新,否则只负责删除,而把更新代码交给父组件做HMR的原理我们将分别从服务端(也就是nodejs)和客户端(也就是浏览器)两部分讲 HMR 热更新的原理。

我这里先画出一张流程图:hmr暂时看不懂没关系,下面我们详细讲解HMR 在server端的实现之前我们讲到 webpack 是从 webpack/bin/webpack 开始启动的,但是如果我们使用了

webpack-dev-server,那么就是从 webpack-dev-server/bin/webpack-dev-server.js 启动的dev-server如上图所示,webpack-dev-server。

包含了三部分:webpack, 负责编译代码webpack-dev-server,主要提供了 in-memory 内存文件系统,他会把webpack的outputFileSystem 替换成一个 inMemoryFileSystem,并且拦截全部的浏览器请求,从这个文件系统中把结果取出来返回。

express,作为服务器先来总结下 webpack 在server端进行热更新的流程初始化阶段:1, webpack-dev-server 初始化的时候var compiler = webpack(options) // 创建 webpack 实例

监听  compiler 也就是 webpack 的 done 事件创建 express 实例创建 WebpackDevMiddleware 实例设置 express router,WebpackDevServer 会作为 express的一个中间件拦截所有请求

2, WebpackDevMiddleware 在初始化的时候创建 一个 MemoryFileSystem  实例,替换掉 webpack.outputFileSystem,这样 webpack 编译出的文件其实都是存在内存中,而不是磁盘上

把对编译后的文件的请求,都重定向到上面创建的 MemoryFileSystem 中热更新阶段:1, webpack 监听文件变化,并完成编译2, webpack-dev-server 监听 done 事件,并通过

websocket 向客户端发送消息3, 客户端经过处理后,请求新的JS模块代码4, WebpackDevServer 从 MemoryFileSystem 中取出代码,并返回下面我们来看看代码:初始化阶段,

webpack-dev-server.js 会创建一个 webpack 实例:let compiler;try {    compiler = webpack(webpackOptions);  } catch

 (e) {  }let server;try {    server = new Server(compiler, options);  } catch (e) {  }接着,webpack-dev-server/lib/Server.js

会创建 express 和 WebpackDevMiddleware:functionServer(compiler, options) {  compiler.plugin(done, (stats) => {

this._sendStats(this.sockets, stats.toJson(clientStats)); // 当完成编译的时候,就通过 websocket 发送给客户端一个消息(一个 `hash` 和 一个`ok`)

this._stats = stats;  });// Init express serverconst app = this.app = new express(); // eslint-disable-line

// middleware for serving webpack bundlethis.middleware = webpackDevMiddleware(compiler, options);}// 后面会有一行

app.use(this.middleware); // middleware会拦截所有请求,如果发现对应的请求是要请求 `dist` 中的文件,则会进行处理在热更新阶段,首先会触发这几行代码:  compiler.plugin(。

done, (stats) => {this._sendStats(this.sockets, stats.toJson(clientStats));this._stats = stats;  });_sendStats

会通过 websocket 给客户端发送两条消息客户端收到消息后,会去请求一个 json 配置文件,然后根据配置请求新的JS模块代码这些请求都会被 WebpackDevMiddleware 拦截:function

webpackDevMiddleware(req, res, next) {functiongoNext() {if(!context.options.serverSideRender) return next();

returnnewPromise(function(resolve) {                shared.ready(function() {                    res.locals.webpackStats = context.webpackStats;

                    resolve(next());                }, req);            });        }if(req.method !== 

"GET") {return goNext();        }// 如果发现这个请求是 publicPath 中的文件内容,那么就从 fs 中取出内容并返回// 如果不是,那么 next,交给 `webpack-dev-server` 进行处理

var filename = getFilenameFromUrl(context.options.publicPath, context.compiler, req.url);if(filename === 

false) return goNext();returnnewPromise(function(resolve) {            shared.handleRequest(filename, processRequest, req);

functionprocessRequest() {// ...// server contentvar content = context.fs.readFileSync(filename);// ....

if(res.send) res.send(content);else res.end(content);                resolve();            }        });

    }HMR 在浏览器中的工作流程webpack 在 启用HMR之后,会在server端(nodejs端)监听文件改动,并且一旦发生变动就把新的代码编译后发送到浏览器浏览器中也会有HMR相关的代码,会通过socket和server保持通信,获取新代码并进行热替换。

这里我们先不看webpack在nodejs端是如何编译的,而是先看看在浏览器中是如何工作的hmr nodejsHMR 工作流程:client 和 server 建立一个 websocket 通信当有文件发生变动的时候,webpack编译文件,并通过 websocket 向client发送一条更新消息。

client 根据收到的hash值,通过ajax获取一个 manifest 描述文件client 根据manifest 获取新的JS模块的代码当取到新的JS代码之后,会更新 modules tree,(installedModules)

调用之前通过 module.hot.accept 注册好的回调,可能是loader提供的,也可能是你自己写的这里以 用 webpack-dev-server 为例,当我们启用了 HMR 后,他会把我们的入口文件包一层,加上两个依赖:

/***/0:// 这个模块是新加的,我们的入口就是 index,而这里加了一个模块,引用了 index,并且额外加了两行 require/***/ (function(module, exports, __webpack_require__

) {__webpack_require__("./node_modules/webpack-dev-server/client/index.js?http://localhost:8080");__webpack_require__(

"./node_modules/webpack/hot/dev-server.js");module.exports = __webpack_require__("./src/index.js");/***/

 })/******/ })其中:client/index.js 主要负责建立socket 通信,并在收到消息后调用对应的方法dev-server.js 会调用 module.hot.check 方法,最终真正去做代码更新的,是在 。

webpack/lib/HotModuleReplacement.runtime.js 文件中顺便说下,你在console中看到的HMR log,几乎都是在 dev-server.js 中输出的,有兴趣可以看下这个文件的源码。

我们的代码启用了 HMR 之后会多出9000行代码,很大一部分就是由于引入了 webpack 中 HMR runtime相关的代码导致的我们先从这个文件开始看:初始化的时候,client.js 会启动一个 socket 和 。

webpack-dev-server 建立连接,然后等待 hash 和 ok 消息当有文件内容改动的时候,首先会收到 webpack-dev-server 发来的 hash 消息,得到新的 哈希值并保存起来。

然后会立刻接收到 ok 消息,表示现在可以加载最新的代码了,于是进入 reloadApp 方法reloadApp -> check()check => hotDownloadManifest, 这里会下载一个本次热更新的manifest文件,url就是用上面存的 。

hash 拼接出来的,大概这样:8b52a72952cca784407e.hot-update.json,结果大概长这样:{"h":"8b52a72952cca784407e","c":{"0":true}}

这里仔细观察会发现,每一次取到的manifest中的hash 都是上一次 hash 消息的值,这样应该是为了保证顺序hotDownloadManifest 下载完配置文件后,可以看到其中有一个 h ,这个hash就是我们等会要取编译后的新代码的地址,在

hotEnsureUpdateChunk 方法中最终会通过 jsonp的方式把新的代码加载进来加载到新的模块代码后,会有一系列的对 依赖树 比如 installedModules 的更新操作最终,在 hotApply

中会执行我们的 module.hot.accept 注册的回调函数上面说的这些JS代码,都是被webpack打包在我们的 bundle.js 头部的代码都是在浏览器中执行的我们来看一下代码:首先,我们的bundle文件会被加入 。

webpack-dev-server/client.js,他会创建一个 socket 和 devserver 连接,监听事件,主要代码如下:const onSocketMsg = {hash: function

msgHash(hash) { // 在 `hash` 事件触发的时候,把 `hash` 记下来    currentHash = hash;  },ok: functionmsgOk() { // `ok` 事件触发的时候,表示server已经便已完成最新代码,

    sendMsg(Ok);if (useWarningOverlay || useErrorOverlay) overlay.clear();if (initial) return initial = 

false; // eslint-disable-line no-return-assign    reloadApp();  }};// 省略functionreloadApp() {if (hot) {

    log.info([WDS] App hot update...);// eslint-disable-next-line global-requireconst hotEmitter = require

(webpack/hot/emitter);    hotEmitter.emit(webpackHotUpdate, currentHash) // 触发这个事件的时候,会触发 `dev-server.js` 中的 check 方法

  }// 省略}dev-server.js 中的check方法,最终会进入到这里:functionhotCheck(apply) {if(hotStatus !== "idle") thrownewError

("check() is only allowed in idle status");         hotApplyOnUpdate = apply;         hotSetStatus("check"

);return hotDownloadManifest(hotRequestTimeout).then(function(update) {// 取到了 manifest后,就可以通过jsonp 加载最新的模块的JS代码了  

                 hotEnsureUpdateChunk(chunkId);         });     }加载JS的代码如下:functionhotDownloadUpdateChunk

(chunkId) { // eslint-disable-line no-unused-vars// 通过jsonp的方式加载var head = document.getElementsByTagName(

"head")[0];var script = document.createElement("script");         script.type = "text/javascript";         script.charset = 

"utf-8";         script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js"

;         ;         head.appendChild(script);     }加载到的代码如下:webpackHotUpdate(0,{/***/"./build/css-loader/index.js?modules!./src/style.css"

:/***/ (function(module, exports, __webpack_require__) {exports = module.exports = __webpack_require__(

"./build/css-loader/css-base.js")();;exports.i(__webpack_require__("./src/global.css"));exports.push([

module.i, "@import ./global.css;\n\nh1 {\n  color: blue;\n}\n\n._input_css_12__avatar {\n  width: 100px;\n  height: 100px;\n  background-image: url("

 + __webpack_require__("./src/avatar.jpeg") + ");\n  background-size: cover;\n}\n\n._input_css_12__avatar2 {\n  width: 100px;\n  height: 100px;\n  background-image: url("

 + __webpack_require__("./src/m.png") + ");\n  background-size: cover;\n}\n", ""]);;exports.locals ={

"avatar":"_input_css_12__avatar","avatar2":"_input_css_12__avatar2"}/***/ })})//# sourceMappingURL=0.8b52a72952cca784407e.hot-update.js.map

到此为止,我们就已经得到了新模块的JS代码了,下面要做的就是调用对应的 accept 回调,这也是在 hotApply 方法的后面部分做的:for(moduleId in outdatedDependencies) {

if(Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {module = installedModules[moduleId];

if(module) {                     moduleOutdatedDependencies = outdatedDependencies[moduleId];var callbacks = [];

for(i = 0; i < moduleOutdatedDependencies.length; i++) {                         dependency = moduleOutdatedDependencies[i];

                         cb = module.hot._acceptedDependencies[dependency]; // 先去到所有对这个模块注册的 accept 回调

if(cb) {if(callbacks.indexOf(cb) >= 0) continue;                             callbacks.push(cb);                         }

                     }for(i = 0; i < callbacks.length; i++) {                         cb = callbacks[i];

try {                             cb(moduleOutdatedDependencies); // 挨个调用一遍                         } 

// ...                     }                 }             }         }

免责声明:本站所有信息均搜集自互联网,并不代表本站观点,本站不对其真实合法性负责。如有信息侵犯了您的权益,请告知,本站将立刻处理。联系QQ:1640731186