基於adminlte的後臺管理系統開發

gongxufan發表於2019-03-03

前言

現在的大前端技術來勢凶猛,Vue&React&Angular三足鼎立。如果為了開發一個內部使用的管理系統需要去學習Node&Webpack等各種新概念,況且我們的系統並沒有那麼複雜到需要用上現在這些新技術。

我要的僅僅是一個單頁應用,相容IE8+,能基於固定模式新增功能,基於Bootstrap和各種jQuery外掛進行開發即可。我開始尋找後端管理系統模板,最後將目光鎖定在adminlte。其實現效果如下:

adminlite-plus​

基於adminlte的後臺管理系統開發

單頁應用

在沒有接觸Angular.js之前我都是通過jQuery.load來進行頁面的分片載入,這種模式下需要手動去管理和切換檢視。既然擯棄了前端框架的路由檢視切換來實現單頁,我決定使用knockout.js和require.js來實現。

關於knockout(後邊一律簡稱ko),可能大家都不太熟悉。這個庫應該是資料繫結的早期實踐者。選擇他的原因是支援IE6+,畢竟我所處的政務行業至少需要支援到IE8。ko提供了資料到檢視的雙向繫結,但他有個缺陷就是model資料無法巢狀。其預設只有一個全域性的作用域,如果需要對DOM進行分割槽域進行資料繫結則需要用到knockout-multimodels外掛。如果你不瞭解這些技術可以看看其文件即可快速掌握。然後是require,主要是用來對JS分模組進行管理和載入。

接下來我們需要一個簡單的路由功能,切換導航選單頁面不需要手動進行DOM的替換。這個功能我們選擇使用director.js來實現。其原理主要是監聽瀏覽器hash,當切換選單觸發瀏覽器位址列發生變化,呼叫相應模組的載入。

動手實現

從使用者點選一個導航選單到頁面載入,資料請求和檢視渲染的過程如下:

基於adminlte的後臺管理系統開發

對於一個後臺管理系統其檢視結構就是一個主頁index.html,然後按照tab來進行檢視的切換。在上面載入html環節就是替換當前tab的內容,html模板一般會有瀏覽器快取只會請求一次。

實現路由

1,我們首選需要頂一個路由資料,其實也就是導航選單的資料:

js/framework/routes.js

/**
 * 定義系統路由資訊
 */
define([`jquery`, `common`,`index`], function ($, common,index) {
    var routes = {
        404: {
            title: `系統出錯了`
        }
    };
    var sysNavMenusObj = index.sysNavMenus;
    function getResource(parentid) {
        for (var i = 0; i < sysNavMenusObj.length; i++) {
            var obj = sysNavMenusObj[i];
            if (obj.resourceid == parentid) {
                return obj;
            }
        }
    }
    if (sysNavMenusObj && sysNavMenusObj.length > 0) {
        for (var i = 0; i < sysNavMenusObj.length; i++) {
            var obj = sysNavMenusObj[i];
            if (obj.routeUrl && obj.type ==1) {
                routes[obj.routeUrl] = obj;
                if (obj.parentid)
                    obj.parent = getResource(obj.parentid).routeUrl;
            }
        }
    }
    routes.sysNavMenus = sysNavMenusObj;
    return routes;
});
複製程式碼

routes 物件講routerUrl作為key遍歷選單資料,在後邊依據routerUrl就可以找到對應選單資料。其中sysNavMenus是選單資料,包括每個選單下的子選單,其結構如下:

 sysNavMenus: [{
            "resourceid": 1,
            "name": "首頁",
            "resIco": "fa fa-dashboard",
            "resurl": "/dashboard",
            "hrefUrl": "/",
            "type": 1,
            "displayorder": 100,
            "routeUrl": "dashboard",
            "childResourceList": []
        }, {
            "resourceid": 2,
            "name": "租戶管理",
            "resIco": "fa fa-link",
            "resurl": "/tenant",
            "hrefUrl": "/tenant",
            "type": 1,
            "displayorder": 200,
            "routeUrl": "tenant",
            "childResourceList": []
        }
            , {
                "resourceid": 3,
                "name": "使用者管理",
                "resIco": "fa fa-user-circle-o",
                "resurl": "/unit",
                "hrefUrl": "/user",
                "type": 1,
                "displayorder": 300,
                "routeUrl": "user",
                "childResourceList": [{
                    "resourceid": 4,
                    "name": "組織機構管理",
                    "resIco": "fa fa-university",
                    "resurl": "/unit",
                    "hrefUrl": "/user/unit",
                    "type": 1,
                    "parentid": 3,
                    "displayorder": 310,
                    "routeUrl": "unit",
                    "childResourceList": []
                }, {
                    "resourceid": 5,
                    "name": "崗位管理",
                    "resIco": "fa fa-id-card-o",
                    "resurl": "/role",
                    "hrefUrl": "/user/role",
                    "type": 1,
                    "parentid": 3,
                    "displayorder": 320,
                    "routeUrl": "role",
                    "childResourceList": []
                }, {
                    "resourceid": 6,
                    "name": "人員管理",
                    "resIco": "fa fa-user",
                    "resurl": "/human",
                    "hrefUrl": "/user/human",
                    "type": 1,
                    "parentid": 3,
                    "displayorder": 330,
                    "routeUrl": "human",
                    "childResourceList": []
                }, {
                    "resourceid": 7,
                    "name": "許可權管理",
                    "resIco": "fa fa-lock",
                    "resurl": "/auth",
                    "hrefUrl": "/user/auth",
                    "type": 1,
                    "parentid": 3,
                    "displayorder": 340,
                    "routeUrl": "auth",
                    "childResourceList": []
                }]
            }, {
                "resourceid": 4,
                "name": "組織機構管理",
                "resIco": "fa fa-university",
                "resurl": "/unit",
                "hrefUrl": "/user/unit",
                "type": 1,
                "parentid": 3,
                "displayorder": 310,
                "routeUrl": "unit",
                "childResourceList": []
            }, {
                "resourceid": 5,
                "name": "崗位管理",
                "resIco": "fa fa-id-card-o",
                "resurl": "/role",
                "hrefUrl": "/user/role",
                "type": 1,
                "parentid": 3,
                "displayorder": 320,
                "routeUrl": "role",
                "childResourceList": []
            }, {
                "resourceid": 6,
                "name": "人員管理",
                "resIco": "fa fa-user",
                "resurl": "/human",
                "hrefUrl": "/user/human",
                "type": 1,
                "parentid": 3,
                "displayorder": 330,
                "routeUrl": "human",
                "childResourceList": []
            }, {
                "resourceid": 7,
                "name": "許可權管理",
                "resIco": "fa fa-lock",
                "resurl": "/auth",
                "hrefUrl": "/user/auth",
                "type": 1,
                "parentid": 3,
                "displayorder": 340,
                "routeUrl": "auth",
                "childResourceList": []
            }, {
                "resourceid": 10,
                "name": "程式碼示例",
                "resIco": "fa fa-lock",
                "resurl": "/demo",
                "hrefUrl": "/demo",
                "type": 1,
                isIframe:true,
                "displayorder": 340,
                "routeUrl": "demo",
                "childResourceList": []
            }]
複製程式碼

在我的系統每一個選單其實就是一個許可權實體定義。resurl,hrefUrl,routeUrl分別代表資源地址(可是選單的連結地址也可以是某個許可權的訪問地址),導航選單連線地址,路由定義的key。這裡的定義可以依據自己實際情況進行修改。

2,路由和檢視切換繫結

js/framework/router.js

define([`knockout`, `controller`, `routes`, `router`, `jquery`], function (ko, controller, routes, Router, $) {
    function dispatch(path) {
        var route = routes[path];
        if (!route.hrefUrl) return;
        if (route.hrefUrl.indexOf(`#`) == -1)
            route.hrefUrl = `#` + route.hrefUrl;
        controller.initJSAndCSS(path, route);
    }

    //初始化路由
    var router = new Router().configure({
        //404
        notfound: function () {
            dispatch(`404`);
        },
        html5history: false
    });
    //根據系統選單設定路由
    var sysNavMenus = routes.sysNavMenus;
    if (sysNavMenus && sysNavMenus.length > 0) {
        for (var i = 0; i < sysNavMenus.length; i++) {
            var obj = sysNavMenus[i];
            //非導航類資料
            if (obj.type != 1)
                continue;
            //立即執行路由繫結
            (function (obj) {
                router.on(obj.hrefUrl, function () {
                    var url = obj.routeUrl;
                    //直接開啟子選單連結
                    if (obj.parentid)
                        dispatch(url);
                    else {//當前是父選單
                        var childResourceList = obj.childResourceList;
                        //有二級選單,如果有三級選單可以繼續判斷下一級
                        if (childResourceList && childResourceList.length > 0)
                            dispatch(childResourceList[0].routeUrl);
                        else//只有一個根選單
                            dispatch(url);
                    }
                });
            })(obj);
        }
    }
    var urlNotAtRoot = window.location.pathname && (window.location.pathname != baseUrl);

    if (urlNotAtRoot) {
        router.init();
    } else {
        router.init(`/`);
    }
    //預設跳轉到第一個選單
    document.location.href = "#" + sysNavMenus[0].hrefUrl;
    return router;
});
複製程式碼

這裡的路由配置使用的director.js提供的Router物件監聽選單跳轉地址。需要注意的是在遍歷選單資料進行路由繫結的時候需要使用自執行函式進行立即執行路由繫結。

3,載入js/css和資料繫結

router.js中的dispatch方法實現了根據選單跳轉地址進行後續的操作,包括資源載入和資料繫結。實現在js/framework/controller.js

define([`common`, `knockout-multimodels`, `tab`, `jquery`, `router`, `routes`, `index`], function (common, ko, tab, $) {
    function isEndSharp() { // url end with #
        if (controller.lastUrl != "" && location.toString().indexOf(controller.lastUrl) != -1 &&
            location.toLocaleString().indexOf(`#`) != -1 && location.hash == "") {
            return true;
        }
        return false;
    }
    var controller = {
        /**
         * 當前啟用的頁面和路由引數
         * @param pageName
         * @param routes
         */
        initJSAndCSS: function (pageName, route) {
            require([pageName + `-js`, `css!` + pageName + `-css`], function (page) {
                controller.init(pageName, page, route);
            });
        },
        init: function (pageName, pageData, route) {
            if (isEndSharp()) {
                return;
            }
            //使用TAB載入頁面
            tab.addTabs({
                id: route.resurl,
                title: route.name,
                close: route.resurl == `/dashboard` ? false : true,
                url: paths[route.routeUrl + `-html`],
                isIframe: route.isIframe,
                urlType: "relative",
                modelId: route.routeUrl,
                pageData: pageData,
                callback: function () {
                    pageData.init();
                    //每一個TAB頁籤繫結一個資料模型,以modelId進行區分
                    //繫結的資料模型物件也即每個define模組的返回值
                    //attach代替原有的applyBindings,因為後者只支援一個物件繫結
                    ko.attach(route.routeUrl, pageData);
                    pageData.afterRender();
                }
            });
        }
    };
    return controller;
});
複製程式碼

initJSAndCSS通過rquire載入模組的JS和CSS檔案,接著呼叫init開啟一個tab頁。

tab的配置如下:

  tab.addTabs({
                id: route.resurl,
                title: route.name,
                close: route.resurl == `/dashboard` ? false : true,
                url: paths[route.routeUrl + `-html`],
                isIframe: route.isIframe,
                urlType: "relative",
                modelId: route.routeUrl,
                pageData: pageData,
                callback: function () {
                    pageData.init();
                    //每一個TAB頁籤繫結一個資料模型,以modelId進行區分
                    //繫結的資料模型物件也即每個define模組的返回值
                    //attach代替原有的applyBindings,因為後者只支援一個物件繫結
                    ko.attach(route.routeUrl, pageData);
                    pageData.afterRender();
                }
            });
複製程式碼

close:決定tab頁籤是否可關閉,這裡第一個選單是首頁,其resurl是`/dashboard`.
url:頁面模板URL,其值是在main.js中配置的HTML模板名字,也就是routerUrl加上字尾
ifIframe:是否以iframe方式開啟tab
pageData:js模組的返回值,通過他可以呼叫init,afterRender等
callback:載入完HTML模板執行的回撥

addTabs的具體實現不在此討論,其主要是通過jQuery獲取模板檔案然後插入到tab,頁面模板完成載入後開始執行模組的回撥,呼叫init()和afterRender()進行資料的初始化和檢視的繫結。

如何使用

對於使用者來說不需要過多關注上邊的具體實現,下載專案後新增新的模組只要按照下面的步驟即可:

專案目錄結構如下:

基於adminlte的後臺管理系統開發

我們需要新增的模組位於templates目錄,其下按照業務模組劃分,js/css/html檔案都在一個目錄。現在我要新增一個新的模組test:

1,在templates下新建test目錄和檔案test.js/test.css/test.html

2,在主模組main.js中新增相應的模組定義

基於adminlte的後臺管理系統開發

注意:html檔案模板後面的字尾要填寫

3,編輯控制器模組test.js

define([`dialog`, `common`, `knockout`, `knockout-mapping`, `jquery`, `gotoTop`], function (dialog, common, ko, mapping, $) {
    ko.mapping = mapping;

    function test() {
        //資料初始化和KO繫結
        this.init = function () {

        };
        this.initUI = function () {
            this.initEvent();

        };
        this.initEvent = function () {

        };
        //渲染UI
        this.afterRender = function () {
            this.initUI()
        };
    };

    return new test();
});
複製程式碼

模組實現部分按照上邊的模式進行編寫即可,init主要是定義模型資料,afterRender則是傳送請求獲取資料然後通過ko進行資料的繫結。

4,頁面編寫

在test.html編寫HTML程式碼即可

總結

這個後端開發框架只是傳統的基於jQuery和knockout進行資料雙向繫結,提供簡單的路由檢視切換功能。使用者只需按照固定的模式新增功能模組即可。因為使用require進行模組管理,也便於對系統進行模組劃分和功能複用。

除了提供一個腳手架的開發框架之後,系統還預設繼承了諸多的jquery外掛,並對require進行試了適配,具體的相關庫如下:

datatables
highcharts
highcharts-map
jquery.treegrid
jquery.fileupload
jquery.storageapi
jquery.mCustomScrollbar
jquery.imgareaselect
icheck
select2
ztree

專案地址:

gongxufan/adminlite-plus​

相關文章