-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
48 changed files
with
406 additions
and
47 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { ComponentInternalInstance } from 'vue'; | ||
import { Container } from 'inversify'; | ||
|
||
const key = Symbol('container'); | ||
|
||
export function setContainer( | ||
component: ComponentInternalInstance, | ||
container: Container | ||
) { | ||
(component as any)[key] = container; | ||
} | ||
|
||
export function getContainer(component: ComponentInternalInstance) { | ||
return (component as any)[key]; | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
import { Container, interfaces } from 'inversify'; | ||
import { | ||
provide, | ||
inject, | ||
getCurrentInstance, | ||
onUnmounted, | ||
reactive, | ||
hasInjectionContext, | ||
ComponentInternalInstance, | ||
Plugin, | ||
App, | ||
} from 'vue'; | ||
import { setContainer } from './component-container'; | ||
export { findService, findAllServices } from './utils'; | ||
|
||
export const POST_REACTIVE = 'METADATA_KEY_POST_REACTIVE'; | ||
export const MULTIPLE_POST_REACTIVE = | ||
'Cannot apply @postReactive decorator multiple times in the same class'; | ||
export const POST_REACTIVE_ERROR = ( | ||
clazz: string, | ||
errorMessage: string | ||
): string => `@postReactive error in class ${clazz}: ${errorMessage}`; | ||
export function postReactive() { | ||
return (target: { constructor: NewableFunction }, propertyKey: string) => { | ||
const metadata = { key: POST_REACTIVE, value: propertyKey }; | ||
if (Reflect.hasOwnMetadata(POST_REACTIVE, target.constructor)) { | ||
throw new Error(MULTIPLE_POST_REACTIVE); | ||
} | ||
Reflect.defineMetadata(POST_REACTIVE, metadata, target.constructor); | ||
}; | ||
} | ||
function _postReactive<T extends NewableFunction>(instance: T) { | ||
const constr = instance.constructor; | ||
if (Reflect.hasMetadata(POST_REACTIVE, constr)) { | ||
const data = Reflect.getMetadata(POST_REACTIVE, constr) as any; | ||
try { | ||
return (instance as interfaces.Instance<T>)[data.value as string]?.(); | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
throw new Error(POST_REACTIVE_ERROR(constr.name, e.message)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
export const CONTAINER_TOKEN = 'USE_VUE_SERVICE_CONTAINER_TOKEN'; | ||
export function createToken<T>(desc: string): interfaces.ServiceIdentifier<T> { | ||
return Symbol.for(desc); | ||
} | ||
export const CURRENT_COMPONENT = createToken<ComponentInternalInstance>( | ||
'USE_VUE_SERVICE_COMPONENT_TOKEN' | ||
); | ||
|
||
interface ContainerOptions extends interfaces.ContainerOptions { | ||
instance?: ComponentInternalInstance | null; | ||
} | ||
export type ExtractToken<T> = T extends interfaces.Newable<infer U> ? U : never; | ||
|
||
const DEFAULT_CONTAINER_OPTIONS: ContainerOptions = { | ||
autoBindInjectable: false, | ||
defaultScope: 'Singleton', | ||
skipBaseClassChecks: false, | ||
}; | ||
function getOptions(options?: ContainerOptions) { | ||
return Object.assign({}, DEFAULT_CONTAINER_OPTIONS, options); | ||
} | ||
function makeReactiveObject(_: any, obj: any) { | ||
// 这里默认obj是一个对象 | ||
// 因为当前库只是劫持了to/toSelf方法,所以obj一定是类的实例 | ||
// 虽然通过特殊构造函数可以返回非对象的实例,但是这里不考虑这种特殊情况 | ||
const res = reactive(obj); | ||
_postReactive(res); | ||
return res; | ||
} | ||
function createContainer(parent?: Container, opts?: ContainerOptions) { | ||
let container: Container; | ||
const options = getOptions(opts); | ||
if (parent) { | ||
container = parent.createChild(options); | ||
} else { | ||
container = new Container(options); | ||
} | ||
if (opts?.instance) { | ||
// 组件实例绑定容器 | ||
setContainer(opts.instance, container); | ||
// 容器绑定组件实例 | ||
container.bind(CURRENT_COMPONENT).toConstantValue(opts.instance); | ||
} | ||
return reactiveContainer(container); | ||
} | ||
|
||
export const DEFAULT_CONTAINER = createContainer(); | ||
|
||
function reactiveContainer(container: Container) { | ||
const originalBind = container.bind; | ||
const newBind = (serviceIdentifier: any) => { | ||
const bindingToSyntax = originalBind.call(container, serviceIdentifier); | ||
const methods = ['to', 'toSelf']; | ||
for (let i = 0; i < methods.length; i++) { | ||
const method = methods[i]; | ||
const originalMethod = (bindingToSyntax as any)[method]; | ||
(bindingToSyntax as any)[method] = (...args: any[]) => { | ||
const result = originalMethod.apply(bindingToSyntax, args); | ||
if (result?.onActivation) { | ||
result.onActivation(makeReactiveObject); | ||
} | ||
return result; | ||
}; | ||
} | ||
return bindingToSyntax; | ||
}; | ||
container.bind = newBind as any; | ||
return container; | ||
} | ||
function bindContainer(container: Container, providers: any) { | ||
if (typeof providers === 'function') { | ||
providers(container); | ||
} else { | ||
for (let i = 0; i < providers.length; i++) { | ||
const s = providers[i]; | ||
container.bind(s).toSelf(); | ||
} | ||
} | ||
} | ||
function getServiceFromContainer<T>( | ||
container: Container, | ||
token: interfaces.ServiceIdentifier<T> | ||
) { | ||
return container.get(token); | ||
} | ||
function getCurrentContainer() { | ||
const instance: any = getCurrentInstance(); | ||
if (instance) { | ||
const token = CONTAINER_TOKEN; | ||
const provides = instance.provides; | ||
const parentProvides = instance.parent && instance.parent.provides; | ||
if ( | ||
provides && | ||
provides !== parentProvides && | ||
Object.prototype.hasOwnProperty.call(provides, token) | ||
) { | ||
return provides[token]; | ||
} | ||
} else { | ||
console.warn( | ||
`declareProviders can only be used inside setup() or functional components.` | ||
); | ||
} | ||
} | ||
function getContextContainer() { | ||
if (hasInjectionContext()) { | ||
const token = CONTAINER_TOKEN; | ||
const defaultValue = DEFAULT_CONTAINER; | ||
const instance: any = getCurrentInstance(); | ||
if (instance) { | ||
const provides = instance.provides; | ||
return provides[token] || defaultValue; | ||
} else { | ||
return inject(token, defaultValue); | ||
} | ||
} else { | ||
console.warn( | ||
`declareAppProviders|declareProviders|useService can only be used inside setup() or functional components.` | ||
); | ||
} | ||
} | ||
|
||
export function useService<T>(token: interfaces.ServiceIdentifier<T>) { | ||
const container = getContextContainer(); | ||
return getServiceFromContainer(container, token); | ||
} | ||
export function useRootService<T>(token: interfaces.ServiceIdentifier<T>) { | ||
return getServiceFromContainer(DEFAULT_CONTAINER, token); | ||
} | ||
|
||
export function declareProviders( | ||
providers: (c: Container) => void, | ||
options?: ContainerOptions | ||
): void; | ||
export function declareProviders( | ||
providers: interfaces.Newable<any>[], | ||
options?: ContainerOptions | ||
): void; | ||
export function declareProviders(providers: any, options?: ContainerOptions) { | ||
const currentContainer = getCurrentContainer(); | ||
if (currentContainer) { | ||
bindContainer(currentContainer, providers); | ||
} else { | ||
const parent = getContextContainer(); | ||
if (parent) { | ||
const instance = getCurrentInstance(); | ||
let container = createContainer(parent, { instance, ...options }); | ||
bindContainer(container, providers); | ||
onUnmounted(() => { | ||
container.unbindAll(); | ||
container = null as any; | ||
}); | ||
provide(CONTAINER_TOKEN, container); | ||
} | ||
} | ||
} | ||
|
||
export function declareRootProviders(providers: (c: Container) => void): void; | ||
export function declareRootProviders( | ||
providers: interfaces.Newable<any>[] | ||
): void; | ||
export function declareRootProviders(providers: any) { | ||
bindContainer(DEFAULT_CONTAINER, providers); | ||
} | ||
|
||
export function declareAppProviders( | ||
providers: (c: Container) => void, | ||
options?: ContainerOptions | ||
): Plugin; | ||
export function declareAppProviders( | ||
providers: interfaces.Newable<any>[], | ||
options?: ContainerOptions | ||
): Plugin; | ||
export function declareAppProviders( | ||
providers: any, | ||
options?: ContainerOptions | ||
) { | ||
return (app: App) => { | ||
app.runWithContext(() => { | ||
const appContainer = inject(CONTAINER_TOKEN, void 0) as | ||
| Container | ||
| undefined; | ||
if (appContainer) { | ||
bindContainer(appContainer, providers); | ||
} else { | ||
let container = createContainer(DEFAULT_CONTAINER, options); | ||
bindContainer(container, providers); | ||
app.onUnmount(() => { | ||
container.unbindAll(); | ||
container = null as any; | ||
}); | ||
app.provide(CONTAINER_TOKEN, container); | ||
} | ||
}); | ||
}; | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { interfaces } from 'inversify'; | ||
import { | ||
VNode, | ||
VNodeChild, | ||
VNodeArrayChildren, | ||
VNodeNormalizedChildren, | ||
ComponentInternalInstance, | ||
} from 'vue'; | ||
import { getContainer } from './component-container'; | ||
|
||
/** | ||
* useService是从当前组件开始像父组件以及祖先组件查找服务实例 | ||
* findService是从当前组件【不包含当前组件】向子组件以及后代组件查找服务实例 | ||
* 可以通过component.container尝试获取该组件是否有绑定对应的inversify的container | ||
* 然后通过container.isBound(token)来判断是否有绑定对应的服务 | ||
* 如果有绑定则通过container.get(token)来获取服务实例 | ||
* 注意component.subTree.children是当前组件的子组件 | ||
* 整个过程是一个递归的过程,因为子组件还有子组件,所以需要递归查找 | ||
* vnode.children | ||
* vnode.component [node.component.subTree] | ||
* vnode.suspense [node.suspense.activeBranch] | ||
* vnode 判断所有vnode是否符合条件 | ||
*/ | ||
|
||
function nodesAsObject( | ||
value: VNodeChild | VNodeArrayChildren | ||
): value is VNodeArrayChildren | VNode { | ||
return !!value && typeof value === 'object'; | ||
} | ||
|
||
function walk<T>( | ||
vnode: VNode, | ||
token: interfaces.ServiceIdentifier<T>, | ||
results: T[] | ||
): T[] { | ||
if (vnode.component) { | ||
const container = getContainer(vnode.component); | ||
if (container && container.isCurrentBound(token)) { | ||
results.push(container.get(token)); | ||
} | ||
} | ||
// 优先遍历当前组件的子组件树 | ||
walkChildren(vnode.children, token, results); | ||
if (vnode.component) { | ||
walk(vnode.component.subTree, token, results); | ||
} | ||
if (vnode.suspense && vnode.suspense.activeBranch) { | ||
walk(vnode.suspense.activeBranch, token, results); | ||
} | ||
return results; | ||
} | ||
|
||
function walkChildren<T>( | ||
children: VNodeNormalizedChildren, | ||
token: interfaces.ServiceIdentifier<T>, | ||
results: T[] | ||
): T[] { | ||
if (children && Array.isArray(children)) { | ||
const filteredNodes = children.filter(nodesAsObject); | ||
filteredNodes.forEach((node: VNodeArrayChildren | VNode) => { | ||
if (Array.isArray(node)) { | ||
walkChildren(node, token, results); | ||
} else { | ||
walk(node, token, results); | ||
} | ||
}); | ||
} | ||
return results; | ||
} | ||
|
||
/** | ||
* @param component ComponentInternalInstance 当前组件 | ||
* @param token interfaces.ServiceIdentifier<T> 服务标识 | ||
* @returns T | undefined 是从当前组件【不包含当前组件】的子组件以及后代组件中查找服务实例,返回第一个找到的服务实例 | ||
*/ | ||
export function findService<T>( | ||
component: ComponentInternalInstance, | ||
token: interfaces.ServiceIdentifier<T> | ||
): T | undefined { | ||
const results: T[] = []; | ||
walk(component.subTree, token, results); | ||
return results[0]; | ||
} | ||
|
||
/** | ||
* @param component ComponentInternalInstance 当前组件 | ||
* @param token interfaces.ServiceIdentifier<T> 服务标识 | ||
* @returns T[] 是从当前组件【不包含当前组件】的子组件以及后代组件中查找服务实例,返回所有找到的服务实例 | ||
*/ | ||
export function findAllServices<T>( | ||
component: ComponentInternalInstance, | ||
token: interfaces.ServiceIdentifier<T> | ||
): T[] { | ||
const results: T[] = []; | ||
walk(component.subTree, token, results); | ||
return results; | ||
} |
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Oops, something went wrong.