媒介
很早之前写过一篇用RequireJS包装AjaxChart,当时用Highcharts做图表,在其上封装了一层ajax,末了只是简朴套用了一下requireJS。由于当时本身才打仗模块化,明白层面还太浅,厥后经由其他项目的考验以及练习取得的见地,想从新连系一个示例来写点前端模块化的开发方式。
项目背景
近来在做一个平安运维监控的项目,个中有一条是依据装备猎取到的进击数据,在舆图上做可视化。对照了Highcharts和ECharts
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的操作历程,对外只暴露setView
和setAttacks
两个要领,以完成舆图维度的缩放以及进击线的表现。个中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链接中看到,不仅应用层页面的按钮能够切换舆图维度,直接点击舆图里的”中国”地区也能切换舆图,同时又能关照到应用层页面的按钮转变状况。因而应用层页面是须要体贴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(){
// 细节省略...
}
};
装点的动画结果
时钟模块
比较简朴,源码在 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