热加载,最初接触的时候是使用create-react-app的时候,创建一个项目出来,修改一点代码,页面自动刷新了,贫道当时就感叹,这是造福开发者的事情。

再后来编写静态页面的时候使用 VS Code 的插件 Liver Server, 也是及时刷新,平僧幸福感慢慢,什么单不单身,狗不狗的,都不重要了。

有一天喝酒回家后,睡的特别好,醒来后突然脑袋一晃,出现一个念头,世界那么大。我想看看 hot load 是咋实现的。

当然这里有两点应该是确认

  1. 肯定是监听文件变化
  2. WebSocket 监听服务端变化的通知,刷新文件

于是打开Liver Server 找到源码ritwickdey/vscode-live-server,再通过 lib/live-server/index.js 的标注

#!/usr/bin/env node

"use strict";

/*
Taken from https://github.com/tapio/live-server for modification
*/

找到live-server,就开始了奇妙的探索之旅。

按照正常流程打开 index.js, 先略去非核心代码:

    chokidar = require('chokidar');

    ......

    // Setup file watcher
LiveServer.watcher = chokidar.watch(watchPaths, {
ignored: ignored,
ignoreInitial: true
});
function handleChange(changePath) {
var cssChange = path.extname(changePath) === ".css" && !noCssInject;
if (LiveServer.logLevel >= 1) {
if (cssChange)
console.log("CSS change detected".magenta, changePath);
else console.log("Change detected".cyan, changePath);
}
clients.forEach(function(ws) {
if (ws)
ws.send(cssChange ? 'refreshcss' : 'reload');
});
}
LiveServer.watcher
.on("change", handleChange)
.on("add", handleChange)
.on("unlink", handleChange)
.on("addDir", handleChange)
.on("unlinkDir", handleChange)
.on("ready", function () {
if (LiveServer.logLevel >= 1)
console.log("Ready for changes".cyan);
})
.on("error", function (err) {
console.log("ERROR:".red, err);
}); return server;
};

从上可以得知,通过 chokidar 监听文件或者目录,当 change|add|addDir 等等时调用 handleChange。

handleChange 判断了一下变更的文件是不是 css,然后通过 socket 发送不通的事件。

那么问题来了, 如果客服端要能接受事件,必然要创建 WebSocket 连接。当然有人说,可以轮询或者 SSE 等这种嘛。我就不这么认为。

再看一段代码


es = require("event-stream") var INJECTED_CODE = fs.readFileSync(path.join(__dirname, "injected.html"), "utf8"); ...... function inject(stream) {
if (injectTag) {
// We need to modify the length given to browser
var len = INJECTED_CODE.length + res.getHeader('Content-Length');
res.setHeader('Content-Length', len);
var originalPipe = stream.pipe;
stream.pipe = function(resp) {
originalPipe.call(stream, es.replace(new RegExp(injectTag, "i"), INJECTED_CODE + injectTag)).pipe(resp);
};
}
} send(req, reqpath, { root: root })
.on('error', error)
.on('directory', directory)
.on('file', file)
.on('stream', inject)
.pipe(res);
};

可以看到,如果需要注入,就会注入代码, 这里是直接更新了 stream。

插曲, 这个 es 就是那个搞事情的 event-stream, 哈哈。

我们再看看 INJECTED_CODE 的内容

<!-- Code injected by live-server -->
<script type="text/javascript">
// <![CDATA[ <-- For SVG support
if ("WebSocket" in window) {
(function() {
function refreshCSS() {
var sheets = [].slice.call(
document.getElementsByTagName("link")
);
var head = document.getElementsByTagName("head")[0];
for (var i = 0; i < sheets.length; ++i) {
var elem = sheets[i];
head.removeChild(elem);
var rel = elem.rel;
if (
(elem.href && typeof rel != "string") ||
rel.length == 0 ||
rel.toLowerCase() == "stylesheet"
) {
var url = elem.href.replace(
/(&|\?)_cacheOverride=\d+/,
""
);
elem.href =
url +
(url.indexOf("?") >= 0 ? "&" : "?") +
"_cacheOverride=" +
new Date().valueOf();
}
head.appendChild(elem);
}
}
var protocol =
window.location.protocol === "http:" ? "ws://" : "wss://";
var address =
protocol +
window.location.host +
window.location.pathname +
"/ws";
var socket = new WebSocket(address);
socket.onmessage = function(msg) {
if (msg.data == "reload") window.location.reload();
else if (msg.data == "refreshcss") refreshCSS();
};
console.log("Live reload enabled.");
})();
}
// ]]>
</script>

简单的来讲,如果是 refreshcss 就先删除原来的 css 标签 link, 然后插入新的,并更新

