hot load那点事

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

前言



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


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


Live Server



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


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


  1. 肯定是监听文件变化
  2. 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,一起学习。


相关文章
|
安全 Python
YAML+PyYAML笔记 8 | PyYAML源码之full_load(),full_load_all(),safe_load(),unsafe_load(),unsafe_load_all()
YAML+PyYAML笔记 8 | PyYAML源码之full_load(),full_load_all(),safe_load(),unsafe_load(),unsafe_load_all()
124 1
|
6月前
|
机器学习/深度学习 人工智能
【CatBoost报错解决】CatBoostError: Bad value for num feature[non default doc idx=0,feature idx=19]=
【CatBoost报错解决】CatBoostError: Bad value for num feature[non default doc idx=0,feature idx=19]=
|
API 数据格式
TensorFlow2._:model.summary() Output Shape为multiple解决方法
TensorFlow2._:model.summary() Output Shape为multiple解决方法
274 0
TensorFlow2._:model.summary() Output Shape为multiple解决方法
Atomics.load()
Atomics.load()
131 1
|
存储 索引
Elastic:data_hot,data_warm,data_cold角色有什么用
data_hot:热点数据节点,热点数据节点在进入es时存储时间序列数据,热层必须快速读取和写入,并且需要更多的硬件资源 data_warm;暖数据节点,存储不再定期更新但仍在查询的索引。查询两的频率通常低于索引出于热层时的频率,性能较低的硬件通常可用于此层中的节点 data_cold:冷数据节点,存储访问频率较低的只读索引,磁层使用性能较低的硬件,并且可以利用可搜索快照索引来最小化所需的资源
167 0
Elastic:data_hot,data_warm,data_cold角色有什么用
|
机器人
GazeboRosControlPlugin::Load 函数详解
GazeboRosControlPlugin::Load 函数详解
GazeboRosControlPlugin::Load 函数详解
|
算法框架/工具 Windows
成功解决_catboost.CatBoostError: Invalid cat_features[4] = 8 value: index must be < 8.
成功解决_catboost.CatBoostError: Invalid cat_features[4] = 8 value: index must be < 8.
|
移动开发
同样是保存模型,model.save()和model. save_weights ()有何区别
同样是保存模型,model.save()和model. save_weights ()有何区别
624 0
|
数据挖掘 TensorFlow 算法框架/工具
Load and preprocess images
This tutorial shows how to load and preprocess an image dataset in three ways. First, you will use high-level Keras preprocessing utilities and layers to read a directory of images on disk. Next, you will write your own input pipeline from scratch using tf.data. Finally, you will download a dataset
209 0