RxJS+WebPack+TS实现组件间的松耦合

本文将介绍如何通过RxJS+TS实现模块间的松耦合。所谓松耦合是指模块间不能存在显式的引用关系。公司项目中经常看到junior程序员写出如下的代码:

var moduleA = {
  foo: function() {
    moduleB.doSomeThing();
    moduleC.doSomeThing();
  }

  dummy: function() {}
}

var moduleB = {
  doSomeThing: function() {}
  doAnotherThing: function() {
    moduleA.dummy();
  }
}

var moduleC = {
  doSomeThing: function() {}
}

显然这种代码极其糟糕,模块间互相引用,一旦修改就会漏洞百出。 那么作为tech manager,我提出了几种修改建议。 首先,建立demo环境

npm init -y
npm install rxjs ts-loader typescript webpack webpack-dev-server --save
npm install webpack-cli --save-dev

tsconfig.json

{
    "compilerOptions": {
      "outDir": "./dist/",
      "noImplicitAny": true,
      "module": "es6",
      "moduleResolution": "node",
      "sourceMap": true,
      "target": "es6",
      "experimentalDecorators": true,
      "typeRoots": [
        "node_modules/@types"
      ],
      "lib": [
        "es2017",
        "dom"
      ]
    }
  }

webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.ts',
    devtool: 'inline-source-map',
    module: {
        rules: [
        {
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: /node_modules/
        }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

public/index.html

<html>
    <head>
        <title>Demo</title>
    </head>
    <body>
        <style>
            #app {
                width: 100%;
                height: 400px;
                display: flex;
            }
            #toolDiv {
                width: 100%;
                height: 50px;
                background-color:cornsilk;
            }
            #leftDiv {
                width: 50%;
                height: 100%;
                background-color:chocolate;
            }
            #rightDiv {
                width: 50%;
                height: 100%;
                background-color: burlywood;
            }
        </style>
        <div id="toolDiv">
            <input type="button" id="btnCount" value="+1" />
            <input type="button" id="btnMinus" value="-1" />
        </div>
        <div id="app" style="width:100%; height: 400px;">
            <div id="leftDiv">
                <p id="leftCount">0</p>
            </div>
            <div id="rightDiv">
                <p id="rightCount">0</p>
            </div>
            <div id="middleDiv">
                <p id="middleCount">0</p>
            </div>
        </div>
    </body>
    <script src="/bundle.js"></script>
</html>

src/index.ts
方案一: 假设我们有ToolBar,Left,Right三个模块,ToolBar中的Button发出Click事件,通过App传递给ToolBar的onAddClick方法调用Left、Right中的处理事件。 从而避免了ToolBar直接引用Left、Right。

namespace Index {
    
    type addClickEvent = () => void;

    class ToolBar {
        private btnAdd: HTMLElement;
        private addCB: addClickEvent

        constructor(_addCallBack: addClickEvent) {
            this.btnAdd = document.getElementById("btnCount");
            this.btnAdd.onclick = this.AddClickHandler.bind(this);

            this.addCB = _addCallBack;
        }

        private AddClickHandler() {
            this.addCB && this.addCB();
        }
    }

    class Left {
        public handleAdd() {
            const counter = document.getElementById('leftCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }
    }

    class Right {
        public handleAdd() {
            const counter = document.getElementById('rightCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }
    }

    class App {
        private toolBar: ToolBar;
        private leftCom: Left;
        private rightCom: Right;

        constructor() {
            this.leftCom = new Left();
            this.rightCom = new Right();
            this.toolBar = new ToolBar(this.onAddClick.bind(this));
        }

        private onAddClick() {
            this.leftCom.handleAdd();
            this.rightCom.handleAdd();
        }
    }
    const app = new App();
}

src/index2.ts
方案二:方案一虽然总体上可以work,但还是略微繁琐,需要由App模块作为总控制,有没有办法不用App控制,而是在ToolBar和Left、Right间实现事件监听呢?
于是我想到了RxJS的Subject(主题,或称为事件订阅)。

import { Subject } from "rxjs";

namespace Index2 {
    enum actions {
        add = 0,
        minus
    }

    let subjectFoo = new Subject();

    class ToolBar {
        private btnAdd: HTMLElement;

        constructor() {
            this.btnAdd = document.getElementById("btnCount");
            this.btnAdd.onclick = this.AddClickHandler.bind(this);
        }

        private AddClickHandler() {
            subjectFoo.next(actions.add);
        }
    }

    class Left {
        constructor()
        {
            subjectFoo.subscribe(action => {
                switch(action) {
                    case actions.add: 
                        this.handleAdd();
                    break;
                    case actions.minus: 
                        this.handleMinus();
                    break;
                    default: break;
                }
            })
        }
        public handleAdd() {
            const counter = document.getElementById('leftCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }

        public handleMinus() {
            const counter = document.getElementById('leftCount');
            counter.innerText = (parseInt(counter.innerText)-1).toString();
        }
    }

    class Right {
        constructor()
        {
            subjectFoo.subscribe(action => {
                switch(action) {
                    case actions.add: 
                        this.handleAdd();
                    break;
                    case actions.minus: 
                        this.handleMinus();
                    break;
                    default: break;
                }
            })
        }
        public handleAdd() {
            const counter = document.getElementById('rightCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }
        public handleMinus() {
            const counter = document.getElementById('rightCount');
            counter.innerText = (parseInt(counter.innerText)-1).toString();
        }
    }

    class App {
        private toolBar: ToolBar;
        private leftCom: Left;
        private rightCom: Right;

        constructor() {
            this.leftCom = new Left();
            this.rightCom = new Right();
            this.toolBar = new ToolBar();
        }
    }

    const app = new App();
}

上述代码可以看到App中除了初始化三个模块,没有其他逻辑。 但这还有个问题,就是在Left、Right中都加入了订阅方法(subscribe),看上去还是太繁琐。 于是我又想到了第三种实现方式

src/index3.ts

import { Subject } from "rxjs";

namespace Index3 {
    enum actions {
        add = 0,
        minus
    }

    let subjectFoo = new Subject();

    interface IAdd {
        handleAdd: ()=>void;
    }

    interface IMinus {
        handleMinus: ()=>void;
    }

    class EventPublisher {
        private btnAdd: HTMLElement;
        private btnMinus: HTMLElement;

        constructor() {
            this.btnAdd = document.getElementById("btnCount");
            this.btnAdd.onclick = this.AddClickHandler.bind(this);

            this.btnMinus = document.getElementById("btnMinus");
            this.btnMinus.onclick = this.MinusClickHandler.bind(this);
        }

        private AddClickHandler() {
            subjectFoo.next(actions.add);
        }

        private MinusClickHandler() {
            subjectFoo.next(actions.minus);
        }
    }

    class Left implements IAdd, IMinus {
        public handleMinus() {
            const counter = document.getElementById('leftCount');
            counter.innerText = (parseInt(counter.innerText)-1).toString();
        }

        public handleAdd() {
            const counter = document.getElementById('leftCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }
    }

    class Right implements IAdd, IMinus { 
        public handleAdd() {
            const counter = document.getElementById('rightCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }

        public handleMinus() {
            const counter = document.getElementById('rightCount');
            counter.innerText = (parseInt(counter.innerText)-1).toString();
        }
    }

    class EventHub {
        private components: any[] = [];

        constructor() {
            this.components.push(new Left());
            this.components.push(new Right());

            subjectFoo.subscribe(action => {
                switch(action) {
                    case actions.add: 
                        this.handleAdd();
                    break;
                    case actions.minus: 
                        this.handleMinus();
                    break;
                    default: break;
                }
            })
        }

        public handleAdd() {
            this.components.forEach(com => {
                (com as IAdd).handleAdd && (com as IAdd).handleAdd();
            });
        }
        public handleMinus() {
            this.components.forEach(com => {
                (com as IMinus).handleMinus && (com as IMinus).handleMinus();
            });
        }
    }

    const hub = new EventHub();
    const publisher = new EventPublisher();
}

这个方案中,我把事件订阅放到了App中,先注册所有组件,在收到消息时,通过接口判断调用哪些组件的处理方法。看上去似乎已经不错了,但觉得还有其他方式可以实现,于是我又写了第四种方案,代码如下

src/index4.ts

import { Subject } from "rxjs";

namespace Index4 {
    let subjectFoo = new Subject();

    //自定义装饰器
    function MySubject(title: string) {
        return function (constructor: Function) {
            constructor.prototype.title = title;
            subjectFoo.subscribe(action => {
                switch(action) {
                    case actions.add: 
                        constructor.prototype.handleAdd();
                    break;
                    case actions.minus: 
                        constructor.prototype.handleMinus();
                    break;
                    default: break;
                }
            });
        }
    }
      
    enum actions {
        add = 0,
        minus
    }

    class ToolBar {
        private btnAdd: HTMLElement;
        private btnMinus: HTMLElement;

        constructor() {
            this.btnAdd = document.getElementById("btnCount");
            this.btnAdd.onclick = this.AddClickHandler.bind(this);

            this.btnMinus = document.getElementById("btnMinus");
            this.btnMinus.onclick = this.MinusClickHandler.bind(this);
        }

        private AddClickHandler() {
            subjectFoo.next(actions.add);
        }

        private MinusClickHandler() {
            subjectFoo.next(actions.minus);
        }
    }

    @MySubject('add')
    class Left { 
        constructor() {
            console.log((this as any).title);
        }

        public handleAdd() {
            const counter = document.getElementById('leftCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }

        public handleMinus() {
            const counter = document.getElementById('leftCount');
            counter.innerText = (parseInt(counter.innerText)-1).toString();
        }
    }

    @MySubject('add')
    class Right {
        public handleAdd() {
            const counter = document.getElementById('rightCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }
        public handleMinus() {
            const counter = document.getElementById('rightCount');
            counter.innerText = (parseInt(counter.innerText)-1).toString();
        }
    }

    @MySubject('add')
    class Middle {
        public handleAdd() {
            const counter = document.getElementById('middleCount');
            counter.innerText = (parseInt(counter.innerText)+1).toString();
        }
        public handleMinus() {
            const counter = document.getElementById('middleCount');
            counter.innerText = (parseInt(counter.innerText)-1).toString();
        }
    }

    class App {
        private toolBar: ToolBar;
        private leftCom: Left;
        private rightCom: Right;
        private midCom: Middle;


        constructor() {
            this.leftCom = new Left();
            this.rightCom = new Right();
            this.midCom = new Middle();
            this.toolBar = new ToolBar();
        }
    }

    const app = new App();
}

这个方案中,我把事件订阅放到了装饰器里面。只要加了该装饰器,就会自动订阅Add事件,并执行handleAdd方法。 App里面不再需要写逻辑代码。

综上所述,我们有许多的方式可以避免模块间的耦合,关键在于去实践。最后附上源码链接

posted @ 2021-09-24 09:29  老胡Andy  阅读(159)  评论(0编辑  收藏  举报