Nodejs 代码热更新实现
发布于 1 年前 作者 Goofo 6727 次浏览 来自 分享

实现原理: 利用node file system模块的watch接口监视文件夹的文件变更事件 事件触发后,移除require.cache内的对应的缓存 使用vm模块编译新加载的代码(基础检查语法,后续可在vm content内测试运行) 成功后 使用require加载代码,新代码就会缓存在 require.cache内 如果失败 恢复require.cache的缓存数据 代码如下:

const fs = require("fs");
const path = require("path");
const vm = require("vm");


const handlerMap = {};
const cacheMap = {};

/**
* 加载文件内的代码并监视更新热加载
*	[@return](/user/return) {Promise.<void>}
*/
const loadHandlers = async function () {
	/// 查看指定文件夹下的所有文件
 	const files = await new Promise((resolve, reject) => {
    	fs.readdir(path.join(__dirname, 'hots'), function (err, files) {
        	if (err) {
            	reject(err);
        	} else {
            	resolve(files);
        	}
   	 	});
 	});
	/// 遍历加载文件
	for (let f in files) {
   	 	if (/.*?\.js$/.test(files[f])) {
        	handlerMap[files[f]] = await loadHandler(path.join(__dirname, 'hots', files[f]));
    	}
	}
	/// 监视文件变动
	watchHandlers();
	};

/**
* 监视文件变动
*/
const watchHandlers = function () {
	console.log('watching ', path.join(__dirname, 'hots'));
	fs.watch(path.join(__dirname, 'hots'), {recursive: true}, function (eventType, filename) {
   	 	if (/.*?\.js$/.test(filename)) {
			/// 这里先删除旧的缓存代码 防止内存泄漏
			if( cacheMap[require.resolve(path.join(__dirname, 'hots', filename))] )
				delete cacheMap[require.resolve(path.join(__dirname, 'hots', filename))];
			/// 这里缓存现在运行的代码,热加载失败后恢复用,还有就是防止现有运行的代码异步没有返回就删除会因为逻辑可能没有执行完毕引起逻辑bug
        	cacheMap[require.resolve(path.join(__dirname, 'hots', filename))] = require.cache[require.resolve(path.join(__dirname, 'hots', filename))];
			///重置require.cache缓存
        	require.cache[require.resolve(path.join(__dirname, 'hots', filename))] = null;
			
        	loadHandler(path.join(__dirname, 'hots', filename)).then(function (data) {
            	if (data) {
                	handlerMap[filename] = data;
            	} else {
                	delete handlerMap[filename];
            	}
            	console.log("热更成功", filename, "当前代码", handlerMap);
        	}).catch(function (err) {
            	console.log("热更失败: 代码包含以下错误:", err, "当前代码:", handlerMap);
            	require.cache[require.resolve(path.join(__dirname, 'hots', filename))] = cacheMap[require.resolve(path.join(__dirname, 'hots', filename))];
            	cacheMap[require.resolve(path.join(__dirname, 'hots', filename))] = null;
        	});
    	}
	});
};

/**
* 加载文件
* [@param](/user/param) filename
* [@return](/user/return) {Promise.<*>}
*/
const loadHandler = async function (filename) {
	const exists = await new Promise(resolve => {
    	/// 查看代码文件是否存在
    	fs.access(filename, fs.constants.F_OK | fs.constants.R_OK, err => {
        	if (err) {
            	resolve(false);
        	} else {
            	resolve(true);
        	}
    	});
	});
	if (exists) {
    	return await new Promise((resolve, reject) => {
        	fs.readFile(filename, function (err, data) {
            	if (err) {
                	resolve(null);
            	} else {
                	try {
                    	/// 使用vm script编译热加载的代码
                    	new vm.Script(data);
                    	//const script = new vm.Script(data);
                    	// const context = new vm.createContext({
                    	//     require: require,
                    	//     module: {}
                    	// });
                    	// script.runInContext(context);
                	} catch (e) {
                    	/// 语法错误,编译失败
                    	reject(e);
                    	return;
                	}
                	/// 编译成功的代码
                	resolve(require(filename));
            	}
        	});
    	});
	} else {
    	/// 文件被删除
    	return null;
	}
};

loadHandlers().then(function () {
	console.log("run...");
	}).catch(function (e) {
	console.error(e);
});

注意: 使用这种方法被管理热更新的代码 不能使用全局的require的去单独加载,如果不想统一管理 去除handleMap变量缓存, 在每一处使用被管理的代码时 使用require获取代码(require会先从缓存内检查,然后才会去找文件加载)

8 回复

虽不明,但觉厉。

话说,lz 你的代码的 md 好像挂了啊,require vm 和之前的都不在代码高亮里面。

我想知道一下,不会内存泄露吗?(代码没仔细看

怎么处理reference 呢?

模块互相依赖的问题你没有处理,这也正是3楼说的引用的问题。你需要构建个模块的依赖树先

请问node代码热更新的使用场景是什么?

@steambap 已更正结构

@xadillax 我考虑的内存泄漏点 有两个 一个是require.cache我的处理是整个进程 对一个文件做一个记录的cache,就是全局的那个cacheMap对象,刚才又更严谨的加了一句 delete cacheMap[require.resolve(path.join(dirname, ‘hots’, filename))]; 还一个点是 用户正在执行的代码如果没有执行完毕(异步请求没有返回) 用户逻辑可能会造成内存泄漏, 我的想法是 用一个cache做缓存 并不直接删除热更新的代码所替换的老代码, 这样异步返回后会继续执行热更前的逻辑.

@yyrdl @royalrover 确实没考虑依赖关系,我的想法是 一般热更的代码都是松散的逻辑,我写这个代码就是基于网络消息路由后的处理代码,他们之间无耦合,就没有相互的引用.

回到顶部