热加载,最初接触的时候是使用create-react-app的时候,创建一个项目出来,修改一点代码,页面自动刷新了,贫道当时就感叹,这是造福开发者的事情。
再后来编写静态页面的时候使用 VS Code 的插件 Liver Server, 也是及时刷新,平僧幸福感慢慢,什么单不单身,狗不狗的,都不重要了。

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

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

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

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

  1. #!/usr/bin/env node
  2. "use strict";
  3. /*
  4. Taken from https://github.com/tapio/live-server for modification
  5. */

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

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

  1. chokidar = require('chokidar');
  2. ......
  3. // Setup file watcher
  4. LiveServer.watcher = chokidar.watch(watchPaths, {
  5. ignored: ignored,
  6. ignoreInitial: true
  7. });
  8. function handleChange(changePath) {
  9. var cssChange = path.extname(changePath) === ".css" && !noCssInject;
  10. if (LiveServer.logLevel >= 1) {
  11. if (cssChange)
  12. console.log("CSS change detected".magenta, changePath);
  13. else console.log("Change detected".cyan, changePath);
  14. }
  15. clients.forEach(function(ws) {
  16. if (ws)
  17. ws.send(cssChange ? 'refreshcss' : 'reload');
  18. });
  19. }
  20. LiveServer.watcher
  21. .on("change", handleChange)
  22. .on("add", handleChange)
  23. .on("unlink", handleChange)
  24. .on("addDir", handleChange)
  25. .on("unlinkDir", handleChange)
  26. .on("ready", function () {
  27. if (LiveServer.logLevel >= 1)
  28. console.log("Ready for changes".cyan);
  29. })
  30. .on("error", function (err) {
  31. console.log("ERROR:".red, err);
  32. });
  33. return server;
  34. };

从上可以得知,通过 chokidar 监听文件或者目录,当 change|add|addDir 等等时调用 handleChange。
handleChange 判断了一下变更的文件是不是 css,然后通过 socket 发送不通的事件。

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

再看一段代码

  1. es = require("event-stream")
  2. var INJECTED_CODE = fs.readFileSync(path.join(__dirname, "injected.html"), "utf8");
  3. ......
  4. function inject(stream) {
  5. if (injectTag) {
  6. // We need to modify the length given to browser
  7. var len = INJECTED_CODE.length + res.getHeader('Content-Length');
  8. res.setHeader('Content-Length', len);
  9. var originalPipe = stream.pipe;
  10. stream.pipe = function(resp) {
  11. originalPipe.call(stream, es.replace(new RegExp(injectTag, "i"), INJECTED_CODE + injectTag)).pipe(resp);
  12. };
  13. }
  14. }
  15. send(req, reqpath, { root: root })
  16. .on('error', error)
  17. .on('directory', directory)
  18. .on('file', file)
  19. .on('stream', inject)
  20. .pipe(res);
  21. };

可以看到,如果需要注入,就会注入代码, 这里是直接更新了 stream。
插曲, 这个 es 就是那个搞事情的 event-stream, 哈哈。

我们再看看 INJECTED_CODE 的内容

  1. <!-- Code injected by live-server -->
  2. <script type="text/javascript">
  3. // <![CDATA[ <-- For SVG support
  4. if ("WebSocket" in window) {
  5. (function() {
  6. function refreshCSS() {
  7. var sheets = [].slice.call(
  8. document.getElementsByTagName("link")
  9. );
  10. var head = document.getElementsByTagName("head")[0];
  11. for (var i = 0; i < sheets.length; ++i) {
  12. var elem = sheets[i];
  13. head.removeChild(elem);
  14. var rel = elem.rel;
  15. if (
  16. (elem.href && typeof rel != "string") ||
  17. rel.length == 0 ||
  18. rel.toLowerCase() == "stylesheet"
  19. ) {
  20. var url = elem.href.replace(
  21. /(&|\?)_cacheOverride=\d+/,
  22. ""
  23. );
  24. elem.href =
  25. url +
  26. (url.indexOf("?") >= 0 ? "&" : "?") +
  27. "_cacheOverride=" +
  28. new Date().valueOf();
  29. }
  30. head.appendChild(elem);
  31. }
  32. }
  33. var protocol =
  34. window.location.protocol === "http:" ? "ws://" : "wss://";
  35. var address =
  36. protocol +
  37. window.location.host +
  38. window.location.pathname +
  39. "/ws";
  40. var socket = new WebSocket(address);
  41. socket.onmessage = function(msg) {
  42. if (msg.data == "reload") window.location.reload();
  43. else if (msg.data == "refreshcss") refreshCSS();
  44. };
  45. console.log("Live reload enabled.");
  46. })();
  47. }
  48. // ]]>
  49. </script>

