忙里偷闲,打开平时关注的前端相关的网站,浏览最近最新的前端动态。佼佼者,平凡的我做不到,但还是要争取不做落后者。
前端中的IoC理念,看到这个标题就被吸引了。IoC 理念,不认识呢,点击去一看,果然没让我失望,原文结合案例把概念诠释的很清楚。原来 Ioc 是 控制反转 或 依赖倒置。
控制反转、依赖倒置、依赖注入 这三个名词,我倒是很耳熟了,毕竟在大学学 java web
课程的时候接触过,记得当时还很认真的学了并做了笔记。时间真是遗忘的罪魁祸首,现在就只记得名词,而全然忘了概念。
什么是 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 内部去修改这两个模块,而对于之前测试通过了的 App 来说,也必须重新测试。
很明显,这不是一个好的应用结构,高层次的模块 App 依赖了两个低层次的模块 Router 和 Track,对低层次模块的修改都会影响高层次的模块 App。那么如何解决这个问题呢,解决方案就是接下来要讲述的 依赖注入(Dependency Injection)。
什么是依赖注入?
所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖「注入」到模块内部,上面的代码可以通过依赖注入的方式最终改造成如下方式:
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 中按照一定的「约定」去初始化相关配置即可。
要实现一个可以被 App.use()
的模块,就必须满足两个约定:
- 模块必须包含 init 属性
- init 必须是一个函数
这其实就是 IoC
思想中对面向接口编程 而不要面向实现编程这一准则的很好的体现。App
不关心模块具体实现了什么,只要满足对 接口init
的约定就可以了。
在理解了本文的后,再回首看 koa 中的 app.use()
,理解起来就没那么难了。
koa 中的 app.use()
koa 的源码文件中关于 constructor 的代码如下:
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
koa 的源码文件中关于 use 的代码如下:
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
可以看到,可上面的案例很相似,只不过再没有init方法,而是直接调用方法。
而实际使用的时候,是这样的:
const Koa = require('koa')
const app = new Koa()
const m1 = require('./middleware/koa-m1') // 模拟的中间件
app.use(m1())
// 中间件
function m1 (ctx) {
global.console.log('m1', ctx.path)
}
module.exports = function () {
return async function (ctx, next) {
global.console.log('m1 start')
m1(ctx)
await next()
global.console.log('m1 end')
}
}
总结:IoC 的理念,很值得编程借鉴。刚接触前端时,只知道html、css、JavaScript。而现在发现难的地方,很多都是借鉴了后端的编程思想。比如设计模式,SSR(其实本质应该就是JSP)。IoC 的理念,我想最初的实现应该也是后端。
最后深入了解: