前端 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). 作用:将各层的对象以松耦合的方式组织在一 ...
随机推荐
- 对Spring与SpringMVC的理解
Spring 在我的Spring --简介及环境搭建跑通Hello提到关于Spring的基本结构与功能 SpringMVC 先上一张SpringMVC的流程图 Spring MVC 是一个模型 - 视 ...
- laravel and lumen 软删除操作
知识都是有联系的,这绝对是真理.作为一名小白,看了一点官方文档,把我自己理解的软删除操作给大家讲讲.有些就是套用官方文档的话. 定义:什么是软删除呢,所谓软删除指的是数据表记录并未真的从数据库删除,而 ...
- scalajs_初体验
scalajs是将scala编译成js的编译器,目的在于使用scala的众多类库和强类型特征构建出稳定可扩展的js应用. build.sbt构建文件如下: enablePlugins(ScalaJSP ...
- [置顶]
xamarin android Fragment实现底部导航栏
前段时间写了篇关于Fragment的文章,介绍了基础的概念,用静态和动态的方式加载Fragment Xamarin Android Fragment的两种加载方式.下面的这个例子介绍xamarin ...
- ArcGIS API for JavaScript 4.2学习笔记[11] 官方第五章Popups(弹窗)概览与解释
直接跳过第三第四章了,第三章Layer和第四章可视化,怎么说呢,Layer是组织数据的,是Map的属性之一.可视化属于符号化编程,暂时不看. 第五章是对数据.结果的显示,类似于alert()..NET ...
- 写给小白的JAVA链接MySQL数据库的步骤(JDBC):
作为复习总结的笔记,我罗列了几个jdbc步骤,后边举个简单的例子,其中的try块请读者自行处理. /* * 1.下载驱动包:com.mysql.jdbc.Driver;网上很多下载资源,自己找度娘,此 ...
- Navicat for MySQL定时备份数据库及数据恢复
在做数据库修改或删除操作中,可能会导致数据错误,甚至数据库奔溃,而有效的定时备份能很好地保护数据库.本篇文章主要讲述Navicat for MySQL定时备份数据库和数据恢复等功能,同时可以定时播放电 ...
- 01-Java基础及面向对象
JAVA基础知识 Java 是SUN(Stanford University Network,斯坦福大学网络公司)1995年推出的一门面向 Internet 的高级编程语言. Java 虚拟机(JVM ...
- Javascript流程控制
Javascript流程控制 1.条件语句 (1)if(exp)执行一句代码 (2)if(exp){执行代码段;} (3)if(exp){exp为true执行代码段}else{exp为false执行的 ...
- 教育改革——国家认证 “网红” 编程语言 Python
特大消息!!! 不止是上海计算机二级考试 ,全国计算机考试等级考试也有要求 如果你正打算考计算机等级,那你需要学习以下知识 要求学习的知识太多了,我就不一一在这里展示了! 一.考试改革的目标 据悉 ...