基於nodejs線上代碼熱部署原理與實現

本文做者:第一名的小蝌蚪javascript

導語 一個讓你不用重啓nodejs線上服務,就能使新更新代碼生效的熱部署方案java

背景

你們都知道,nodejs啓的後端服務,若是有代碼變更,要重啓進程,代碼才能生效。node

nodejs的進程在重啓的時候,用戶去訪問服務,就會出現短暫的 502 bad gatewaygit

若是你的服務器加上了watch機制github

當服務器上的代碼頻繁發生變更,或者短期內發生高頻變更,那就會一直 502 bad gatewaynpm

近段時間在作線上服務編譯相關需求的時候,就出現了短期內線上服務代碼高頻變更,代碼功能模塊高頻更新,在不能重啓服務的狀況下,讓更新的代碼生效的場景。後端

這就涉及到一個熱部署的概念,在不重啓服務的狀況下,讓新部署的代碼生效。緩存

接下來我來給你們講解熱部署的原理和實現方案服務器

代碼無法實時生效的緣由

當咱們經過require('xx/xx.js')去加載一個功能模塊的時候,node會把require('xx/xx.js')獲得的結果緩存在require.cache('xx/xx.js')async

當咱們屢次調用require('xx/xx.js'),node就再也不從新加載,而是直接從require.cache('xx/xx.js')讀取緩存

因此當小夥伴在服務器上修改xx/xx.js這個路徑下的文件時,node只會去讀取緩存,不會去加載小夥伴的最新代碼

源碼地址和使用

爲了實現這個熱部署機制,在網上處處查資料,踩了好多坑才弄好

如下代碼是提煉出來、完整可運行的熱部署基礎原理代碼,你們能夠基於這個代碼去自行拓展:smart-node-reload

注意最新版本12版本的node運行會報錯,官方對require.cache作了調整,已經上報問題給官方,建議使用nodejs版本:v10.5.0

git clone下來之後,無需安裝,直接運行

npm start
複製代碼

這時候就開啓了熱部署變更監聽

如何看到效果呢

小夥伴請看/hots/hot.js文件

const hot = 1
    module.exports = hot
複製代碼

將第一行代碼改成const hot = 111

const hot = 111
    module.exports = hot
複製代碼

這時候就能看到終端裏監聽到代碼變更,而後動態加載你的最新代碼並獲得執行結果,輸出爲:

熱部署文件: hot.js ,執行結果: { 'hot.js': 111 }
複製代碼

熱部署服務監聽到代碼變更,並從新加載了代碼,小夥伴就能夠實時拿到最新代碼的執行結果了,整個過程都在線上環境運行,node進程也沒有重啓

源碼解析

loadHandlers主函數

const handlerMap = {};// 緩存
const hotsPath = path.join(__dirname, "hots");

// 加載文件代碼 並 監聽指定文件夾目錄文件內容變更
const loadHandlers = async () => {
  // 遍歷出指定文件夾下的全部文件
  const files = await new Promise((resolve, reject) => {
    fs.readdir(hotsPath, (err, files) => {
      if (err) {
        reject(err);
      } else {
        resolve(files);
      }
    });
  });
  // 初始化加載全部文件 把每一個文件結果緩存到handlerMap變量當中
  for (let f in files) {
    handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f]));
  }

  // 監聽指定文件夾的文件內容變更
  await watchHandlers();
};

複製代碼

loadHandlers是整個熱部署服務的主函數,咱們指定了服務器根目錄下的hots文件夾是用來監聽變更和熱部署的文件夾

fs.readdir掃描hots文件夾下的全部文件,經過loadHandler方法去加載和運行每個掃描到的文件,將結果緩存到handlerMap

而後用watchHandlers方法開啓文件變更監聽

watchHandlers監聽文件變更

// 監視指定文件夾下的文件變更
const watchHandlers = async () => {
  // 這裏建議用chokidar的npm包代替文件夾監聽
  fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => {
    // 獲取到每一個文件的絕對路徑
    // 包一層require.resolve的緣由,拼接好路徑之後,它會主動去幫你判斷這個路徑下的文件是否存在
    const targetFile = require.resolve(path.join(hotsPath, filename));
    // 當你適應require加載一個模塊後,模塊的數據就會緩存到require.cache中,下次再加載相同模塊,就會直接走require.cache
    // 因此咱們熱加載部署,首要作的就是清除require.cache中對應文件的緩存
    const cacheModule = require.cache[targetFile];
    // 去除掉在require.cache緩存中parent對當前模塊的引用,不然會引發內存泄露,具體解釋能夠看下面的文章
	//《記錄一次由一行代碼引起的「血案」》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63
	//《一行 delete require.cache 引起的內存泄漏血案》https://zhuanlan.zhihu.com/p/34702356
    if (cacheModule.parent) {
        cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
    }
    // 清除指定路徑對應模塊的require.cache緩存
    require.cache[targetFile] = null;

    // 從新加載發生變更後的模塊文件,實現熱加載部署效果,並將從新加載後的結果,更新到handlerMap變量當中
    const code = await loadHandler(targetFile)
    handlerMap[filename] = code;
    console.log("熱部署文件:", filename, ",執行結果:", handlerMap);
  });
};

複製代碼

watchHandlers函數是用來監聽指定文件夾下的文件變更、清理緩存更新緩存用的。

fs.watch原生函數監聽hots文件夾下文件變更,當文件發生變更,就算出文件的絕對路徑targetFile

require.cache[targetFile]就是requiretargetFile原文件的緩存,清除緩存用require.cache[targetFile] = null;

坑爹的地方來了,僅僅只是將緩存置爲null,會發生內存泄露,咱們還須要清除緩存父級的引用require.cache[targetFile].parent,就是下面這段代碼

if (cacheModule.parent) {
        cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
    }
複製代碼

loadHandler加載文件

// 加載指定文件的代碼
const loadHandler = filename => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if (err) {
        resolve(null);
      } else {
        try {
          // 使用vm模塊的Script方法來預編譯發生變化後的文件代碼,檢查語法錯誤,提早發現是否存在語法錯誤等報錯
          new vm.Script(data);
        } catch (e) {
          // 語法錯誤,編譯失敗
          reject(e);
          return;
        }
        // 編譯經過後,從新require加載最新的代碼
        resolve(require(filename));
      }
    });
  });
};

複製代碼

loadHandler函數的做用是加載指定文件,並校驗新文件代碼語法等。

經過fs.readFile讀取文件內容

用node原生vm模塊vm.Script方法去預編譯發生變化後的文件代碼,檢查語法錯誤,提早發現是否存在語法錯誤等報錯

檢驗經過後,經過resolve(require(filename))方法從新將文件require加載,並自動加入到require.cache緩存中

結尾:

以上就是熱部署的全部內容了,代碼地址是:smart-node-reload

這個代碼是我通過極簡後的代碼,方便你們閱讀和理解,感興趣的小夥伴能夠經過這個代碼去進行深一步拓展

相關文章
相關標籤/搜索