搜尋
首頁web前端js教程Node.js巧妙實作網頁應用程式碼熱更新_node.js

背景

相信使用 Node.js 開發過 Web 應用的同學一定苦惱過新修改的程式碼必須要重啟 Node.js 進程後才能更新的問題。習慣使用 PHP 開發的同學更會非常的不適用,大呼果然還是我大PHP才是全世界最好的程式語言。手動重啟進程不僅僅是非常惱人的重複勞動,當應用程式規模稍大以後,啟動時間也逐漸開始不容忽視。

當然身為程式猿,無論使用哪種語言,都不會讓這樣的事情折磨自己。解決這類問題最直接、普適的手段就是監聽文件修改並重新啟動程序。這個方法也已經有很多成熟的解決方案提供了,例如已經被棄坑的 node-supervisor,以及現在比較火的 PM2 ,或者比較輕量級的 node-dev 等等均是這樣的思路。

本文則提供了另外一種思路,只需要很小的改造,就可以實現真正的0重啟熱更新程式碼,解決 Node.js 開發 Web 應用時惱人的程式碼更新問題。

整體思路

說起代碼熱更新,當下最有名的當屬Erlang 語言的熱更新功能,這門語言的特色在於高並發和分散式編程,主要的應用場景則是類似證券交易、遊戲服務端等領域。這些場景都或多或少要求服務擁有在運行中運維的手段,而程式碼熱更新就是其中非常重要的一環,因此我們可以先簡單的了解一下 Erlang 的做法。

由於我也沒有使用過 Erlang ,以下內容均為道聽途說,如果希望深入和準確的了解 Erlang 的代碼熱更新實現,最好還是查閱官方文檔。

Erlang 的程式碼載入由一個名為code_server的模組管理,除了啟動時的一些必要程式碼外,大部分的程式碼都是由code_server載入。
當code_server發現模組程式碼被更新後,會重新載入模組,之後的新請求會使用新模組執行,而原有還在執行的請求則繼續使用舊模組執行。
舊模組會在新模組載入後,被打上old標籤,新模組則是current標籤。當下一次熱更新的時候,Erlang 會掃描還在執行舊模組的進行並殺掉,然後繼續按照這個邏輯更新模組。
Erlang 中並非所有程式碼都允許熱更新,如 kernel, stdlib, compiler 等基礎模組預設是不允許更新的
我們可以發現 Node.js 中也有與code_server類似的模組,即 require 體系,因此 Erlang 的做法應該也可以在 Node.js 上做一些嘗試。透過了解 Erlang 的做法,我們可以大概的總結出在 Node.js 中解決程式碼熱更新的關鍵問題點

如何更新模組程式碼
如何使用新模組處理請求
如何釋放舊模組的資源

那麼接下來我們就逐一解析的這些問題點。

如何更新模組程式碼

要解決模組程式碼更新的問題,我們需要去閱讀 Node.js 的模組管理器實現,直接上連結 module.js。透過簡單的閱讀,我們可以發現核心的程式碼就在於 Module._load ,稍微精簡一下程式碼貼出來。

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
// filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
Module._load = function(request, parent, isMain) {
 var filename = Module._resolveFilename(request, parent);

 var cachedModule = Module._cache[filename];
 if (cachedModule) {
 return cachedModule.exports;
 }

 var module = new Module(filename, parent);
 Module._cache[filename] = module;
 module.load(filename);

 return module.exports;
};

require.cache = Module._cache;

可以發現其中的核心就是 Module._cache ,只要清除了這個模組緩存,下一次 require 的時候,模組管理器就會重新載入最新的程式碼了。

寫一個小程式驗證一下

// main.js
function cleanCache (module) {
 var path = require.resolve(module);
 require.cache[path] = null;
}

setInterval(function () {
 cleanCache('./code.js');
 var code = require('./code.js');
 console.log(code);
}, 5000);
// code.js
module.exports = 'hello world';

我們執行 main.js ,同時取修改 code.js 的內容,就可以發現控制台中,我們程式碼成功的更新為了最新的程式碼。

Node.js巧妙實作網頁應用程式碼熱更新_node.js

那麼模組管理器更新程式碼的問題已經解決了,接下來再看看在 Web 應用中,我們如何讓新的模組可以實際執行。

如何使用新模組處理請求

為了更符合大家的使用習慣,我們就直接以 Express 為例來展開這個問題,實際上使用類似的思路,絕大部分 Web應用 均可適用。

首先,如果我們的服務是像 Express 的 DEMO 一樣所有的程式碼都在同一模組內的話,我們是無法針對模組進行熱載入的

var express = require('express');
var app = express();

app.get('/', function(req, res){
 res.send('hello world');
});

app.listen(3000);

要實現熱加載,和 Erlang 中不允許的基礎庫一樣,我們需要一些無法進行熱更新的基礎程式碼控制更新流程。而且類似 app.listen 這類操作如果重新執行了,那麼和重啟 Node.js 進程也沒太大的差別了。因此我們需要一些巧妙的程式碼將頻繁更新的業務程式碼與不頻繁更新的基礎程式碼隔離。

// app.js 基础代码
var express = require('express');
var app = express();
var router = require('./router.js');

app.use(router);

app.listen(3000);
// router.js 业务代码
var express = require('express');
var router = express .Router();

// 此处加载的中间件也可以自动更新
router.use(express.static('public'));

router.get('/', function(req, res){
 res.send('hello world');
});

module.exports = router;

然而很遗憾,经过这样处理之后,虽然成功的分离了核心代码, router.js 依然无法进行热更新。首先,由于缺乏对更新的触发机制,服务无法知道应该何时去更新模块。其次, app.use 操作会一直保存老的 router.js 模块,因此即使模块被更新了,请求依然会使用老模块处理而非新模块。

那么继续改进一下,我们需要对 app.js 稍作调整,启动文件监听作为触发机制,并且通过闭包来解决 app.use 的缓存问题

// app.js
var express = require('express');
var fs = require('fs');
var app = express();

var router = require('./router.js');

app.use(function (req, res, next) {
 // 利用闭包的特性获取最新的router对象,避免app.use缓存router对象
 router(req, res, next);
});

app.listen(3000);

// 监听文件修改重新加载代码
fs.watch(require.resolve('./router.js'), function () {
 cleanCache(require.resolve('./router.js'));
 try {
  router = require('./router.js');
 } catch (ex) {
  console.error('module update failed');
 }
});

function cleanCache(modulePath) {
 require.cache[modulePath] = null;
}

再试着修改一下 router.js 就会发现我们的代码热更新已经初具雏形了,新的请求会使用最新的 router.js 代码。除了修改 router.js 的返回内容外,还可以试试看修改路由功能,也会如预期一样进行更新。

当然,要实现一个完善的热更新方案需要更多结合自身方案做一些改进。首先,在中间件的使用上,我们可以在 app.use 处声明一些不需要热更新或者说每次更新不希望重复执行的中间件,而在 router.use 处则可以声明一些希望可以灵活修改的中间件。其次,文件监听不能仅监听路由文件,而是要监听所有需要热更新的文件。除了文件监听这种手段外,还可以结合编辑器的扩展功能,在保存时向 Node.js 进程发送信号或者访问一个特定的 URL 等方式来触发更新。

如何释放老模块的资源

要解释清楚老模块的资源如何释放的问题,实际上需要先了解 Node.js 的内存回收机制,本文中并不准备详加描述,解释 Node.js 的内存回收机制的文章和书籍很多,感兴趣的同学可以自行扩展阅读。简单的总结一下就是当一个对象没有被任何对象引用的时候,这个对象就会被标记为可回收,并会在下一次GC处理的时候释放内存。

那么我们的课题就是,如何让老模块的代码更新后,确保没有对象保持了模块的引用。首先我们以 如何更新模块代码 一节中的代码为例,看看老模块资源不回收会出现什么问题。为了让结果更显著,我们修改一下 code.js

// code.js
var array = [];

for (var i = 0; i < 10000; i++) {
 array.push('mem_leak_when_require_cache_clean_test_item_' + i);
}

module.exports = array;
// app.js
function cleanCache (module) {
 var path = require.resolve(module);
 require.cache[path] = null;
}

setInterval(function () {
 var code = require('./code.js');
 cleanCache('./code.js');
}, 10);

