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,一起学习。


相关文章
|
4月前
|
机器学习/深度学习 人工智能
【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]=
|
人工智能 编解码 自动驾驶
YOLOv7: Trainable bag-of-freebies sets new state-of-the-art for real-time object detectors
YOLOv7在5 FPS到160 FPS的范围内,在速度和精度方面都超过了所有已知的物体检测器,在GPU V100上以30 FPS或更高的速度在所有已知的实时物体检测器中具有最高的精度56.8% AP。
433 0
|
存储 索引
Elastic:data_hot,data_warm,data_cold角色有什么用
data_hot:热点数据节点,热点数据节点在进入es时存储时间序列数据,热层必须快速读取和写入,并且需要更多的硬件资源 data_warm;暖数据节点,存储不再定期更新但仍在查询的索引。查询两的频率通常低于索引出于热层时的频率,性能较低的硬件通常可用于此层中的节点 data_cold:冷数据节点,存储访问频率较低的只读索引,磁层使用性能较低的硬件,并且可以利用可搜索快照索引来最小化所需的资源
142 0
Elastic:data_hot,data_warm,data_cold角色有什么用
Re1:读论文 C&S (Correct and Smooth) Combining Label Propagation and Simple Models Out-performs Graph Ne
Re1:读论文 C&S (Correct and Smooth) Combining Label Propagation and Simple Models Out-performs Graph Ne
Re1:读论文 C&S (Correct and Smooth) Combining Label Propagation and Simple Models Out-performs Graph Ne
|
机器人
GazeboRosControlPlugin::Load 函数详解
GazeboRosControlPlugin::Load 函数详解
GazeboRosControlPlugin::Load 函数详解
|
算法框架/工具 Windows
|
XML 数据格式
Adobe form batch output print mode - multiple
Customer wants to render the body page 4 times on 4 different master page in one PDF. Take “Invoice” as example, the first copy will be printed on master page with text “for Customer”, the second copy will be printed on master page with text “for archive”.
|
移动开发
同样是保存模型,model.save()和model. save_weights ()有何区别
同样是保存模型,model.save()和model. save_weights ()有何区别
594 0
|
BI
Five Steps to Becoming a Better Data Analyst
In this post, a data analyst from wangjubao.com, a leading data-marketing firm in China, shares some advice on making better data insights.
1888 0
Five Steps to Becoming a Better Data Analyst
|
SQL 测试技术
myrocks fast load data
# Fast data load Load data相比普通insert效率更高,Load data批量插入数据有效减少了解析SQL的开销。MyRocks 同其他MySQL 引擎一样也支持Load data语法,同时MyRocks对data load也做了特殊优化。RocksDB引擎有一个规律是,**数据最终会存储在最底层SST文件中**,MyRocks通过参数rocksdb_bulk_
2237 0