这种模式适用于相当小的 JS 应用程序吗?

Is this pattern okay for reasonnably small JS applications?

我想重构一些我编写的没有使用任何特定模式的 JS 单页应用程序。 现在我已经阅读了一些有趣的框架(redux ...),但我的公司一般不热衷于采用框架,这里的每个人都在使用 vanilla JS。所以我想让事情尽可能简单。 我在我的旧代码中发现的最明显的缺陷是整体式风格,因此引入基于组件的架构和关注点分离似乎已经是一个巨大的改进。 这是我想出的模型:

let eventGenerator = (function () {
    let id = -1;
    return {
        generate: () => {
            ++id;
            return id;
        }
    };
}) ();

let dispatcher = (function () {
    let components = [];
    return {
        addComponent: (component) => {
            components.push (component);
        },
        dispatch: (id, detail = null) => {
            for (let c of components) {
                c.handleEvent (id, detail);
            }
        }
    };
}) ();


const EVT_FAKE_API_RUNNING = eventGenerator.generate ();
const EVT_FAKE_API_SUCCESS = eventGenerator.generate ();
const EVT_FAKE_API_FAILURE = eventGenerator.generate ();
const EVT_FAKE_API_ABORTED = eventGenerator.generate ();
class ComponentFakeAPI {
    constructor (param) { // param = nb de secondes à attendre
        dispatcher.addComponent (this);
        this.param = param;
        this.timer = null;
        this.result = null;
    }
    handleEvent (id, detail) {
        switch (id) {
        case EVT_FETCH_BUTTON_CLICKED:
            this.timer = setTimeout (() => {
                this.result = Math.round (Math.random () * 100);
                if (this.result >= 20)
                    dispatcher.dispatch (EVT_FAKE_API_SUCCESS, { result: this.result });
                else
                    dispatcher.dispatch (EVT_FAKE_API_FAILURE);
            }, this.param);
            dispatcher.dispatch (EVT_FAKE_API_RUNNING);
            break;
        case EVT_ABORT_BUTTON_CLICKED:
            clearTimeout (this.timer);
            dispatcher.dispatch (EVT_FAKE_API_ABORTED);
        }
    }
}


const EVT_FETCH_BUTTON_CLICKED = eventGenerator.generate ();
class ComponentFetchButton {
    constructor (elt) {
        dispatcher.addComponent (this);
        elt.innerHTML = `<button>fetch</button>`;
        this.elt = elt;
        this.but = elt.querySelector ('button');
        this.but.onclick = () => dispatcher.dispatch (EVT_FETCH_BUTTON_CLICKED);
    }
    handleEvent (id, detail) {
        switch (id) {
        case EVT_FAKE_API_RUNNING:
            this.but.disabled = true;
            break;
        case EVT_FAKE_API_SUCCESS: case EVT_FAKE_API_FAILURE: case EVT_FAKE_API_ABORTED:
            this.but.disabled = false;
            break;
        }
    }
}

const EVT_ABORT_BUTTON_CLICKED = eventGenerator.generate ();
class AbortButton {
    constructor (elt) {
        dispatcher.addComponent (this);
        elt.innerHTML = `<button disabled>abort</button>`;
        this.elt = elt;
        this.but = elt.querySelector ('button');
        this.but.onclick = () => dispatcher.dispatch (EVT_ABORT_BUTTON_CLICKED);
    }
    handleEvent (id, detail) {
        switch (id) {
        case EVT_FAKE_API_SUCCESS: case EVT_FAKE_API_FAILURE: case EVT_FAKE_API_ABORTED:
            this.but.disabled = true;
            break;
        case EVT_FAKE_API_RUNNING:
            this.but.disabled = false;
            break;
        }
    }
}

class ComponentValueDisplay {
    constructor (elt) {
        dispatcher.addComponent (this);
        elt.textContent = '';
        this.elt = elt;
    }
    handleEvent (id, detail) {
        switch (id) {
        case EVT_FAKE_API_SUCCESS:
            this.elt.textContent = detail.result;
            break;
        case EVT_FAKE_API_FAILURE:
            this.elt.textContent = 'failure !';
            break;
        case EVT_FAKE_API_ABORTED:
            this.elt.textContent = 'aborted !';
            break;
        case EVT_FAKE_API_RUNNING:
            this.elt.textContent = '';
            break;
        }
    }
}

class ComponentAverage {
    constructor (elt) {
        dispatcher.addComponent (this);
        elt.textContent = '';
        this.elt = elt;
        this.sum = 0;
        this.avg = 0;
        this.n = 0;
    }
    handleEvent (id, detail) {
        switch (id) {
        case EVT_FAKE_API_SUCCESS:
            ++ this.n;
            this.sum += detail.result;
            this.elt.textContent = Math.round (this.sum / this.n);
            break;
        }
    }
}


window.addEventListener ('load', () => {
    let componentFakeAPI = new ComponentFakeAPI (2000);
    let componentFetchButton = new ComponentFetchButton (document.querySelector ('#componentFetchButton'));
    let componentAbortButton = new AbortButton (document.querySelector ('#componentAbortButton'));
    let componentValueDisplay = new ComponentValueDisplay (document.querySelector ('#componentValueDisplay'));
    let componentAverage = new ComponentAverage (document.querySelector ('#componentAverage'));
});
#componentValueDisplay, #componentAverage {
    margin-left: 10px;
    border: 1px solid black;
    min-width: 50px;
}
<div style="display: flex">
    <div id="componentFetchButton"></div>
    <div id="componentAbortButton"></div>
    <div>Result</div>
    <div id="componentValueDisplay"></div>
    <div>Average</div>
    <div id="componentAverage"></div>
</div>

我想知道这种模式是否会在更大、更复杂的应用程序中的某个时候碰壁。有什么建议吗?

my company is not keen to adopt frameworks in general, everyone here is using vanilla JS

很好奇,但我熟悉您无法控制的限制,所以我将跳过整个“避免重新发明轮子”的讨论。

所以,这里有两件事适合您:事件(observer pattern) and components (composite pattern,有点)。前者将帮助您避免组件之间的直接依赖,而后者将帮助您封装逻辑(并可能构建组件树)。随着应用程序的增长,两者都会很好地为您服务,并且假设随着应用程序的增长和复杂性的增加,您对这些模式进行迭代,那么您所拥有的就足够了。

根据您的示例代码,我想提供两个建议。随心所欲

首先,我将修改 dispatcher 以遵循更传统的事件发射器/观察器 API。也就是说,让它按类型对事件进行分组。这将在几个方面改进:

  1. dispatcher 只需要通知订阅特定事件的事件处理程序。这样您就不需要随着应用程序的增长而遍历可能成百上千个组件(订阅者)。

  2. handleEvent 方法可以拆分并处理特定事件,您可以跳过所有 switch 语句。您还可以为多个事件重用相同的事件处理程序,例如 disabling/enabling 按钮。请注意使用 this 关键字!

  3. 现在可以命名事件,您可以跳过eventGenerator,例如:

    const AppEvents = {
      api: {
        running: 'api.running',
        succees: 'api.success',
        failure: 'api.failure',
        aborted: 'api.aborted'
      }
    };
    
    dispatcher.subscribe(AppEvents.api.running, (event) => {
      // do something
    })
    
    // later
    dispatcher.notify(AppEvents.api.running, someEventData)
    

其次,为了提高测试的便利性,考虑提供 dispatcher 作为组件的参数。它对于 dispatcher 似乎并不特别重要,但它确实可以帮助您与组件使用外部依赖项的方式保持一致。在测试的情况下,您可以更轻松地在需要时提供模拟或存根。

额外提示:避免在 CSS 中使用 #id。它们的样式更难覆盖,而且它们的可重用性也更差。


最终,您的代码可以工作,从业务角度来看就足够了。但对于作为开发人员的您而言,问题在于理解、维护和添加附加功能的难易程度。最重要的是,您将希望从 peers/colleagues 中获得 buy-in,这样他们也可以理解他们应该如何以及为什么应该遵循这些模式。祝你好运!