Node.js 熱更新(一)

背景

剛思考這個話題的時候,首先想到的是 VueReact 的組件熱更新(基於 Webpack HMR),後來又想到了 LuaErlang 等語言的熱更新,不過在實際開發 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 驗證可行

從 Webpack Wiki hot module replacement · webpack/docs Wiki 瞭解到,Webpack 能知道「哪一個模塊須要熱更新」,並提供一些鉤子,另外 webpack 自有一套模塊管理,可以管理替換模塊,讓你訪問的是熱更新以後的模塊。另外,要實現熱加載的不只要知足「再次加載」,還要考慮如何清空相關的「持久資源」。web

因此說,若是基於 webpack HMR 來實現的話,須要完成幾件事情:segmentfault

  1. 把事件處理器的代碼模塊化,便於 webpack 管理。緩存

  2. 自動加載全部處理器模塊微信

  3. 某個事件處理模塊更新後須要拿到老的模塊,用來移除老的監聽處理器。ide

  4. 要知道文件的增長和刪除,而且拿到模塊內容。模塊化

1. 業務代碼模塊化

簡單地把每一個事件處理器定義爲一個文件 *.biz.js

// msg.biz.js
module.exports = {
    evt: 'msg',
    fn() {
        console.log('msg hanlder....')
    }
};

其中 evt 是事件名, fn 是處理器,因而加載一個業務模塊後就能拿到事件名稱和處理器。
(可能不知足實際要求,先簡單驗證熱更新是否可行哈!)

2. 自動加載

咱們約定,業務模塊 *.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)
    
});

3. 修改後熱更新

參考 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 的代碼都能自動熱更新了。

4. 增刪文件後熱更新

上面的代碼已經不當心實現了 「增長文件後熱更新」,由於 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 來玩是能夠的,固然咱們做了很多嚴格約定,不過不影響這一階段的思路。

完整代碼請移步:zhenyong/webpack-hot-nodejs-demo: Webpack HMR demo use in Node.js, showing how to auto add/remove listeners.

思路二:基於 Webpack 進階

上面一種思路存在一些問題

  1. 業務代碼的格式限制太死,不夠靈活

  2. 在生產階段也耦合了 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 玩了一輪,看來擇日還得再寫一篇(二)了

問題

熱更新的主要目的是爲了提升開發效率,並非爲了在生產上玩熱更新,畢竟還有不少潛在問題,例如,模塊中涉及全局狀態或者單例資源,經過熱更新可能會引發混亂......

參考

相關文章
相關標籤/搜索