前端 IoC 理念入门
背景
近几年,前端应用(WebApp)正朝着大规模方向发展,在这个过程中我们会对项目拆解成多个模块/组件来组合使用,以此提高我们代码的复用性,最终提高研发效率。
在编写一个复杂组件的时候,总会依赖其他组件来协同完成某个逻辑功能。组件越复杂,依赖越多,可复用性就越差,我们可以借助软件工程中优秀的编程理念来提高复杂组件的可复用性,以下将详述其中之一的依赖倒置理念。
什么是 IoC
IoC 全称 Inversion of Control,中文术语为依赖倒置(反转),包含两个准则:
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
抽象不应该依赖于具体实现,具体实现应该依赖于抽象
其背后的核心思想还是:面向接口编程。
我们用一个例子来说明:我们要实现一个列表 A,能够加载一系列的信息并展示,
于是很自然的我们遵守职责单一功能,将展示和加载两个逻辑拆分成2个类:
// Loader.js |
列表 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 语言中,接口则是隐式的。
我们正好实践下该准则:
我们定义一个隐式的接口 ILoader,ILoader 声明了一个 load 方法,该方法签名是返回一个包含请求结果的 Promise。
将 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();
}
}为了完成列表 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 |
最终的一个依赖关系图如下:
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 中就被看成是一系列组件的组装和获取调用:注册组件 -> 获取组件 -> 调用组件方法完成业务逻辑,基于以上概念我们来着手改造前面的例子:
安装 uioc:
npm install uioc --save
新建一个 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 声明的顺序作为参数传入。
最后我们在 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 还提供了更多强大的功能:
结合前端 AMD Loader 的异步组件模块加载
多种注入方式
实例生命周期管理
aop 支持
多种依赖类型支持
不同的组件创建方式
插件机制
细节参考:https://github.com/ecomfe/uioc/wiki/Index
正文END
前端 IoC 理念入门的更多相关文章
- 从前端中的IOC理念理解koa中的app.use()
忙里偷闲,打开平时关注的前端相关的网站,浏览最近最新的前端动态.佼佼者,平凡的我做不到,但还是要争取不做落后者. 前端中的IoC理念,看到这个标题就被吸引了.IoC 理念,不认识呢,点击去一看,果然没 ...
- Spring中IoC的入门实例
Spring中IoC的入门实例 Spring的模块化是很强的,各个功能模块都是独立的,我们可以选择的使用.这一章先从Spring的IoC开始.所谓IoC就是一个用XML来定义生成对象的模式,我们看看如 ...
- 前端之Android入门(3):MVC模式(上)
很多Android的入门书籍,在前面介绍完布局后就会逐个介绍组件,然后开始编写组件使用的例子.每每到此时小伙伴们都可能会有些疑问:是否应该先啃完一本<Java编程思想>学点 Java 知识 ...
- gulp 前端构建工具入门
gulp 前端构建工具入门 标签(空格分隔): gulp 1. 安装gulp npm i -g gulp 2. 创建gulp项目 2.1 Hello world 使用npm init初始化项目文件夹. ...
- web前端怎么样才能入门
web前端怎么样才能入门,首先我们要从什么是初级web前端工程师说起: 按照我的想法,我把前端工程师分为了入门.初级.中级.高级这四个级别: 入门级别指的是了解什么是前端(前端到底是什么其实很多人还是 ...
- 前端中的 IoC 理念
背景 前端应用在不断壮大的过程中,内部模块间的依赖可能也会随之越来越复杂,模块间的 低复用性 导致应用 难以维护,不过我们可以借助计算机领域的一些优秀的编程理念来一定程度上解决这些问题,接下来要讲述的 ...
- 前端神器avalonJS入门(一)
转自:http://www.cnblogs.com/vajoy/p/4063824.html avalonJS是司徒正美开发和维护的前端mvvm框架,可以轻松实现数据的隔离和双向绑定,相比angula ...
- 2015年最热门前端框架React 入门实例教程
现在最热门的前端框架,毫无疑问是 React . 上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑. React 起源于 Face ...
- .Net IOC框架入门之一 Unity
一.概述 IOC:英文全称:Inversion of Control,中文名称:控制反转,它还有个名字叫依赖注入(Dependency Injection). 作用:将各层的对象以松耦合的方式组织在一 ...
随机推荐
- ArcGIS API for JavaScript 4.3 与ArcGIS Server联动使用【地图服务】
[前言] 有好些网友问我怎么使用Server发布的地图服务了,其实非常的简单. 我在这里先声明:不提供Server软件,需要的请自行使用互联网搜索资源: 不阐述Server如何发布各各种服务,但是我会 ...
- 点分治X2
1.聪聪可可 点分治板子 然而想那个 t1[1]*t1[2]*2+t1[0]*t1[0]想了好久 就是最基本的组合方法 毕竟(2,5)和(5,2)可是要算两次的 画画图就好了 (不要像我一样盯着大佬们 ...
- vue入坑总结
1.Do not mount Vue to <html> or <body> - mount to normal elements instead. Vue2.x之后不推荐挂载 ...
- 使用 JSON.parse 反序列化 ISO 格式的日期字符串, 将返回Date格式对象
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- [编织消息框架][JAVA核心技术]动态代理应用7-IRpcSend实现
根据设计生成两个接口,IRpcSend send方法返回数据要求包装成QResult对象 public interface IRpcSend { public <T> QResult< ...
- http1.0 的哑代理问题
感觉这就和回字的四种写法一样,并无卵用 原以为http1.1的新特性是提供了keep-alive,后来才知道,keep-alive选项http1.1已经不支持了,http1.1对该功能进行了改版 关于 ...
- Sticky Footer 绝对底部的两种套路
最近面了好几个前端,工作经验有高有低,居然都不知道绝对底部是什么,也没有人能说出一种实现方式,让我不禁感慨前端领域的良莠不齐 绝对底部,或者说 Sticky Footer,是一种古老且经典的页面效果: ...
- jQuery 文档操作方法 (四)
方法 描述 addClass() 向匹配的元素添加指定的类名. after() 在匹配的元素之后插入内容. append() 向匹配元素集合中的每个元素结尾插入由参数指定的内容. appendTo() ...
- CSS Sprites使用
CSS Sprites在国内很多人叫css精灵,是一种网页图片应用处理方式.它允许你将一个页面涉及到的所有零星图片都包含到一张大图中去,这样一来,当访问该页面时,载入的图片就不会像以前那样一幅一幅地慢 ...
- Oracle死锁情况
ORACLE EBS操作某一个FORM界面,或者后台数据库操作某一个表时发现一直出于"假死"状态,可能是该表被某一用户锁定,导致其他用户无法继续操作 复制代码 代码如下: --锁表 ...