hot load那點事


熱加載,最初接觸的時候是使用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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM