这种模式适用于相当小的 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。也就是说,让它按类型对事件进行分组。这将在几个方面改进:
dispatcher
只需要通知订阅特定事件的事件处理程序。这样您就不需要随着应用程序的增长而遍历可能成百上千个组件(订阅者)。
handleEvent
方法可以拆分并处理特定事件,您可以跳过所有 switch
语句。您还可以为多个事件重用相同的事件处理程序,例如 disabling/enabling 按钮。请注意使用 this
关键字!
现在可以命名事件,您可以跳过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,这样他们也可以理解他们应该如何以及为什么应该遵循这些模式。祝你好运!
我想重构一些我编写的没有使用任何特定模式的 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。也就是说,让它按类型对事件进行分组。这将在几个方面改进:
dispatcher
只需要通知订阅特定事件的事件处理程序。这样您就不需要随着应用程序的增长而遍历可能成百上千个组件(订阅者)。handleEvent
方法可以拆分并处理特定事件,您可以跳过所有switch
语句。您还可以为多个事件重用相同的事件处理程序,例如 disabling/enabling 按钮。请注意使用this
关键字!现在可以命名事件,您可以跳过
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,这样他们也可以理解他们应该如何以及为什么应该遵循这些模式。祝你好运!