简单的来讲,如果是 refreshcss 就先删除原来的 css 标签 link, 然后插入新的,并更新
_cacheOverride 的值, 强制刷新。
否则就是 reload 整个页面。

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

同理,先看 index.js
这个add方法就是添加监听的方法。

  1. var NodeFsHandler = require('./lib/nodefs-handler');
  2. var FsEventsHandler = require('./lib/fsevents-handler');
  3. ......
  4. FSWatcher.prototype.add = function(paths, _origAdd, _internal) {
  5. ......
  6. if (this.options.useFsEvents && FsEventsHandler.canUse()) {
  7. if (!this._readyCount) this._readyCount = paths.length;
  8. if (this.options.persistent) this._readyCount *= 2;
  9. paths.forEach(this._addToFsEvents, this);
  10. } else {
  11. if (!this._readyCount) this._readyCount = 0;
  12. this._readyCount += paths.length;
  13. asyncEach(paths, function(path, next) {
  14. this._addToNodeFs(path, !_internal, 0, 0, _origAdd, function(err, res) {
  15. if (res) this._emitReady();
  16. next(err, res);
  17. }.bind(this));
  18. }.bind(this), function(error, results) {
  19. results.forEach(function(item) {
  20. if (!item || this.closed) return;
  21. this.add(sysPath.dirname(item), sysPath.basename(_origAdd || item));
  22. }, this);
  23. }.bind(this));
  24. }
  25. return this;
  26. };

可以看到这里有两种handler,NodeFsHandler和FsEventsHandler。 还没没有得到是咋监听的,那么继续go on, 先看看NodeFsHandler._addToNodeFs。
打开chokidar/lib/nodefs-handler.js
_addToNodeFs ==> _handleFile ==> _watchWithNodeFs ==> setFsWatchListener ==> createFsWatchInstance

  1. var fs = require('fs');
  2. ......
  3. function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
  4. var handleEvent = function(rawEvent, evPath) {
  5. listener(path);
  6. emitRaw(rawEvent, evPath, {watchedPath: path});
  7. // emit based on events occurring for files from a directory's watcher in
  8. // case the file's watcher misses it (and rely on throttling to de-dupe)
  9. if (evPath && path !== evPath) {
  10. fsWatchBroadcast(
  11. sysPath.resolve(path, evPath), 'listeners', sysPath.join(path, evPath)
  12. );
  13. }
  14. };
  15. try {
  16. return fs.watch(path, options, handleEvent);
  17. } catch (error) {
  18. errHandler(error);
  19. }
  20. }

调用的就是fs模块的watch
呵呵,感觉自己读书少,还是得多看文档。

我们再看看FsEventsHandler
_addToFsEvents ==>_watchWithFsEvents==> createFSEventsInstance==>setFSEventsListener

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

那我们再接着看看fsevents

  1. /* jshint node:true */
  2. 'use strict';
  3. if (process.platform !== 'darwin') {
  4. throw new Error(`Module 'fsevents' is not compatible with platform '${process.platform}'`);
  5. }
  6. const { stat } = require('fs');
  7. const Native = require('./fsevents.node');
  8. const { EventEmitter } = require('events');
  9. const native = new WeakMap();
  10. class FSEvents {
  11. constructor(path, handler) {
  12. if ('string' !== typeof path) throw new TypeError('path must be a string');
  13. if ('function' !== typeof handler) throw new TypeError('function must be a function');
  14. Object.defineProperties(this, {
  15. path: { value: path },
  16. handler: { value: handler }
  17. });
  18. }
  19. start() {
  20. if (native.has(this)) return;
  21. const instance = Native.start(this.path, this.handler);
  22. native.set(this, instance);
  23. return this;
  24. }
  • 平台只支持darwin,这是嘛呢,我问node开发,告诉我大致是Mac OS吧,那我就相信吧。
  • require(‘./fsevents.node’) 引用的是c++扩展
  • Native.start(this.path, this.handler) 就是监听,哦哦,原来是这样。

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

  1. const watcher = chokidar.watch(watchPath, options);
  2. watcher.on('change', () => {
  3. this.sockWrite(this.sockets, 'content-changed');
  4. });

也是这个chokidar, 那么我感觉我能做好多事情了。
亲,你做一个修改后直接发布的应用吧,好歹,好歹。

当然这里,只是弄明白监听和通知的大概。
等有时间,好好研究一下webpack-dev-server.

版权声明:本文为cloud-原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/cloud-/p/10281768.html