Node.js 實現 Hot Reload
最新文章見:《Node.js 性能調優之調試篇(四)——supervisor-hot-reload》
====================================================================
Hot Reload(熱載入)在前端界是人盡皆知的名詞了,比如 React 靜態資源的熱載入通過 webpack-dev-server 和 react-hot-loader 實現,webpack-dev-server 負責重新編譯代碼,react-hot-loader 負責熱載入。那在 Node.js 應用中,如何實現 Hot Reload 呢?supervisor? supervisor 或者類似的進程管理工具可以做到代碼改動後重啟應用,而真正的熱載入是不應該重啟應用的。
第一種思路——replace variable
user.js
module.exports = { id: 1, name: nswbmw}
最開始的思路是:替換變數。即將:
use strict;const user = require(./user)module.exports = { sayHi: function() { console.log(Hi %s, user.name); }}
替換為:
use strict;module.exports = { sayHi: function() { console.log(Hi %s, require(./user).name); }}
將所有在文件頭部引入的模塊賦值的變數所用之處都替換為 require(xxx),然後 watch 文件(如 user.js),如果發生改動則刪除 require.cache 中的緩存,那麼下次調用 sayHi 就會重新載入 user.js。上面只是一個簡單的例子,如果遇到複雜的情況,則實現起來比較困難,比如:
use strict;const users = require(./users);const user = users[0];module.exports = { sayHi: function() { console.log(Hi %s, user.name); }, printAllUsers: function() { console.log(users); }}
需要替換成:
use strict;module.exports = { sayHi: function() { console.log(Hi %s, require(./users)[0].name); }, printAllUsers: function() { console.log(require(./users)); }}
需要藉助 esprima 和 escodegen 去解析代碼並替換後重新生成代碼。實現起來複雜度高,最後可能還有 bug,遂放棄了這種思路。
第二種思路——Proxy
後來想到是否可以用 ES6 中的 Proxy 實現,於是嘗試用 Proxy 寫了個 demo 還真跑通了。
什麼是 Proxy?
Proxy 是 ECMAScript 2015(簡稱 ES6)新添加的特性,Proxy 用於修改某些操作的默認行為,等同於在語言層面做出修改,所以屬於一種『元編程』,顧名思義,即在要訪問的對象之前架設一層攔截,要訪問該對象成員必須先經過這層攔截。示例代碼:
var obj = new Proxy({}, { get: function (target, key) { console.log(`getting ${key}!`); return haha; }});console.log(obj.count);// getting count!// haha
Proxy 的第一個參數這裡是一個空對象,也可以是一個其他的複雜的對象,或者函數(畢竟 js 中函數也是對象)。
那如何使用 Proxy 實現 Hot Reload 呢?核心原理在於:之前我們引用了一個模塊對象上的屬性,即使刪除這個模塊的緩存重新載入,之前引用的屬性還是老的屬性。使用 Proxy 將模塊導出的對象加一層『代理』,即導出的對象是一個 Proxy 實例,獲取實例上的屬性,內部是去獲取最新的 require.cache 中的對象上的屬性。同時,監測代碼文件變化,如有改動,則更新 require.cache。
可見:Proxy 可以實現屬性攔截,即也可實現斷開"強引用"的作用。
順便發布了一個 proxy-hot-reload 模塊。核心代碼如下:
module.exports = function proxyHotReload(opts) { opts = opts || {}; const includes = glob.sync(process.env.PROXY_HOT_RELOAD_INCLUDES || opts.includes || **/*.js, globOpt) || []; const excludes = glob.sync(process.env.PROXY_HOT_RELOAD_EXCLUDES || opts.excludes || **/node_modules/**, globOpt) || []; const filenames = _.difference(includes, excludes); debug(Watch files: %j, filenames); chokidar .watch(filenames, { usePolling: true }) .on(change, (path) => { try { if (require.cache[path]) { delete require.cache[path]; require(path); debug(Reload file: %s, path); } } catch (e) { ... } }) .on(error, (error) => console.error(error)); shimmer.wrap(Module.prototype, _compile, function (__compile) { return function proxyHotReloadCompile(content, filename) { if (!_.includes(filenames, filename)) { try { return __compile.call(this, content, filename); } catch(e) { ... } } else { const result = __compile.call(this, content, filename); this._exports = this.exports; try { this.exports = new Proxy(this._exports, { get: function (target, key, receiver) { try { if (require.cache[filename]) { debug(Get %s from require.cache[%s], key, filename); return require.cache[filename]._exports[key]; } else { debug(Get %s from original %s, key, filename); return Reflect.get(target, key, receiver); } } catch (e) { ... } } }); } catch (e) { ... } } } });};
簡單講解一下:
- 可傳入 includes 和 excludes 參數,設置 watch 哪些代碼文件
- 用 chokidar 模塊 watch 代碼文件,如有改動,重新載入該文件代碼緩存
- 用 shimmer 模塊重載 Module.prototype._compile 方法,如果是要 watch 的代碼文件,則嘗試將導出的對象包裝成 Proxy 實例。獲取該模塊上的屬性時,將從 require.cache 中讀取最新值。
更多細節可以閱讀 proxy-hot-reload 源碼,只有 90 行代碼,如發現 bug 或提出改進,歡迎 PR。
使用示例:
user.js
module.exports = { id: 1, name: nswbmw}
app.js
use strict;if (process.env.NODE_ENV !== production) { require(proxy-hot-reload)({ includes: **/*.js });}const express = require(express);const app = express();const user = require(./user);app.get(/, function (req, res) { res.send(user);})app.listen(3000);
瀏覽器訪問 localhost:3000,查看結果,然後修改 user.js 中欄位的值,然後刷新瀏覽器查看結果。
proxy-hot-reload 缺點也比較明顯,比如:
- 通過 module.exports=1 這樣導出一個非 Object 的值則不會生效,目前只支持對象和數組
- 程序入口文件不會生效,比如上面的 app.js,改個埠號只能重啟才會生效
- 不要在 production 環境下使用
推薦閱讀:
※Node應用內存泄漏分析方法論與實戰
※狼叔的2017年總結
※如何分析 Node.js 中的內存泄漏
※node學習的第一步
