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里面不再需要写逻辑代码。
综上所述,我们有许多的方式可以避免模块间的耦合,关键在于去实践。最后附上源码链接。