上次的远程加载简单描述了app reload(开发时运行也是一个逻辑)和MetroServer交互的流程,现在开始讲讲HMR,对应到RN调试界面中的Enable HotLoading,HMR全名为Hot Module Replacement。它能让你在修改代码后马上在app上展现结果而且不会丢失状态。
先看其创建的位置,也是在runServer中
//metro/src/index.js.flow
exports.runServer = async (
......
) => {
....
const {
attachHmrServer,
middleware,
metroServer,
end,
} = await exports.createConnectMiddleware(config);
serverApp.use(middleware);
....
let httpServer;
.....
httpServer = http.createServer(serverApp);
....
if (hmrEnabled) {
attachHmrServer(httpServer);
}
来看看attachHmrServer函数:
index.js.flow
attachHmrServer(httpServer: HttpServer | HttpsServer) {
attachWebsocketServer({
httpServer,
path: '/hot',
websocketServer: new MetroHmrServer(
metroServer.getBundler(),
metroServer.getCreateModuleId(),
config,
),
});
}
这里没有逻辑,放出attachWebsocketServer代码:
module.exports = function attachWebsocketServer({
httpServer,
websocketServer,
path,
}: HMROptions) {
const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({
server: httpServer,
path,
});
wss.on('connection', async ws => {
let connected = true;
const url = ws.upgradeReq.url;
const sendFn = (...args) => {
if (connected) {
ws.send(...args);
}
};
const client = await websocketServer.onClientConnect(url, sendFn);
...
ws.on('error', e => {
websocketServer.onClientError && websocketServer.onClientError(client, e);
});
ws.on('close', () => {
websocketServer.onClientDisconnect &&
websocketServer.onClientDisconnect(client);
connected = false;
});
ws.on('message', message => {
websocketServer.onClientMessage &&
websocketServer.onClientMessage(client, message);
});
});
};
attachWebsocketServer里面主要是创建WebSocketServer并监听连接,将逻辑全交给MetroHmrServer处理,不得不说这代码把连接和逻辑处理分得很清楚
在来看MetroHmrServer:
//HmrServer.js.flow
async onClientConnect(
clientUrl: string,
sendFn: (data: string) => void,
): Promise {
const urlObj = nullthrows(url.parse(clientUrl, true));
const query = nullthrows(urlObj.query);
let revPromise;
if (query.bundleEntry != null) {
urlObj.pathname = query.bundleEntry.replace(/\.js$/, '.bundle');
delete query.bundleEntry;
const {options} = parseOptionsFromUrl(
url.format(urlObj),
new Set(this._config.resolver.platforms),
);
const {entryFile, transformOptions} = splitBundleOptions(options);
const resolutionFn = await transformHelpers.getResolveDependencyFn(
this._bundler.getBundler(),
transformOptions.platform,
);
const resolvedEntryFilePath = resolutionFn(
`${this._config.projectRoot}/.`,
entryFile,
);//1<-- 这之前都是在转换来获取到打包入口文件的绝对路径
const graphId = getGraphId(resolvedEntryFilePath, transformOptions);
revPromise = this._bundler.getRevisionByGraphId(graphId);
if (!revPromise) {//2<--由于是热加载,所以肯定需要是之前打包过的,否则报错
send([sendFn], {
type: 'error',
body: formatBundlingError(new GraphNotFoundError(graphId)),
});
return null;
}
} else {
const revisionId = query.revisionId;
revPromise = this._bundler.getRevision(revisionId);
if (!revPromise) {
send([sendFn], {
type: 'error',
body: formatBundlingError(new RevisionNotFoundError(revisionId)),
});
return null;
}
}
const {graph, id} = await revPromise;//3<--获取到前版本的ud
const client = {//4<--注意,这里的client是后面交互的载体
sendFn,
revisionId: id,
};
let clientGroup = this._clientGroups.get(id);//5<--所有client都保存在_clientGroups
if (clientGroup != null) {
clientGroup.clients.add(client);
} else {
clientGroup = {
clients: new Set([client]),
unlisten: () => unlisten(),
revisionId: id,
};
this._clientGroups.set(id, clientGroup);
const unlisten = this._bundler
.getDeltaBundler()
.listen(//6<--这里监听了文件的变化,文件有变化后调用_handleFileChange函数
graph,
debounceAsyncQueue(
this._handleFileChange.bind(this, clientGroup),
50,//7<--节流
),
);
}
await this._handleFileChange(clientGroup);
return client;
}
以上为有客户端连接上socket时的的处理,主要是创建client对象作为后面真正执行热更新的对象,文件监听变化仅仅在初始化clientGroup时设置了一次,后面的交互其实就围绕着文件来,即_handleFileChange
HmrServer.js.flow
async _handleFileChange(group: ClientGroup) {
const processingHmrChange = log(
createActionStartEntry({action_name: 'Processing HMR change'}),
);
const sendFns = [...group.clients].map(client => client.sendFn);
send(sendFns, {type: 'update-start'});
const message = await this._prepareMessage(group);
send(sendFns, message);
send(sendFns, {type: 'update-done'});
}
可以看到文件更新消息是群发给client的,_prepareMessage函数中包含了所有打包的逻辑
HMRServer.js.flow
async _prepareMessage(
group: ClientGroup,
): Promise {
try {
const revPromise = this._bundler.getRevision(group.revisionId);//1<-- 获取整个group的版本信息
if (!revPromise) {
return {
type: 'error',
body: formatBundlingError(
new RevisionNotFoundError(group.revisionId),
),
};
}
const {revision, delta} = await this._bundler.updateGraph(//2<--根据现有版本计算出差量delta并返回新版本
await revPromise,
false,
);
this._clientGroups.delete(group.revisionId);
group.revisionId = revision.id;//3<--更新group版本信息
for (const client of group.clients) {
client.revisionId = revision.id;//4<--更新client信息
}
this._clientGroups.set(group.revisionId, group);
const hmrUpdate = hmrJSBundle(delta, revision.graph, {//5<--获取差量包
createModuleId: this._createModuleId,
projectRoot: this._config.projectRoot,
});
return {
type: 'update',
body: {
revisionId: revision.id,
...hmrUpdate,
},
};
} catch (error) {
const formattedError = formatBundlingError(error);
this._config.reporter.update({type: 'bundling_error', error});
return {type: 'error', body: formattedError};
}
}
这里主要就是打包并返回更新内容,delta是差量信息,hmrUpdate对象的格式如下:
export type HmrUpdate = {|
+revisionId: string, 版本号
+modules: ModuleMap, 修改的、添加的module
+deleted: $ReadOnlyArray,被删除的moduleID
+sourceMappingURLs: $ReadOnlyArray, 源码map,顺序和modules一一对应
+sourceURLs: $ReadOnlyArray, 源码地址,顺序和modules一一对应,即js文件路径
|};
顺着this._bundler.updateGraph的执行深度,我们找到了计算差量的模块DeltaCalculator.js.flow
async _getChangedDependencies(
modifiedFiles: Set,
deletedFiles: Set,
): Promise> {
if (!this._graph.dependencies.size) {//1<--这里是初始化打包的操作,忽略
const {added} = await initialTraverseDependencies(
this._graph,
this._options,
);
return {
added,
modified: new Map(),
deleted: new Set(),
reset: true,
};
}
// If a file has been deleted, we want to invalidate any other file that
// depends on it, so we can process it and correctly return an error.
deletedFiles.forEach(filePath => {//2<--遍历删除的文件
const module = this._graph.dependencies.get(filePath);
if (module) {//3<--获取模块的反向依赖,这步算是RN的兜底操作
module.inverseDependencies.forEach(path => {
// Only mark the inverse dependency as modified if it's not already
// marked as deleted (in that case we can just ignore it).
if (!deletedFiles.has(path)) {
modifiedFiles.add(path);
}
});
}
});
// We only want to process files that are in the bundle.
const modifiedDependencies = Array.from(modifiedFiles).filter(filePath =>
this._graph.dependencies.has(filePath),//4<--这里过滤掉不在依赖图中文件<--
);
// No changes happened. Return empty delta.
if (modifiedDependencies.length === 0) {
return {
added: new Map(),
modified: new Map(),
deleted: new Set(),
reset: false,
};
}
//5<--在这才正在计算打包差量
const {added, modified, deleted} = await traverseDependencies(
modifiedDependencies,
this._graph,
this._options,
);
return {
added,
modified,
deleted,
reset: false,
};
}
打包后的代码如何执行呢?在Server发送消息 send(sendFns, message);给HMRClient,由HMRClient处理:
//HMRClient.js.flow
class HMRClient extends WebSocketHMRClient {
constructor(url: string) {
super(url);
this.on('update', update => {
injectUpdate(update);
});
}
}
这个client实现很简单,WebSocketHMRClient只是做连接和转发工作,没有业务逻辑,所以不做展开,而HMRClient的逻辑只有一个——injectUpdate
function injectModules(
modules: ModuleMap,
sourceMappingURLs: $ReadOnlyArray,
sourceURLs: $ReadOnlyArray,
) {
modules.forEach(([id, code], i) => {
// In JSC we need to inject from native for sourcemaps to work
// (Safari doesn't support `sourceMappingURL` nor any variant when
// evaluating code) but on Chrome we can simply use eval.
const injectFunction =
typeof global.nativeInjectHMRUpdate === 'function'
? global.nativeInjectHMRUpdate
: eval; // eslint-disable-line no-eval
// Fool regular expressions trying to remove sourceMappingURL comments from
// source files, which would incorrectly detect and remove the inlined
// version.
const pragma = 'sourceMappingURL';
injectFunction(//3<--这里使用的一般是eval
code + `\n//# ${pragma}=${sourceMappingURLs[i]}`,
sourceURLs[i],
);
});
}
function injectUpdate(update: HmrUpdate) {
injectModules(//1<--先注入添加的module
update.added,
update.addedSourceMappingURLs,
update.addedSourceURLs,
);
injectModules(//2<--再注入修改后的module
update.modified,
update.modifiedSourceMappingURLs,
update.modifiedSourceURLs,
);
}
上面描述了HMRServer如何启动、如何建立连接、差量包打包、差量包运行,但却留下了几个疑问:1、是谁与HMRServer建立连接 2、差量包运行在哪里
与HMRServer建立连接的是 App 本身,起点是DevSupportManagerImpl
if (mDevSettings.isHotModuleReplacementEnabled() && mCurrentContext != null) {
try {
URL sourceUrl = new URL(getSourceUrl());
String path = sourceUrl.getPath().substring(1); // strip initial slash in path
String host = sourceUrl.getHost();
int port = sourceUrl.getPort();
mCurrentContext.getJSModule(HMRClient.class).enable("android", path, host, port);
} catch (MalformedURLException e) {
showNewJavaError(e.getMessage(), e);
}
}
这里调用了js模块 HMRClient的enable函数。
/**
* HMR Client that receives from the server HMR updates and propagates them
* runtime to reflects those changes.
*/
const HMRClient = {
enable(platform: string, bundleEntry: string, host: string, port: number) {
... ...
// Moving to top gives errors due to NativeModules not being initialized
const HMRLoadingView = require('HMRLoadingView');
const wsHostPort = port !== null && port !== '' ? `${host}:${port}` : host;
bundleEntry = bundleEntry.replace(/\.(bundle|delta)/, '.js');
// Build the websocket url
const wsUrl =
`ws://${wsHostPort}/hot?` +
`platform=${platform}&` +
`bundleEntry=${bundleEntry}`;
const hmrClient = new MetroHMRClient(wsUrl);
hmrClient.on('connection-error', e => {
let error = `Hot loading isn't working because it cannot connect to the development server.
... ...
});
hmrClient.on('update-start', () => {
HMRLoadingView.showMessage('Hot Loading...');
});
hmrClient.on('update', () => {
if (Platform.OS === 'ios') {
const RCTRedBox = require('NativeModules').RedBox;
RCTRedBox && RCTRedBox.dismiss && RCTRedBox.dismiss();
} else {
const RCTExceptionsManager = require('NativeModules').ExceptionsManager;
RCTExceptionsManager &&
RCTExceptionsManager.dismissRedbox &&
RCTExceptionsManager.dismissRedbox();
}
});
hmrClient.on('update-done', () => {
HMRLoadingView.hide();
});
hmrClient.on('error', data => {
HMRLoadingView.hide();
... ...
});
hmrClient.on('close', data => {
HMRLoadingView.hide();
... ...
});
hmrClient.enable();
},
};
这个函数中new 了MetroHMRClient并与HMRServer建立了连接,并根据收到的消息展示UI,MetroHMRClient就是Metro包中的HMRClient模块
由于MetroHMRClient也是运行在js模块HMRClient中,因此差量包是运行在RN代码运行的js环境中的。而这个环境在我们没开debug的时候是本地的jscore,开了debug就是一个代理的jscore,这个代理的jscore下次再解释。
App<-->HMRClient jsModule<-->MetroHMRClient<-->HMRServer
1、App通过js模块HMRClient创建了MetroHMRClient对象并建立连接,MetroHMRClient的“update”监听是执行差量包的函数,而MetroHMRClient又是在js模块中创建,因此差量包运行在RN代码的运行环境
2、HMRServer接收到连接后把连接后的句柄保存为client,并添加到clientGroup数组中,初始化时注册了文件变更监听
3、当有文件更改时,促发文件变更监听,HMRServer群发MetroHMRClient,消息体为变更后的所有js代码差量
4、MetroHMRClient接收到差量代码并执行