前言
热加载,最初接触的时候是使用create-react-app的时候,创建一个项目出来,修改一点代码,页面自动刷新了,贫道当时就感叹,这是造福开发者的事情。
再后来编写静态页面的时候使用 VS Code 的插件 Liver Server, 也是及时刷新,平僧幸福感慢慢,什么单不单身,狗不狗的,都不重要了。
Live Server
有一天喝酒回家后,睡的特别好,醒来后突然脑袋一晃,出现一个念头,世界那么大。我想看看 hot load 是咋实现的。
当然这里有两点应该是确认
- 肯定是监听文件变化
- WebSocket 监听服务端变化的通知,刷新文件
于是打开Live 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
到达这里,基本的东西就完了。 我们要好奇心多一点。我们再多看看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.
写在最后
不忘初衷,有所得,而不为所累,如果你觉得不错,你的一赞一评就是我前行的最大动力。
技术交流群请到 这里来。 或者添加我的微信 dirge-cloud,一起学习。