好~我们用了一个非常笨拙但是有效的方法,提高了 router.js 模块的内存占用,那么再次启动 main.js 后,就会发现内存出现显著的飙升,不到一会 Node.js 就提示 process out of memory。然而实际上从 app.js 与 router.js 的代码中观察的话,我们并没发现哪里保存了旧模块的引用。

我们借助一些 profile 工具如 node-heapdump 就可以很快的定位到问题所在,在 module.js 中我们发现 Node.js 会自动为所有模块添加一个引用

function Module(id, parent) {
 this.id = id;
 this.exports = {};
 this.parent = parent;
 if (parent && parent.children) {
 parent.children.push(this);
 }

 this.filename = null;
 this.loaded = false;
 this.children = [];
}

因此相应的,我们可以调整一下cleanCache函数,将这个引用在模块更新的时候一并去除。

// app.js
function cleanCache(modulePath) {
 var module = require.cache[modulePath];
 // remove reference in module.parent
 if (module.parent) {
  module.parent.children.splice(module.parent.children.indexOf(module), 1);
 }
 require.cache[modulePath] = null;
}

setInterval(function () {
 var code = require('./code.js');
 cleanCache(require.resolve('./code.js'));
}, 10); 

再执行一下,这次好多了,内存只会有轻微的增长,说明老模块占用的资源已经正确的释放掉了。

使用了新的 cleanCache 函数后,常规的使用就没有问题,然而并非就可以高枕无忧了。在 Node.js 中,除了 require 系统会添加引用外,通过 EventEmitter 进行事件监听也是大家常用的功能,并且 EventEmitter 有非常大的嫌疑会出现模块间的互相引用。那么 EventEmitter 能否正确的释放资源呢?答案是肯定的。

// code.js
var moduleA = require('events').EventEmitter();

moduleA.on('whatever', function () {
});

當 code.js 模組被更新,並且所有引用被移出後,只要 moduleA 沒有被其他未釋放的模組引用, moduleA 也會自動釋放,包括我們在其內部的事件監聽。

只有一種畸形的EventEmitter 應用場景在這套體系下無法應對,也就是code.js 每次執行的時候都會去監聽一個全域物件的事件,這樣會造成全域物件上不停的掛載事件,同時Node.js 會很快的提示偵測到過多的事件綁定,疑似記憶體外洩。

至此,可以看到只要處理好了require 系統中Node.js 為我們自動添加的引用,老模組的資源回收並不是大問題,雖然我們無法做到像Erlang 一樣實現下一次熱更新對還留存的老模組進行掃描這樣細粒度的控制,但是我們可以透過合理的規避手段,解決舊模組資源釋放的問題。

在Web 應用下,還有一個引用問題就是未釋放的模組或核心模組對需要熱更新的模組有引用,如app.use,導致舊模組的資源無法釋放,並且新的請求無法正確的使用新模組進行處理。解決這個問題的手段是控制全域變數或引用的暴露的入口,在熱更新執行的過程中手動更新入口。如 如何使用新模組處理請求 中對 router 的封裝就是一個例子,透過這一個入口的控制,我們在 router.js 中無論如何引用其他模組,都會隨著入口的釋放而釋放。

另一個會造成資源釋放問題的就是類似 setInterval 這類操作,會保持物件的生命週期無法釋放,不過在 Web 應用中我們極少會使用這類技術,因此方案中並未註意。

尾聲

至此,我們就解決了Node.js 在Web 應用下程式碼熱更新的三大問題,不過由於Node.js 本身缺乏對有效的留存物件的掃描機制,因此並不能100%的消除類似setInterval 導致的老模組的資源無法釋放的問題。也是由於這樣的局限性,目前我們提供的 YOG2 框架中,主要還是將此技術應用於開發調試期,透過熱更新實現快速開發。而生產環境的程式碼更新仍是使用重啟或 PM2 的 hot reload 功能來確保線上服務的穩定性。

由於熱更新實際上與框架和業務架構緊密相關,因此本文並未給出一個通用的解決方案。作為參考,簡單的介紹一下在 YOG2 框架中我們是如何使用這項技術的。由於 YOG2 框架本身就支援前後端子系統 App 拆分,因此我們的更新策略是以 App 為粒度更新程式碼。同時由於類似fs.watch 這類操作會有相容性問題,有些替代方案如fs.watchFile 則會比較消耗效能,因此我們結合了YOG2 的測試機部署功能,透過上傳部署新程式碼的形式告知框架需要更新App 程式碼。在以 App 為粒度更新模組快取的同時,會更新路由快取與範本緩存,來完成所有程式碼的更新工作。

