忙里偷閑,打開平時關注的前端相關的網站,瀏覽最近最新的前端動態。佼佼者,平凡的我做不到,但還是要爭取不做落后者。
前端中的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 的理念,我想最初的實現應該也是后端。
最后深入了解: