背景

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

什么是 IoC

IoC 的全称叫做 Inversion of Control,可翻译为为「控制反转」或「依赖倒置」,它主要包含了三个准则:

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

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

面向接口编程 而不要面向实现编程

概念总是抽象的,所以下面将以一个例子来解释上述的概念:

假设需要构建一款应用叫 App,它包含一个路由模块 Router 和一个页面监控模块 Track,一开始可能会这么实现:

// app.js

import Router from './modules/Router';

import Track from './modules/Track';

class App {

constructor(options) {

this.options = options;

this.router = new Router();

this.track = new Track();

    this.init();
} init() {
window.addEventListener('DOMContentLoaded', () => {
this.router.to('home');
this.track.tracking();
this.options.onReady();
});
}

}

// index.js

import App from 'path/to/App';

new App({

onReady() {

// do something here...

},

});

嗯,看起来没什么问题,但是实际应用中需求是非常多变的,可能需要给路由新增功能(比如实现 history 模式)或者更新配置(启用 history, new Router({ mode: 'history' }))。这就不得不在 App 内部去修改这两个模块,这是一个 INNER BREAKING 的操作,而对于之前测试通过了的 App 来说,也必须重新测试。

很明显,这不是一个好的应用结构,高层次的模块 App 依赖了两个低层次的模块 Router 和 Track,对低层次模块的修改都会影响高层次的模块 App。那么如何解决这个问题呢,解决方案就是接下来要讲述的 依赖注入(Dependency Injection)。

依赖注入

所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖「注入」到模块内部,上面的代码可以通过依赖注入的方式改造成如下方式:

// app.js

class App {

constructor(options) {

this.options = options;

this.router = options.router;

this.track = options.track;

    this.init();
} init() {
window.addEventListener('DOMContentLoaded', () => {
this.router.to('home');
this.track.tracking();
this.options.onReady();
});
}

}

// index.js

import App from 'path/to/App';

import Router from './modules/Router';

import Track from './modules/Track';

new App({

router: new Router(),

track: new Track(),

onReady() {

// do something here...

},

});

可以看到,通过依赖注入解决了上面所说的 INNER BREAKING 的问题,可以直接在 App 外部对各个模块进行修改而不影响内部。

是不是就万事大吉了?理想很丰满,但现实却是很骨感的,没过两天产品就给你提了一个新需求,给 App 添加一个分享模块 Share。这样的话又回到了上面所提到的 INNER BREAKING 的问题上:你不得不对 App 模块进行修改加上一行 this.share = options.share,这明显不是我们所期望的。

虽然 App 通过依赖注入的方式在一定程度上解耦了与其他几个模块的依赖关系,但是还不够彻底,其中的 this.router 和 this.track 等属性其实都还是对「具体实现」的依赖,明显违背了 IoC 思想的准则,那如何进一步抽象 App 模块呢。

Talk is cheap, show you the code:

class App {

static modules = []

constructor(options) {

this.options = options;

this.init();

}

init() {

window.addEventListener('DOMContentLoaded', () => {

this.initModules();

this.options.onReady(this);

});

}

static use(module) {

Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);

}

initModules() {

App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));

}

}

经过改造后 App 内已经没有「具体实现」了,看不到任何业务代码了,那么如何使用 App 来管理我们的依赖呢:

// modules/Router.js

import Router from 'path/to/Router';

export default {

init(app) {

app.router = new Router(app.options.router);

app.router.to('home');

}

};

// modules/Track.js

import Track from 'path/to/Track';

export default {

init(app) {

app.track = new Track(app.options.track);

app.track.tracking();

}

};

// index.js

import App from 'path/to/App';

import Router from './modules/Router';

import Track from './modules/Track';

App.use([Router, Track]);

new App({

router: {

mode: 'history',

},

track: {

// ...

},

onReady(app) {

// app.options ...

},

});

可以发现 App 模块在使用上也非常的方便,通过 App.use() 方法来「注入」依赖,在 ./modules/some-module.js 中按照一定的「约定」去初始化相关配置,比如此时需要新增一个 Share 模块的话,无需到 App 内部去修改内容:

// modules/Share.js

import Share from 'path/to/Share';

export default {

init(app) {

app.share = new Share();

app.setShare = data => app.share.setShare(data);

}

};

// index.js

App.use(Share);

new App({

// ...

onReady(app) {

app.setShare({

title: 'Hello IoC.',

description: 'description here...',

// some other data here...

});

}

});

直接在 App 外部去 use 这个 Share 模块即可,对模块的注入和配置极为方便。

那么在 App 内部到底做了哪些工作呢,首先从 App.use 方法说起:

class App {

static modules = []

static use(module) {

Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);

}

}

可以很清楚的发现,App.use 做了一件非常简单的事情,就是把依赖保存在了 App.modules 属性中,等待后续初始化模块的时候被调用。

接下来我们看一下模块初始化方法 this.initModules() 具体做了什么事情:

class App {

initModules() {

App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));

}

}

可以发现该方法同样做了一件非常简单的事情,就是遍历 App.modules 中所有的模块,判断模块是否包含 init 属性且该属性必须是一个函数,如果判断通过的话,该方法就会去执行模块的 init 方法并把 App 的实例 this 传入其中,以便在模块中引用它。

从这个方法中可以看出,要实现一个可以被 App.use() 的模块,就必须满足两个「约定」:

模块必须包含 init 属性

init 必须是一个函数

