zoukankan      html  css  js  c++  java
  • 从前端中的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 的理念,我想最初的实现应该也是后端。

    最后深入了解:

  • 相关阅读:
    不务正业系列-浅谈《过气堡垒》,一个RTS玩家的视角
    [LeetCode] 54. Spiral Matrix
    [LeetCode] 40. Combination Sum II
    138. Copy List with Random Pointer
    310. Minimum Height Trees
    4. Median of Two Sorted Arrays
    153. Find Minimum in Rotated Sorted Array
    33. Search in Rotated Sorted Array
    35. Search Insert Position
    278. First Bad Version
  • 原文地址:https://www.cnblogs.com/EnSnail/p/10305363.html
Copyright © 2011-2022 走看看