Nodejs 代码热更新实现

实现原理:利用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.}
*/
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会先从缓存内检查,然后才会去找文件加载)

你可能感兴趣的:(node)