依赖注入 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 前端开发 安全
Vue 3 + TypeScript 现代前端开发最佳实践(2025版指南)
每日激励:“如果没有天赋,那就一直重复”。我是蒋星熠Jaxonic,一名执着于代码宇宙的星际旅人。用Vue 3与TypeScript构建高效、可维护的前端系统,分享Composition API、状态管理、性能优化等实战经验,助力技术进阶。
Vue 3 + TypeScript 现代前端开发最佳实践(2025版指南)
|
11月前
|
JavaScript 前端开发 编译器
Vue与TypeScript:如何实现更强大的前端开发
Vue.js 以其简洁的语法和灵活的架构在前端开发中广受欢迎,而 TypeScript 作为一种静态类型语言,为 JavaScript 提供了强大的类型系统和编译时检查。将 Vue.js 与 TypeScript 结合使用,不仅可以提升代码的可维护性和可扩展性,还能减少运行时错误,提高开发效率。本文将介绍如何在 Vue.js 项目中使用 TypeScript,并通过一些代码示例展示其强大功能。
446 22
|
JavaScript 前端开发 安全
在众多的测试工具中,Cypress以其强大的端到端测试能力和与TypeScript的完美结合,成为了前端开发者的首选
【6月更文挑战第11天】Cypress结合TypeScript,打造前端测试新体验。TypeScript增强代码可读性和稳定性,Cypress提供强大端到端测试,二者结合提升测试准确性和可靠性。通过类型定义、自定义命令和断言,优化测试代码;Cypress模拟真实用户操作、时间旅行功能及内置调试工具,确保应用功能性能。推荐前端开发者使用TypeScript+Cypress进行端到端测试。
329 2
|
前端开发 JavaScript 开发者
前端项目代码规范工具 (ESLint. Prettier. Stylelint. TypeScript)
前端项目代码规范工具 (ESLint. Prettier. Stylelint. TypeScript)
965 4
|
JavaScript 前端开发 安全
2024年前端开发新趋势:TypeScript、Deno与性能优化
2024年前端开发迎来新趋势:TypeScript 5.0引入装饰器正式支持、const类型参数及枚举改进;Deno 1.42版推出JSR包注册表、增强Node.js兼容性并优化性能;性能优化策略涵盖代码分割、懒加载及现代构建工具的应用。这些变化推动前端开发向更高效率和安全性发展。
|
开发者 自然语言处理 存储
语言不再是壁垒:掌握 JSF 国际化技巧,轻松构建多语言支持的 Web 应用
【8月更文挑战第31天】JavaServer Faces (JSF) 框架提供了强大的国际化 (I18N) 和本地化 (L10N) 支持,使开发者能轻松添加多语言功能。本文通过具体案例展示如何在 JSF 应用中实现多语言支持,包括创建项目、配置语言资源文件 (`messages_xx.properties`)、设置 `web.xml`、编写 Managed Bean (`LanguageBean`) 处理语言选择,以及使用 Facelets 页面 (`index.xhtml`) 显示多语言消息。通过这些步骤,你将学会如何配置 JSF 环境、编写语言资源文件,并实现动态语言切换。
269 1
|
JavaScript 前端开发 安全
【技术革新】Vue.js + TypeScript:如何让前端开发既高效又安心?
【8月更文挑战第30天】在使用Vue.js构建前端应用时,结合TypeScript能显著提升代码质量和开发效率。TypeScript作为JavaScript的超集,通过添加静态类型检查帮助早期发现错误,减少运行时问题。本文通过具体案例展示如何在Vue.js项目中集成TypeScript,并利用其类型系统提升代码质量。首先,使用Vue CLI创建支持TypeScript的新项目,然后构建一个简单的待办事项应用,通过定义接口描述数据结构并在组件中使用类型注解,确保代码符合预期并提供更好的编辑器支持。
339 1
|
JavaScript 前端开发 编译器
TypeScript:一场震撼前端开发的效率风暴!颠覆想象,带你领略前所未有的编码传奇!
【8月更文挑战第22天】TypeScript 凭借其强大的静态类型系统和丰富的工具支持,已成为前端开发的优选语言。它通过类型检查帮助开发者早期发现错误,显著提升了代码质量和维护性。例如,定义函数时明确参数类型,能在编译阶段捕获类型不匹配的问题。TypeScript 还提供自动补全功能,加快编码速度。与 Angular、React 和 Vue 等框架的无缝集成进一步提高了开发效率,使 TypeScript 成为现代前端开发中不可或缺的一部分。
241 1
|
开发框架 JSON 缓存
基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理
基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一的表单编辑和表单详情查看处理
|
开发框架 前端开发 JavaScript
基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面
基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

热门文章

最新文章

  • 1
    前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
    984
  • 2
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(九):强势分析Animation动画各类参数;从播放时间、播放方式、播放次数、播放方向、播放状态等多个方面,完全了解CSS3 Animation
    420
  • 3
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(八):学习transition过渡属性;本文学习property模拟、duration过渡时间指定、delay时间延迟 等多个参数
    329
  • 4
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(七):学习ransform属性;本文学习 rotate旋转、scale缩放、skew扭曲、tanslate移动、matrix矩阵 多个参数
    301
  • 5
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(六):全方面分析css的Flex布局,从纵、横两个坐标开始进行居中、两端等元素分布模式;刨析元素间隔、排序模式等
    421
  • 6
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(五):背景属性;float浮动和position定位;详细分析相对、绝对、固定三种定位方式;使用浮动并清除浮动副作用
    608
  • 7
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(四):元素盒子模型;详细分析边框属性、盒子外边距
    770
  • 8
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(三):元素继承关系、层叠样式规则、字体属性、文本属性;针对字体和文本作样式修改
    213
  • 9
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(二):CSS伪类:UI伪类、结构化伪类;通过伪类获得子元素的第n个元素;创建一个伪元素展示在页面中;获得最后一个元素;处理聚焦元素的样式
    621
  • 10
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(一):CSS发展史;CSS样式表的引入;CSS选择器使用,附带案例介绍
    380