如果你使用的是類似 Express 或 Koa 這類框架,只需要按照文中的方法結合自身業務需要,對主路由進行一些改造,就可以很好的應用這項技術。

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
在JavaScript中替換字符串字符在JavaScript中替換字符串字符Mar 11, 2025 am 12:07 AM

JavaScript字符串替換方法詳解及常見問題解答 本文將探討兩種在JavaScript中替換字符串字符的方法:在JavaScript代碼內部替換和在網頁HTML內部替換。 在JavaScript代碼內部替換字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 該方法僅替換第一個匹配項。要替換所有匹配項,需使用正則表達式並添加全局標誌g: str = str.replace(/fi

構建您自己的Ajax Web應用程序構建您自己的Ajax Web應用程序Mar 09, 2025 am 12:11 AM

因此,在這裡,您準備好了解所有稱為Ajax的東西。但是,到底是什麼? AJAX一詞是指用於創建動態,交互式Web內容的一系列寬鬆的技術。 Ajax一詞,最初由Jesse J創造

10個JQuery Fun and Games插件10個JQuery Fun and Games插件Mar 08, 2025 am 12:42 AM

10款趣味橫生的jQuery遊戲插件,讓您的網站更具吸引力,提升用戶粘性!雖然Flash仍然是開發休閒網頁遊戲的最佳軟件,但jQuery也能創造出令人驚喜的效果,雖然無法與純動作Flash遊戲媲美,但在某些情況下,您也能在瀏覽器中獲得意想不到的樂趣。 jQuery井字棋遊戲 遊戲編程的“Hello world”,現在有了jQuery版本。 源碼 jQuery瘋狂填詞遊戲 這是一個填空遊戲,由於不知道單詞的上下文,可能會產生一些古怪的結果。 源碼 jQuery掃雷遊戲

jQuery視差教程 - 動畫標題背景jQuery視差教程 - 動畫標題背景Mar 08, 2025 am 12:39 AM

本教程演示瞭如何使用jQuery創建迷人的視差背景效果。 我們將構建一個帶有分層圖像的標題橫幅,從而創造出令人驚嘆的視覺深度。 更新的插件可與JQuery 1.6.4及更高版本一起使用。 下載

如何創建和發布自己的JavaScript庫?如何創建和發布自己的JavaScript庫?Mar 18, 2025 pm 03:12 PM

文章討論了創建,發布和維護JavaScript庫,專注於計劃,開發,測試,文檔和促銷策略。

如何在瀏覽器中優化JavaScript代碼以進行性能?如何在瀏覽器中優化JavaScript代碼以進行性能?Mar 18, 2025 pm 03:14 PM

本文討論了在瀏覽器中優化JavaScript性能的策略,重點是減少執行時間並最大程度地減少對頁面負載速度的影響。

使用jQuery和Ajax自動刷新DIV內容使用jQuery和Ajax自動刷新DIV內容Mar 08, 2025 am 12:58 AM

本文演示瞭如何使用jQuery和ajax自動每5秒自動刷新DIV的內容。 該示例從RSS提要中獲取並顯示了最新的博客文章以及最後的刷新時間戳。 加載圖像是選擇

Matter.js入門:簡介Matter.js入門:簡介Mar 08, 2025 am 12:53 AM

Matter.js是一個用JavaScript編寫的2D剛體物理引擎。此庫可以幫助您輕鬆地在瀏覽器中模擬2D物理。它提供了許多功能,例如創建剛體並為其分配質量、面積或密度等物理屬性的能力。您還可以模擬不同類型的碰撞和力,例如重力摩擦力。 Matter.js支持所有主流瀏覽器。此外,它也適用於移動設備,因為它可以檢測觸摸並具有響應能力。所有這些功能都使其值得您投入時間學習如何使用該引擎,因為這樣您就可以輕鬆創建基於物理的2D遊戲或模擬。在本教程中,我將介紹此庫的基礎知識,包括其安裝和用法,並提供一

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

SublimeText3 英文版

SublimeText3 英文版

推薦:為Win版本,支援程式碼提示!

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具