首頁 > 軟體

JavaScript 設計模式之洋蔥模型原理及實踐應用

2022-09-04 18:03:11

前言

先來聽聽一個故事吧,今天產品提了一個業務需求:使用者在一個編輯頁面,此時使用者點選退出登入,應用需要提示使用者當前有編輯內容未儲存,是否儲存;當用戶操作完畢後再提示使用者是否退出登入。

流程如下:

因為退出登入是屬於公共部分由另一位同學維護,此時和他交流後“善良”的把需求仍給了他。並告知他可以通過某某方法獲取我當前是否有編輯內容。然後我繼續摸魚,他開始瘋狂輸出

const handlerLogout = async () => {
    if (window.location.href === 'xxx') {
        if (getEditState() === 'xxx') {
            await editConfirm()
        }
    }
    await logoutConfirm();
}

功能如約上線,新需求也如約到達:產品期望使用者在VIP充值頁面退出登入的時候,先彈出一個VIP充值廣告,當用戶關閉廣告後再提示使用者是否退出登入。

流程如下:

然後熟悉的場景、熟悉的人,在一番交流過後,那位同學略微暴躁的又開始瘋狂輸出,然後我繼續摸魚

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();
}

然後的然後功能又如約上線,然後需求又來了,一個場景中有多個彈窗業務,優先順序不同,如果彈窗1不滿足彈出條件,就使用彈窗2依此類推。眾所周知產品的需求怎麼做的完,他終於受不了了,開始思考怎麼樣自己才能摸摸魚。與似乎邪惡的想法油然而生,如果自己維護的退出登入就只關注處理退出登入的業務,而其他業務的各種彈窗讓業務方自己去處理那我就可以摸魚啦。想法有了,拆解一下邏輯,底層邏輯就是在觸發時需要有很多中間層的處理,等中間層處理完成後再處理自己的。那這不就像是洋蔥模型嗎。

洋蔥模型

提到洋蔥模型,koa的實現簡單且優雅。koa中主要使用koa-compose來實現該模式。核心內容只有十幾行,但是卻涉及到高階函數、閉包、遞迴、尾呼叫優化等知識,不得不說非常驚豔沒有一行是多餘的。簡單來說,koa-compose暴露出一個compose方法,該方法接受一箇中介軟體陣列,並返回一個Promise函數。原始碼如下

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主要做了三件事

  • 第一步:進行入參校驗
  • 第二步:返回一個函數,並利用閉包儲存middleware和index的值
  • 第三步:呼叫時,執行dispatch(0),預設從第一個中介軟體執行

dispatch函數的作用(dispatch其實就是next函數)

  • 第一步:通過i <= index來避免在同一個中介軟體中連續next呼叫
  • 第二步:設定index的值為當前中介軟體位置的值,並且拿到當前中介軟體函數
  • 第三步:判斷當前是否還有中介軟體,沒有返回Promise.resolve()
  • 第四步:返回Promise.resolve並把當前中介軟體執行結果做為返回,且傳入context和next(dispatch)方法。這裡利用尾調優化,避免了fn重新建立新的棧幀,同時提升了速度和節省了記憶體(大佬就是大佬)

我們可以通過其測試用例瞭解到執行的過程,有條件的讀者可以通過下載原始碼進行斷點偵錯,更能理解每一步的過程

  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接收一個引數,該引數是一個Promise陣列,注入中介軟體後返回了一個執行函數並執行。此時會按照上訴我標記的步驟進行執行。設定koa檔案中的gif範例和流程圖更好理解。通過不斷的遞迴加上Promise鏈式呼叫完成了整個中介軟體的執行

實踐

已經瞭解到洋蔥模型的設計,按照當前摸魚的訴求,期望stack.push這部分內容由業務方自己去注入,而退出登入只需要執行compose(stack)({})即可,額外訴求是專案中期望對彈窗有優先順序的處理,那就是不是誰先進入誰先執行。對此改造一下middleware定義,新增level表示優先順序後續它進行排序,優先順序越高設定level值越高即可。

type Middleware<T = unknown> = {
  level: number;
  middleware: (context: T | undefined, next: () => Promise<any>) => void;
};

因為我們需要提供給業務方一個介面來新增中介軟體,這裡使用類來實現,通過暴露出add和remove方法對中介軟體進行新增和刪除,利用add方法在新增時利用level對中介軟體進行排序,使用stack來儲存已經排序好的中介軟體。dispatch通過CV大法實現

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;

然後修改業務中的處理,之後再加類似需求就可以摸魚了。

// 暴露一個logoutScheduler方法
export const logoutScheduler = new Scheduler();
const handleLogout = () => {
    logoutScheduler.dispatch().then(() => {
        logoutConfirm();
    })
}
// 編輯頁面
logoutScheduler.add({
    level: 2,
    middleware: async (_, next) => {
        if (getEditState() === 'xxx') {
          await editConfirm()
        }
        await next();
    }
})
// vip頁面
logoutScheduler.add({
    level: 2,
    middleware: async (_, next) => {
        if (getUserVipState() === 'xxx') {
            await vipConfirm()
        }
        await next();
    }
})

總結

一個好的設計能在實際開發中更好的去解耦業務,而好的設計需要我們去閱讀那些優秀的原始碼去學習和理解才能為我們所用。

以上就是JavaScript 設計模式之洋蔥模型原理及實踐應用的詳細內容,更多關於JavaScript 設計模式洋蔥模型的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com