_cacheOverride 的值, 强制刷新。

否则就是 reload 整个页面。

到达这里,基本的东西就完了。 我们要好奇心多一点。我们再多看看chokidar

同理,先看 index.js

这个add方法就是添加监听的方法。

var NodeFsHandler = require('./lib/nodefs-handler');
var FsEventsHandler = require('./lib/fsevents-handler'); ...... FSWatcher.prototype.add = function(paths, _origAdd, _internal) { ...... if (this.options.useFsEvents && FsEventsHandler.canUse()) {
if (!this._readyCount) this._readyCount = paths.length;
if (this.options.persistent) this._readyCount *= 2;
paths.forEach(this._addToFsEvents, this);
} else {
if (!this._readyCount) this._readyCount = 0;
this._readyCount += paths.length;
asyncEach(paths, function(path, next) {
this._addToNodeFs(path, !_internal, 0, 0, _origAdd, function(err, res) {
if (res) this._emitReady();
next(err, res);
}.bind(this));
}.bind(this), function(error, results) {
results.forEach(function(item) {
if (!item || this.closed) return;
this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item));
}, this);
}.bind(this));
} return this;
};

可以看到这里有两种handler,NodeFsHandler和FsEventsHandler。 还没没有得到是咋监听的,那么继续go on, 先看看NodeFsHandler._addToNodeFs。

打开chokidar/lib/nodefs-handler.js

_addToNodeFs ==> _handleFile ==> _watchWithNodeFs ==> setFsWatchListener ==> createFsWatchInstance

var fs = require('fs');

......

function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
var handleEvent = function(rawEvent, evPath) {
listener(path);
emitRaw(rawEvent, evPath, {watchedPath: path}); // emit based on events occurring for files from a directory's watcher in
// case the file's watcher misses it (and rely on throttling to de-dupe)
if (evPath && path !== evPath) {
fsWatchBroadcast(
sysPath.resolve(path, evPath), 'listeners', sysPath.join(path, evPath)
);
}
};
try {
return fs.watch(path, options, handleEvent);
} catch (error) {
errHandler(error);
}
}

调用的就是fs模块的watch

呵呵,感觉自己读书少,还是得多看文档。

我们再看看FsEventsHandler

_addToFsEvents >_watchWithFsEvents> createFSEventsInstance==>setFSEventsListener


try { fsevents = require('fsevents'); } catch (error) {
if (process.env.CHOKIDAR_PRINT_FSEVENTS_REQUIRE_ERROR) console.error(error)
} // Returns new fsevents instance
function createFSEventsInstance(path, callback) {
return (new fsevents(path)).on('fsevent', callback).start();
}

那我们再接着看看fsevents

/* jshint node:true */
'use strict'; if (process.platform !== 'darwin') {
throw new Error(`Module 'fsevents' is not compatible with platform '${process.platform}'`);
} const { stat } = require('fs');
const Native = require('./fsevents.node');
const { EventEmitter } = require('events'); const native = new WeakMap();
class FSEvents {
constructor(path, handler) {
if ('string' !== typeof path) throw new TypeError('path must be a string');
if ('function' !== typeof handler) throw new TypeError('function must be a function');
Object.defineProperties(this, {
path: { value: path },
handler: { value: handler }
});
}
start() {
if (native.has(this)) return;
const instance = Native.start(this.path, this.handler);
native.set(this, instance);
return this;
}
  • 平台只支持darwin,这是嘛呢,我问node开发,告诉我大致是Mac OS吧,那我就相信吧。
  • require('./fsevents.node') 引用的是c++扩展
  • Native.start(this.path, this.handler) 就是监听,哦哦,原来是这样。

最后我们打开 webpack-dev-server/lib/Server.js 文件。

  const watcher = chokidar.watch(watchPath, options);

  watcher.on('change', () => {
this.sockWrite(this.sockets, 'content-changed');
});

也是这个chokidar, 那么我感觉我能做好多事情了。

亲,你做一个修改后直接发布的应用吧,好歹,好歹。

当然这里,只是弄明白监听和通知的大概。

等有时间,好好研究一下webpack-dev-server.

hot load那点事的更多相关文章

  1. 【javascript】onload load ready的那些事

    首先明确一下页面加载的步骤: 1.下载解析HTML文档结构 2.加载外部脚本文件与样式表文件 3.解析并执行脚本代码 4.构造HTML DOM模型 5 .加载图片等外部文件 6.页面加载完毕 接下来, ...

  2. OpenNLP:驾驭文本,分词那些事

    OpenNLP:驾驭文本,分词那些事 作者 白宁超 2016年3月27日19:55:03 摘要:字符串.字符数组以及其他文本表示的处理库构成大部分文本处理程序的基础.大部分语言都包括基本的处理库,这也 ...

