本文做者:第一名的小蝌蚪javascript
導語 一個讓你不用重啓nodejs線上服務,就能使新更新代碼生效的熱部署方案java
你們都知道,nodejs啓的後端服務,若是有代碼變更,要重啓進程,代碼才能生效。node
nodejs的進程在重啓的時候,用戶去訪問服務,就會出現短暫的 502 bad gateway
git
若是你的服務器加上了watch機制github
當服務器上的代碼頻繁發生變更,或者短期內發生高頻變更,那就會一直 502 bad gateway
npm
近段時間在作線上服務編譯相關需求的時候,就出現了短期內線上服務代碼高頻變更,代碼功能模塊高頻更新,在不能重啓服務的狀況下,讓更新的代碼生效的場景。後端
這就涉及到一個熱部署的概念,在不重啓服務的狀況下,讓新部署的代碼生效。緩存
接下來我來給你們講解熱部署的原理和實現方案服務器
當咱們經過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進程也沒有重啓
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
方法開啓文件變更監聽
// 監視指定文件夾下的文件變更
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]
就是require
對targetFile
原文件的緩存,清除緩存用require.cache[targetFile] = null;
坑爹的地方來了,僅僅只是將緩存置爲null,會發生內存泄露,咱們還須要清除緩存父級的引用require.cache[targetFile].parent
,就是下面這段代碼
if (cacheModule.parent) {
cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);
}
複製代碼
// 加載指定文件的代碼
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
這個代碼是我通過極簡後的代碼,方便你們閱讀和理解,感興趣的小夥伴能夠經過這個代碼去進行深一步拓展