Skip to content

Commit

Permalink
同时发布默认版本和inversify版本
Browse files Browse the repository at this point in the history
  • Loading branch information
kaokei committed Nov 26, 2024
1 parent a623a8c commit c183c3f
Show file tree
Hide file tree
Showing 48 changed files with 406 additions and 47 deletions.
2 changes: 1 addition & 1 deletion demo/components/demo1/DemoComp.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { getCurrentInstance, ref } from 'vue';
import { declareProviders, useService } from '../../../src/index';
import { declareProviders, useService } from '../../../src/inversify';
import { DemoService } from './DemoService';
defineProps<{ msg: string }>();
Expand Down
15 changes: 15 additions & 0 deletions src/inversify/component-container.ts
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];
}
241 changes: 241 additions & 0 deletions src/inversify/index.ts
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);
}
});
};
}
97 changes: 97 additions & 0 deletions src/inversify/utils.ts
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;
}
2 changes: 1 addition & 1 deletion tests/DemoComp.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { DemoService } from './DemoService';
import { declareProviders, useService } from '../src/index';
import { declareProviders, useService } from '../src/inversify';
defineProps({
msg: String,
Expand Down
2 changes: 1 addition & 1 deletion tests/test1/DemoComp.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { DemoService } from './DemoService';
import { declareProviders, useService } from '../../src/index';
import { declareProviders, useService } from '../../src/inversify';
defineProps({
msg: String,
Expand Down
2 changes: 1 addition & 1 deletion tests/test1/DemoService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { computed } from 'vue';
import { postReactive } from '../../src/index';
import { postReactive } from '../../src/inversify';

export class DemoService {
public count = 1;
Expand Down
2 changes: 1 addition & 1 deletion tests/test10/DemoComp.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { useService, useRootService } from '../../src/index';
import { useService, useRootService } from '../../src/inversify';
import { DemoService } from './DemoService';
import { OtherService } from './OtherService';
Expand Down
Loading

0 comments on commit c183c3f

Please sign in to comment.