前端模块化开辟demo之进击舆图

媒介

很早之前写过一篇用RequireJS包装AjaxChart,当时用Highcharts做图表,在其上封装了一层ajax,末了只是简朴套用了一下requireJS。由于当时本身才打仗模块化,明白层面还太浅,厥后经由其他项目的考验以及练习取得的见地,想从新连系一个示例来写点前端模块化的开发方式。

项目背景

近来在做一个平安运维监控的项目,个中有一条是依据装备猎取到的进击数据,在舆图上做可视化。对照了HighchartsECharts

  • ECharts对国内舆图的支撑更多

  • ECharts在模块化和扩大方面做的比Highcharts更好

所以末了我挑选了基于ECharts去封装。相似的收集进击的监控舆图可看外洋的Norse Attack Map,也算是同类的参照。

需求整顿

数据请求

  • 供应的数据只需IP到IP的进击,包含进击时候、进击范例等,须要自行依据IP定位到响应的经纬度。

展示请求

  • 舆图供应天下、中国、省份,这三种维度(只针对中国)

  • 要在舆图上表现出进击的泉源与目的之间的动画

  • 须要强调出进击受灾地区,可一眼看出那里是重灾区

  • 能够轮回表现进击,也可及时革新进击数据

目次构造

- index.html 主页面
- assets
    - css
        - normalize.css 浏览器初始化款式
        - common.css 从bootstrap里扒了一些基本款式
    - img/ 
- js
    - app
        - mainMap.js index页面的主实行js
    - lib
        - echarts/ 用了源码包
        - zrender/ 一样源码包,详细看echarts官方申明
        - geo 一些地舆数据的定义
            - china/
            - world/
        - mods
            - attackMap/ 对echarts map的封装
            - util.js 等等其他协助或插件模块的封装
            - xxxx.js
    - config.js

requireJS的config设置

requirejs.config({
    baseUrl: 'js/lib',
    paths: {
        jquery: 'http://cdn.staticfile.org/jquery/1.7.2/jquery.min',
        underscore: 'http://cdn.staticfile.org/underscore.js/1.7.0/underscore-min'
    },
    packages: [
        {
            name: 'echarts',
            location: 'echarts/src',
            main: 'echarts'
        },
        {
            name: 'zrender',
            location: 'zrender/src',
            main: 'zrender'
        }
    ]
});

map封装历程

开端封装 mods/attackMap/main.js

