基于nodejs線上代碼熱部署原理與實(shí)現(xiàn)
背景
大家都知道,nodejs啟的后端服務(wù),如果有代碼變動(dòng),要重啟進(jìn)程,代碼才能生效。
nodejs的進(jìn)程在重啟的時(shí)候,用戶去訪問(wèn)服務(wù),就會(huì)出現(xiàn)短暫的 502 bad gateway
如果你的服務(wù)器加上了watch機(jī)制
當(dāng)服務(wù)器上的代碼頻繁發(fā)生變動(dòng),或者短時(shí)間內(nèi)發(fā)生高頻變動(dòng),那就會(huì)一直 502 bad gateway
近段時(shí)間在做線上服務(wù)編譯相關(guān)需求的時(shí)候,就出現(xiàn)了短時(shí)間內(nèi)線上服務(wù)代碼高頻變動(dòng),代碼功能模塊高頻更新,在不能重啟服務(wù)的情況下,讓更新的代碼生效的場(chǎng)景。
這就涉及到一個(gè)熱部署的概念,在不重啟服務(wù)的情況下,讓新部署的代碼生效。
接下來(lái)我來(lái)給大家講解熱部署的原理和實(shí)現(xiàn)方案
代碼沒(méi)法實(shí)時(shí)生效的原因
當(dāng)我們通過(guò)require('xx/xx.js')去加載一個(gè)功能模塊的時(shí)候,node會(huì)把require('xx/xx.js')得到的結(jié)果緩存在require.cache('xx/xx.js')中
當(dāng)我們多次調(diào)用require('xx/xx.js'),node就不再重新加載,而是直接從require.cache('xx/xx.js')讀取緩存
所以當(dāng)小伙伴在服務(wù)器上修改xx/xx.js這個(gè)路徑下的文件時(shí),node只會(huì)去讀取緩存,不會(huì)去加載小伙伴的最新代碼
源碼地址和使用
為了實(shí)現(xiàn)這個(gè)熱部署機(jī)制,在網(wǎng)上到處查資料,踩了好多坑才弄好
以下代碼是提煉出來(lái)、完整可運(yùn)行的熱部署基礎(chǔ)原理代碼,大家可以基于這個(gè)代碼去自行拓展:smart-node-reload(https://github.com/airuikun/smart-node-reload)
注意最新版本12版本的node運(yùn)行會(huì)報(bào)錯(cuò),官方對(duì)require.cache做了調(diào)整,已經(jīng)上報(bào)問(wèn)題給官方,建議使用nodejs版本:v10.5.0
git clone下來(lái)以后,無(wú)需安裝,直接運(yùn)行
????npm?start這時(shí)候就開(kāi)啟了熱部署變動(dòng)監(jiān)聽(tīng)
如何看到效果呢
小伙伴請(qǐng)看/hots/hot.js文件
const hot = 1????module.exports?=?hot
將第一行代碼改為const hot = 111
const hot = 111????module.exports?=?hot
這時(shí)候就能看到終端里監(jiān)聽(tīng)到代碼變動(dòng),然后動(dòng)態(tài)加載你的最新代碼并得到執(zhí)行結(jié)果,輸出為:
????熱部署文件:hot.js ,執(zhí)行結(jié)果:{?'hot.js':?111?}熱部署服務(wù)監(jiān)聽(tīng)到代碼變動(dòng),并重新加載了代碼,小伙伴就可以實(shí)時(shí)拿到最新代碼的執(zhí)行結(jié)果了,整個(gè)過(guò)程都在線上環(huán)境運(yùn)行,node進(jìn)程也沒(méi)有重啟
源碼解析
loadHandlers主函數(shù)
const handlerMap = {};// 緩存const hotsPath = path.join(__dirname, "hots");// 加載文件代碼 并 監(jiān)聽(tīng)指定文件夾目錄文件內(nèi)容變動(dòng)const loadHandlers = async () => {// 遍歷出指定文件夾下的所有文件const files = await new Promise((resolve, reject) => {fs.readdir(hotsPath, (err, files) => {if (err) {reject(err);} else {resolve(files);}});});// 初始化加載所有文件 把每個(gè)文件結(jié)果緩存到handlerMap變量當(dāng)中for (let f in files) {handlerMap[files[f]] = await loadHandler(path.join(hotsPath, files[f]));}// 監(jiān)聽(tīng)指定文件夾的文件內(nèi)容變動(dòng)await watchHandlers();};
loadHandlers是整個(gè)熱部署服務(wù)的主函數(shù),我們指定了服務(wù)器根目錄下的hots文件夾是用來(lái)監(jiān)聽(tīng)變動(dòng)和熱部署的文件夾
用fs.readdir掃描hots文件夾下的所有文件,通過(guò)loadHandler方法去加載和運(yùn)行每一個(gè)掃描到的文件,將結(jié)果緩存到handlerMap里
然后用watchHandlers方法開(kāi)啟文件變動(dòng)監(jiān)聽(tīng)
watchHandlers監(jiān)聽(tīng)文件變動(dòng)
// 監(jiān)視指定文件夾下的文件變動(dòng)const watchHandlers = async () => {// 這里建議用chokidar的npm包代替文件夾監(jiān)聽(tīng)fs.watch(hotsPath, { recursive: true }, async (eventType, filename) => {// 獲取到每個(gè)文件的絕對(duì)路徑// 包一層require.resolve的原因,拼接好路徑以后,它會(huì)主動(dòng)去幫你判斷這個(gè)路徑下的文件是否存在const targetFile = require.resolve(path.join(hotsPath, filename));// 當(dāng)你適應(yīng)require加載一個(gè)模塊后,模塊的數(shù)據(jù)就會(huì)緩存到require.cache中,下次再加載相同模塊,就會(huì)直接走require.cache// 所以我們熱加載部署,首要做的就是清除require.cache中對(duì)應(yīng)文件的緩存const cacheModule = require.cache[targetFile];// 去除掉在require.cache緩存中parent對(duì)當(dāng)前模塊的引用,否則會(huì)引起內(nèi)存泄露,具體解釋可以看下面的文章//《記錄一次由一行代碼引發(fā)的“血案”》https://cnodejs.org/topic/5aaba2dc19b2e3db18959e63//《一行 delete require.cache 引發(fā)的內(nèi)存泄漏血案》https://zhuanlan.zhihu.com/p/34702356if (cacheModule.parent) {cacheModule.parent.children.splice(cacheModule.parent.children.indexOf(cacheModule), 1);}// 清除指定路徑對(duì)應(yīng)模塊的require.cache緩存require.cache[targetFile] = null;// 重新加載發(fā)生變動(dòng)后的模塊文件,實(shí)現(xiàn)熱加載部署效果,并將重新加載后的結(jié)果,更新到handlerMap變量當(dāng)中const code = await loadHandler(targetFile)handlerMap[filename] = code;console.log("熱部署文件:", filename, ",執(zhí)行結(jié)果:", handlerMap);});};
watchHandlers函數(shù)是用來(lái)監(jiān)聽(tīng)指定文件夾下的文件變動(dòng)、清理緩存更新緩存用的。
用fs.watch原生函數(shù)監(jiān)聽(tīng)hots文件夾下文件變動(dòng),當(dāng)文件發(fā)生變動(dòng),就算出文件的絕對(duì)路徑targetFile
而require.cache[targetFile]就是require對(duì)targetFile原文件的緩存,清除緩存用require.cache[targetFile] = null;
坑爹的地方來(lái)了,僅僅只是將緩存置為null,會(huì)發(fā)生內(nèi)存泄露,我們還需要清除緩存父級(jí)的引用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方法來(lái)預(yù)編譯發(fā)生變化后的文件代碼,檢查語(yǔ)法錯(cuò)誤,提前發(fā)現(xiàn)是否存在語(yǔ)法錯(cuò)誤等報(bào)錯(cuò)new vm.Script(data);} catch (e) {// 語(yǔ)法錯(cuò)誤,編譯失敗reject(e);return;}// 編譯通過(guò)后,重新require加載最新的代碼resolve(require(filename));}});});};
loadHandler函數(shù)的作用是加載指定文件,并校驗(yàn)新文件代碼語(yǔ)法等。
通過(guò)fs.readFile讀取文件內(nèi)容
用node原生vm模塊vm.Script方法去預(yù)編譯發(fā)生變化后的文件代碼,檢查語(yǔ)法錯(cuò)誤,提前發(fā)現(xiàn)是否存在語(yǔ)法錯(cuò)誤等報(bào)錯(cuò)
檢驗(yàn)通過(guò)后,通過(guò)resolve(require(filename))方法重新將文件require加載,并自動(dòng)加入到require.cache緩存中
結(jié)尾:
以上就是熱部署的所有內(nèi)容了,代碼地址是:smart-node-reload(https://github.com/airuikun/smart-node-reload)
這個(gè)代碼是我經(jīng)過(guò)極簡(jiǎn)后的代碼,方便大家閱讀和理解,感興趣的小伙伴可以通過(guò)這個(gè)代碼去進(jìn)行深一步拓展
