背景

近几年,前端应用(WebApp)正朝着大规模方向发展,在这个过程中我们会对项目拆解成多个模块/组件来组合使用,以此提高我们代码的复用性,最终提高研发效率。

在编写一个复杂组件的时候,总会依赖其他组件来协同完成某个逻辑功能。组件越复杂,依赖越多,可复用性就越差,我们可以借助软件工程中优秀的编程理念来提高复杂组件的可复用性,以下将详述其中之一的依赖倒置理念。

什么是 IoC

IoC 全称 Inversion of Control,中文术语为依赖倒置(反转),包含两个准则:

  1. 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。

  2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象

其背后的核心思想还是:面向接口编程。

我们用一个例子来说明:我们要实现一个列表 A,能够加载一系列的信息并展示,

于是很自然的我们遵守职责单一功能,将展示和加载两个逻辑拆分成2个类:

// Loader.js
export default class Loader {
   constructor(url) {
       this.url = url;
   }    async load() {
       let result = await fetch(this.url);
       return result.text();
   }
} // List.js
import Loader from './Loader';
export default class List {
   constructor(container) {
       this.container = container;
       this.loader = new Loader('list.json');
   }    async render() {
       let items = await this.loader.load();
       this.container.textContent = items;
   }
} // main.js
import List from './List';
let list = new List(document.getElementById('a'));
List.render();

列表 A 很快开发完毕,于是你要继续开发下一个列表 B,B 的功能和 A 类似,也是加载数据展示数据,区别在于 B 的数据来源是一个第三方的服务,他们提供一个 js sdk 给你调用能够返回数据信息。很自然的我们想到 A 的展示逻辑是可以复用的,对于数据加载这个逻辑我们重新实现一个 ThirdLoader 来专门加载第三方服务就是了,但回到 List 模块,我们发现在其构造函数中写死了对 Loader 的依赖:this.loader = new Loader(‘/list’); 导致无法对 List 设置第三方数据加载逻辑。这个问题就在于 List 依赖了具体的实现而不是依赖一个 Loader 接口。

IoC 正是解决这一类问题的最佳良药,我们再回顾 IoC 的两条准则,看看如何利用 IoC 理念解决这类问题:

1. 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象

上述代码中,列表模块是高层次的模块,Loader 是低层次模块, 高层次的 List 依赖了低层次的 Loader,违背了该准则。好在准则也提供了解决方案:应该依赖于抽象。那什么是抽象? 放在我们编程语言中正是广为周知的接口,放在 JS 语言中,接口则是隐式的。

我们正好实践下该准则:

  1. 我们定义一个隐式的接口 ILoader,ILoader 声明了一个 load 方法,该方法签名是返回一个包含请求结果的 Promise。

  2. 将 List 模块对 Loader 模块的依赖调整为对 ILoader 接口的依赖:我们在 List 模块中移除对 Loader 模块的依赖(即移除 import 语句),同时构造函数中增加一个参数,该参数是一个实现了 ILoader 接口的实例。

    // List.js
    export default class List {
       constructor(container, loader) {
           this.container = container;
           this.loader = loader;
       }    async render() {
           this.container.textContent = await this.loader.load();
       }
    }
  3. 为了完成列表 A 的功能,我们还要改造 main.js,将实现了 ILoader 的 Loader 模块实例化传递给 List 模块:

    // main.js
    import List from './List';
    import Loader from './Loader';
    let list = new List(document.getElementById('a'), new Loader('list.json'));
    list.render();

至此,我们完成了对 List 模块的一次改造,List 从对具体实现 Loader 的依赖变成了对抽象接 口 ILoader 的依赖,而 List 模块中对 Loader 模块的导入和实例化过程转移到了 main.js, 这一过程就是我们的依赖倒置,依赖创建的控制权交给了外部(main.js),而在 main.js 中查找创建依赖并将依赖传递给 List 模块的这一过程我们称之为依赖注入(Denpendency Injection)。

我们再来看看 IoC 的第二个准则:抽象不应该依赖于具体实现,具体实现应该依赖于抽象,我们的 ILoader 接口显然不会依赖于任何具体实现,而 Loader 这个具体实现了依赖于 ILoader 接口,完全符合了 IoC 的第二个准则。

原有系统的依赖关系图转变结果如下:

aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC" alt="" data-src="http://mmbiz.qpic.cn/mmbiz_jpg/btsCOHx9LAOic8eiasWZ6HiaHJ6HkYoLBnyjRqhRLzKy7246r2C5xUFkELcib3bR2ibZgYml1RIE6llaHhDvZ4QpaibQ/0?wx_fmt=jpeg" data-type="jpeg" data-ratio="0.2672" data-w="1250" />

原有系统的依赖关系图

基于新的依赖架构,List 模块具备了设置不同数据加载逻辑的能力,现在我们可以复用 List 模块再实现列表 B 的 数据加载逻辑并在 main 中组装即可完成列表 B 的功能:

// ThirdLoader.js
import {request} from '../third/sdk';
export default class ThirdServiceLoader {
   async load() {
       return request();
   }
} // main.js
import List from './List';
import Loader from './Loader';
import ThirdLoader from './ThirdLoader';
let listA = new List(document.getElementById('a'), new Loader('list.json'));
listA.render(); let listB = new List(document.getElementById('b'), new ThirdLoader());
listB.render();

最终的一个依赖关系图如下:

aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkJDQzA1MTVGNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkJDQzA1MTYwNkE2MjExRTRBRjEzODVCM0Q0NEVFMjFBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QkNDMDUxNUQ2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QkNDMDUxNUU2QTYyMTFFNEFGMTM4NUIzRDQ0RUUyMUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6p+a6fAAAAD0lEQVR42mJ89/Y1QIABAAWXAsgVS/hWAAAAAElFTkSuQmCC" alt="" data-src="http://mmbiz.qpic.cn/mmbiz_jpg/btsCOHx9LAOic8eiasWZ6HiaHJ6HkYoLBnyPX5Fce8tnQW64dIR8QmmHAaKPUYoYoMUDBNdyAOL8clpn2LibzrJoibw/0?wx_fmt=jpeg" data-type="jpeg" data-ratio="0.3333333333333333" data-w="1026" />

最终依赖关系图

至此我们上面演示了应用 IoC 理念对高层模块的一个依赖架构改造,提高了高层模块的可复用性。

IoC 小结

总结我们最开始遇到的问题:类 A 直接依赖类 B,假如要将类 A 改为依赖类 C,则必须通过修改类 A 的代码来达成。这种场景下,类 A 一般是高层模块,负责复杂的业务逻辑;类 B 和类 C 是低层模块,负责基本的原子操作;假如修改类 A,会给程序带来不必要的风险。

IoC 解决方案:将类 A 修改为依赖接口 I,类 B 和类 C 各自实现接口 I,类 A 通过接口 I 间接与类 B 或者类 C 发生联系,则会大大降低修改类 A 的几率。

利用 uioc 框架简化依赖注入过程

在上一节中我们抽象了 List 中的数据加载逻辑,而依赖注入这一过程转移到了应用的入口文件 main.js 中,这也导致了我们需要在 main.js 中手动创建并组装各个依赖,随着项目规模的增加,依赖数量必然也是成规模上升,手动组装模块显然是一件繁琐的事;再加上模块对依赖注入的方式,依赖的创建方式,依赖的实例数量等都有多方面的需求,于是就有了 IoC 框架来帮助我们解决这些问题,简化依赖注入这个过程,最终让业务开发者精力集中在业务逻辑层。

接下来我要安利一下如何利用我们开发的 uioc 框架来实现依赖注入,在这之前先介绍一下 uioc 中的一些术语:

  • 组件:是完成一项或一系列功能的集合,对外提供相关功能的接口。

  • 注册组件:即声明一个组件如何创建(类,工厂方法),创建时需要什么样的依赖,创建完毕后又需要哪些依赖,组件是单例的还是多例的。

  • 获取组件:IoC 容器根据组件的注册信息创建并返回组件的过程。

整个应用在 IoC 中就被看成是一系列组件的组装和获取调用:注册组件 -> 获取组件 -> 调用组件方法完成业务逻辑,基于以上概念我们来着手改造前面的例子:

  1. 安装 uioc:

    npm install uioc --save
  2. 新建一个 config.js 文件用来存放组件的注册信息

    // config.js
    import List from './List';
    import Loader from './Loader';
    import ThirdLoader from './ThirdLoader' export default {
       listA: {
           creator: List,
           args: [document.getElementById('a'), {$ref: 'loader'}]
       },
       listB: {
           creator: List,
           args: [document.getElementById('b'), {$ref: 'thirdLoader'}]
       },
       loader: {
           creator: Loader,
           args: ['list.json']
       },
       thirdLoader: {
           creator: ThirdLoader
       }
    };

    上面的代码我们声明了4个组件配置:listA, listB, loader, thirdLoader。 其中组件配置中的 creator 表示创建组件的类,组件 listA, listB 对应的 creator 都是 List,那如何给 listA 和 listB 分别注入不同的 loader 实现呢?这个工作由 args 配置来完成,args 是一个数组,表示要传递给组件构造函数的参数配置。

    args 中的第一个元素是一个 dom 节点,作为 List 的容器;第二元素中使用了 $ref 关键字,其作用是声明该参数是一个组件,$ref 对应的值则是组件名称。 listA 第二个参数为 loader 组件,listB 则是 thirdLoader。

    在获取组件时,uioc 会先创建该关键字声明的组件,接着调用 creator 将 dom 节点和 $ref 对应的组件按其在 args 声明的顺序作为参数传入。

  3. 最后我们在 main.js 中实例化一个 IoC 容器并传入组件注册信息,最后通过容器获取组件并渲染页面:

    // main.js
    import {IoC} from 'uioc';
    import config from './config'; // 实例化 IoC 容器
    let ioc = new IoC(config); // 获取应用初始化需要的组件
    ioc.getComponent(['listA', 'listB']).then(([listA, listB]) => {
       listA.render();
       listB.render();
    });

至此我们借助了 uioc 对原应用进行了依赖注入的改造,通过配置化的方式完成了整个应用的组装。

上述示例中完整的代码可以查看:https://github.com/ecomfe/uioc/tree/develop/examples/simple-es6-module

事实上 uioc 还提供了更多强大的功能:

  1. 结合前端 AMD Loader 的异步组件模块加载

  2. 多种注入方式

  3. 实例生命周期管理

  4. aop 支持

  5. 多种依赖类型支持

  6. 不同的组件创建方式

  7. 插件机制

细节参考:https://github.com/ecomfe/uioc/wiki/Index

正文END

前端 IoC 理念入门的更多相关文章

  1. 从前端中的IOC理念理解koa中的app.use()

    忙里偷闲,打开平时关注的前端相关的网站,浏览最近最新的前端动态.佼佼者,平凡的我做不到,但还是要争取不做落后者. 前端中的IoC理念,看到这个标题就被吸引了.IoC 理念,不认识呢,点击去一看,果然没 ...

  2. Spring中IoC的入门实例

    Spring中IoC的入门实例 Spring的模块化是很强的,各个功能模块都是独立的,我们可以选择的使用.这一章先从Spring的IoC开始.所谓IoC就是一个用XML来定义生成对象的模式,我们看看如 ...

  3. 前端之Android入门(3):MVC模式(上)

    很多Android的入门书籍,在前面介绍完布局后就会逐个介绍组件,然后开始编写组件使用的例子.每每到此时小伙伴们都可能会有些疑问:是否应该先啃完一本<Java编程思想>学点 Java 知识 ...

  4. gulp 前端构建工具入门

    gulp 前端构建工具入门 标签(空格分隔): gulp 1. 安装gulp npm i -g gulp 2. 创建gulp项目 2.1 Hello world 使用npm init初始化项目文件夹. ...

  5. web前端怎么样才能入门

    web前端怎么样才能入门,首先我们要从什么是初级web前端工程师说起: 按照我的想法,我把前端工程师分为了入门.初级.中级.高级这四个级别: 入门级别指的是了解什么是前端(前端到底是什么其实很多人还是 ...

  6. 前端中的 IoC 理念

    背景 前端应用在不断壮大的过程中,内部模块间的依赖可能也会随之越来越复杂,模块间的 低复用性 导致应用 难以维护,不过我们可以借助计算机领域的一些优秀的编程理念来一定程度上解决这些问题,接下来要讲述的 ...

  7. 前端神器avalonJS入门(一)

    转自:http://www.cnblogs.com/vajoy/p/4063824.html avalonJS是司徒正美开发和维护的前端mvvm框架,可以轻松实现数据的隔离和双向绑定,相比angula ...

  8. 2015年最热门前端框架React 入门实例教程

    现在最热门的前端框架,毫无疑问是 React . 上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑. React 起源于 Face ...

  9. .Net IOC框架入门之一 Unity

    一.概述 IOC:英文全称:Inversion of Control,中文名称:控制反转,它还有个名字叫依赖注入(Dependency Injection). 作用:将各层的对象以松耦合的方式组织在一 ...

随机推荐

  1. 查看系统分区df,查看、设置、修改、删除ACL权限

    df [root@localhost ~]# df -h 文件系统 容量 已用 可用 已用% 挂载点 /dev/sda5 16G 1.4G 15G 9% / devtmpfs 479M 0 479M ...

  2. iOS APP内购

    看到网上文章一大把,看了这个觉得挺不错的,谢谢 iOS大全 公众平台; 原文:http://mp.weixin.qq.com/s?__biz=MzAxMzE2Mjc2Ng==&mid=2652 ...

  3. Kafka+Storm写入Hbase和HDFS

    1.Storm整合Kafka 使用Kafka作为数据源,起到缓冲的作用 // 配置Kafka订阅的Topic,以及zookeeper中数据节点目录和名字 String zks = KafkaPrope ...

  4. bat获取系统时间修改系统密码

    @echo off %1 mshta vbscript:CreateObject("Shell.Application").ShellExecute("cmd.exe&q ...

  5. 用原生实现点击删除点击的li

    简单的实现方式 <!DOCTYPE html> <html> <head> <title></title> <meta charset ...

  6. 我的第一个python web开发框架(19)——产品发布相关事项

    好不容易小白将系统开发完成,对于发布到服务器端并没有什么经验,于是在下班后又找到老菜. 小白:老大,不好意思又要麻烦你了,项目已经弄完,但要发布上线我还一头雾水,有空帮我讲解一下吗? 老菜:嗯,系统上 ...

  7. Android 内存暴减的秘密?!

    作者:杨超,腾讯移动客户端开发 工程师  商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. WeTest 导读 在我这样减少了26.5M Java内存! 一文中内存优化一期已经告一段落, ...

  8. Sql Server 里的向上取整、向下取整、四舍五入取整的实例!

    http://blog.csdn.net/dxnn520/article/details/8454132 =============================================== ...

  9. 测试库的接收到的数据是否完整(jrtplib为列)

    最近使用jrtplib来接收RTP包,然后解码播放 发现解码出来的是绿屏,马赛克 于是开始排查 首先直接用wireshark抓进来的包,转为可以被vlc播放的文件 操作如下 http://blog.c ...

  10. C#设计模式之二十三解释器模式(Interpreter Pattern)【行为型】

    一.引言   今天我们开始讲"行为型"设计模式的第十一个模式,也是面向对象设计模式的最后一个模式,先要说明一下,其实这个模式不是最后一个模式(按Gof的排序来讲),为什么把它放在最 ...