  3. Session中load/get方法的详细区别

    Session.load/get方法均可以根据指定的实体类和id从数据库读取记录,并返回与之对应的实体对象.其区别在于: 如果未能发现符合条件的记录,get方法返回null,而load方法会抛出一个O ...

  4. jQuery延迟加载插件(Lazy Load)详解

    最 新版本的Lazy Load并不能替代你的网页.即便你使用JavaScript移除了图片的src属性,有些现代的浏览器仍然会加载图片.现在你必须修改你的html代 码,使用占位图片作为img标签的s ...

  5. db2 import export load

    DB2中所谓的数据移动,包括: 1. 数据的导入(Import) 2. 数据的导出(Export) 3. 数据的装入(Load) 导入和装入都是利用DB2的相关命令把某种格式的文件中的数据保存到数据库 ...

  6. Java你可能不知道的事(3)HashMap

    概述 HashMap对于做Java的小伙伴来说太熟悉了.估计你们每天都在使用它.它为什么叫做HashMap?它的内部是怎么实现的呢?为什么我们使用的时候很多情况都是用String作为它的key呢?带着 ...

  7. 优化UITableViewCell高度计算的那些事

    优化UITableViewCell高度计算的那些事 我是前言 这篇文章是我和我们团队最近对 UITableViewCell 利用 AutoLayout 自动高度计算和 UITableView 滑动优化 ...

  8. ready是先执行的,load后执行,DOM文档的加载步骤

    在jq中在文档载入完毕后有这几种方式去执行指定函数: $(document).ready(function() { // ...代码... }); //document ready 简写 $(func ...

  9. load and initialize

    NSObject是一切OC类的基类,所以我们必须对NSObject所有的方法有一个清楚的认识. + (void)load; 当类或者分类被加入到runtime时,load方法会被调用,也就是说在mai ...

随机推荐

  1. Codeforces 101487E - Enter The Dragon

    101487E - Enter The Dragon 思路:做的时候两个地方理解错了,第一个事我以为龙吸了水,水就干了,其实龙是在下雨之前吸的,下雨时湖水又满了,所以湖水永远不会干:第二个是以为只要找 ...

  2. Python基础--数据类型

    一.数据类型是什么鬼? 计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值.但是,计算机能处理的远不止数值,还可以处理文本.图形.音频.视频.网页等各种各样的数据,不同 ...

  3. 河南省第十一届ACM大学生程序设计竞赛

    nyoj-1365-山区修路 内存限制:128MB 时间限制:3000ms 特判: No通过数:4 提交数:4 难度:3 题目描述: SNJ位于HB省西部一片群峰耸立的高大山地,横亘于A江.B水之间, ...

  4. UVA-10271 Chopsticks (线性DP)

    题目大意:在n个数中,找出k个三元组(a<=b<=c),求最小的(a-b)*(a-b)之和. 题目分析:将所有数从大到小排序,定义dp(i,j)表示前 i 个数中找出 j 个三元组时的最小 ...

  5. 24.2 网络编程基础——System.Net 命名空间

    使用C#进行网络编程时,通常要用到: System. Net  命名空间. System. Net. Sockets  命名空间. System. Net. Mail  命名空间. 24.2.1 Sy ...

  6. 使用dbms_output.put_line打印异常所在的行

    dbms_output.put_line(dbms_utility.format_error_stack); dbms_output.put_line(dbms_utility.format_call ...

  7. 【LeetCode】Unique Binary Search Trees II 异构二叉查找树II

    本文为大便一箩筐的原创内容,转载请注明出处,谢谢:http://www.cnblogs.com/dbylk/p/4048209.html 原题: Given n, generate all struc ...

  8. STM 软件事务内存——本质是为提高并发,通过事务来管理内存的读写访问以避免锁的使用

    对Java程序员来说,我们对面向对象的编程(OOP)自然都是烂熟于胸的,但语言也极大地影响了我们构建面向对象应用程序的方式.(现在的OOP已经和Alan Kay当初创造这个词时候的初衷大不相同了,他的 ...

  9. JavaScript学习总结(十二)——JavaScript编写类

    在工作中经常用到JavaScript,今天总结一下JavaScript编写类的几种写法以及这几种写法的优缺点,关于JavaScript编写类的方式,在网上看到很多,而且每个人的写法都不太一样,经常看到 ...

  10. MySQL 5.7 分区表性能下降的案例分析

    转载自:https://mp.weixin.qq.com/s/K3RpSBAIWFwGCIWyfF0QPA 前言:希望通过本文,使MySQL5.7.18的使用者知晓分区表使用中存在的陷阱,避免在该版本 ...