後台管理系統通用解決方案

後台管理系統通用解決方案

導語:後台管理系統在很多業務場景下都會用到,由於其面向人群的特殊性,我們一般不太關注其界面布局,樣式等,而在於功能的實用性。需求那麼多,悲傷辣么大,我還想早點回家干點什麼呢?前段時間一直在做一個商城的管理後台,積累了一些經驗,抽象出了大部分管理後台都會用到並且可以復用的東西,如許可權設計,希望對後面需要搭建管理後台的童鞋有所幫助。

ps:結尾有彩蛋

一、技術選型

既然要快速搭建、敏捷響應產品大大提出的需求,那麼我們希望盡量復用已有並且功能實用的東西。對於管理後台,第一大特點就是表單輸入賊多,手動輸入、下拉列表、級聯選擇、單選、多選圖片上傳、日期等等五花八門。因此,我們需要有基礎的表單組件可以讓我們復用,這樣可以節省很大一部分人力。其次是我們選擇的技術要能快速上手。於是我們想到了當前最為流行的三個前端框架---angular,react,vue,三者都符合我們的技術選型。考慮到國內vue較為流行,並且相對而言輕量、易於上手,因此主體採用vue,結合狀態管理vuex,路由管理vue-router,基礎組件庫element-ui。由於管理台對首屏的要求不高,因此我們可以搭建為一個SPA(單頁應用)。

PS:後面抽象出來的內容與具體技術無關,完全可以使用你所偏好技術,這裡我對抽象出來的方案用Vue進行了實現,詳情請看 後台管理系統通用解決方案

二、 項目結構

考慮到每個團隊都有一套自己熟悉的頁面結構搭建、構建和部署方式,這裡不做具體討論,但是有些建議:

1、公共組件

頁面使用的公共組件最好放在同一個目錄下,如果公共組件過多,最好在進行分類,因為項目一旦大了的話,分類相當於索引,可以快速查看某個組件,便於維護。如:

2、頁面目錄結構

每個頁面的目錄結構最好包含數據源,業務組件,vuex配置文件等,這樣這個頁面調用了哪些介面、有哪些業務組件都一目了然,如:

當然啦,有些介面很多地方都會使用,就不適合放到某個頁面下,可以單獨建一個models目錄(類似於公共組件那樣),專門放置公用介面來方便調用。

3、構建

構建如果使用webpack的話,強烈建議加上webpack的alias和extensions功能,它能讓你更優雅的載入模塊:

有了上面的配置後,比如你要載入公共組件裡面組件或者或者models裡面的js,在任何地方你都可以直接像下面這樣寫,避免使用一長串的相對路徑:

import Loading from comps/loadingimport PopA from comps/pop/pop-aimport checkUserRight from models/check-user-right

詳情請見 webpack alias

三、最佳實踐

1、許可權設計,前端渲染下許可權控制的通用解決方案

大部分的後台管理系統都離不開許可權的設計,如:

用戶A具有超級許可權,能看到所有頁面和進行所有操作

用戶B只有客服許可權,只能看到部分頁面,也不能進行一些操作

這裡實現了一套具有通用性許可權設計方案,任何管理後台都可以按這個方案快速實現許可權控制,粒度從頁面到組件,再到dom,可以說是360度無死角許可權控制。

(1)對比

(2)分析

可以看到,列舉出的傳統方案缺點較為明顯。那該怎麼做呢?許可權控制的本質是針對不同許可權的人展示不同的頁面,組件,dom,再本質點就是操作dom的顯隱性。框架所提供的自定義指令功能就是讓我們手動操作dom的,既然不能使用內置判斷指令,為什麼我們不自己定義一個許可權指令呢?

我們可以自定義一個許可權指令,指令內部將第三方傳入的許可權和事先從後台拿到的許可權集合做對比,沒有許可權則對第三方做隱藏或其他操作。特別的,如果第三方是頁面並且沒有訪問許可權的話,做出相應提示或跳轉。這樣頁面、組件或者組件內元素在判斷許可權時只需要提供所需許可權給自定義指令即可。

(3)具體做法

a.後台提供一個介面,前端調用該介面可以拿到當前用戶的許可權集合,如{1001:true,1002:true},其中1001表示首頁的訪問許可權,1002表示按鈕的訪問許可權。

b.定義許可權和後台返回的許可權控制數據的關係A,用於指令調用時傳參的語義化。如後台返回的許可權數據{1001:true,1002:true},那麼關係A可以定義為{MAIN_READ:1001,BUTTON_READ:1002};定義頁面路由和許可權的對應關係B,用於頁面的訪問許可權控制。如首頁路由為/index.html,則關係B可以定義為{『index.html』:MAIN_READ}。

c.從後台獲取所有的許可權控制數據,包含頁面的,組件的和組件內部元素的,並且進行全局共享,比如放在window下,便於自定義指令獲取數據。

d.定義全局自定義許可權指令,由於各粒度的許可權只需要判斷一次,不存在變動,因此許可權的判斷邏輯將只執行一次,跟只執行一次的鉤子函數綁定。這裡有區分不同操作需求,一般情況下,許可權驗證不通過做的是隱藏操作,但頁面的訪問則需要給出相應提示或跳轉。因此,對於傳入的許可權參數,如果參數為空,則認為是需要判斷頁面訪問許可權,獲取當前路由,通過關係B獲取頁面所需要的許可權參數,由許可權參數和關係A獲取到許可權數據,如果後台返回的數據中沒有該許可權則給出相應提示或跳轉。其他的情況則直接由傳入的許可權參數和關係A獲取到許可權數據,沒有許可權則隱藏,或者做其他的操作,這裡許可權驗證不通過或者通過所做的操作完全是可擴展的。

e.給頁面各個需要許可權控制的地方加上自定義指令,並傳入需要的許可權參數,如某個按鈕,按照定義的關係A,則傳入BUTTON_READ;如果是頁面,則傳入空。

(2)優缺點

優點: 實現了前端對頁面各粒度許可權的統一控制,封裝許可權處理邏輯到自定義指令中,與其他業務邏輯解耦,非常大程度上提高了代碼的可閱讀和可維護性。

缺點: 你發現缺點了一定要告訴我

2、數據獲取---fetch

管理台實用性較強,一個頁面調用十幾個介面,這時候對獲取數據的封裝就非常重要了。傳統非同步獲取數據底層用的是XMLHttpRequest,它的設計非常粗糙,配置和調用方式非常混亂,而且是基於事件回調的,遠沒有用Promise友好。而fetch(fetch---傳送門)的話,語法簡潔,更加語義化,並且基於標準的Promise實現,可以快樂的使用es7的async/await,擺脫回調地獄,寫非同步代碼就跟寫同步代碼是一樣的。這裡要特別注意,fetch默認不會給你帶cookie到服務端,如需帶cookie,加上{credentials: include}。

當然咯,用async/await的話,要進行錯誤處理的話還得try-catch,然後我又不是喜歡寫try-catch,怎麼辦呢,await操作符後面的promise 若處理異常(rejected),await會把 promise 的異常原因拋出,為了不寫try-catch,不寫try-catch,不寫try-catch,我們不能讓await後面的promise reject,因此,我們需要hook介面返回的promise,並返回一個新的resolve promise給await。具體如下:

const RIGHT_ERR_CODE=0 //介面數據正常的錯誤碼假設為0function awaitHook(promise){ return promise.then(data=>{ if(data&&data.iErrCode===RIGHT_ERR_CODE){ return [null,data] //正常數據 } return [data] //異常數據 }).catch(err=>[err])}//獲取用戶信息var getUserInfo=require("models/user-info")var [err,data]=await awaitHook(getUserInfo())if(err){ //do something console.log("獲取數據失敗") return} //do something console.log("獲取數據成功")

這裡特別要注意,介面必須有個標誌告訴我們數據是正常的,我這裡寫的是iErrCode為0表示正常,這裡使用者可自行定義。

3、路由設計

首先,雖然管理台在整個架構在是SPA,但也存在多頁的場景,比如詳情頁或者列印頁,需要打開新的窗口顯示,對這些頁面對應的組件定義,完全沒必要在首屏的時候載入,我們可以使用非同步組件配合webpack的code split功能,實現路由懶載入(雖然是管理台,我們還是要有點情懷),提高首屏。這裡的原理是webpack把非同步組件的定義單獨打包到一個js文件,並且在主js文件中插入代碼,代碼的內容是:當訪問的是懶載入的路由,如果該路由對應的組件沒有被定義,則載入原來單獨打包的裡面包含非同步組件定義的js。

其次,如果涉及到頁面曝光上報的話,我們可以將路由命名,然後在路由全局前置守衛裡面對這個命名進行上報。如:

var route=[{ path: /index.html, component: PageIndex, name:index}]//訪問index.html的時候進行曝光router.beforeEach((to, from, next) => { var page=to.name||"index" //對即將要跳轉的路由進行曝光上報 console.log("曝光上報") next()})

再次,管理台的大多數頁面一般是有一個固定的布局,但也存在布局完全不一樣的情況,這個時候就需要用到命名視圖了,代碼如下,具體請看 命名視圖

var route=[{ path:/login.html, components:{ login:Login }, name:login}]//單訪問login.html時候,Login組件會渲染到name=login的router-view<router-view name="login"></router-view>

最後,最好使用vue-router的history模式,hash模式實在太丑了,而且不能通過url訪問指定頁面。

4、狀態管理

由於管理台的實用性和功能性,必然涉及到非常多的介面,因此我們引入vuex進行狀態管理。一般情況下,我們都是把所有狀態掛在一個state下,但對有非常多頁面的管理台,不推薦這樣做,要不然我們的狀態樹就太大了,而且命名也會有問題,我們需要將狀態劃分模塊。強烈建議使用vuex的modules(傳送門 vuex modules)功能,並且加一個namespaced:true;這樣每個頁面對應的組件有一個store文件,裡面定義state,action,mutation,不容易導致混亂。

除此之外,通常一個數據錄入頁面要承擔數據的修改和新增兩個功能,新增的話,需要我們提前準備好初始的業務對象state,修改就需要我們在拿到數據後,補上沒有返回的屬性,如果不補上的話,數據錄入很有可能會有問題,並且初始值要跟新增初始值保持一致。手動添加不存在的屬性並賦初值就太low了,而且極有可能漏掉某些屬性。我們可以定義一個文件存儲新增時需要的初始業務state,如果是修改,在拿到後台的數據對象後,遞歸遍歷該對象並賦值到初始state上,最終返回這個被遞歸賦值的初始state,代碼如下:

/** *策略:target中屬性值為null,,undefined的保留origin原值,屬性值為0的置為 *如果target是空對象或空數組的話,就用origin裡面的對象或數組 *origin 初始業務對象 *target 後台返回業務對象 */export const override=(origin,target)=>{ for(var key in target){ var type=getType(target[key]) if(!target[key]||(type!==object&&type!==array)){ origin[key]=isNull(target[key])?origin[key]:(target[key]===false?false:target[key]||) }else{ origin[key]=override(origin[key]||(type===array?[]:{}),target[key]) } } return origin}function getType(type){ var matched=Object.prototype.toString.call(type).match(/[object (w+)]/)||[] return (matched[1]||).toLowerCase()}function isNull(val) { if (val === || val === undefined || val === null) { return true } return false}

5、基礎搭建

(1)全局註冊組件

對應頁面中常用的公共組件,如翻頁組件、Loading組件等,可以在new Vue之前進行全局註冊,這樣就不需要每次使用時候,進行一次局部引入。詳情請看 基礎組件的自動化全局註冊

(2)全局通信

全局通信對管理台也是非常重要的,具體做法是new一個Vue實例作為全局通信對象,並掛載在window下。比如在頁面上進行了一個操作,操作成功後給一個toast提示,很顯然,我們不能把toast組件加到每個頁面所對應的組件裡面,為了只引入一次,我們可以把toast加入口vue文件裡面,並使用全局通信對象監聽,這樣在需要toast的時候,只需要觸發該事件就好。

(3)自定義組件實現v-model

//定義test.vue<template> <div> <input v-model="val"> </div></template><script> export default { props:[value], data(){ return { val:this.value } }, watch:{ value(){ this.val=this.value }, val(){ this.$emit(input,this.val) } } }</script>//使用import Test from test.vue<Test v-model="name"></Test> //行為跟表單上使用的v-model一致

(4)表單輸入校驗

管理台涉及非常多的表單輸入,當然有非常多的輸入有效性驗證啦,最好再項目搭建的時候就提供這麼一個模塊,裡面全部是基礎的有效性檢查函數,比如isNumber,isInteger,isZero等等

(5)組件上移,下移,置頂,刪除

對組件的上移,下移,置頂,刪除在管理台也是很常見的,比如運營想把更優質的模塊排在更前面點,直接進行這些操作也是可以的,但是我感覺這樣非常生硬,一閃操作就完成了,體驗不好(我可是有情懷的開發),於是引入了vue的transition和transition-group,用transition-group來實現組件上移,下移,置頂,刪除的過渡效果,效果如下:

https://www.zhihu.com/video/997279343135072256

實現如下:

<template> <div> <!-- 假定ModList就是你要移動的模塊 --> <!-- 一定要注意加唯一的key props傳給key的最好是個對象字元串 --> <transition-group name=list-complete tag=div> <ModList v-for=(mod,index) in vecModData :key=getStr(mod)></ModList> </transition-group> </div></template><style> /*列表順序移動軌跡*/.list-complete-move{ transition: all 1s;}.list-complete-leave-to{ transform: translateX(10px); opacity:0}.list-complete-leave-active { transition: all 1s;}</style>

6、對element-ui中組件的再封裝

element-ui中提供的基礎組件需要配合我們的業務場景進行使用,比如給input的左邊配置一個title,那麼就需要我們對element-ui進行再封裝。由於業務場景的複雜性,我們不能預測到所有的case,因此我們在封裝時,可以提供一些slot,供外界調用時進行個性化模板傳入。其次element-ui的基礎組件提供了大量的屬性,為了能在調用時任何屬性都能用到,推薦在定義基礎組件時加上inheritAttrs: false,禁用特性繼承,不讓特性加到跟元素上,並把v-bind="$attrs"加到element-ui的基礎組件上,這樣就跟調用沒封裝的element-ui一樣。

四、結尾

如採用上述方案遇到問題,歡迎聯繫、留言。或者有任何更好的實踐,歡迎探討,共同進步。

上述方案,我採用Vue進行了實現,腳手架請看 後台管理系統通用解決方案

彩蛋1:要是有哪位大神能猜出文章封面圖是在哪裡拍的,我一定要送只可愛的公仔給你

推薦閱讀:

vue中,父組件可以向子組件傳遞一個組件(不僅是數據)嗎?
Vue.js 怎麼讓 B 組件「繼承」 A 組件的 props 屬性?
Vue2.0 v-for 中 :key 到底有什麼用?

TAG:Vuejs | 許可權管理 | 管理系統 |