Nodejsコードホットアップデート実装

44707 ワード

実現原理: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はキャッシュ内からチェックしてからファイル・ロードを探します).