JavaScript?設(shè)計(jì)模式之洋蔥模型原理及實(shí)踐應(yīng)用
前言
先來聽聽一個(gè)故事吧,今天產(chǎn)品提了一個(gè)業(yè)務(wù)需求:用戶在一個(gè)編輯頁(yè)面,此時(shí)用戶點(diǎn)擊退出登錄,應(yīng)用需要提示用戶當(dāng)前有編輯內(nèi)容未保存,是否保存;當(dāng)用戶操作完畢后再提示用戶是否退出登錄。
流程如下:

因?yàn)橥顺龅卿浭菍儆诠膊糠钟闪硪晃煌瑢W(xué)維護(hù),此時(shí)和他交流后“善良”的把需求仍給了他。并告知他可以通過某某方法獲取我當(dāng)前是否有編輯內(nèi)容。然后我繼續(xù)摸魚,他開始瘋狂輸出
const handlerLogout = async () => {
if (window.location.href === 'xxx') {
if (getEditState() === 'xxx') {
await editConfirm()
}
}
await logoutConfirm();
}
功能如約上線,新需求也如約到達(dá):產(chǎn)品期望用戶在VIP充值頁(yè)面退出登錄的時(shí)候,先彈出一個(gè)VIP充值廣告,當(dāng)用戶關(guān)閉廣告后再提示用戶是否退出登錄。
流程如下:

然后熟悉的場(chǎng)景、熟悉的人,在一番交流過后,那位同學(xué)略微暴躁的又開始瘋狂輸出,然后我繼續(xù)摸魚
const pages = {
editPage: async () => {
if (getEditState() === 'xxx') {
await editConfirm()
}
},
vipPage: async () => {
if (getUserVipState() === 'xxx') {
await vipConfirm()
}
}
}
const handlerLogout = async () => {
const curPage = getPage();
await pages[curPage];
await logoutConfirm();
}
然后的然后功能又如約上線,然后需求又來了,一個(gè)場(chǎng)景中有多個(gè)彈窗業(yè)務(wù),優(yōu)先級(jí)不同,如果彈窗1不滿足彈出條件,就使用彈窗2依此類推。眾所周知產(chǎn)品的需求怎么做的完,他終于受不了了,開始思考怎么樣自己才能摸摸魚。與似乎不好的想法油然而生,如果自己維護(hù)的退出登錄就只關(guān)注處理退出登錄的業(yè)務(wù),而其他業(yè)務(wù)的各種彈窗讓業(yè)務(wù)方自己去處理那我就可以摸魚啦。想法有了,拆解一下邏輯,底層邏輯就是在觸發(fā)時(shí)需要有很多中間層的處理,等中間層處理完成后再處理自己的。那這不就像是洋蔥模型嗎。
洋蔥模型
提到洋蔥模型,koa的實(shí)現(xiàn)簡(jiǎn)單且優(yōu)雅。koa中主要使用koa-compose來實(shí)現(xiàn)該模式。核心內(nèi)容只有十幾行,但是卻涉及到高階函數(shù)、閉包、遞歸、尾調(diào)用優(yōu)化等知識(shí),不得不說非常驚艷沒有一行是多余的。簡(jiǎn)單來說,koa-compose暴露出一個(gè)compose方法,該方法接受一個(gè)中間件數(shù)組,并返回一個(gè)Promise函數(shù)。源碼如下
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
源碼中compose主要做了三件事
- 第一步:進(jìn)行入?yún)⑿r?yàn)
- 第二步:返回一個(gè)函數(shù),并利用閉包保存middleware和index的值
- 第三步:調(diào)用時(shí),執(zhí)行dispatch(0),默認(rèn)從第一個(gè)中間件執(zhí)行
dispatch函數(shù)的作用(dispatch其實(shí)就是next函數(shù))
- 第一步:通過
i <= index來避免在同一個(gè)中間件中連續(xù)next調(diào)用 - 第二步:設(shè)置index的值為當(dāng)前中間件位置的值,并且拿到當(dāng)前中間件函數(shù)
- 第三步:判斷當(dāng)前是否還有中間件,沒有返回
Promise.resolve() - 第四步:返回
Promise.resolve并把當(dāng)前中間件執(zhí)行結(jié)果做為返回,且傳入context和next(dispatch)方法。這里利用尾調(diào)優(yōu)化,避免了fn重新創(chuàng)建新的棧幀,同時(shí)提升了速度和節(jié)省了內(nèi)存(大佬就是大佬)
我們可以通過其測(cè)試用例了解到執(zhí)行的過程,有條件的讀者可以通過下載源碼進(jìn)行斷點(diǎn)調(diào)試,更能理解每一步的過程
it('should work', async () => {
const arr = []
const stack = []
stack.push(async (context, next) => {
arr.push(1) // 步驟1
await wait(1) // 步驟2
await next() // 步驟3
await wait(1) // 步驟14
arr.push(6) // 步驟15
})
stack.push(async (context, next) => {
arr.push(2) // 步驟4
await wait(1) // 步驟5
await next() // 步驟6
await wait(1) // 步驟12
arr.push(5) // 步驟13
})
stack.push(async (context, next) => {
arr.push(3) // 步驟7
await wait(1) // 步驟8
await next() // 步驟9
await wait(1) // 步驟10
arr.push(4) // 步驟11
})
await compose(stack)({})
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
compose接收一個(gè)參數(shù),該參數(shù)是一個(gè)Promise數(shù)組,注入中間件后返回了一個(gè)執(zhí)行函數(shù)并執(zhí)行。此時(shí)會(huì)按照上訴我標(biāo)記的步驟進(jìn)行執(zhí)行。配置koa文檔中的gif示例和流程圖更好理解。通過不斷的遞歸加上Promise鏈?zhǔn)秸{(diào)用完成了整個(gè)中間件的執(zhí)行


實(shí)踐
已經(jīng)了解到洋蔥模型的設(shè)計(jì),按照當(dāng)前摸魚的訴求,期望stack.push這部分內(nèi)容由業(yè)務(wù)方自己去注入,而退出登錄只需要執(zhí)行compose(stack)({})即可,額外訴求是項(xiàng)目中期望對(duì)彈窗有優(yōu)先級(jí)的處理,那就是不是誰(shuí)先進(jìn)入誰(shuí)先執(zhí)行。對(duì)此改造一下middleware定義,新增level表示優(yōu)先級(jí)后續(xù)它進(jìn)行排序,優(yōu)先級(jí)越高設(shè)置level值越高即可。
type Middleware<T = unknown> = {
level: number;
middleware: (context: T | undefined, next: () => Promise<any>) => void;
};
因?yàn)槲覀冃枰峁┙o業(yè)務(wù)方一個(gè)接口來添加中間件,這里使用類來實(shí)現(xiàn),通過暴露出add和remove方法對(duì)中間件進(jìn)行添加和刪除,利用add方法在添加時(shí)利用level對(duì)中間件進(jìn)行排序,使用stack來保存已經(jīng)排序好的中間件。dispatch通過CV大法實(shí)現(xiàn)
class Scheduler<T> {
stack: Middleware<T>[] = [];
add(middleware: Middleware<T>) {
const index = this.stack.findIndex((it) => it.level <= middleware.level);
this.stack.splice(index === -1 ? this.stack.length : index, 0, middleware);
return () => {
this.remove(middleware);
};
}
remove(middleware: Middleware<T>) {
const index = this.stack.findIndex((it) => it === middleware);
index > -1 && this.stack.splice(index, 1);
}
dispatch(context?: T) {
// eslint-disable-next-line
const that = this;
let index = -1;
return mutate(0);
function mutate(i: number): Promise<void> {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
const fn = that.stack[i];
if (index === that.stack.length) return Promise.resolve();
try {
return Promise.resolve(fn.middleware(context, mutate.bind(null, i + 1)));
} catch (error) {
return Promise.reject(error);
}
}
}
}
export default Scheduler;
然后修改業(yè)務(wù)中的處理,之后再加類似需求就可以摸魚了。
// 暴露一個(gè)logoutScheduler方法
export const logoutScheduler = new Scheduler();
const handleLogout = () => {
logoutScheduler.dispatch().then(() => {
logoutConfirm();
})
}
// 編輯頁(yè)面
logoutScheduler.add({
level: 2,
middleware: async (_, next) => {
if (getEditState() === 'xxx') {
await editConfirm()
}
await next();
}
})
// vip頁(yè)面
logoutScheduler.add({
level: 2,
middleware: async (_, next) => {
if (getUserVipState() === 'xxx') {
await vipConfirm()
}
await next();
}
})
總結(jié)
一個(gè)好的設(shè)計(jì)能在實(shí)際開發(fā)中更好的去解耦業(yè)務(wù),而好的設(shè)計(jì)需要我們?nèi)ラ喿x那些優(yōu)秀的源碼去學(xué)習(xí)和理解才能為我們所用。
以上就是JavaScript 設(shè)計(jì)模式之洋蔥模型原理及實(shí)踐應(yīng)用的詳細(xì)內(nèi)容,更多關(guān)于JavaScript 設(shè)計(jì)模式洋蔥模型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 頁(yè)面跳轉(zhuǎn)如何實(shí)現(xiàn)傳值
這篇文章主要介紹了微信小程序 頁(yè)面跳轉(zhuǎn)如何實(shí)現(xiàn)傳值的相關(guān)資料,需要的朋友可以參考下2017-04-04
Lodash加減乘除add?subtract?multiply?divide方法源碼解讀
這篇文章主要介紹了Lodash加減乘除add?subtract?multiply?divide方法源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
Web?Components實(shí)現(xiàn)類Element?UI中的Card卡片
這篇文章主要為大家介紹了Web?Components實(shí)現(xiàn)類Element?UI中的Card卡片實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
實(shí)現(xiàn)基于飛書webhook監(jiān)聽github代碼提交
這篇文章主要為大家介紹了實(shí)現(xiàn)基于飛書webhook監(jiān)聽github代碼提交示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
前端可視化搭建定義聯(lián)動(dòng)協(xié)議實(shí)現(xiàn)
這篇文章主要為大家介紹了前端可視化搭建定義聯(lián)動(dòng)協(xié)議實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
Javascript設(shè)計(jì)模式之原型模式詳細(xì)
這篇文章主要介紹了Javascript設(shè)計(jì)模式之原型模式,原型模式用于在創(chuàng)建對(duì)象時(shí),通過共享某個(gè)對(duì)象原型的屬性和方法,從而達(dá)到提高性能、降低內(nèi)存占用、代碼復(fù)用的效果。下面小編將詳細(xì)介紹 ,需要的朋友可以參考下2021-09-09