define(function(require){

    var U = require('underscore');
    var EC = require('echarts/echarts');
    var ecMap = require('echarts/chart/map');
    var ecMapParams = require('echarts/util/mapData/params').params;

    var EVENT = require('echarts/config').EVENT;
    var MAP_TYPE_WORLD = 'world';
    var MAP_TYPE_CHINA = 'china';
    
    var AttackMap = function(config){
        this.config = U.extend({
            view: MAP_TYPE_WORLD
        }, config);

        this.el = document.getElementById(this.config.id);
        // 初始化echarts
        this._init();
    };

    // 不带下划线的为对外暴露的要领
    AttackMap.prototype = {
        _init: function(){
            // _chart对象私有
            this._chart = EC.init(this.el);
            // default view
            var mapOption = U.extend({}, require('mods/attackMap/mapOption'));
            // 兼并option
            U.extend(mapOption.series[0], this._getViewOption(this.config.view));
            // render
            this._chart.setOption(mapOption);

            // 交互
            this._bindEvents();
        },

        _bindEvents: function(){
            var that = this;
            this._chart.on(EVENT.CLICK, function(e, chart){
                // 仅对中国钻取
                if(e.data.name === '中国' || e.data.name === 'China'){
                    that.setView(MAP_TYPE_CHINA);
                }
                // and中国省份钻取
                else if(e.data.name in ecMapParams){
                    that.setView(e.data.name);
                }
            });
        },

        // view涉及到的series里须要设置的属性
        _getViewOption: function(viewType){
            if(viewType === MAP_TYPE_WORLD){
                return {
                    mapType: MAP_TYPE_WORLD,
                    nameMap: require('geo/world/countryName')
                }
            }
            else if(viewType === MAP_TYPE_CHINA){
                return {
                    mapType: MAP_TYPE_CHINA
                };
            }
            else if(viewType in ecMapParams){
                return {
                    mapType: viewType
                };
            }
            return {};
        },

        _setOtherOption: function(viewType){
            if(viewType === MAP_TYPE_WORLD){
                this._chart.chart.map.series[0].itemStyle.normal.label.show = false;
                this._chart.chart.map.series[0].markLine.effect.period = 15;
            }
            else if(viewType === MAP_TYPE_CHINA){
                this._chart.chart.map.series[0].itemStyle.normal.label.show = false;
                this._chart.chart.map.series[0].markLine.effect.period = 8;
            }
            else{
                this._chart.chart.map.series[0].itemStyle.normal.label.show = true;
                this._chart.chart.map.series[0].markLine.effect.period = 4;
            }
        },

        // 设置舆图视图
        setView: function(viewType){
            // 上一次的view
            (typeof this._lastView === 'undefined') && (this._lastView = this.config.view);
            // 防备反复set
            if(viewType === this._lastView){
                return false;
            }
            this._lastView = viewType;

            // 汗青开过的view(string逗号分开)
            (typeof this._historyViews === 'undefined') && (this._historyViews = this.config.view);
            // 用来推断是不是加载过
            if(this._historyViews.indexOf(viewType) === -1){
                this._historyViews += (',' + viewType);
                // loading
                this._chart.showLoading();
                // 假loading
                var that = this;
                setTimeout(function(){
                    that._chart.hideLoading();
                }, 350);
            }

            // 要先reset再draw
            this.reset();
            var viewOption = this._getViewOption(viewType);
            this._chart.setSeries([viewOption]);
            // 多级的option没法merge本来的,所以得手动设置
            this._setOtherOption(viewType);
        },

        // 进击线
        setAttacks: function(data, isLoop){
            // 是不是轮回显现markline(暂未用到)
            isLoop = isLoop || true;
            // 留个data备份(暂未用到)
            this._mData = data;

            // TODO: 要对IP聚合
            // 国内最小定位到市级,外洋只能定位到国度
            // 而markline只能经由过程 name-name 来标识
            // 聚合后雷同 name-name 的进击累计次数视为强度

            var lineData = U.map(data, function(v){
                return [
                    {name: v['srcName'], geoCoord: [v['srcLocX'], v['srcLocY']]},
                    {name: v['destName'], geoCoord: [v['destLocX'], v['destLocY']]}
                ]
            });

            var pointData = U.map(data, function(v){
                return {
                    name: v['destName'],
                    geoCoord: [v['destLocX'], v['destLocY']]
                }
            });

            // ECharts内部的中心变量
            var _map = this._chart.chart.map;
            // 防备addMarkLine抛非常 seriesIndex 0
            // _map.buildMark(0);

            try{
                this._chart.addMarkLine(0, {data: lineData});
            }catch(e){
                // console.error(e);
            }
            
            try{
                this._chart.addMarkPoint(0, {data: pointData});
            }catch(e){
                // console.error(e);
            }
        },
        
        // 通用要领
        refresh: function(){
            this._chart.refresh();
        },
        reset: function(){
            this._chart.restore();
        }
    };

    return AttackMap;
});

这里我用echarts中的MarkLine作为进击线,MarkPoint作为受益所在,AttackMap封装了对echarts的操作历程,对外只暴露setViewsetAttacks两个要领,以完成舆图维度的缩放以及进击线的表现。个中echarts map的通用设置项都拎到了mods/attactMap/mapOption.js中,这里AttackMap只手工操作部份option,比方依据舆图的维度修正MarkLine动画的速度。

应用层 js/app/mainMap.js

