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


忙里偷閑,打開平時關注的前端相關的網站,瀏覽最近最新的前端動態。佼佼者,平凡的我做不到,但還是要爭取不做落后者。

前端中的IoC理念,看到這個標題就被吸引了。IoC 理念,不認識呢,點擊去一看,果然沒讓我失望,原文結合案例把概念詮釋的很清楚。原來 Ioc控制反轉依賴倒置

控制反轉依賴倒置依賴注入 這三個名詞,我倒是很耳熟了,畢竟在大學學 java web 課程的時候接觸過,記得當時還很認真的學了並做了筆記。時間真是遺忘的罪魁禍首,現在就只記得名詞,而全然忘了概念。

什么是 IoC ?

IoC 的全稱是Inversion of Control,翻譯為 控制反轉依賴倒置,主要包含了三個准則:

  1. 高層次的模塊不應該依賴於底層次的模塊,它們都應該依賴於抽象
  2. 抽象不應該依賴於具體實現,具體實現應該依賴於抽象
  3. 面向接口編程,而不要面向實現編程

舉例

假設需要構建一款應用叫 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() 的模塊,就必須滿足兩個約定

  1. 模塊必須包含 init 屬性
  2. 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 的理念,我想最初的實現應該也是后端。

最后深入了解:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM