🧊

ファイルを更新したら自動でリロードする仕組みを作った(Node x ChromeExtension)

YeomanとかLiveReloadとかそういうのは色々あるみたいですが、きっと素敵機能が山盛りで使いこなせない気がしたので、必要なとこだけ自作してみました。
それ用のサーバー建てるとか、スクリプト挿入するとか面倒やし。

概要

  • Nodeのfsモジュールの類で指定したディレクトリ以下を監視
  • ファイルの更新を検知したら、Socket.IOでクライアントへ通知
  • Chrome拡張でSocket.IOの通知を受け、ページをリロード

仕組みはシンプルだと思います。

その他に実装したのは、

  • 指定ディレクトリ以下を再帰的に監視する
  • 除外したいパターンも指定できる

といったところです。

CentOS4系など、Linuxのカーネルが古い場合は、inotify-tools等がインストールされていない影響もあり、fs.watchが使えません。(=本記事の内容が使えない)
この場合、fs.statなどで代用するしかないみたいです。

サーバーサイド

以前の記事で構築したExpressの上で作ったので、Express仕様になってます。
とは言え必要なところを切り出せば全然動くと思います。

依存しているモジュール

  • express(おまけ)
  • ejs(おまけ)
  • socket.io
  • fs
  • fs-watch-tree


Node標準のfsモジュールは色々イケてないらしいので、fs-watch-treeというモジュールを利用しています。
npmのページの説明は多少古いようなので、Githubの方を要参照です。

参考:busterjs/fs-watch-tree · GitHub

このモジュールも根本ではfsモジュールを利用しているそうで、イケてない部分は実はそのままイケてなかったりします・・w
やたらと変更通知するとか・・。

target.json

{
  "target": [
    "/var/www/node/hoge/",
    "/var/www/node2/public_html/",
    "/var/www/php/wp/",
    "/var/www/piyo/public_html/script/"
  ]
}

絶対パスで監視したいディレクトリを指定します。

このファイルはjsonなので、コメント書くとエラーになります・・。
jsonってしましたが、別にjsファイルでも問題ないぽいです。

app.js

var app = module.parent.exports,
  io = app.get('io'),
  fs = require('fs'),
  watchTree = require("fs-watch-tree").watchTree,
  consoleDetail = function(e){
    var detail = (e.isDelete())
      ? 'deleted'
      : (e.isModify())
        ? 'modified'
        : (e.isMkdir())
          ? 'created'
          : 'changed';
    return e.name + ' was ' + detail;
  },
  targetList = require('./target.json').target,
  excludeList = ["node_modules", "~", "#", /^\./, /^_/],
  watchObj;

// First: Check target dir existence.
targetList.forEach(function(d){
  fs.exists(d, function(exist){
    if(exist){
      console.info('Start watching: %s', d);
    }else{
      console.error('Dir %s doesn\'t exist.', d);
      process.exit();
    }
  });
});

// Second: Client connected, then watch indicated dir.
var autoReloaderSocket = io.of('/reloader').on('connection', function(client) {
  client.emit('connected');
  console.info('Client has connected.');

  targetList.forEach(function(d){
    watchObj = watchTree(d, {
      exclude: excludeList
    }, function (e) {
      console.info(consoleDetail(e));
      client.emit('reload');
    });
  });

  client.on('disconnect', function() {
    watchObj.end();
    console.info('Client has disconnected.');
  });

});

あとは親のExprssサーバーを起動すればOKという感じ。

クライアントサイド

Extensionの概要

  • manifest.jsonで指定した開発環境では、Content Scriptを挿入
  • Content ScriptがSocket.IOの通知を受け、ページをリロード

Manifest.json

{
  "manifest_version": 2,
  "name": "Reloader",
  "version": "1.0",

  "description": "Reload pages automatically.",
  "icons": {
    "16": "icons/icon_16x16.png",
    "48": "icons/icon_48x48.png",
    "128": "icons/icon_128x128.png"
  },
  "content_scripts": [{
    "run_at": "document_end",
    "matches": ["http://*.YOURSERVER/*"],
    "js": ["socket.io.js", "reload.js"]
  }],
  "browser_action": {
    "default_icon": "icons/icon_19x19.png"
  },
  "permissions": ["tabs", "http://*/*", "https://*/*"]
}

Content Script

(function(io) {
  var socket = io.connect('http://YOURNODESERVER/reloader', {port: 9999}),
  isReloading = 0;

  socket.on('connected', function() {
    console.log('Waiting for updates...');
  });

  socket.on('reload', function() {
    if(isReloading){
      return;
    }
    isReloading = 1;
    console.log('Reloading.')
    location.reload();
  });

}(io));

isReloadingは、通知がやたらと飛ぶイケてないfs.watchのためです。

未達、不満など

excludeできてない

勉強不足なせいだとは思いますが、除外の指定をしているにも関わらず、普通に通知されたりします。
なんでやろ・・。

excludeListもtargetみたく外出ししたかった

正規表現のとこが引っかかって、外から呼べなかったんです。
どうにかならんかなあ。

watchしてるファイルの変更通知がやたら飛ぶ

fsモジュールのイケてないところだそうです。
どうしようもないのでクライアントサイドで間引いてます。
その内改善されると思ってます。

Content ScriptのOn/Offとかしたかった

今だとmanifest.json内でしか設定ができません。
ただContentScriptは後からOn/Offできるものではないそうなので、どうすりゃいいのかしら。

公開したかった

Githubとか、ChromeStoreとか。
でもちょっと納得できない出来なので、ブログに書くにとどめます・・。

とは言え個人で使う分には十分なので、今のとここのまま使ってます。
意外と便利です。