require([
    'jquery',
    'mods/attackMap/main',
    'mods/attackMap/mock'

], function($, AttackMap, Mock){

    var View = {
        // 作为一个视图模版来初始化
        init: function(){
            // 此View片断的root元素
            // this.$el = $('body');

            // 初始化成员
            this.aMap = new AttackMap({
                id: 'mapChart',
                view: 'world'
            });

            // 绑定事宜
            this._bindEvents();
        },

        _bindEvents: function(){
            var that = this;
            // 视图切换
            this._bindMapViewEvents();

            // 其他binding
            $(window).on('resize', function(){
                that.aMap.resize();
            });
        },

        // 视图切换事宜
        _bindMapViewEvents: function(){
            var that = this;

            // NOTE: 会有动态天生的元素
            $('.J_changeView').live('click', function(){
                that.aMap.setView($(this).attr('data-type'));
            });
        },

        // 进击数据展示
        _renderAttacks: function(data){
            // render map
            this.aMap.setAttacks(data);

            // render table
            var $tbody = $('#attacksTable').find('tbody');
            // var $frags = [];
            $.each(data, function(i, v){
                var $tr = $('<tr><td>'+v['srcIp']+'</td><td>'+v['srcName']+'</td><td>'+v['destIp']+'</td><td>'+v['destName']+'</td><td>'+v['type']+'</td><td>'+v['time']+'</td></tr>');
                $tbody.append($tr);
            });
        },

        // 猎取进击数据
        getAttacks: function(){
            var that = this;
            // ajax TODO

            // 当地mock数据
            that.attacksData = Mock.data;
            that._renderAttacks(that.attacksData);
        }
    };

    // execution
    View.init();

    // lazy load
    setTimeout(function(){
        View.getAttacks();
    }, 16);

});

至此,在应用层页面上,能够经由过程点击.J_changeView按钮来切换舆图的维度(天下/中国/省份),进击数据的展示临时没有ajax挪用,只是简朴用了mock数据来做,大致结果是一样的。

终究demo

自定义事宜封装

在上面的demo链接中看到,不仅应用层页面的按钮能够切换舆图维度,直接点击舆图里的”中国”地区也能切换舆图,同时又能关照到应用层页面的按钮转变状况。因而应用层页面是须要体贴AttackMap的状况(事宜)的,一样将鼠标放在进击线上涌现的进击概况,也是经由过程监听AttackMap的事宜完成的。

1、在 mods/attackMap/main.js 中定义事宜范例

// 对外事宜
AttackMap.EVENTS = {
    VIEW_CHANGED: 'viewChanged',
    LINE_HOVERED: 'marklineHovered',
    LINE_BLURED: 'marklineBlured'
};

2、在AttackMap中完成事宜触发器

AttackMap.prototype = {
    on: function(type, fn){
        (typeof this._handlers === 'undefined') && (this._handlers = {});
        (typeof this._handlers[type] === 'undefined') && (this._handlers[type] = []);
        this._handlers[type].push(fn);
    },
    fire: function(type, data, event){
        if(typeof this._handlers === 'undefined' || 
            typeof this._handlers[type] === 'undefined'){
            return false;
        }

        var that = this;
        var eventObj = {
            type: type,
            data: data
        };
        // 原生event对象
        (typeof event !== 'undefined') && (eventObj.event = event);
        
        U.each(this._handlers[type], function(fn){
            fn(eventObj, that);
        });
    }
};

3、在AttackMap内部恰当的要领中fire自定义事宜

AttackMap.prototype = {
    _bindEvents: function(){
        var that = this;
        // 省略...

        this._chart.on(EVENT.HOVER, function(e, chart){
            // 是markline
            if(e.name.indexOf('>') !== -1){
                // 阻挠此时的tooltip
                that._chart.chart.map.component.tooltip.hideTip();

                // 由外部去衬着
                that.fire(
                    AttackMap.EVENTS.LINE_HOVERED,
                    { name: e.name },
                    e.event
                );
            }
            // 不是markline,通知外部
            else{
                // 效力有点低 每次hover都邑触发
                that.fire(AttackMap.EVENTS.LINE_BLURED);
            }
        });
    },
    setView: function(viewType){
        // 省略...

        // 对外fire事宜
        this.fire(
            AttackMap.EVENTS.VIEW_CHANGED, 
            { viewType: viewType }
        );
    }
};

