前言

现在的大前端技术来势凶猛,Vue&React&Angular三足鼎立。如果为了开发一个内部使用的管理系统需要去学习Node&Webpack等各种新概念,况且我们的系统并没有那么复杂到需要用上现在这些新技术。

我要的仅仅是一个单页应用,兼容IE8 ,能基于固定模式添加功能,基于Bootstrap和各种jQuery插件进行开发即可。我开始寻找后端管理系统模板,最后将目光锁定在adminlte。其实现效果如下:

adminlite-plus​

单页应用

在没有接触Angular.js之前我都是通过jQuery.load来进行页面的分片加载,这种模式下需要手动去管理和切换视图。既然摈弃了前端框架的路由视图切换来实现单页,我决定使用knockout.js和require.js来实现。

关于knockout(后边一律简称ko),可能大家都不太熟悉。这个库应该是数据绑定的早期实践者。选择他的原因是支持IE6 ,毕竟我所处的政务行业至少需要支持到IE8。ko提供了数据到视图的双向绑定,但他有个缺陷就是model数据无法嵌套。其默认只有一个全局的作用域,如果需要对DOM进行分区域进行数据绑定则需要用到knockout-multimodels插件。如果你不了解这些技术可以看看其文档即可快速掌握。然后是require,主要是用来对JS分模块进行管理和加载。

接下来我们需要一个简单的路由功能,切换导航菜单页面不需要手动进行DOM的替换。这个功能我们选择使用director.js来实现。其原理主要是监听浏览器hash,当切换菜单触发浏览器地址栏发生变化,调用相应模块的加载。

动手实现

从用户点击一个导航菜单到页面加载,数据请求和视图渲染的过程如下:

对于一个后台管理系统其视图结构就是一个主页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()进行数据的初始化和视图的绑定。

如何使用

对于使用者来说不需要过多关注上边的具体实现,下载项目后添加新的模块只要按照下面的步骤即可:

项目目录结构如下:

我们需要添加的模块位于templates目录,其下按照业务模块划分,js/css/html文件都在一个目录。现在我要添加一个新的模块test:

1,在templates下新建test目录和文件test.js/test.css/test.html

2,在主模块main.js中添加相应的模块定义

注意: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​


Logo

前往低代码交流专区

更多推荐