前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端需要知道的 依赖注入(Dependency Injection, DI)

前端需要知道的 依赖注入(Dependency Injection, DI)

作者头像
IMWeb前端团队
发布2017-12-29 14:55:03
1.9K0
发布2017-12-29 14:55:03
举报
文章被收录于专栏:IMWeb前端团队IMWeb前端团队

前端需要知道的 依赖注入(Dependency Injection, DI)

1. 前言

XX库实现了依赖注入,哇塞,好牛X呀~~~

切,依赖注入的实现那么简单,不就一个map + 函数参数解析而已吗?

可是,你真的了解 依赖注入(Dependency Injection, DI) 吗?

本文将详细解释什么是依赖注入,并解释属于前端的依赖注入

注意

  1. 本文专门为前端同学解释什么是依赖注入,文中例子也是js,非前端同学可以选择绕道
  2. 已经知道依赖注入的同学也可以绕道

2. 什么是 依赖注入

2.1. 它是模式

首先,依赖注入是一个设计模式,因为它解决的是一类问题

2.2. 理解它的作用域

要知道依赖注入是解决什么问题,最好先了解一个原则:

依赖倒转原则(Dependence Inversion Priciple, DIP)提倡:

  1. 高层模块不应该依赖低层模块。两个都应该依赖抽象
  2. 抽象不应该依赖细节,细节应该依赖抽象
  3. 针对接口编程,不要针对实现编程

在编程时,我们对系统进行模块化,它们之间有依赖,比如模块A依赖模块B

那么依据DIP,模块A应该依赖模块B的接口,而不应该依赖模块B的实现

这样做的好处就不详叙了

下图描述了这个关系图:

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

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

也就是说,模块A在【运行时】需要有一个接口的实现模块作为它的属性

那么这个实现模块怎么来?它是怎么初始化,然后怎么传给模块A的?

解决这个问题的就是依赖注入,这就是它的作用域

上面的结构图再扩展一下就是非常著名的设计模式——桥接

2.3. 前端的依赖注入

对于前端来说,很少有抽象,更别说有接口了

但是,依赖注入却是一直都存在,只是许多同学没有认出来而已

下面来看看前端最常见的一个依赖注入:

代码语言:javascript
复制
// moduleA.js
define('moduleA', ['moduleB'], function(moduleB) {
    return {
        init: function() {
            this.I_need = ModuleB.someFun();
        }
    };
});

这是个很普通的代码,太正常了,我们每天都会写这些代码,即使define包裹可能是构建帮我们写的

还记得前面说的依赖注入的作用域,它只做两件事:

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

这个时候应该知道了,define就是做这些事的:

  1. 它负责初始化moduleB
  2. 它通过函数参数的形式注入到moduleA里面去

3. 依赖注入的作用

为什么需要依赖注入?它的作用和意义是什么?

关于这个,我们还是要从依赖注入做了什么事来探索:

1. 初始化被依赖的模块

如果不通过依赖注入模式来初始化被依赖的模块,那么就要依赖模块自己去初始化了

那么问题来了:依赖模块就耦合了被依赖模块的初始化信息了

2. 注入到依赖模块中

被依赖模块已经被其他管理器初始化了,那么依赖模块要怎么获取这个模块呢?

有两种方式:

  1. 自己去问
  2. 别人主动给你

没用依赖注入模式的话是1,用了之后就是2

想想,你需要某个东西的时候,你去找别人要,你需要提供别人什么信息?

最简单的就是那个东西叫什么,是的,正式一点,你需要一个名称

没错,方式1的问题是:依赖模块耦合了被依赖模块的【名称】还有那个【别人】

而方式2解决了这个问题,让依赖模块只依赖需要的模块的接口

可以看到,注入的两个方式的主动权是相反的 因此,依赖注入(Dependency Injection, DI) 有时候也被称为 控制反转(Inversion of Control, IoC) 它们不是一个东西,有兴趣的同学可以深入学习

3.1. 代码解释

文字比较抽象,那么我们用代码来说明依赖注入的作用以及好处

代码语言:javascript
复制
// config.js
require.config = {
    path: {
        jquery: 'common/jquery'
    }
};

// moduleA.js
define('moduleA', ['jquery'], function($) {
    return {
        init: function() {
            this.$dom = $('#id');
        }
    };
});

用过模块加载器的都知道,一般我们可以配置怎样去获取模块的定义,也就是模块的实现代码

一般是通过配置文件的形式

上面的代码很简单,moduleA依赖了jquery库,在模块加载器中,我们配置了jquery模块在哪里初始化

可以看到,jquery模块的代码是在本地的

现在,不管什么原因,我们想要使用一个线上代码库版本的jquery,怎么办?简单:

代码语言:javascript
复制
// config.js
require.config = {
    path: {
        jquery: 'http://path/to/online/jquery'
    }
};

可以看到,我们只需要修改模块加载器的配置就可以了

这个配置就是被依赖模块(jquery)的初始化信息

这个就是依赖注入的第一个好处:依赖模块与被依赖模块的初始化信息解耦

这个例子也是很常见的代码:

代码语言:javascript
复制
// moduleA.js
var $ = require('jquery');

module.exports = {
    init: function() {
        this.$dom = $('#id');
    }
};

聪明的同学已经看到问题在哪里了,没错,这个模块依赖了被依赖模块的名字

这里会有两个问题:

  1. 模块重名问题,还记得那些年我们给模块起名字的日子吗?
  2. 改变模块依赖方式

像jquery这种库,有许多都是最先加载,并且全局使用的:

代码语言:javascript
复制
// moduleA.js
module.exports = {
    init: function() {
        this.$dom = $('#id');
    }
};

对于这种情况,我们的组件代码就得改动了

不同的模块依赖方式给通用组件的实现造成了很大的困扰

为了不改动组件代码,通常我们这样做:

代码语言:javascript
复制
// jquery.js
module.exports = window.$;

当然,这是题外话了

从上面的例子应该可以知道,依赖注入帮助我们解决了依赖模块对被依赖模块的初始化解耦

4. 依赖注入模式的实现细节

4.1. 组件容器(模块管理器)

一般依赖注入模式都实现在某个容器中,在前端我们可以管它为模块管理器

组件容器负责管理所有的组件,管理他们的初始化,以及依赖,并提供接口获取组件

通常容器会把组件的初始化信息聚集在某个配置文件中,比如xml文件或者json文件等

这样做的好处是可以很轻易的修改组件的初始化信息,并且可以实现组件的热启动

对于前端来说,模块管理器,比如requireJs,就是负责模块的初始化工作的

但是模块加载器的重心不是依赖注入

因此这里提供一个依赖注入容器的简单例子:

代码语言:javascript
复制
// injector
// APP Instance -- Global & Singleton
var injector = {
    set: function(name, factory) {
        // name: the dependency name
        // factory: can be a factory function
        //          or just a value
    },
    get: function(name) {}
};

// a.js
injector.set('env', 'dev');

// b.js
injector.set('b', function() {
    return {
        sayYes: function() {
            console.log('Yes!');
        },
        sayNo: function() {
            console.log('No!');
        }
    };
});

// c.js
injector.set('c', function(env, b) {
    if (env === 'dev') {
        b.sayYes();
    } else {
        b.sayNo();
    }
});

实现起来并没有难点,injector其实就只是个map

用factory函数的好处是可以延迟模块的初始化

另外一个难点是要读取函数的形参名,但是我们也可以这样改来避开这个难点:

代码语言:javascript
复制
// injector
var injector = {
    set: function(name, array) {
        // name: the dependency name
    },
    get: function(name) {}
};

// c.js
injector.set('c', ['env', 'b', function(env, b) {
    if (env === 'dev') {
        b.sayYes();
    } else {
        b.sayNo();
    }
}]);

4.2. 初始化

可以看到模块管理器实际上只是一个容器

现在我们需要一个初始化模块,下面提供一个小栗子:

代码语言:javascript
复制
// initializer.js
function initializer() {
    // to load the module in initializer.config
}

initializer.config = {
    initList: ['./a.js', './b.js', 'http://path/to/other/module.js'],
    map: {
        'jquery': 'http://path/to/online/jquery.js'
    }
};

initializer();

可以看到,如果文件内容本身就有注册模块的代码的话,initializer只需要加载js文件即可,比如上面的a.js和b.js文件

当然也可以加载线上资源

如果文件内容没有注册模块的代码的话,就需要initializer自己帮忙注册了

比如栗子中的jquery

如果系统是服务器端的nodejs代码的话,就可以实现模块的热插拔了

4.3. 注入方式

被依赖模块怎样赋值给依赖模块,主要有三种方式

4.3.1. 构造函数注入

前面define和angular的依赖注入都是使用构造函数的注入方式,如下:

代码语言:javascript
复制
// define
define('moduleA', ['moduleB'], function(moduleB) {
    return {
        init: function() {
            this.I_need = ModuleB.someFun();
        }
    };
});

// anguler
someModule.controller('MyController', ['$scope', 'greeter', function($scope, greeter) {
  // ...
}]);
4.3.2. setter注入

直接上例子:

代码语言:javascript
复制
// moduleA.js
var moduleA = {
    do: function() {
        this.helper.doSomething();
    },
    setHelper: function(helper) {
        this.helper = helper;
    }
};

// initializer.js
function initializer() {
    // ...
    moduleA.setHelper(new Helper());
}
4.3.3. 接口注入

接口注入主要是把注入过程抽象成接口的形式,让注入方式可以被轻易扩展

在前端并不怎么使用接口,因此这种注入方式就不详述

5. 对比——服务定位模式 (Service Locator, SL)

读者可能对服务定位模式不太了解,但是看了下面的代码就知道了

代码语言:javascript
复制
var fs = require('fs');
var path = require('path');
var moduleB = require('./moduleB');
var moduleC = require('path/to/moduleC');

没错,require就是一个服务定位模式

所谓的服务定位模式就是把所有服务(模块)资源的管理都放到一个定位者那里

所有需要服务的模块都找它要就行了,就是这么简单

服务定位模式也能解决依赖注入的作用域问题

服务定位者负责初始化服务,它也提供服务资源

只是依赖注入是被动,服务定位模式需要模块自己主动去请求,详见【3. 依赖注入的作用】

对于前端来说,

服务定位模式肯定更常见,它的优点就是简单,缺点是所有模块都需要依赖定位者

依赖注入模式的优点是控制反转,更利于组件化,缺点是不是前端的基础能力(谁让require是基础。。。)

6. 结语

依赖注入模式并不神秘,也不是什么高大上

Java时代的Spring就已经把依赖注入推向顶峰

本文只想向前端同学传达:依赖注入的思想非常值得学习

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前端需要知道的 依赖注入(Dependency Injection, DI)
    • 1. 前言
      • 2. 什么是 依赖注入
        • 2.1. 它是模式
        • 2.2. 理解它的作用域
        • 2.3. 前端的依赖注入
      • 3. 依赖注入的作用
        • 3.1. 代码解释
      • 4. 依赖注入模式的实现细节
        • 4.1. 组件容器(模块管理器)
        • 4.2. 初始化
        • 4.3. 注入方式
      • 5. 对比——服务定位模式 (Service Locator, SL)
        • 6. 结语
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档