Last active
October 19, 2020 11:11
-
-
Save buhichan/6793975c7b38aacff766e2379b7dcc93 to your computer and use it in GitHub Desktop.
A typescript DI solution with no need of reflect-metadata (and can also need no decorators, if you prefer it)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Idea is borrowed from https://github.com/gnaeus/react-ioc | |
//Differences are: | |
//1. Does not require reflect-metadata | |
//2. Has an additional "useProvider" method | |
import * as React from "react" | |
export interface Disposable { | |
dispose?(): void | |
} | |
type SID<T = unknown> = string & { | |
interface: T | |
} | |
type ServiceFromServiceId<S> = S extends SID<infer T> ? T : never | |
export function createServiceId<T>(name: string): SID<T> { | |
return (name as unknown) as SID<T> | |
} | |
class InjectorResolutionError extends Error { | |
public cause = "Injector" | |
constructor() { | |
super("找不到Injector, 请确保被注入的目标类不是手动创建的.") | |
} | |
} | |
class ServiceResolutionError extends Error { | |
constructor(public cause: SID) { | |
super(`未能解析的服务: ${cause.toString()}`) | |
} | |
} | |
const CURRENT_INJECTOR = Symbol("CurrentInjector") | |
class Injector { | |
static ServiceResolutionError = ServiceResolutionError | |
private instanceMap: Record<SID, Disposable> & object = {} | |
private bindingMap: Record<SID, new () => Disposable> & object = {} | |
constructor(private parent?: Injector) {} | |
dispose() { | |
for(let k in this.instanceMap){ | |
this.instanceMap[k].dispose() | |
} | |
} | |
register(serviceId: SID, impl: new () => Disposable) { | |
this.bindingMap[serviceId] = impl | |
} | |
get<T extends Disposable>(serviceId: SID): T { | |
if (this.bindingMap[serviceId]) { | |
if (this.instanceMap[serviceId]) { | |
return this.instanceMap[serviceId] as T | |
} | |
const res = new this.bindingMap[serviceId]() | |
res[CURRENT_INJECTOR] = this | |
this.instanceMap[serviceId] = res | |
return res as T | |
} else { | |
if (this.parent) { | |
return this.parent.get(serviceId) | |
} else { | |
throw new Injector.ServiceResolutionError(serviceId) | |
} | |
} | |
} | |
} | |
const rootInjector = new Injector() | |
const InjectorContext = React.createContext(rootInjector) | |
export type Declaration<T> = [SID<T>, new ()=>T] | |
export function useProvider<T1>(d1: Declaration<T1>): (node: React.ReactNode) => React.ReactElement | |
export function useProvider<T1, T2>(d1: Declaration<T1>, d2:Declaration<T2>): (node: React.ReactNode) => React.ReactElement | |
export function useProvider<T1, T2, T3>(d1: Declaration<T1>, d2:Declaration<T2>, d3: Declaration<T3>): (node: React.ReactNode) => React.ReactElement | |
//eslint-disable-next-line | |
export function useProvider(...declarations: any[]): (node: React.ReactNode) => React.ReactElement { | |
const parent = React.useContext(InjectorContext) | |
const childInjector = React.useMemo( | |
() => { | |
const childInjector = new Injector(parent) | |
declarations.forEach(([id, impl]) => childInjector.register(id as SID, impl as new () => Disposable)) | |
return childInjector | |
}, | |
declarations.map(x => x[1]) | |
) | |
React.useEffect( | |
() => () => { | |
childInjector.dispose() | |
}, | |
[] | |
) | |
return node => { | |
return <InjectorContext.Provider value={childInjector}>{node}</InjectorContext.Provider> | |
} | |
} | |
export function Inject<Id extends SID>(serviceId: Id, target: unknown): ServiceFromServiceId<Id> | |
export function Inject<Id extends SID>(serviceId: Id): (proto: unknown, propName: string) => void | |
export function Inject<Id extends SID>(serviceId: Id, target?: unknown) { | |
if (target === undefined) { | |
return (proto, propName) => { | |
Object.defineProperty(proto, propName, { | |
get() { | |
const injector = this[CURRENT_INJECTOR] | |
if (!injector) { | |
throw new InjectorResolutionError() | |
} | |
console.log("get", serviceId) | |
return (injector.get(serviceId) as unknown) as ServiceFromServiceId<Id> | |
}, | |
}) | |
} | |
} else { | |
const injector = ((target as unknown) as Record<typeof CURRENT_INJECTOR, Injector>)[CURRENT_INJECTOR] | |
if (!injector) { | |
throw new InjectorResolutionError() | |
} | |
return (injector.get(serviceId) as unknown) as ServiceFromServiceId<Id> | |
} | |
} | |
export function useService<Id extends SID>(serviceId: Id) { | |
const injector = React.useContext(InjectorContext) | |
console.log("get ", serviceId) | |
return injector.get(serviceId) as ServiceFromServiceId<Id> | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Then in some other files: