函數語言程式設計在前端許可權管理中的應用

賈克深發表於2018-12-16

函數語言程式設計在前端許可權管理中的應用

解決什麼問題

本文主要是自己在實際業務開發中的一些總結,寫出來希望與大家一起探討。

首先介紹一下業務背景:

  • 我們開發的是一套2B的企業培訓SaaS系統,企業可以在平臺上用直播的方式對自己的員工進行培訓。
  • 這套SaaS系統可以對接不同的平臺,如釘釘、微信等等(不同平臺會限制一些功能,如釘釘不能顯示員工手機號),也可以進行內網部署(內網會關閉一些線上功能)。由於部署環境和對接系統的不同,平臺所能使用的功能受限,對應的前端許可權也不一樣。
  • 這裡前端的開發主要涉及賬戶系統級的培訓管理和單個房間內直播時的控制兩個部分,它們在一個SPA裡面,需要一套系統管理兩個部分的許可權。
  • 培訓管理會分為賬戶管理員,子管理員(後續可能會增加系統管理角色),直播控制人員分為講師,嘉賓,助手等角色。這裡所有的人員都可能在一個控制頁,但是由於角色的不同,UI也會不一樣。

綜上,在不同部署平臺下,不同級別(角色)的人員在同一個房間裡,他們所看到的介面和能使用的功能是不一樣的。而且一個角色受限於部署平臺和主管理員所購買的平臺服務,或者隨著主管理員關閉/開放某些功能,看到的介面也會不一樣。

所以我們需要做一套許可權管理系統來綜合處理這些資訊(平臺、賬戶、角色),保證各角色看到不同的介面。可以看到我們這裡說的許可權已經不僅限於簡單的角色許可權,還包括角色之上的平臺和賬戶管理員的限制。

因為最後的許可權取決於所登入的賬戶,所以在開發中,我們將許可權和賬戶資訊放到了一起,統稱為metaConfig,即賬戶元資訊,包含賬戶名字、角色等基本資訊,所屬主賬號,具體角色資訊,角色許可權等,它將決定最終的介面顯示。

如何解決

我們使用React和Redux來開發,metaConfig物件可以直接存在Redux中進行管理。

  • 在檢視元件中可以通過connect函式,將metaConfig裡配置的屬性或許可權資料對映成各元件所需的檢視資料,最終呈現出不同的介面。
  • 在路由控制器內,也可以從Redux中拿到metaConfig來決定路由許可權。
  • 一些請求方法的許可權也可以根據對應的metaConfig屬性來決定。

在我們的系統中,metaConfig的處理是放在reducer中的,會有一個預設的defaultMetaConfig,通過reducer生成最後的metaConfig。許可權管理最關鍵的就是如何生成各個角色對應的metaConfig,總結起來就是:

metaConfig=f(defaultMetaConfig)

分層(管道處理)

把複雜問題拆分成簡單問題是開發中的一個重要手段。這裡我們可以通過分層處理的方式,將許可權管理拆分成多個層級,每層對應一個處理模組。這樣一個大的許可權處理過程就變成解決每個層級的許可權處理。

我們先來看看系統許可權管理受到哪些因素影響:部署方式(外網內網),對接平臺,賬戶管理員購買的服務和開啟/關閉的功能,賬戶級角色(賬戶管理員、子管理員),房間角色(管理員、講師、助手、嘉賓等)。

我們把每一層抽象成一個處理器,稱為parser,簡單區分之後可以分為:deployParser(部署方式),platformParser(平臺),accountParser(賬戶),roleParser(角色)。

UNIX中有一個pipeline(管道)的概念,一個程式的輸出直接成為下一個程式的輸入。結合到我們的業務,可以輸入一個預設的metaConfig,然後依次通過各個層級的parser函式,最終輸出一個metaConfig。

js中pipeline的簡單實現如下,compose函式的處理順序是反著的,可以檢視Redux中compose的實現。

// 管道函式
const pipe = (...funcs) => {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => (...args) => b(a(...args)))
}
複製程式碼

把我們抽象好的parser丟到pipe函式中,結果如下:

metaConfig = {
            ...pipe(
                deployParser,
                platformParser,
                accountParser.createAccountParser(account),
                roleParser,
            )(defaultMetaConfig)
        }
複製程式碼

注意accountParser.createAccountParser(account)這行,我們下面一節分析。

這樣我們通過管道函式將許可權的處理拆分成多個層級,每個層級會操作metaConfig內對應的屬性,是不是簡單明瞭。因為是函式式的處理,可以直接放在reducer中計算出metaConfig然後儲存到Redux中。

這裡處理許可權(不僅限於許可權,還包括一些賬戶基礎資訊)的操作分為兩種情況:

  • 直接賦值,如賬戶資訊。
// 需要重置的資料
        newConfig.isSuperManager = false
        newConfig.isAdmin = true
        newConfig.name = manager.name
複製程式碼
  • merge操作,將當前層級的許可權與上一級傳過來的許可權進行merge操作,得出許可權結果傳給下一級。因為此處有一個許可權限制的概念,如果platformParser(平臺)中沒有簡訊功能,則accountParser(賬戶)也應該沒有,在merge函式中使用&操作將該層級與上級許可權merge,得出該許可權結果傳給下一級。
accountParser = metaConfig => ({
    ...metaConfig,
    // 在accountParser中進行merge操作,合併從上一層傳來的metaConfig,這樣的許可權處理可能有多處
	somePermission: mergeSomePermission(metaConfig.somePermission),
	...
})

// merge函式內進行具體的&操作,
mergeSomePermission = prePermission => {
	// 當前層級沒有使用簡訊的許可權
    prePermission.canUseMsg = prePermission & false,
    // 每個merge函式可以處理多個許可權點,這裡只寫了一個
    ...
}
複製程式碼

細化分層(柯里化與組合)

通過上面的分層可以從大的方向上去解決許可權問題,但是業務中的許可權是動態的,不斷擴充套件的,如何處理業務迭代中產生的這些問題?比如上面例子中mergeSomePermission的簡訊許可權是限定死的為false,但是可能有的角色有這個許可權,而其他角色沒有這個許可權。在account這層一個簡單的parser無法處理不同賬戶間之間的差異, 而且不同級別賬戶需要處理的許可權範圍可能也不一樣,同一層級還需要不同的處理函式,用account作為引數,來細化各個處理器。

我們可能需要下面的程式碼,在賬戶許可權處理中加入不同賬戶的處理器:


accountParser = (account, metaConfig) => {
	cosnt { superManager = null, normalManager = null } = account

    // 分別處理superManager和normalManager的許可權
    let newConfig = superManagerParser(superManager, metaConfig)
    newConfig = normalManagerParser(normalManager, newConfig)
    
    return newConfig
}

superManagerParser = (superManager = null , metaConfig) => 
// 如果是主管理員則處理
superManager
? ({
    ...metaConfig,
    // 根據superManager資訊處理
    somePermission: mergeSomePermission(superManager, metaConfig.somePermission),

    // 主管理員功能需要多處理一些許可權
    someSystemPermission: mergeSomeSystemPermission(superManager, metaConfig.somePermission)
	
})
: metaConfig

normalManagerParser = (normalManager, metaConfig) =>
normalManager
? ({
    ...metaConfig,
    // 根據normalManager資訊處理
	somePermission: mergeSomePermission(normalManager, metaConfig.somePermission)
})
: metaConfig
複製程式碼

從之前的管道處理中我們已經看到一些函數語言程式設計的影子,我們可以繼續使用一些函式式的方法來加工上面的函式。管道處理中的accountParser.createAccountParser(account)就是處理這個問題的。

// 函式柯里化
createSuperManagerParser = (superManager = null) => metaConfig => 
// 如果是主管理員則處理
superManager
? ({
    ...metaConfig,
    // 主管理員功能需要多處理一些許可權
    someSystemPermission: mergeSomeSystemPermission(superManager, metaCofig.somePermission)
	somePermission: mergeSomePermission(superManager, metaCofig.somePermission)
})
: metaConfig

// 函式柯里化
createNormalManagerParser = (normalManager = null) => metaConfig =>
normalManager
? ({
    ...metaConfig,
	somePermission: mergeSomePermission(normalManager, metaCofig.somePermission)
})
: metaConfig


// 合併成一個賬戶級的parser
const createAccountParser = account => {
    const { normalManger = null, super_manager = null } = account || {}

    return pipe(
        createSuperManagerParser(super_manager),
        createNormalManagerParser(normalManger),
    )
}

複製程式碼

我們使用柯里化將兩個parser函式處理後,可以使它們都接受metaCofig作為引數,並繼續使用一個管道組合成賬戶級別的accountParser,它的引數還是metaConfig。這樣我們在account這層用柯里化和組合使得parser也可以用管道進行再次分層處理。

同樣的操作也可以應用在角色處理器roleParser中。應用RBAC許可權管理,一個角色對應一個parser,使用柯里化和pipe合成一個大的roleParser。

介紹到這裡,本文所要說的函數語言程式設計在前端許可權管理中的應用就差不多了。

為什麼這麼處理

大致有以下幾點原因:

  • 分層解耦。將各部分的程式碼分隔開,每個層級只處理自己的部分。程式碼清晰易維護,團隊其他成員也能迅速理解思路。
  • 可組合擴充套件。通過柯里化和管道、組合,可以實現無限分級,即使後面許可權變得更復雜,也可以通過新增層級、組合parser來應對。
  • 整個處理過程是函式式的,只有簡單的輸入輸出,對外界系統無影響,放在Redux的reducer中真香。

總結

本文主要介紹了函數語言程式設計(管道、柯里化、組合)在前端許可權管理中的應用,通過分層解耦,多級分層將複雜的許可權管理拆解成細粒度的parser函式。水平有限,其實也沒有用的很深,只是基本解決了現有的問題。業務開發久了,可能覺得沒什麼提升,但是在日常的開發中也是可以活學活用的,將一些程式設計的基礎思想積極應用到開發中也許有意向不到的結果。這裡寫出來供大家參考,如果有更好的想法也歡迎一起討論。

原文地址:github.com/woxixiulayi…

相關文章