• 前端分离规范


    整理轻量级的前端分离规范

     

        背景    

     
        支持的业务需要做客户端的页面嵌入,并且此类页面大部分是单页应用。为了能够是组内的人都能快速的入手,并且可以分工开发,制订了这么一个规范,主要做的就是能够快速入手,便于分工协作;另一方面清晰的思路便于查找问题。

        为什么要做分离?
        
        我们知道一个网页的展示,离不开一下部分:UI(包括结构和样式)、UI事件(DOM上绑定的键盘或者是鼠标事件)、逻辑处理(各个事件有各自相应的处理逻辑,发送相关请求或DOM操作)、数据(包括UI自身的数据传递、接口数据、客户端数据)。如果将这几个部分进行分离,一方面可以清晰结构,另一方面出现问题也可以很方便的查找,之间通过一种通信方式进行通信,各部分不用关注各自的具体实现,只需要在自己想要使用的时候进行调用即可。如果没做做分离,那么逻辑层是沟通UI和数据的桥梁,这样的话,所有的内容都会混在一起,就是普通的一种开发模式。
        
        确定通信方式    
     
        作为前端,我们最熟悉的就是DOM的事件了,jquery在实现事件的时候,有这么一套东西,可以做到事件绑定($.fn.on)、解绑($.fn.off)、触发($.fn.trigger或$.fn.triggerHandler)。那么我们能不能自己封装一下这个事件,让它作为我们通信的一种方式呢?答案当然是可以的。既然已经确定了通信方式,那我们就可以实现上述分离开的各部分进行通信了,当然最主要的就是逻辑层和UI层的通信。
        
        期望呈现的是形式
     
        我们期望达到的一种状态是:我们所有使用的数据(客户端数据和接口返回数据)都写在一个文件里,这样如果是数据层面的调整,如:接口调整,只需要更换接口地址即可,不需要在每次调用的地方找接口地址进行替换,做到一改全改的效果;所有DOM绑定的事件需要做的事情,都通过逻辑层按需发送相应请求,当请求返回后,我们肯定是需要拿到数据然后对DOM进行操作(重绘、重排),在逻辑层里,我们并不关注DOM到底应该怎么操作,我们关注的是达到一定的阶段需要通知UI做相应的操作,至于UI是否响应对于逻辑层来说并不关注。此时,我们可以使用这个通信事件做很多事情,如:打点统计,暴漏钩子。。。方便业务在使用的时候,监听某个步骤发生了什么,在单元测试或者是代码使用率测试上都有作用。

        基础结构具体实现(配合代码分析)
     
        下面就以业务中实现的一个例子(QClient,客户端开发工具)来分析一下前端分离规范的实现过程吧。
        
        1. 核心模块(core.js)
        
        做的就是创建可能会使用到的命名空间,便于其他模块使用。
     
     
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /**
     * @Overview 核心模块
     * 1.创建命名空间
     * 2.初始化公共事件
     */
    (function(window) {
        'use strict';
     
        var QClient = window.QClient = {
            //框架类型
            $ : jQuery,
            //工具类
            utils: {},
            //UI集合
            ui: {},
            //数据集合
            sync: {},
            //事件
            events: {},
            //调试模式
            DEBUG: false
        };
         
    })(window);
     
        2. 数据模块(data.js)
     
        将常见的数据获取方式放在这里处理,常见的数据方式有:接口请求返回数据和客户端返回数据(这个在客户端内嵌页面会比较常用)。可以处理get、post请求以及客户端请求,同时处理同域和跨域问题。这样在调用的时候就不用关注这个请求是什么形式了,暴漏相应的API方便调用即可。这里使用promise方式封装,使用起来更加方便。这里使用的接口主要是get的跨域请求和客户端数据,如果想实现其他请求可以参考之前的一篇文章,跨域请求解决方案
     
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    /**
     * @Overview  数据接口模块
     * 数据交互,如果接口很少可以放到logic中
     */
    (function(Q){
     
        var $ = Q.$;
     
        /**
         * 交互类
         * @param {object} param 要提交的数据
         * @param {Object} [ajaxOpt] ajax配置
         * @constructor
         */
        var Sync = function(param, ajaxOpt) {
            if(!param) {
                return;
            }
            var protocol = this.protocol = 'http';
      
            var ajaxOptDefault = {
                url: protocol + '://'+location.host,
                type: 'GET',
                dataType: 'jsonp',
                timeout: 20000
            };
      
            this.protocol = protocol;
            this.param = $.extend({}, param);
            this.ajaxOpt = $.extend({data: this.param}, ajaxOptDefault, ajaxOpt);
            this.HOST = protocol + '://'+location.host;
        };
         
        /* 示例:window.external.getSID(arg0)需要改为 external_call("getSID",arg0) 的形式进行调用 */
        function external_call(extName,arg0,arg1,arg2){
            var args = arguments, fun = args.callee, argsLen = args.length, funLen = fun.length;
            if(argsLen>funLen){
                throw new Error("window.external_call仅接受"+funLen+"个形参,但当前(extName:"+extName+")传入了"+argsLen+"个实参,请适当调整external_call,以保证参数正常传递,避免丢失!");
            }
            if(window.external_call_test){
                return window.external_call_test.apply(null,[].slice.apply(args));
            }
            /* 这里的参数需要根据external_call的形参进行调整,以保证正常传递
             *   IE系列external方法不支持apply、call...
             *   甚至部分客户端对参数长度也要求必须严格按约定传入
             *   所以保证兼容性就必须人肉维护下面这么一坨..
            */
            if(argsLen==1)return window.external[extName](); 
            if(argsLen==2)return window.external[extName](arg0); 
            if(argsLen==3)return window.external[extName](arg0,arg1); 
            if(argsLen==4)return window.external[extName](arg0,arg1,arg2); 
        }
      
        $.extend(Sync.prototype, {
            /**
             * 通过get方式(jsonp)提交
             * @param {String} [url] 请求链接
             * @return {Object} promise对象
             */
            get: function(url) {
                var self = this;
                var send = $.ajax(url, this.ajaxOpt);
                return send.then(this.done, function(statues) {
                    return self.fail(statues);
                });
            },
            /**
             * 通知客户端
             */
            informClient: function() {
                var self = this;
                var deferred = $.Deferred();
                var args = [].slice.apply(arguments);
                try {
                    var data = external_call.apply(null, args);
                    deferred.resolve(data);
                }catch (e) {
                    deferred.reject({
                        errno: 10000,
                        errmsg: '通知客户端异常'
                    });
                }
                return deferred.promise()
                    .then(self.done, self.fail);
            },
            /**
             * 收到响应时默认回调
             * @param {Object} data 数据
             * @return {Object}
             */
            done: function (data) {
                var deferred = $.Deferred();
                deferred.resolve(data);
                return deferred.promise();
            },
            /**
             * 未收到响应时默认回调
             * @param {Object} error 错误信息
             * @return {Object}
             */
            fail: function(error) {
                var deferred = $.Deferred();
                deferred.reject({
                    errno: 999999,
                    errmsg: '网络超时,请稍后重试'
                });
                return deferred.promise();
            }
        });
         
        QClient.Sync = Sync;
     
    })(QClient);
     
        3. 逻辑模块(logic_factory.js)
     
        主要关联UI和逻辑层,这里主要做了这么一些事情:第一,作为整个应用的入口,传入相关参数后,初始化UI;第二:处理整个逻辑内的数据传递;第三:根据实际情况暴漏相关接口给外部调用;第四:建立基础的通信方式,实现逻辑层与UI层的事件通信。具体实现方式和解释,如下:
     
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    /**
     * @Overview  逻辑模块工厂
     * 1.定义各功能公用的功能
     * 2.单功能间数据缓存
     */
    (function(Q) {
        //'use strict';
     
        var $ = Q.$;
        var $events = $(Q.events);
     
        var Logic = function(props) {
            this.name = 'func_' + Q.utils.getGuid();
            this.extend(props);
     
            this._initFlag = false;
            this._data = {};
        };
     
        $.extend(Logic.prototype, {
            /**
             * 初始化函数
             */
            init : function() {
                var self = this;
                if (!self._initFlag) {
                    self._initFlag = true;
                    Q.ui[self.name].init(self);
                    self.initJsBind();
                }
                return self;
            },
            /**
             * 获取是否已经初始化的标记
             * @returns {boolean}
             */
            isInit: function() {
                return this._initFlag;
            },
            /**
             * 获取数据
             * @param {String} key
             * @param {*} defaultValue
             * @returns {*}
             */
            get : function(key, defaultValue) {
                var value = this._data[key];
                return value !== undefined ? value : defaultValue;
            },
            /**
             * 设置数据
             * @param {String|Object} key
             * @param {*} value
             */
            set : function(key, value) {
                if ($.isPlainObject(key)) {
                    $.extend(this._data, key);
                else {
                    this._data[key] = value;
                }
                return this;
            },
            /**
             * 清理数据
             */
            clear : function() {
                this._data = {};
                return this;
            },
            /**
             * 客户端调用页面JS
             */
            initJsBind: function () {
                var self = this;
                window.jsBind = function(funcName) {
                    var args = [].slice.apply(arguments, [1]);
                    return self[funcName].apply(self, args);
                };
            },
             
            /**
             * 扩展实例方法
             * @param {...object} - 待mixin的对象
             */
            extend : function() {
                var args = [].slice.apply(arguments);
                args.unshift(this);
                $.extend.apply(null, args);
            }
        });
         
            //创建事件通信方式
        $.each(['on''off''one''trigger'], function(i, type) {
            Logic.prototype[type] = function() {
                $.fn[type].apply($events, arguments);
                return this;
            };
        });
     
        Q.getLogic = function(props) {
            return new Logic(props);
        };
    })(QClient);
     
        4. 工具模块(utils.js)
     
        这个模块儿没什么特殊的含义,只是存放一些工具方法。可以在基础包,也可以自己在使用的时候定义。
     
        如何使用?
     
        1. 引入上面所讲到的基础文件
     
        2.分别创建上面对应的功能文件,如:data.js、ui.js、logic.js、utils.js,当然如果项目并不是很大,也可以放在一个文件里实现,这里分开是为了结构更加的清晰
     
        2.1 创建data.js,存放业务将要使用到的所有数据接口
        
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    (function(Q) {
        'use strict';
     
        var Sync = Q.Sync;
         
        Q.sync = {
            //获取class
            getClassify: function(catgoryConf) {
                var sync = new Sync();
                return sync.informClient('onGetClassify', catgoryConf);
            },
            //获取当前皮肤
            getCurrentSkin: function() {
                var sync = new Sync();
                return sync.informClient('GetCurrentSkinName');
            }
        };
    }(QClient));
     
        2.2 创建logic.js,建议每个功能创建一个,可以更好的组装和分离
     
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    /**
     * @Overview  逻辑交互
     * 接收UI状态,通知UI新操作
     */
    (function(Q){
        'use strict';
         
        var utils = Q.utils;
         
        var logic = Q.getLogic({
            name: 'changeSkin',
             
            run: function(opts){
                var _this = this;
                var catgoryConf = opts.catgoryConf;
                 
                _this.init();
                 
                Q.sync.getClassify(utils.stringify(catgoryConf));
                     
                Q.sync.getCurrentSkin()
                    .done(function(data) {
                        var currKey = data.extra_info;
                        _this.setCurrentSkin( currKey );
                    });
            },
            //通过事件进行通信
            setCurrentSkin: function (key) {
                this.trigger('setCurrentSkin', key);
            },
             
            setDownLoadStart: function(key) {
                this.trigger('setDownLoadStart', key);
            },
             
            setDownLoadSuccess: function(key) {
                this.trigger('setDownLoadSuccess', key);
            },
             
            setDownLoadFailed: function(key) {
                this.trigger('setDownLoadFailed', key);
            }
          //也可以通过promise对结果进行处理,方便UI直接调用逻辑操作,同时在这里可以保证UI使用到的数据是完全可信的状态,UI不用判断数据是否为空等异常情况
        });
     
        Q.changeSkin = function(opts) {
            logic.run(opts);
        };
     
    })(QClient);

     
     
        2.3 创建ui.js,和逻辑层配合使用
     
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    /**
     * @Overview  UI模块
     * 页面交互,通知状态
     */
    (function(Q){
        'use strict';
     
        var $ = Q.$;
        var $skinlist = $('.skin-list');
         
        var ui = {
            init : function(model) {
                this.model = model;
                this.initEvent();
                this.initModelEvent();
            },
             
            initModelEvent: function() {
                var _this = this;
                //监听逻辑层触发的事件
                this.model
                    .on('setDownLoadFailed'function( e, key ){
                        var $item = _this.getCurrentItem( key );
                        _this.stopLoading( $item );
                        $item.addClass('err');
                    })
                    .on('setDownLoadSuccess'function( e, key ){
                        var $item = _this.getCurrentItem( key );
                        _this.stopLoading( $item );
                    })
                    .on('setDownLoadStart'function( e, key ){
                        var $item = _this.getCurrentItem( key );
                        var $loading = $item.find('i span');
                        var i = 0;
                        $item.addClass('loading').removeClass('err hover');
                        $item[0].timer = setInterval(function(){
                            i = i >= 12 ? 0 : i;
                            var x = -i*32 ;
                            $loading.css('left' , x );
                            i++;
                        },100);
                    })
                    .on('setCurrentSkin'function( e, key ){
                        var $item = _this.getCurrentItem( key );
                        _this.stopLoading( $item );
                        $item.addClass('selected').siblings().removeClass('selected');
                    });
            },
             
            initEvent: function() {
                var _this = this;
                //Q.utils.disabledKey();
                 
                $skinlist.on('click','a',function(){
                    var $item = $(this).parent();
                    if( $item.hasClass('loading') || $item.hasClass('selected')){
                        return false;
                    }
                });
                 
                //hover状态
                $skinlist.on('mouseover','a',function(){
                    var $parent = $(this).parent();
                    (!$parent.hasClass('loading') && !$parent.hasClass('selected')) && $parent.addClass('hover');
                }).on('mouseout','a',function(){
                    $(this).parent().removeClass('hover');
                });
     
                //图片延迟加载
                var $img = $skinlist.find('img');
     
                $img.lazyload({
                    container: $skinlist
                });
     
                //初始化滚动条
                _this.scrollBar = new CusScrollBar({
                    scrollDir:"y",
                    contSelector: $skinlist ,
                    scrollBarSelector:".scroll",
                    sliderSelector:".slider",
                    wheelBindSelector:".wrapper",
                    wheelStepSize:151
                     
                });
                _this.scrollBar._sliderW.hover(function(){
                    $(this).addClass('cus-slider-hover');
                }, function(){
                    $(this).removeClass('cus-slider-hover');
                });
                _this.scrollBar.on("resizeSlider",function(){
                    $(".slider-bd").css("height",this.getSliderSize()-10);
                }).resizeSlider();
            },
             
            reload: function () {
                var _this = this;
                var $cur = [];
                $(".item").each(function(){
                    if( $(this).css('display') == 'block' ){
                        $cur.push($(this));
                    }
                });
                $.each( $cur , function( index ){
                    if( index <= 9 ){
                        var $img = $(this).find('img');
                        $img.attr('src',$img.attr('data-original'));
                    }
                });
                if( $cur.length <=6 ){
                    $(_this.scrollBar.options.scrollBarSelector).hide();
                }
                else{
                    $(_this.scrollBar.options.scrollBarSelector).show();
                }
                $(_this.scrollBar.options.sliderSelector).css('top',0);
                _this.scrollBar.resizeSlider().scrollToAnim(0);
            },
             
            LoadCatgory : function( type ){
                if( type && type!="all" ){
                    var $items = $skinlist.find('.item[data-type="'+ type +'"]');
                    $skinlist.find('.item').hide();
                    $items.fadeIn(100);
                }
                else{
                    $skinlist.find('.item').fadeIn(100);
                }
                this.reload();
            },
             
            setErrByLevel : function(){
                console&&console.log('等级不符,快去升级吧!');
            },
             
            getCurrentItem: function( key ){
                return $skinlist.find('.item[data-key="'+ key +'"]');
            },
             
            stopLoading : function( $item ){
                if( $item.hasClass('loading') ){
                    clearInterval($item[0].timer);
                    $item[0].timer = null;
                    $item.removeClass('loading');
                    $item.find('i span').css('left','0');
                }
            }
        };
     
        Q.ui.changeSkin = {
            init : function() {
                ui.init.apply(ui, arguments);
            }
        };
    })(QClient);
     
        2.4 创建utils.js,除了基础包中存在的工具,自己业务可能使用到的可以放在这里(可有可无,非必须),以下是举例这个项目使用到的工具方法
       
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    /**
     * @Overview  工具方法
     * 各种子功能方法:cookie、滚动条、屏蔽按键等等
     */
    (function(Q){
        'use strict';
     
        var utils = Q.utils;
        var guid = parseInt(new Date().getTime().toString().substr(4), 10);
     
        /**
         * 获取唯一ID
         * @returns {number}
         */
        utils.getGuid = function() {
            return guid++;
        };
     
        /**
         * 通用回调解析函数
         * @param {String|Function|Boolean} callback 回调函数 或 跳转url 或 true刷新页面
         * @returns {Function} 解析后的函数
         */
        utils.parseCallback = function(callback) {
            if ($.type(callback) == 'function') {
                return callback;
            else if (callback === true) {
                return function() {
                    location.reload();
                };
            else if ($.type(callback) == 'string' && callback.indexOf('http') === 0) {
                return function() {
                    location.href = callback;
                };
            else {
                return function() {};
            }
        };
         
        /**
         * 阻止各种按键
         */
        utils.disabledKey = function() {
            document.onkeydown = function(e){
                //屏蔽刷新  F5  Ctrl + F5  Ctrl + R Ctrl + N
                var event = e || window.event;
                var k = event.keyCode;
                if((event.ctrlKey === true && k == 82) || (event.ctrlKey === true && k == 78) || (k == 116) || (event.ctrlKey === true && k == 116))
                {
                    event.keyCode = 0;
                    event.returnValue = false;
                    event.cancelBubble = true;
                    return false;
                }
            };
            document.onclick = function( e ){
                //屏蔽 Shift + click Ctrl + click
     
                var event = e || window.event;
     
                var tagName = '';
                try{
                    tagName = (event.target || event.srcElement).tagName.toLowerCase();
                }catch(error){}
     
                if( (event.shiftKey || event.ctrlKey) && tagName == 'a' ){
                    event.keyCode = 0;
                    event.returnValue = false;
                    event.cancelBubble = true;
                    return false;
                }
            };
            document.oncontextmenu = function(){
                //屏右键菜单
                return false;
            };
            document.ondragstart = function(){
                //屏蔽拖拽
                return false;
            };
            document.onselectstart = function( e ){
                //屏蔽选择,textarea 和 input 除外
                var event = e || window.event;
                var tagName = '';
                try{
                    tagName = (event.target || event.srcElement).tagName.toLowerCase();
                }catch(error){}
     
                if( tagName != 'textarea' && tagName != 'input'){
                    return false;
                }
            };
        };
        /**
         * 对象转字符串
         * @param {Object} obj
         */
        utils.stringify = function(obj) {       
            if ("JSON" in window) {
                return JSON.stringify(obj);
            }
     
            var t = typeof (obj);
            if (t != "object" || obj === null) {
                // simple data type
                if (t == "string") obj = '"' + obj + '"';
     
                return String(obj);
            else {
                // recurse array or object
                var n, v, json = [], arr = (obj && obj.constructor == Array);
     
                for (n in obj) {
                    v = obj[n];
                    t = typeof(v);
                    if (obj.hasOwnProperty(n)) {
                        if (t == "string") {
                            v = '"' + v + '"';
                        else if (t == "object" && v !== null){
                            v = Safe.stringify(v);
                        }
     
                        json.push((arr ? "" '"' + n + '":') + String(v));
                    }
                }
     
                return (arr ? "[" "{") + String(json) + (arr ? "]" "}");
            }
        };
     
    })(QClient);

      

        3. 通过入口方法进行调用
     
      
    1
    2
    3
    QClient.changeSkin({
        catgoryConf: CATGORY_CONF //需要从页面初始化的参数
    });
     
     
        优势
     
    • 实现了前端各功能结构上的分离,使得结构上更加清晰
    • UI和逻辑分离使用事件通信,可以更好的进行分工开发
    • 便于扩展,方便代码的重构和升级
     
        劣势
     
        最明显的是一个功能可能产出多个文件,不过可以按照上面提到的方式(各模块都放在一个文件里)解决;也或者搞一个工具来做文件的合并处理,这个现在也不是什么难事。这里针对我们的业务实现了一个专门处理这个的工具,仅供参考(QClient开发工具,该工具集成了上述前端分离规范)【由于第一次开发这种工具,代码组织的还不是很好,大概就是说明有这么一种形式】
     
        感谢
     
        感谢摩天营的同学提出的改进建议,特别感谢@jedmeng对结构的梳理。
     
    标签: javascriptnodejs
  • 相关阅读:
    今日SGU 5.2
    奇异值分解(SVD)小结
    计蒜客16495 Truefriend(fwt)
    计蒜客16492 building(二分线段树/分块)
    hihocoder 1323 回文字符串(字符串+dp)
    hihocoder 1320 压缩字符串(字符串+dp)
    hdu6121 build a tree(树)
    hdu6103 Kirinriki(trick+字符串)
    hdu6097 Mindis(几何)
    hdu 6057 Kanade's convolution(子集卷积)
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/4217456.html
Copyright © 2020-2023  润新知