这其实就是 IoC 思想中对「面向接口编程 而不要面向实现编程」这一准则的很好的体现。App 不关心模块具体实现了什么,只要满足对 接口 init 的「约定」就可以了。

此时回去看 Router 的模块的实现就可以很容易理解为什么要怎么写了:

// modules/Router.js

import Router from 'path/to/Router';

export default {

init(app) {

app.router = new Router(app.options.router);

app.router.to('home');

}

};

总结

App 模块此时应该称之为「容器」比较合适了,跟业务已经没有任何关系了,它仅仅只是提供了一些方法来辅助管理注入的依赖和控制模块如何执行。

控制反转(Inversion of Control)是一种「思想」,依赖注入(Dependency Injection)则是这一思想的一种具体「实现方式」,而这里的 App 则是辅助依赖管理的一个「容器」。

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

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

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

  2. 前端 IoC 理念入门

    背景 近几年,前端应用(WebApp)正朝着大规模方向发展,在这个过程中我们会对项目拆解成多个模块/组件来组合使用,以此提高我们代码的复用性,最终提高研发效率. 在编写一个复杂组件的时候,总会依赖其他 ...

  3. 说说移动前端中 viewport (视口)

    转载网络资源的文章:来源不详~~ 移动前端中常说的 viewport (视口)就是浏览器显示页面内容的屏幕区域.其中涉及几个重要概念是 dip ( device-independent pixel 设 ...

  4. 移动前端中viewport(视口) 转

    移动前端中常说的 viewport (视口)就是浏览器显示页面内容的屏幕区域.其中涉及几个重要概念是 dpi ( device-independent pixel 设备逻辑像素 )和 CSS 像素之间 ...

  5. Android 进阶 Android 中的 IOC 框架 【ViewInject】 (下)

    上一篇博客我们已经带大家简单的吹了一下IoC,实现了Activity中View的布局以及控件的注入,如果你不了解,请参考:Android 进阶 教你打造 Android 中的 IOC 框架 [View ...

  6. spring.net中的IOC和DI-初使用

    前面准备:下载spring.net并解压 下载地址:spring.net下载地址 Ioc:控制反转         DI:依赖注入 一.IOC(控制反转) 1.新建一个控制台程序springTest, ...

  7. Castle框架中的IOC和AOP机制

    反转控制(IOC)和面向切面编程(AOP)技术作为当前比较流行的技术,其优势已受到广泛关注,但是这两项新技术在实际项目上的应用研究却很落后,而且在.NET平台下实现这两项技术没有形成可以广泛套用的框架 ...

  8. 前端中的SEO

    前端中的SEO: mate.title META标签分两大部分:HTTP标题信息(HTTP-EQUIV)和页面描述信息(NAME). <Meta name="Keywords" ...

  9. Spring——Web应用中的IoC容器创建(WebApplicationContext根应用上下文的创建过程)

    基于Spring-4.3.7.RELEASE Spring的配置不仅仅局限在XML文件,同样也可以使用Java代码来配置.在这里我使用XML配置文件的方式来粗略地讲讲WebApplicationCon ...

随机推荐

  1. 解决调用WebService报基础连接已经关闭: 服务器关闭了本应保持活动状态的连接的错误的方法

    问题可能原因之一:网速的快慢,我经过测试,如果外网访问的话网速慢就是出现此类问题,但是我没有精确测出当在网络流量最低在什么情况下可以避免此类问题问题可能之二:程序发布之前没把原引用的web servi ...

  2. 小白也能看懂的mySQL进阶【单表查询】

    目录 1.查询基础 SELECT语句基础 列的查询 为列设定别名 常数的查询 过滤表中重复数据 根据WHERE语句来选择记录 注释的书写方法 算术运算符和比较运算符 算术运算符 需要注意NULL 比较 ...

  3. Django搭建示例项目实战与避坑细节

    Django 开发项目是很快的,有多快?看完本篇文章,你就知道了. 安装 Django 前提条件:已安装 Python. Django 使用 pip 命令直接就可以安装: pip install dj ...

  4. 安装spyder记录

    sudo apt-get install spyder 报错:ERROR: Could not find a version that satisfies the requirement pyqt5& ...

  5. redis的配置文件redis.conf常用配置

    参数说明redis.conf 配置项说明如下:1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程 daemonize no2. 当Redis以守护进程方式运行时 ...

  6. PyQt信号connect连接槽方法时报:native Qt signal is not callable错误

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 在将一个信号连接到槽方法时,程序异常退出,捕获异常 ...

  7. 第11.23节 Python 中re模块的搜索替换功能:sub及subn函数

    一. 引言 在<第11.3节 Python正则表达式搜索支持函数search.match.fullmatch.findall.finditer>重点介绍了几个搜索函数,除了搜索,re模块也 ...

  8. java课堂作业--异常处理

    一. 运行结果: 二. 结果: ArrayIndexOutOfBoundsException/内层try-catch 发生ArithmeticException 三. 结果: ArrayIndexOu ...

  9. 百度前端技术学院-基础-day2

    2020.9.15 今天是学习前端基础技术的第二天,今天制作了一个在线简历,纯html的那种.帮助我复习了以前的基础知识,也增加了新的知识点,比如制作列表可以用fieldset来整合数据. 我的在线简 ...

  10. v-lazyload数据变化图片不切换

    这个问题让我很困惑,明明得到的商品数据已经改变了,但是就图片不变化,随后找到了解决办法,那就是多加一个动态的key <img v-lazy="item.productImage&quo ...