当触发AttackMap.EVENTS.LINE_HOVERED事宜时,由于应用层页面要绘制进击概况的浮层,须要晓得鼠标位置信息,所以这里fire时将原生的event对象也传了进去。(注重fire要领的完成中,传给回调函数的eventObj对象中,有事宜范例type,自定义data,以及原生event对象)

4、在应用层js中监听自定义事宜

// 别号
var MAP_EVENTS = AttackMap.EVENTS;

var View = {
    // 视图切换事宜
    _bindMapViewEvents: function(){
        var that = this;

        // AttackMap监听
        this.aMap.on(MAP_EVENTS.VIEW_CHANGED, function(e){
            var type = e.data.viewType;
            // 清空当前
            $current = $('.view-nav.active');
            $current.removeClass('active');

            // 目的
            var $target = $('.view-nav[data-type="' + type + '"]');
            if($target.length == 0){
                // 另起一个
                var $copy = $current.clone();
                $copy.addClass('active').attr('data-type', type).text(type);
                $('#dynamicNav').empty().append($copy);
            }
            else{
                $target.addClass('active');
            }
        });

        // 省略...
    },

    // 进击线(舆图markline)事宜
    _bindMapLineEvents: function(){
        var that = this;

        this.aMap.on(MAP_EVENTS.LINE_HOVERED, function(e){
            // 条件:srcName-destName 必需能唯一辨别
            // 外洋IP如今只能定位到国度
            var temps = (e.data.name).split(' > ');
            var source = temps[0];
            var dest = temps[1];

            var attacks = that.attacksData;
            // 遍历data
            for(var i=0; i<attacks.length; i++){
                if(attacks[i]['srcName'] === source && attacks[i]['destName'] === dest){
                    that._drawMapLineDetail(attacks[i], e.event.pageX, e.event.pageY);
                    break;
                }
            }
        });

        this.aMap.on(MAP_EVENTS.LINE_BLURED, function(e){
            that._hideMapLineDetail();
        });
    },

    // 画进击线概况
    _drawMapLineDetail: function(){
        // 细节省略...
    },
    _hideMapLineDetail: function(){
        // 细节省略...
    }
};

再看一遍demo

装点的动画结果

时钟模块

比较简朴,源码在 js/lib/mods/clock.js 中,下面只列出大致构造。

define(['jquery'], function($){
    var Clock = function(config){
        this.$el = $('#' + this.config.id);
        this._init();
    };

    Clock.prototype = {
        _init: function(){
            // 细节省略...
            this.start();
        },
        _update: function(){
            // 细节省略...
        },
        start: function(){
            // 先初始化时候
            this._update();

            var that = this;
            this.timer = setInterval(function(){
                that._update();
            }, 1000);
        },
        stop: function(){
            clearInterval(this.timer);
            this.timer = null;
        }
    };

    return Clock;
});

move动画封装

道理是采纳的css中transform动画,我们底本的做法会是先定义两个css class,一个增加transform的种种css划定规矩,另一个class增加与前一项相反(或消灭动画)的css划定规矩,然后经由过程js操控DOM元素,在两个class之间切换。但我以为这类做法太挫了,能够把雷同结果的transform封装起来(防止写迥然不同的css class),因而我封装了一个只做move挪动的动画util要领。

define(['jquery', 'underscore'], function($, U){
    return {
        /* 挪动动画
            @param el {HTMLElement}
            @param x1 {number}
            @param y1 {number}
            @param x2 {number}
            @param y2 {number}
            @param config {Object}
                @param duration {number}
                @param ease {string}
                @param isShowEl {boolean} 动画完毕后是不是继承显现元素
                @param isClear {boolean} 动画完毕后是不是消灭动画属性
                @param beforeAnim {Function}
                @param afterAnim {Function}
        */
        moveAnim: function(el, x1, y1, x2, y2, config) {
            if(!el){
                return;
            }
            if(!el.tagName && el.length){
                // jquery节点
                el = el[0];
            }

            var style = el.style;
            config = U.extend({
                duration: 400,
                ease: 'ease',
                isShowEl: true,
                isClear: false
            }, config);

            style.display = 'block';
            style.transform = 'translate3d(' + x1 + 'px, ' + y1 + 'px, 0px)';
            style.transitionDuration = '0ms';
            style.webkitTransform = 'translate3d(' + x1 + 'px, ' + y1 + 'px, 0px)';
            style.webkitTransitionDuration = '0ms';

            // before animation
            config.beforeAnim && config.beforeAnim();

            setTimeout(function() {
                style.transform = 'translate3d(' + x2 + 'px, ' + y2 + 'px, 0px)';
                style.transitionDuration = config.duration + 'ms';
                style.transitionTimingFunction = config.ease;
                style.webkitTransform = 'translate3d(' + x2 + 'px, ' + y2 + 'px, 0px)';
                style.webkitTransitionDuration = config.duration + 'ms';
                style.webkitTransitionTimingFunction = config.ease;

                // 下面不会有第二次setTimeout
                if(config.isShowEl && !config.isClear){
                    // after animation
                    config.afterAnim && config.afterAnim();
                }
            }, 0);

            // 动画完毕后不显现元素
            if(!config.isShowEl){
                style.display = 'none';
            }
            // 清空动画属性(下次show时显如今最初的位置)
            if(!config.isShowEl || config.isClear){
                var that = this;
                setTimeout(function() {
                    that._clearTransform(el);
                    // after animation
                    config.afterAnim && config.afterAnim();
                }, config.duration + 10);
            }
        },

        _clearTransform: function(el){
            var style = el.style;
            style.transform = null;
            style.transitionDuration = null;
            style.transitionTimingFunction = null;
            style.webkitTransform = null;
            style.webkitTransitionDuration = null;
            style.webkitTransitionTimingFunction = null;
        }
    }
});

基于move动画的转动表格

在demo中能够看到屏幕下方的进击数据的表格一直在转动播放,如今已很少人还在用<marquee>这类东西了,比如已镌汰的用<table>做页面规划。我这里基于上面的动画util要领,完成了一个转动播放的table组件。

完成思绪是,先要对table元素做预处理,将thead拷贝一份,由于表格转动时thead是不动的(相当于sticky)。代码构造相似上面的Clock类,主动画逻辑包在setInterval中。每次动画轮回到来时,掏出tbody的第一个tr元素的高度h,然后将table团体向上move这段高度h,move完毕后将第一个tr追加到tbody的队尾。详细完成代码见 js/lib/mods/animTable.js

另有什么短缺的

最初的展示需求都已完成了,在这历程中封装了AttackMap,并本身完成了自定义事宜,完整将echarts对外透清楚明了。同时还产出了几个非主要的js小组件,历程看似拉的很长,但都是一步步自然而然会发生的主意。这里还遗留着一个题目,如何将html模板、款式和js模块绑缚起来,即只需reuqire一下模块,模块响应的css会一并载入。

<!-- 不须要 <link rel="stylesheet" href="moduleA.css"> -->
<div>
    <!-- 引入组件的html模板 -->
    {% require moduleA %}
</div>

<script>
require(['mods/moduleA'], function(A){
    // something...
});
</script>

我想到达的结果就像上面,应用层页面不须要引组件模块的css,只需inclue一份html模板,require一下对应的js模块。有晓得详细做法的吗,我想进一步交换。

demo

感受

  • 在忙碌的项目中抽出时候做些整顿和总结,是件主要但不紧要的事变。

  • 和之前写的文章一对照,显著感觉到本身这半年多的生长。

本文最早宣布在我的个人博客上,转载请保存出处 http://jsorz.cn/blog/2015/12/attack-map-with-amd.html

    原文作者:依然宁静
    原文地址: https://segmentfault.com/a/1190000004102924
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注