Skip to content

Commit

Permalink
feat: try to support HMR (#4258)
Browse files Browse the repository at this point in the history
* feat: support dynamic midway container

* fix: lint

* chore: temp code ok

* chore: test ok

* fix: lint

* fix: test
  • Loading branch information
czy88840616 authored Feb 2, 2025
1 parent d89cf28 commit 1574c8c
Show file tree
Hide file tree
Showing 12 changed files with 673 additions and 35 deletions.
28 changes: 22 additions & 6 deletions packages/core/src/context/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,20 @@ export class MidwayContainer implements IMidwayGlobalContainer {
}
}

bind<T>(target: T, options?: Partial<IObjectDefinition>): void;
bind<T>(
target: T,
options?: Partial<IObjectDefinition>
): IObjectDefinition | undefined;
bind<T>(
identifier: ObjectIdentifier,
target: T,
options?: Partial<IObjectDefinition>
): void;
bind<T>(identifier: any, target: any, options?: any): void {
): IObjectDefinition | undefined;
bind<T>(

Check warning on line 114 in packages/core/src/context/container.ts

View workflow job for this annotation

GitHub Actions / lintAndTestLegacy (lts/*, ubuntu-latest)

'T' is defined but never used
identifier: any,
target: any,
options?: any
): IObjectDefinition | undefined {
if (Types.isClass(identifier) || Types.isFunction(identifier)) {
return this.bindModule(identifier, target);
}
Expand Down Expand Up @@ -257,14 +264,19 @@ export class MidwayContainer implements IMidwayGlobalContainer {
if (definition) {
this.registry.registerDefinition(definition.id, definition);
}

return definition;
}

protected bindModule(module: any, options: Partial<IObjectDefinition>) {
protected bindModule(
module: any,
options: Partial<IObjectDefinition>
): IObjectDefinition | undefined {
if (Types.isClass(module)) {
const providerId = DecoratorManager.getProviderUUId(module);
if (providerId) {
this.identifierMapping.saveClassRelation(module, options?.namespace);
this.bind(providerId, module, options);
return this.bind(providerId, module, options);
} else {
// no provide or js class must be skip
}
Expand All @@ -280,7 +292,7 @@ export class MidwayContainer implements IMidwayGlobalContainer {
}
const uuid = Utils.generateRandomId();
this.identifierMapping.saveFunctionRelation(info.id, uuid);
this.bind(uuid, module, {
return this.bind(uuid, module, {
scope: info.scope,
namespace: options.namespace,
srcPath: options.srcPath,
Expand Down Expand Up @@ -342,6 +354,10 @@ export class MidwayContainer implements IMidwayGlobalContainer {
this.registry.registerObject(identifier, target);
}

removeObject(identifier: ObjectIdentifier) {
this.registry.removeObject(identifier);
}

onBeforeBind(
fn: (
Clzz: any,
Expand Down
22 changes: 10 additions & 12 deletions packages/core/src/context/definitionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
} from '../interface';
import { DecoratorManager } from '../decorator';

const PREFIX = '_id_default_';

class LegacyIdentifierRelation
extends Map<ObjectIdentifier, string>
implements IIdentifierRelationShip
Expand Down Expand Up @@ -59,6 +57,7 @@ export class ObjectDefinitionRegistry
private singletonIds = [];
private _identifierRelation: IIdentifierRelationShip =
new LegacyIdentifierRelation();
private objectCache = new Map();

get identifierRelation() {
if (!this._identifierRelation) {
Expand All @@ -72,13 +71,7 @@ export class ObjectDefinitionRegistry
}

get identifiers() {
const ids = [];
for (const key of this.keys()) {
if (key.indexOf(PREFIX) === -1) {
ids.push(key);
}
}
return ids;
return Array.from(this.keys());
}

get count() {
Expand Down Expand Up @@ -126,21 +119,26 @@ export class ObjectDefinitionRegistry

clearAll(): void {
this.singletonIds = [];
this.objectCache.clear();
this.clear();
}

hasObject(identifier: ObjectIdentifier): boolean {
identifier = this.identifierRelation.getRelation(identifier) ?? identifier;
return this.has(PREFIX + identifier);
return this.objectCache.has(identifier);
}

registerObject(identifier: ObjectIdentifier, target: any) {
this.set(PREFIX + identifier, target);
this.objectCache.set(identifier, target);
}

removeObject(identifier: ObjectIdentifier) {
this.objectCache.delete(identifier);
}

getObject(identifier: ObjectIdentifier): any {
identifier = this.identifierRelation.getRelation(identifier) ?? identifier;
return this.get(PREFIX + identifier);
return this.objectCache.get(identifier);
}

getIdentifierRelation(): IIdentifierRelationShip {
Expand Down
234 changes: 234 additions & 0 deletions packages/core/src/context/dynamicContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { MidwayContainer } from './container';
import {
ClassType,
IObjectDefinition,
ModuleLoadType,
ObjectIdentifier,
} from '../interface';
import { MidwayEnvironmentService } from '../service/environmentService';
import { loadModule } from '../util';
import { Types } from '../util/types';
import { DecoratorManager, MAIN_MODULE_KEY } from '../decorator';
import { debuglog } from 'util';
const debug = debuglog('midway:container');

/**
* 尝试用于开发时动态更新的 IoC 容器
*/
export class DynamicMidwayContainer extends MidwayContainer {
private moduleType: ModuleLoadType;
private modifyClassMapping = new Map<string, string>();
private idRefMapping = new Map<string, string[]>();

constructor() {
super();
this.onBeforeBind((Clzz, options) => {
const definition = options.definition;
if (definition) {
if (definition.properties) {
// 处理属性
for (const propMetas of definition.properties.values()) {
// 这里未处理懒加载依赖
if (typeof propMetas.id === 'string') {
if (this.idRefMapping.has(propMetas.id)) {
this.idRefMapping.get(propMetas.id).push(definition.id);
} else {
this.idRefMapping.set(propMetas.id, [definition.id]);
}
}
}
}

if (definition.constructorArgs) {
// 处理构造器
for (const constructMeta of definition.constructorArgs) {
if (typeof constructMeta.id === 'string') {
if (this.idRefMapping.has(constructMeta.id)) {
this.idRefMapping.get(constructMeta.id).push(definition.id);
} else {
this.idRefMapping.set(constructMeta.id, [definition.id]);
}
}
}
}
}
});
}

async updateDefinition(modifyFilePath: string): Promise<boolean> {
debug('updateDefinition %s', modifyFilePath);
if (!this.moduleType) {
const environmentService = await this.getAsync(MidwayEnvironmentService);
this.moduleType = environmentService.getModuleLoadType();
}

// 根据文件路径找到老的类
const oldDefinitionList = this.findDefinitionByPath(modifyFilePath);

debug('oldDefinitionList size %s', oldDefinitionList.length);

if (!oldDefinitionList.length) {
return;
}

// 一个文件可能对应多个不同的 ObjectDefinition,但是导出的类名可能是不同的,所以可以使用类名作为 key
const nameList = {};
// 拿到旧的标识符
for (const oldDefinition of oldDefinitionList) {
nameList[oldDefinition.name] = oldDefinition.id;
}

debug('nameList %j', nameList);

// 清除 require cache
const requireCacheCleaned = this.findRequireCacheAndClear(modifyFilePath);

// 清理历史 context 缓存
for (const oldDefinition of oldDefinitionList) {
// 清理依赖的缓存
if (this.idRefMapping.has(oldDefinition.id)) {
for (const refId of this.idRefMapping.get(oldDefinition.id)) {
if (this.hasObject(refId)) {
this.removeObject(refId);
}
}
} else if (this.idRefMapping.has(oldDefinition.name)) {
for (const refId of this.idRefMapping.get(oldDefinition.name)) {
if (this.hasObject(refId)) {
this.removeObject(refId);
}
}
}

// 清理自身
this.removeObject(oldDefinition.id);
}

debug('ready to load module %s', modifyFilePath);

// 重新加载新的文件
const modLoaded = await loadModule(modifyFilePath, {
loadMode: this.moduleType,
});

const newClassList = [];
if (Types.isClass(modLoaded) || Types.isFunction(modLoaded)) {
newClassList.push(modLoaded);
} else {
for (const m in modLoaded) {
const module = modLoaded[m];
newClassList.push(module);
}
}

debug('newClassList size %s', newClassList.length);

let remapping = false;
if (Types.isClass(modLoaded) || Types.isFunction(modLoaded)) {
const newId = DecoratorManager.getProviderUUId(modLoaded);
const name = DecoratorManager.getProviderName(modLoaded);
if (nameList[name]) {
debug('find old class name %s and will be remapping', name);
this.remapping(nameList[name], newId);
}

debug('bindModule %s', newId);
remapping = true;
this.bindModule(modLoaded, {
namespace: MAIN_MODULE_KEY,
srcPath: modifyFilePath,
createFrom: 'file',
});
} else {
for (const m in modLoaded) {
const module = modLoaded[m];
if (Types.isClass(module) || Types.isFunction(module)) {
const newId = DecoratorManager.getProviderUUId(module);
const name = DecoratorManager.getProviderName(module);
if (nameList[name] !== newId) {
if (nameList[name]) {
debug('find old class name %s and will be remapping', name);
this.remapping(nameList[name], newId);
}

debug('bindModule %s', newId);
remapping = true;
this.bindModule(module, {
namespace: MAIN_MODULE_KEY,
srcPath: modifyFilePath,
createFrom: 'file',
});
}
}
}
}

return (
requireCacheCleaned &&
oldDefinitionList.length > 0 &&
newClassList.length > 0 &&
remapping
);
}

getIdentifier(identifier: ClassType | string): string {
// 从老的 id 映射成新的 id
let name = 'unknown';
if (typeof identifier !== 'string') {
name = DecoratorManager.getProviderName(identifier);
identifier = DecoratorManager.getProviderUUId(identifier);
}

debug('check identifier from %s %s', name, identifier);

if (this.modifyClassMapping.has(identifier)) {
debug(
'getIdentifier from modifyClassMapping %s -> %s',
identifier,
this.modifyClassMapping.get(identifier)
);
return this.modifyClassMapping.get(identifier);
}
return identifier;
}

private findDefinitionByPath(filePath: string): IObjectDefinition[] {
const results = [];
// 遍历 registry
for (const definition of (
this.registry as unknown as Map<ObjectIdentifier, IObjectDefinition>
).values()) {
if (definition.srcPath === filePath) {
results.push(definition);
}
}
return results;
}

private findRequireCacheAndClear(absolutePath: string): boolean {
let cleaned = false;
const cacheKey = require.resolve(absolutePath);
const cache = require.cache[cacheKey];
if (cache) {
debug('clear cache %s', cacheKey);
delete require.cache[cacheKey];
cleaned = true;
}
return cleaned;
}

private remapping(oldId, newId) {
// 新老 id 做个重新映射,如果第一次 1 -> 2, 第二次输入 2 -> 3,那么要变成 1 -> 3
for (const [key, value] of this.modifyClassMapping.entries()) {
if (value === oldId) {
debug('remapping key = %s, %s -> %s', key, oldId, newId);
// Update the mapping to the new newId
this.modifyClassMapping.set(key, newId);
}
}

debug('new remapping key = %s, value = %s', oldId, newId);
// Set the new mapping
this.modifyClassMapping.set(oldId, newId);
}
}
Loading

0 comments on commit 1574c8c

Please sign in to comment.