zoukankan      html  css  js  c++  java
  • 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.

  • 相关阅读:
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    阶段3 3.SpringMVC·_07.SSM整合案例_09.ssm整合之Spring整合MyBatis框架配置事务
    阶段3 3.SpringMVC·_07.SSM整合案例_08.ssm整合之Spring整合MyBatis框架
    阶段3 3.SpringMVC·_07.SSM整合案例_07.ssm整合之编写MyBatis框架测试保存的方法
  • 原文地址:https://www.cnblogs.com/cloud-/p/10281768.html
Copyright © 2011-2022 走看看