ReactNative-HMR原理探索
ReactNative-HMR原理探索前言在开始本文前,先简单说下我们在开发RN项目中,本地的node服务究竟扮演的是什么样的角色。在我们的RN APP中有配置本地开发的地方,只要我们输入我们本地的IP和端口号8081就可以开始调试本地代码,其实质是APP发起了一个请求bundle文件的HTTP请求,而我们的node server在接到request后,开始对本地项目文件进行babel,pack,最后返回一个bundle.js。而本地的node服务扮演的角色还不止如此,比如启动基础服务dev tool,HMR等 什么是HMRHMR(Hot Module Replacement)模块热替换,可以类比成Webpack的Hot Reload。可以让你在代码变动后不用reload app,代码直接生效,且当前路由栈不会发生改变 名词说明
实现原理先贴上个人整理的的一个HMR热更新的过程 启动Packerage&HMR serverrun packager server# react-native/local-cli/server/runServer.js const serverInstance = http.createServer(app).listen( args.port,args.host,() => { attachHMRServer({ httpServer: serverInstance,path: '/hot',packagerServer,}); wsProxy = webSocketProxy.attachToServer(serverInstance,'/debugger-proxy'); ms = messageSocket.attachToServer(serverInstance,'/message'); webSocketProxy.attachToServer(serverInstance,'/devtools'); readyCallback(); } ); 本地启动在8081启动HTTP服务的同时,也初始化了本地HMR的服务,这里在初始化的时候注入了packagerServer,为的是能订阅packagerServer提供的watchman回调,同时也为了能拿到packagerServer提供的getDependencies方法,这样能在HMR内部拿到文件的依赖关系(相互require的关系) #react-native/local-cli/server/util/attachHMRServer.js // 略微简化下代码 function attachHMRServer({httpServer,path,packagerServer}) { ... const WebSocketServer = require('ws').Server; const wss = new WebSocketServer({ server: httpServer,path: path,}); wss.on('connection',ws => { ... getDependencies(params.platform,params.bundleEntry) .then((arg) => { client = { ... }; packagerServer.setHMRFileChangeListener((filename,stat) => { ... client.ws.send(JSON.stringify({type: 'update-start'})); stat.then(() => { return packagerServer.getShallowDependencies({ entryFile: filename,platform: client.platform,dev: true,hot: true,}) .then(deps => { if (!client) { return []; } const oldDependencies = client.shallowDependencies[filename]; // 分析当前文件的require关系是否与之前一致,如果require关系有变动,需要重新对文件的dependence进行分析 if (arrayEquals(deps,oldDependencies)) { return packagerServer.getDependencies({ platform: client.platform,entryFile: filename,recursive: true,}).then(response => { const module = packagerServer.getModuleForPath(filename); return response.copy({dependencies: [module]}); }); } return getDependencies(client.platform,client.bundleEntry) .then(({ dependenciesCache: depsCache,dependenciesModulesCache: depsModulesCache,shallowDependencies: shallowDeps,inverseDependenciesCache: inverseDepsCache,resolutionResponse,}) => { if (!client) { return {}; } return packagerServer.buildBundleForHMR({ entryFile: client.bundleEntry,},packagerHost,httpServerAddress.port); }) .then(bundle => { if (!client || !bundle || bundle.isEmpty()) { return; } return JSON.stringify({ type: 'update',body: { modules: bundle.getModulesIdsAndCode(),inverseDependencies: client.inverseDependenciesCache,sourceURLs: bundle.getSourceURLs(),sourceMappingURLs: bundle.getSourceMappingURLs(),}); }) .then(update => { client.ws.send(update); }); } ).then(() => { client.ws.send(JSON.stringify({type: 'update-done'})); }); }); client.ws.on('close',() => disconnect()); }) } RN最舒服的地方就是命名规范,基本看到函数名就能知道他的职能,我们来看上面这段代码,attachHMRServer这个总共做了以下几件事:
HMRClient注册我们已经看到了socket的发送方,那么必定存在一个接收方,也就是这里要讲的HMRClient,首先先来看这边注册函数 #react-native/Libraries/BatchedBridge/BatchedBridge.js const MessageQueue = require('MessageQueue'); const BatchedBridge = new MessageQueue( () => global.__fbBatchedBridgeConfig,serializeNativeParams ); const Systrace = require('Systrace'); const JSTimersExecution = require('JSTimersExecution'); BatchedBridge.registerCallableModule('Systrace',Systrace); BatchedBridge.registerCallableModule('JSTimersExecution',JSTimersExecution); BatchedBridge.registerCallableModule('HeapCapture',require('HeapCapture')); if (__DEV__) { BatchedBridge.registerCallableModule('HMRClient',require('HMRClient')); } 这边就是HMRClient注册阶段,贴这段代码其实是因为发现RN里的JS->Native,Native->JS通信是通过MQ(MessageQueue)实现的,而追溯到最里层发现竟然是一套setTimeout,setImmediate的异步队列...扯远了,有空的话,可以专门分享一下。 HMRClient#react-native/Libraries/Utilities/HMRClient.js activeWS.onmessage = ({ data }) => { ... modules.forEach(({ id,code },i) => { ... const injectFunction = typeof global.nativeInjectHMRUpdate === 'function' ? global.nativeInjectHMRUpdate : eval; code = [ '__accept(',`${id},`,'function(global,require,module,exports){',`${code}`,'n},',`${JSON.stringify(inverseDependencies)}`,');',].join(''); injectFunction(code,sourceURLs[i]); }); } }; HMRClient做的事就很简单了,接到socket传入的String,直接eval运行,这边的code形如下图 真正的热更新过程#react-native/packager/react-packager/src/Resolver/polyfills/require.js const accept = function(id,factory,inverseDependencies) { //在当前模块映射表里查找,如果找的到将其Code进行替换,并执行,若没有,重新进行声明 const mod = modules[id]; if (!mod) { //重新申明 define(id,factory); return true; // new modules don't need to be accepted } const {hot} = mod; if (!hot) { console.warn( 'Cannot accept module because Hot Module Replacement ' + 'API was not installed.' ); return false; } // replace and initialize factory if (factory) { mod.factory = factory; } mod.hasError = false; mod.isInitialized = false; //真正进行热替换的地方 require(id); //当前模块热更新后需要执行的回调,一般用来解决循环引用 if (hot.acceptCallback) { hot.acceptCallback(); return true; } else { // need to have inverseDependencies to bubble up accept if (!inverseDependencies) { throw new Error('Undefined `inverseDependencies`'); } //将当前moduleId的逆向依赖传入,热更新他的逆向依赖,递归执行 return acceptAll(inverseDependencies[id],inverseDependencies); } }; global.__accept = accept; 这边的代码就不进行删减了,accept函数接受三个参数,moduleId,factory,inverseDependencies。
简单来说accept做的事情就是判断变动当前模块是新加的需要define,还是说直接更新内存里已存在的module,同时沿着他的逆向依赖树,全部load一遍,一直到最顶级的AppResigterElement,这样热替换的过程就完成了,形如下图
那么问题就来了,react的View展现对state是强依赖的,重新load一遍,state不会丢失么,实际上在load的过程中,RN把老的ref传入了,所以继承了之前的state 讲到这还略过了最重要的一点,为什么说我这边热替换了内存中module,并执行了一遍,我的App就能拿到这个更新后的代码,我们依旧拿代码来说 #react-native/packager/react-packager/src/Resolver/polyfills/require.js global.require = require; global.__d = define; const modules = Object.create(null); function define(moduleId,factory) { if (moduleId in modules) { // prevent repeated calls to `global.nativeRequire` to overwrite modules // that are already loaded return; } modules[moduleId] = { factory,hasError: false,isInitialized: false,exports: undefined,}; if (__DEV__) { // HMR modules[moduleId].hot = createHotReloadingObject(); // DEBUGGABLE MODULES NAMES // avoid unnecessary parameter in prod const verboseName = modules[moduleId].verboseName = arguments[2]; verboseNamesToModuleIds[verboseName] = moduleId; } } function require(moduleId) { const module = __DEV__ ? modules[moduleId] || modules[verboseNamesToModuleIds[moduleId]] : modules[moduleId]; return module && module.isInitialized ? module.exports : guardedLoadModule(moduleId,module); } RN复写了require,这样所有模块其实拿到的是这里 HMR存在的问题
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |