依赖注入 in 前端 && Typescript 实现依赖注入

简介: ## 背景 最近因为工作需要,研究了vscode的代码,看文档时发现其中提到了Dependency Injection,并且在类的构造函数中看到了这样的写法。 ``` javascript constructor( id: string, @IMessageService messageService: IMessageService, @IStorageService s

背景

最近因为工作需要,研究了vscode的代码,看文档时发现其中提到了Dependency Injection,并且在类的构造函数中看到了这样的写法。

constructor(
        id: string,
        @IMessageService messageService: IMessageService,
        @IStorageService storageService: IStorageService,
        @ITelemetryService telemetryService: ITelemetryService,
        @IContextMenuService contextMenuService: IContextMenuService,
        @IPartService partService: IPartService,
        @IKeybindingService keybindingService: IKeybindingService,
        @IInstantiationService instantiationService: IInstantiationService,
        @IThemeService themeService: IThemeService,
) {
...
}

因为对此依赖注入比较陌生,特地去了解了一番,故有此文章。

注意:此文章从前端角度说明依赖注入,非前端同学可以绕道

什么是依赖注入?

总结的来说,依赖注入是一种设计模式,因为它解决的是一类问题,这类问题是与依赖相关的。

依赖倒转原则

要知道依赖注入是解决什么问题,我们需要先了解一个原则:依赖倒转原则。
这是设计模式的六大原则之一,其核心是面向接口编程。
它要求我们组织代码时:

  • 高层模块不应该依赖低层模块。两个都应该依赖抽象
  • 抽象不应该依赖细节
  • 细节应该依赖抽象
    意思是我们编程时,对系统进行模块化,两个模块之间有依赖,例如模块A依赖模块B,那么根据依赖倒转原则,我们开发时,模块A应该依赖模块B的接口,而不是依赖模块B的实现。

下图描述了此原则提倡的关系:
Fhe152elkbDvQo8SPbpByySnszgN.jpg

注意:虽然模块A只依赖接口编程,但在运行的时候,它还是需要有一个具体的模块来负责模块A需要的功能的,所以模块A在【运行时】是需要一个【真的】模块B,而不是它的接口。

所以上图中,Module和Interface之间的线是包含,而不是关联。

前端中的依赖注入

对前端来说,一般较少有抽象,如果不是使用Typescript的话,就更少接触接口了。

但是,依赖注入却是一直都存在,只是许多同学没有认出来而已。
看下面这个栗子,是前端普遍存在的依赖注入

// moduleA.js
define('moduleA', ['moduleB'], function(moduleB) {
    return {
        init: function() {
            this.I_need = ModuleB.someFun();
        }
    };
});

简单来说,依赖注入就做两件事情

  1. 初始化被依赖的模块
  2. 注入到依赖模块中

再看一次栗子:
一般来说,在开发时,我们需要一个对象的能力,一般是自己去创建一个。
那如果,我需要很多个对象,而我需要的对象又依赖了其他对象,这时我是不是得对每个对象都创建一遍。

假设对象 A 依赖 B , C , D, 同时 B 又依赖 E。
如图:
v2-2830636e440e6aa862dbd31f2bf33f45_hd.jpg

// B 依赖 E
class B {
    public constructor(public e: E) {
    }
}
class C {

}
class D {

}
class E {

}
// A 依赖 B,C,D
class A {
    constructor(public b: B, public c: C, public d: D) {

    }
}

创建一个a的实例:

var e = new E();
var b = new B(e);
var c = new C();
var d = new D();
var a = new A(b,c,d);

可以看到,为了创建类A的一个实例,我们需要先创建 B,C,D 的实例,而为了创建一个B的实例,我们又需要创建一个E的实例。如果依赖的e模块做了改变,构造时需要多传入一个参数,那么我们的代码也得跟着改变,随着项目发展,工程越来越大,代码将会变得非常不好维护。

这是我们很会自然想到,有没方法使依赖模块与被依赖模块的初始化信息解耦。

实现依赖注入

其实,js实现依赖注入的方式有多种,简单列举一些:

  • 基于Injector、Cache和函数参数名的依赖注入
  • AngularJS中基于双Injector的依赖注入
  • inversify.js——Javascript技术栈中的IoC容器
  • TypeScript中基于装饰器和反射的依赖注入
  • ......

那现在我用在工作中使用的模式来讲,也是基于Typescript的装饰器,但不需要用到反射。
先看下如果使用了依赖注入后,模块A是如何使用其他模块的。

import { instantiationService } from 'instantiationService'
class A {
    constructor(
        @IB b, //模块B
        @IC c, //模块C
        @ID d, //模块D
    ) {
        ...
    }
    public method1(){
        b.xxx() //调用b模块的某个方法
    }
}

let a = instantiationService.createInstance(A)

是不是很简单明了,以后使用A模块的能力时,只需要实例化A(实例化时需要使用封装的方法),就不需要管他依赖的参数了。

那么这是如何实现的呢,下面来看下具体方式。
首先,每个类都需要依照自己的接口来实现,接口需要传入createDecorator,此方法会返回一个装饰器,以B模块为例:

// 模块B
import { createDecorator } from 'instantiation';
const IB = createDecorator<IB>('bService');
// 接口在Typescript中只在编译检查中使用,编译后的代码中不会存在
interface IB {
    _variable:any;
    method1(title: string): void;
        method2(title: string): void;
}

class B implements IB{
    public _variable:any;
    method1(title: string): void{
        ...
    }
    method2(title: string): void{
        ...
    }
}
// instantiation.js

function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
    if (target[_util.DI_TARGET] === target) {
        target[_util.DI_DEPENDENCIES].push({ id, index, optional });
    } else {
        target[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
        target[_util.DI_TARGET] = target;
    }
}

/**
 * 此方法会返回一个装饰器
 */
export function createDecorator<T>(serviceId: string): { (...args: any[]): void; type: T; } {

    if (_util.serviceIds.has(serviceId)) {
        return _util.serviceIds.get(serviceId);
    }

    const id = <any>function (target: Function, key: string, index: number): any {
        if (arguments.length !== 3) {
            throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
        }
        storeServiceDependency(id, target, index, false);
    };

    id.toString = () => serviceId;

    _util.serviceIds.set(serviceId, id);
    return id;
}
// instantiationService.js
class InstantiationService implements IInstantiationService {

    _serviceBrand: any;

    private _services: ServiceCollection;
    private _strict: boolean;

    constructor(services: ServiceCollection = new ServiceCollection(), strict: boolean = false) {
        this._services = services;
        this._strict = strict;

        this._services.set(IInstantiationService, this);
    }

    createChild(services: ServiceCollection): IInstantiationService {
        this._services.forEach((id, thing) => {
            if (services.has(id)) {
                return;
            }
            // If we copy descriptors we might end up with
            // multiple instances of the same service
            if (thing instanceof SyncDescriptor) {
                thing = this._createAndCacheServiceInstance(id, thing);
            }
            services.set(id, thing);
        });
        return new InstantiationService(services, this._strict);
    }

    invokeFunction<R>(signature: (accessor: ServicesAccessor, ...more: any[]) => R, ...args: any[]): R {
        let accessor: ServicesAccessor;
        try {
            accessor = {
                get: <T>(id: ServiceIdentifier<T>, isOptional?: typeof optional) => {
                    const result = this._getOrCreateServiceInstance(id);
                    if (!result && isOptional !== optional) {
                        throw new Error(`[invokeFunction] unkown service '${id}'`);
                    }
                    return result;
                }
            };
            return signature.apply(undefined, [accessor].concat(args));
        } finally {
            accessor.get = function () {
                throw illegalState('service accessor is only valid during the invocation of its target method');
            };
        }
    }

    createInstance<T>(param: any, ...rest: any[]): any {

        if (param instanceof AsyncDescriptor) {
            // async
            return this._createInstanceAsync(param, rest);

        } else if (param instanceof SyncDescriptor) {
            // sync
            return this._createInstance(param, rest);

        } else {
            // sync, just ctor
            return this._createInstance(new SyncDescriptor(param), rest);
        }
    }

    private _createInstanceAsync<T>(descriptor: AsyncDescriptor<T>, args: any[]): TPromise<T> {

        let canceledError: Error;

        return new TPromise((c, e, p) => {
            require([descriptor.moduleName], (_module?: any) => {
                if (canceledError) {
                    e(canceledError);
                }

                if (!_module) {
                    return e(illegalArgument('module not found: ' + descriptor.moduleName));
                }

                let ctor: Function;
                if (!descriptor.ctorName) {
                    ctor = _module;
                } else {
                    ctor = _module[descriptor.ctorName];
                }

                if (typeof ctor !== 'function') {
                    return e(illegalArgument('not a function: ' + descriptor.ctorName || descriptor.moduleName));
                }

                try {
                    args.unshift.apply(args, descriptor.staticArguments()); // instead of spread in ctor call
                    c(this._createInstance(new SyncDescriptor<T>(ctor), args));
                } catch (error) {
                    return e(error);
                }
            }, e);
        }, () => {
            canceledError = canceled();
        });
    }

    private _createInstance<T>(desc: SyncDescriptor<T>, args: any[]): T {

        // arguments given by createInstance-call and/or the descriptor
        let staticArgs = desc.staticArguments().concat(args);

        // arguments defined by service decorators
        let serviceDependencies = _util.getServiceDependencies(desc.ctor).sort((a, b) => a.index - b.index);
        let serviceArgs: any[] = [];
        for (const dependency of serviceDependencies) {
            let service = this._getOrCreateServiceInstance(dependency.id);
            if (!service && this._strict && !dependency.optional) {
                throw new Error(`[createInstance] ${desc.ctor.name} depends on UNKNOWN service ${dependency.id}.`);
            }
            serviceArgs.push(service);
        }

        let firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : staticArgs.length;

        // check for argument mismatches, adjust static args if needed
        if (staticArgs.length !== firstServiceArgPos) {
            console.warn(`[createInstance] First service dependency of ${desc.ctor.name} at position ${
                firstServiceArgPos + 1} conflicts with ${staticArgs.length} static arguments`);

            let delta = firstServiceArgPos - staticArgs.length;
            if (delta > 0) {
                staticArgs = staticArgs.concat(new Array(delta));
            } else {
                staticArgs = staticArgs.slice(0, firstServiceArgPos);
            }
        }
        // now create the instance
        const argArray = [desc.ctor];
        argArray.push(...staticArgs);
        argArray.push(...serviceArgs);

        const instance = create.apply(null, argArray);
        desc._validate(instance);
        return <T>instance;
    }

    private _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>): T {
        let thing = this._services.get(id);
        if (thing instanceof SyncDescriptor) {
            return this._createAndCacheServiceInstance(id, thing);
        } else {
            return thing;
        }
    }

    private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>): T {
        assert.ok(this._services.get(id) instanceof SyncDescriptor);

        const graph = new Graph<{ id: ServiceIdentifier<any>, desc: SyncDescriptor<any> }>(data => data.id.toString());

        function throwCycleError() {
            const err = new Error('[createInstance] cyclic dependency between services');
            err.message = graph.toString();
            throw err;
        }

        let count = 0;
        const stack = [{ id, desc }];
        while (stack.length) {
            const item = stack.pop();
            graph.lookupOrInsertNode(item);

            // TODO@joh use the graph to find a cycle
            // a weak heuristic for cycle checks
            if (count++ > 100) {
                throwCycleError();
            }

            // check all dependencies for existence and if the need to be created first
            let dependencies = _util.getServiceDependencies(item.desc.ctor);
            for (let dependency of dependencies) {

                let instanceOrDesc = this._services.get(dependency.id);
                if (!instanceOrDesc) {
                    console.warn(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`);
                }

                if (instanceOrDesc instanceof SyncDescriptor) {
                    const d = { id: dependency.id, desc: instanceOrDesc };
                    graph.insertEdge(item, d);
                    stack.push(d);
                }
            }
        }

        while (true) {
            let roots = graph.roots();

            // if there is no more roots but still
            // nodes in the graph we have a cycle
            if (roots.length === 0) {
                if (graph.length !== 0) {
                    throwCycleError();
                }
                break;
            }

            for (let root of roots) {
                // create instance and overwrite the service collections
                const instance = this._createInstance(root.data.desc, []);
                this._services.set(root.data.id, instance);
                graph.removeNode(root.data);
            }
        }

        return <T>this._services.get(id);
    }
}

总结

依赖注入(Dependency Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。

参考文章:

目录
相关文章
|
6月前
|
JavaScript 前端开发 安全
在众多的测试工具中,Cypress以其强大的端到端测试能力和与TypeScript的完美结合,成为了前端开发者的首选
【6月更文挑战第11天】Cypress结合TypeScript,打造前端测试新体验。TypeScript增强代码可读性和稳定性,Cypress提供强大端到端测试,二者结合提升测试准确性和可靠性。通过类型定义、自定义命令和断言,优化测试代码;Cypress模拟真实用户操作、时间旅行功能及内置调试工具,确保应用功能性能。推荐前端开发者使用TypeScript+Cypress进行端到端测试。
73 2
|
4月前
|
JavaScript 前端开发 编译器
TypeScript:一场震撼前端开发的效率风暴!颠覆想象,带你领略前所未有的编码传奇!
【8月更文挑战第22天】TypeScript 凭借其强大的静态类型系统和丰富的工具支持,已成为前端开发的优选语言。它通过类型检查帮助开发者早期发现错误,显著提升了代码质量和维护性。例如,定义函数时明确参数类型,能在编译阶段捕获类型不匹配的问题。TypeScript 还提供自动补全功能,加快编码速度。与 Angular、React 和 Vue 等框架的无缝集成进一步提高了开发效率,使 TypeScript 成为现代前端开发中不可或缺的一部分。
42 1
|
4月前
|
JavaScript 前端开发 安全
【技术革新】Vue.js + TypeScript:如何让前端开发既高效又安心?
【8月更文挑战第30天】在使用Vue.js构建前端应用时,结合TypeScript能显著提升代码质量和开发效率。TypeScript作为JavaScript的超集,通过添加静态类型检查帮助早期发现错误,减少运行时问题。本文通过具体案例展示如何在Vue.js项目中集成TypeScript,并利用其类型系统提升代码质量。首先,使用Vue CLI创建支持TypeScript的新项目,然后构建一个简单的待办事项应用,通过定义接口描述数据结构并在组件中使用类型注解,确保代码符合预期并提供更好的编辑器支持。
85 0
|
4月前
|
资源调度 JavaScript 前端开发
Vue3+TypeScript前端项目新纪元:揭秘高效事件总线Mitt,轻松驾驭组件间通信的艺术!
【8月更文挑战第3天】Vue3结合TypeScript强化了类型安全与组件化开发。面对大型应用中复杂的组件通信挑战,可通过引入轻量级事件发射器Mitt实现事件总线模式。Mitt易于集成,通过简单几步即可完成安装与配置:安装Mitt、创建事件总线实例、并在组件中使用`emit`与`on`方法发送及监听事件。此外,利用TypeScript的强大类型系统确保事件处理器正确无误。这种方式有助于保持代码整洁、解耦组件,同时提高应用的可维护性和扩展性。不过,在大规模项目中需谨慎使用,以防事件流过于复杂难以管理。
116 1
|
4月前
|
开发框架 JSON 缓存
基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理
基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理
|
4月前
|
开发框架 前端开发 JavaScript
基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面
基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面
|
4月前
|
开发框架 前端开发 JavaScript
在基于vue-next-admin的Vue3+TypeScript前端项目中,为了使用方便全局挂载对象接口
在基于vue-next-admin的Vue3+TypeScript前端项目中,为了使用方便全局挂载对象接口
|
4月前
|
开发框架 前端开发 JavaScript
在Vue3+TypeScript 前端项目中使用事件总线Mitt
在Vue3+TypeScript 前端项目中使用事件总线Mitt
|
4月前
|
JavaScript 前端开发 安全
解锁Vue3与TypeScript的完美搭档:getCurrentInstance带你高效打造未来前端
【8月更文挑战第21天】Vue3的getCurrentInstance方法作为Composition API的一部分,让开发者能在组件内访问实例。结合TypeScript,可通过定义组件实例类型实现更好的代码提示与类型检查,提升开发效率与代码质量。例如,定义一个带有特定属性(如myData)的组件实例类型,可以在setup中获取并安全地修改这些属性。这种方式确保了一致性和减少了运行时错误,使开发更加高效和安全。
153 0
|
6月前
|
前端开发 JavaScript 安全
微前端架构采用 TypeScript 提升开发效率和代码可靠性
【6月更文挑战第12天】微前端架构采用 TypeScript 提升开发效率和代码可靠性。TypeScript 的类型安全防止了微前端间的类型错误,智能提示与自动补全加速开发,重构支持简化代码更新。通过定义公共接口和使用 TypeScript 编写微前端,确保通信一致性与代码质量。在构建流程中集成 TypeScript,保证构建正确性。总之,TypeScript 在微前端架构中扮演关键角色,推荐用于大型前端项目。
75 4
下一篇
无影云桌面