摘要:最終自定義事件封裝在上面的鏈接中看到,不僅應(yīng)用層頁面的按鈕可以切換地圖維度,直接點擊地圖里的中國區(qū)域也能切換地圖,同時又能通知到應(yīng)用層頁面的按鈕改變狀態(tài)。
前言
很早以前寫過一篇用RequireJS包裝AjaxChart,當(dāng)時用Highcharts做圖表,在其上封裝了一層ajax,最后只是簡單套用了一下requireJS。由于當(dāng)時自己才接觸模塊化,理解層面還太淺,后來經(jīng)過其他項目的磨練以及實習(xí)獲得的見識,想重新結(jié)合一個示例來寫點前端模塊化的開發(fā)方式。
項目背景最近在做一個安全運維監(jiān)控的項目,其中有一條是根據(jù)設(shè)備獲取到的攻擊數(shù)據(jù),在地圖上做可視化。對比了Highcharts和ECharts
ECharts對國內(nèi)地圖的支持更多
ECharts在模塊化和擴(kuò)展方面做的比Highcharts更好
所以最后我選擇了基于ECharts去封裝。類似的網(wǎng)絡(luò)攻擊的監(jiān)控地圖可看國外的Norse Attack Map,也算是同類的參照。
需求整理數(shù)據(jù)要求
提供的數(shù)據(jù)只有IP到IP的攻擊,包括攻擊時間、攻擊類型等,需要自行根據(jù)IP定位到相應(yīng)的經(jīng)緯度。
展現(xiàn)要求
地圖提供世界、中國、省份,這三種維度(只針對中國)
要在地圖上表現(xiàn)出攻擊的來源與目標(biāo)之間的動畫
需要強(qiáng)調(diào)出攻擊受災(zāi)地區(qū),可一眼看出哪里是重災(zāi)區(qū)
可以循環(huán)表現(xiàn)攻擊,也可實時刷新攻擊數(shù)據(jù)
目錄結(jié)構(gòu)- index.html 主頁面 - assets - css - normalize.css 瀏覽器初始化樣式 - common.css 從bootstrap里扒了一些基礎(chǔ)樣式 - img/ - js - app - mainMap.js index頁面的主執(zhí)行js - lib - echarts/ 用了源碼包 - zrender/ 同樣源碼包,具體看echarts官方說明 - geo 一些地理數(shù)據(jù)的定義 - china/ - world/ - mods - attackMap/ 對echarts map的封裝 - util.js 等等其他幫助或插件模塊的封裝 - xxxx.js - config.jsrequireJS的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里需要設(shè)置的屬性 _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; } }, // 設(shè)置地圖視圖 setView: function(viewType){ // 上一次的view (typeof this._lastView === "undefined") && (this._lastView = this.config.view); // 防止重復(fù)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原來的,所以得手動設(shè)置 this._setOtherOption(viewType); }, // 攻擊線 setAttacks: function(data, isLoop){ // 是否循環(huán)顯示markline(暫未用到) isLoop = isLoop || true; // 留個data備份(暫未用到) this._mData = data; // TODO: 要對IP聚合 // 國內(nèi)最小定位到市級,國外只能定位到國家 // 而markline只能通過 name-name 來標(biāo)識 // 聚合后相同 name-name 的攻擊累計次數(shù)視為強(qiáng)度 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內(nèi)部的核心變量 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兩個方法,以實現(xiàn)地圖維度的縮放以及攻擊線的表現(xiàn)。其中echarts map的通用配置項都拎到了mods/attactMap/mapOption.js中,這里AttackMap只手工操作部分option,比如根據(jù)地圖的維度修改MarkLine動畫的速率。
應(yīng)用層 js/app/mainMap.jsrequire([ "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: 會有動態(tài)生成的元素 $(".J_changeView").live("click", function(){ that.aMap.setView($(this).attr("data-type")); }); }, // 攻擊數(shù)據(jù)展現(xiàn) _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 = $(""); $tbody.append($tr); }); }, // 獲取攻擊數(shù)據(jù) getAttacks: function(){ var that = this; // ajax TODO // 本地mock數(shù)據(jù) that.attacksData = Mock.data; that._renderAttacks(that.attacksData); } }; // execution View.init(); // lazy load setTimeout(function(){ View.getAttacks(); }, 16); }); "+v["srcIp"]+" "+v["srcName"]+" "+v["destIp"]+" "+v["destName"]+" "+v["type"]+" "+v["time"]+"
至此,在應(yīng)用層頁面上,可以通過點擊.J_changeView按鈕來切換地圖的維度(世界/中國/省份),攻擊數(shù)據(jù)的展現(xiàn)暫時沒有ajax調(diào)用,只是簡單用了mock數(shù)據(jù)來做,大體效果是一樣的。
最終demo
自定義事件封裝在上面的demo鏈接中看到,不僅應(yīng)用層頁面的按鈕可以切換地圖維度,直接點擊地圖里的"中國"區(qū)域也能切換地圖,同時又能通知到應(yīng)用層頁面的按鈕改變狀態(tài)。因此應(yīng)用層頁面是需要關(guān)心AttackMap的狀態(tài)(事件)的,同樣將鼠標(biāo)放在攻擊線上出現(xiàn)的攻擊詳情,也是通過監(jiān)聽AttackMap的事件實現(xiàn)的。
1、在 mods/attackMap/main.js 中定義事件類型// 對外事件 AttackMap.EVENTS = { VIEW_CHANGED: "viewChanged", LINE_HOVERED: "marklineHovered", LINE_BLURED: "marklineBlured" };2、在AttackMap中實現(xiàn)事件觸發(fā)器
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內(nèi)部適當(dāng)?shù)姆椒ㄖ?b>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都會觸發(fā) that.fire(AttackMap.EVENTS.LINE_BLURED); } }); }, setView: function(viewType){ // 省略... // 對外fire事件 this.fire( AttackMap.EVENTS.VIEW_CHANGED, { viewType: viewType } ); } };
當(dāng)觸發(fā)AttackMap.EVENTS.LINE_HOVERED事件時,由于應(yīng)用層頁面要繪制攻擊詳情的浮層,需要知道鼠標(biāo)位置信息,所以這里fire時將原生的event對象也傳了進(jìn)去。(注意fire方法的實現(xiàn)中,傳給回調(diào)函數(shù)的eventObj對象中,有事件類型type,自定義data,以及原生event對象)
4、在應(yīng)用層js中監(jiān)聽自定義事件// 別名 var MAP_EVENTS = AttackMap.EVENTS; var View = { // 視圖切換事件 _bindMapViewEvents: function(){ var that = this; // AttackMap監(jiān)聽 this.aMap.on(MAP_EVENTS.VIEW_CHANGED, function(e){ var type = e.data.viewType; // 清空當(dāng)前 $current = $(".view-nav.active"); $current.removeClass("active"); // 目標(biāo) 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 必須能唯一區(qū)分 // 國外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再看一遍demo
點綴的動畫效果 時鐘模塊比較簡單,源碼在 js/lib/mods/clock.js 中,下面只列出大體結(jié)構(gòu)。
define(["jquery"], function($){ var Clock = function(config){ this.$el = $("#" + this.config.id); this._init(); }; Clock.prototype = { _init: function(){ // 細(xì)節(jié)省略... this.start(); }, _update: function(){ // 細(xì)節(jié)省略... }, 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規(guī)則,另一個class添加與前一項相反(或清除動畫)的css規(guī)則,然后通過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} 動畫結(jié)束后是否繼續(xù)顯示元素 @param isClear {boolean} 動畫結(jié)束后是否清除動畫屬性 @param beforeAnim {Function} @param afterAnim {Function} */ moveAnim: function(el, x1, y1, x2, y2, config) { if(!el){ return; } if(!el.tagName && el.length){ // jquery節(jié)點 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); // 動畫結(jié)束后不顯示元素 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中可以看到屏幕下方的攻擊數(shù)據(jù)的表格一直在滾動播放,現(xiàn)在已經(jīng)很少人還在用這種東西了,好比已經(jīng)淘汰的用
做頁面布局。我這里基于上面的動畫util方法,實現(xiàn)了一個滾動播放的table組件。
實現(xiàn)思路是,先要對table元素做預(yù)處理,將thead拷貝一份,因為表格滾動時thead是不動的(相當(dāng)于sticky)。代碼結(jié)構(gòu)類似上面的Clock類,主動畫邏輯包在setInterval中。每次動畫循環(huán)到來時,取出tbody的第一個tr元素的高度h,然后將table整體向上move這段高度h,move結(jié)束后將第一個tr追加到tbody的隊尾。具體實現(xiàn)代碼見 js/lib/mods/animTable.js
還有什么欠缺的最初的展現(xiàn)需求都已實現(xiàn)了,在這過程中封裝了AttackMap,并自己實現(xiàn)了自定義事件,完全將echarts對外透明了。同時還產(chǎn)出了幾個非主要的js小組件,過程看似拉的很長,但都是一步步自然而然會產(chǎn)生的想法。這里還遺留著一個問題,如何將html模板、樣式和js模塊捆綁起來,即只需reuqire一下模塊,模塊相應(yīng)的css會一并載入。
{% require moduleA %}我想達(dá)到的效果就像上面,應(yīng)用層頁面不需要引組件模塊的css,只要inclue一份html模板,require一下對應(yīng)的js模塊。有知道具體做法的嗎,我想進(jìn)一步交流。
demo在線demo
感想在繁忙的項目中抽出時間做些整理和總結(jié),是件重要但不緊急的事情。
和以前寫的文章一對比,明顯感覺到自己這半年多的成長。
本文最早發(fā)表在我的個人博客上,轉(zhuǎn)載請保留出處 http://jsorz.cn/blog/2015/12/attack-map-with-amd.html
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/78250.html
相關(guān)文章
APICloud Github 5大開源項目集合展示
摘要:自成立之初,一直秉承著開源一切的初心,為了給予廣大開發(fā)者們更多的資源及內(nèi)容。借此,官方將開源項目進(jìn)行分類和介紹,使開發(fā)者們更好的去了解去使用。更多的開源項目均在中。 APICloud自成立之初,一直秉承著開源一切的初心,為了給予廣大開發(fā)者們更多的資源及內(nèi)容。不知不覺,2年時間已過,APICloud的github上已經(jīng)集合了APICloud模塊、前端框架及文檔、云API SDK、開發(fā)工具...
APICloud Github 5大開源項目集合展示
摘要:自成立之初,一直秉承著開源一切的初心,為了給予廣大開發(fā)者們更多的資源及內(nèi)容。借此,官方將開源項目進(jìn)行分類和介紹,使開發(fā)者們更好的去了解去使用。更多的開源項目均在中。 APICloud自成立之初,一直秉承著開源一切的初心,為了給予廣大開發(fā)者們更多的資源及內(nèi)容。不知不覺,2年時間已過,APICloud的github上已經(jīng)集合了APICloud模塊、前端框架及文檔、云API SDK、開發(fā)工具...
APICloud Github 5大開源項目集合展示
摘要:自成立之初,一直秉承著開源一切的初心,為了給予廣大開發(fā)者們更多的資源及內(nèi)容。借此,官方將開源項目進(jìn)行分類和介紹,使開發(fā)者們更好的去了解去使用。更多的開源項目均在中。 APICloud自成立之初,一直秉承著開源一切的初心,為了給予廣大開發(fā)者們更多的資源及內(nèi)容。不知不覺,2年時間已過,APICloud的github上已經(jīng)集合了APICloud模塊、前端框架及文檔、云API SDK、開發(fā)工具...
前端最實用書簽(持續(xù)更新)
摘要:前言一直混跡社區(qū)突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點混亂所以將前端主流技術(shù)做了一個書簽整理不求最多最全但求最實用。 前言 一直混跡社區(qū),突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點混亂; 所以將前端主流技術(shù)做了一個書簽整理,不求最多最全,但求最實用。 書簽源碼 書簽導(dǎo)入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
發(fā)表評論
0條評論
xiaowugui666
男|高級講師
TA的文章
閱讀更多
【數(shù)據(jù)結(jié)構(gòu)初階】第九篇——八大經(jīng)典排序算法總結(jié)(圖解+動圖演示+代碼實現(xiàn)+八大排序比較)
閱讀 1873·2021-11-25 09:43
Rust基金會迎來首任執(zhí)行董事和 CEO
閱讀 2151·2021-11-19 09:40
搬瓦工VPS:2021年最新VPS優(yōu)惠碼、優(yōu)惠套餐、高速線路和高速機(jī)房整理
閱讀 3432·2021-11-18 13:12
python 數(shù)據(jù)庫編程,這篇是針對 mysql 的,滾雪球?qū)WPython第4季第13篇
閱讀 1744·2021-09-29 09:35
Tmhhost:暑期八折,終身優(yōu)惠,日本軟銀/香港BGP/洛杉磯GIA高防,100元/季起
閱讀 666·2021-08-24 10:00
Phaser游戲框架與HTML Dom元素之間的通信交互
閱讀 2512·2019-08-30 15:55
CSS基礎(chǔ)知識之position
閱讀 1718·2019-08-30 12:56
《css and documents》讀書筆記;
閱讀 1821·2019-08-28 17:59
閱讀需要支付1元查看