摘要:最終自定義事件封裝在上面的鏈接中看到,不僅應用層頁面的按鈕可以切換地圖維度,直接點擊地圖里的中國區域也能切換地圖,同時又能通知到應用層頁面的按鈕改變狀態。
前言
很早以前寫過一篇用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.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里需要設置的屬性 _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.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: 會有動態生成的元素 $(".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 = $(""); $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); }); "+v["srcIp"]+" "+v["srcName"]+" "+v["destIp"]+" "+v["destName"]+" "+v["type"]+" "+v["time"]+"
至此,在應用層頁面上,可以通過點擊.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再看一遍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中可以看到屏幕下方的攻擊數據的表格一直在滾動播放,現在已經很少人還在用這種東西了,好比已經淘汰的用
做頁面布局。我這里基于上面的動畫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會一并載入。
{% require moduleA %}我想達到的效果就像上面,應用層頁面不需要引組件模塊的css,只要inclue一份html模板,require一下對應的js模塊。有知道具體做法的嗎,我想進一步交流。
demo在線demo
感想在繁忙的項目中抽出時間做些整理和總結,是件重要但不緊急的事情。
和以前寫的文章一對比,明顯感覺到自己這半年多的成長。
本文最早發表在我的個人博客上,轉載請保留出處 http://jsorz.cn/blog/2015/12/attack-map-with-amd.html
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78250.html
相關文章
APICloud Github 5大開源項目集合展示
摘要:自成立之初,一直秉承著開源一切的初心,為了給予廣大開發者們更多的資源及內容。借此,官方將開源項目進行分類和介紹,使開發者們更好的去了解去使用。更多的開源項目均在中。 APICloud自成立之初,一直秉承著開源一切的初心,為了給予廣大開發者們更多的資源及內容。不知不覺,2年時間已過,APICloud的github上已經集合了APICloud模塊、前端框架及文檔、云API SDK、開發工具...
APICloud Github 5大開源項目集合展示
摘要:自成立之初,一直秉承著開源一切的初心,為了給予廣大開發者們更多的資源及內容。借此,官方將開源項目進行分類和介紹,使開發者們更好的去了解去使用。更多的開源項目均在中。 APICloud自成立之初,一直秉承著開源一切的初心,為了給予廣大開發者們更多的資源及內容。不知不覺,2年時間已過,APICloud的github上已經集合了APICloud模塊、前端框架及文檔、云API SDK、開發工具...
APICloud Github 5大開源項目集合展示
摘要:自成立之初,一直秉承著開源一切的初心,為了給予廣大開發者們更多的資源及內容。借此,官方將開源項目進行分類和介紹,使開發者們更好的去了解去使用。更多的開源項目均在中。 APICloud自成立之初,一直秉承著開源一切的初心,為了給予廣大開發者們更多的資源及內容。不知不覺,2年時間已過,APICloud的github上已經集合了APICloud模塊、前端框架及文檔、云API SDK、開發工具...
前端最實用書簽(持續更新)
摘要:前言一直混跡社區突然發現自己收藏了不少好文但是管理起來有點混亂所以將前端主流技術做了一個書簽整理不求最多最全但求最實用。 前言 一直混跡社區,突然發現自己收藏了不少好文但是管理起來有點混亂; 所以將前端主流技術做了一個書簽整理,不求最多最全,但求最實用。 書簽源碼 書簽導入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
發表評論
0條評論
xiaowugui666
男|高級講師
TA的文章
閱讀更多
【數據結構初階】第九篇——八大經典排序算法總結(圖解+動圖演示+代碼實現+八大排序比較)
閱讀 1864·2021-11-25 09:43
Rust基金會迎來首任執行董事和 CEO
閱讀 2146·2021-11-19 09:40
搬瓦工VPS:2021年最新VPS優惠碼、優惠套餐、高速線路和高速機房整理
閱讀 3422·2021-11-18 13:12
python 數據庫編程,這篇是針對 mysql 的,滾雪球學Python第4季第13篇
閱讀 1739·2021-09-29 09:35
Tmhhost:暑期八折,終身優惠,日本軟銀/香港BGP/洛杉磯GIA高防,100元/季起
閱讀 661·2021-08-24 10:00
Phaser游戲框架與HTML Dom元素之間的通信交互
閱讀 2505·2019-08-30 15:55
CSS基礎知識之position
閱讀 1709·2019-08-30 12:56
《css and documents》讀書筆記;
閱讀 1815·2019-08-28 17:59
閱讀需要支付1元查看