剛思考這個話題的時候,首先想到的是 Vue 或 React 的組件熱更新(基於 Webpack HMR),後來又想到了 Lua、Erlang 等語言的熱更新,不過在實際開發 Node.js 後臺時,使用 remy/nodemon 之類的熱重啓(偵測代碼改動重啓程序)工具也夠用,因而 Node.js 的熱更新(替換模塊,無須重啓)的驗證就一直擱置。node
直到最近在使用「微信機器人」)(Node.js) 時,遇到了強烈的需求。這類機器人程序就是:啓動了一個網頁,登陸 Web 微信,經過抓取識別頁面中的元素得到一些狀態信息,如:消息、好友請求等等,因爲它的啓動時間也比較長,若是每次修改業務代碼後都要重啓,那麼等待程序啓動就要消耗很多時間,致使開發體驗不好,因而實踐 Node.js 的熱更新就迫在眉睫了。webpack
如下是機器人的核心用法:git
robot = new Robot() robot.addEventListener('msg', ...) robot.removeEventListener('msg', ...)
那麼咱們的目標:增/刪/改 業務邏輯(事件處理器)的時候程序無須重啓,自動熱更新業務邏輯代碼,從而提升開發效率。github
從 Webpack Wiki hot module replacement · webpack/docs Wiki 瞭解到,Webpack 能知道「哪一個模塊須要熱更新」,並提供一些鉤子,另外 webpack 自有一套模塊管理,可以管理替換模塊,讓你訪問的是熱更新以後的模塊。另外,要實現熱加載的不只要知足「再次加載」,還要考慮如何清空相關的「持久資源」。web
因此說,若是基於 webpack HMR 來實現的話,須要完成幾件事情:segmentfault
把事件處理器的代碼模塊化,便於 webpack 管理。緩存
自動加載全部處理器模塊微信
某個事件處理模塊更新後須要拿到老的模塊,用來移除老的監聽處理器。ide
要知道文件的增長和刪除,而且拿到模塊內容。模塊化
簡單地把每一個事件處理器定義爲一個文件 *.biz.js
:
// msg.biz.js module.exports = { evt: 'msg', fn() { console.log('msg hanlder....') } };
其中 evt
是事件名, fn
是處理器,因而加載一個業務模塊後就能拿到事件名稱和處理器。
(可能不知足實際要求,先簡單驗證熱更新是否可行哈!)
咱們約定,業務模塊 *.biz.js
都放在 /biz
目錄下,該目錄下的 index.js
會加載全部業務模塊,而 main.js
就只需加載 /biz/index.js
src |--- /biz |--- a.biz.js |--- b.biz.js |--- index.js |--- main.js
藉助 webpack 的 require-context 加載全部 *.biz.js
模塊,避免手寫 require:
// index.js // 加載當前目錄下全部 `*.biz.js` const requireContext = require.context('./', true, /\.biz.js/); // 此時 requireContext.keys() 爲 ['./a.biz.js', './b.biz.js'] requireContext.keys().forEach(key => { const module = requireContext(key); // 至關於 module = require('./biz/a.biz.js') // 因而拿到事件名和處理器,而後進行事件監聽 // robot.addEventListener(module.evt, module.fn) });
參考 Wiki 的例子 Example 3,知道 require.context 如何使用熱更新機制
// index.js // 啓動 webpack HRM 時則 module.hot 爲 true if (module.hot) { // 表示該 context 下的模塊都要檢測更新 module.hot.accept(requireContext.id, () => { const requireContext = require.context('./', true, /\.biz.js/); requireContext.keys().forEach(key => { const newModule = requireContext(key); // 前面首次自動加載全部模塊後,記錄到 oldModules 對象(<key,module>) // 若是模塊內容不同,則表示要做熱更新處理了 if (oldModules[key] !== newModule) { // ... 對老模塊 oldModules[key] 移除事件監聽 // ... 對新模塊 newModule 註冊事件監聽 // 同時更新緩存記錄 oldModules[key] = newModule; } }); }); }
到了這一步,修改任何 *.biz.js
的代碼都能自動熱更新了。
上面的代碼已經不當心實現了 「增長文件後熱更新」,由於 module.hot.accept(requireContext.id
表示檢測 ./biz/*.biz.js
的更新,若是增長一個 c.biz.js
,那麼 requireContext.keys()
就變成 [ ..., './c.biz.js']
,因而新模塊不等於老模塊(不存在),從而使用 c.biz.js
註冊事件監聽器。
對於刪除文件後的熱更新,則在上面代碼基礎上增長:
if (module.hot) { module.hot.accept(requireContext.id, () => { // 在從新加載目錄下的全部模塊前,對老記錄做個副本 const oldKeysRetain = {}; Object.keys(oldModules) .forEach(k => (oldKeysRetain[k] = true)); const requireContext = require.context('./', true, /\.biz.js/); requireContext.keys().forEach(key => { // 若是某模塊存在當前目錄,則從臨時記錄中抹去 delete oldKeysRetain[key]; const newModule = requireContext(key); if (oldModules[key] !== newModule) { ... } }); // 未抹去的部分,意味着不存在當前目錄下了,也就是被刪除了 Object.keys(oldKeysRetain).forEach(key => { // ... 對老模塊移除事件監聽 delete oldModules[key]; }); }); }
通過以上四步,算是初步驗證了,藉助 Webpack 來玩是能夠的,固然咱們做了很多嚴格約定,不過不影響這一階段的思路。
上面一種思路存在一些問題
業務代碼的格式限制太死,不夠靈活
在生產階段也耦合了 webpack
因而我想,約定業務代碼格式是爲了方便經過模塊管理事件的註冊和移除,假如說在不侵入代碼,不做任何約定的狀況下,也能知道某個模塊註冊了哪些事件,是否是就不需約定了,好像是的:
//## a.biz.js 不約定業務代碼格式 robot.addLisenter('msg', ...) //## 入口.js robot = new Robot(); _add = robot.addLisenter robot.addLisenter = () => { // 攔截註冊事件方法 // 從而記錄下 a.biz 模塊都註冊了哪些事件處理器 } require('a.biz') robot.addLisenter = _add
可是問題來了,咱們的目標包括「自動加載全部業務模塊,增刪文件都能熱更新」,那麼在開發階段咱們仍是藉助 webpack 的 require.context 方法,而且約定每一個業務模塊的入口文件命名爲 *.biz.js
,至於裏面代碼怎麼寫就隨意了,而在生產階段能夠遍歷文件找到全部 *.biz.js
進行加載,無須依賴 webpack。
剩下的大部分思路跟 #思路一 相似,代碼可參考 zhenyong/webpack-hot-nodejs-demo: Webpack HMR demo use in Node.js, showing how to auto add/remove listeners.
最開始寫這篇文章是想深扒一下 Node.js 的模塊管理和緩存結構,而後驗證一下經過清除模塊緩存來作熱更新是否可行,後來感受 webpack 給咱們做了不少工做,因而就先用 webpack 玩了一輪,看來擇日還得再寫一篇(二)了
熱更新的主要目的是爲了提升開發效率,並非爲了在生產上玩熱更新,畢竟還有不少潛在問題,例如,模塊中涉及全局狀態或者單例資源,經過熱更新可能會引發混亂......