縱覽全局垂直打擊的組織模式(下)

  • 2020 年 1 月 13 日
  • 筆記

本文詳細記錄了如何在Hexo部落格中實現用圖組織內容的方法,但是,請注意:以下內容並非操作教程,僅表明相信思路以供參考,或許您可以實現出更好的版本,但僅依照下文內容並不保證一定能重現,一些嘗試和debug的細節過於繁瑣並未列出,如有疑問歡迎留言。

程式碼實現

hexo.extend.helper.register

文檔說明,藉助該函數,可以在Hexo渲染生成頁面文件之前,完成用戶的自定義JavaScript程式碼。

其實,在Hexo的框架內,ejs(或其他類型的)模板中的程式碼就是渲染生成html的程式碼,在這些頁面中,藉助Hexo內建的對象,比如.post對象和.achieves對象,可以訪問到其中保存的全部文章資訊及關聯資訊。例如:

let posts = hexo.locals.get('posts');  let Xtags = posts.data[x].tags  let tagsY = Xtags.data[y].name

上述內容,可以最終得到第X篇文章(POST)中的第Y個標籤的文本。類似的方法同樣可以得到某篇文章的Categories的資訊。這就是構造可視化數據的基本方法。(在渲染前構造、藉助.post對象) 關於位置,在ejs模板中放置構造程式碼當然可以,但是不優雅,Hexo中建議的插入方式是:

  1. 在專門放置自定義JavaScript處理邏輯的文件中(plugin.js)放入程式碼,並使用內建函數。
  2. 在ejs(或其他)模板的相關位置,使用<%%>方式調用上述內建函數
  3. 使用console.log在渲染html時(hexo generate時的黑框)輸出至Console里,拿到輸出數據,放入到可視化的頁面中即可。
  4. 或者一氣呵成,直接將可視化的程式碼寫入ejs模板中,即第一次渲染結束時產生的html就已經完成可視化頁面的生成。

由於處在嘗試階段,所以這裡使用步驟3 的方法,這樣各模組相對獨立,對主題源程式碼入侵小。

可視化頁面

這裡採用的是 D3.js 進行的可視化呈現,基本上是復用的 d3 的官方模板,但將文本資訊一併和節點進行可視化展示。這段程式碼首先需要被抽取出來,這對於 d3 來說非常簡單,只需注意引入的JavaScript庫以及使用的json文本數據。

<svg width="1000" height="1000"></svg> //d3繪製的內容全部放置在該畫布上  <script src="https://d3js.org/d3.v4.min.js"></script>  <script>      var sss = 'JSON字元串'; //這就是整個程式碼所可視化的數據      var abc = parseInt($(".card").css("width").replace("px",""));    if(abc>1080) abc=1050;    else if(abc>1040) abc=1020;    else abc=abc-40;    $("svg").css("width",abc);    $("svg").css("height",abc); //此部分將畫布大小跟隨文章頁寬度變化      var svg = d3.select("svg"),      width = abc,      height = abc;      var color = d3.scaleOrdinal(d3.schemeCategory20);      var simulation = d3.forceSimulation()      .force("link", d3.forceLink().id(function(d) { return d.id; }))        .force("charge", d3.forceManyBody().strength(-180).distanceMin(10).distanceMax(300).theta(1))        .force("center", d3.forceCenter(width / 2 - 40, height / 2 - 30));      var graph = JSON.parse(sss);      var link = svg.append("g")        .attr("class", "links")      .selectAll("line")      .data(graph.links)      .enter().append("line")        .attr("stroke-width", function(d) { return Math.sqrt(d.value); });      var node = svg.append("g")        .attr("class", "nodes")      .selectAll("g")      .data(graph.nodes)      .enter().append("g")      var circles = node.append("circle")        .attr("r", function(d) {  			if(d.group>=100) return d.group/100*(10.00/48.00)+1; //取整  			else return d.group+1;  		})        .attr("fill",  function(d) {  			if(d.group>=100) return "#ff4081";  			else return "#3f51b5";  		})        .call(d3.drag()            .on("start", dragstarted)            .on("drag", dragged)            .on("end", dragended));      var lables = node.append("text")        .html(function(d) {  		if(d.group>=100) {  			var p = d.group/100*(10.00/48.00)+10;  			return "<a style='font-size:"+p+"px;font-weight:600;color:red' href='/categories/"+d.id.replace("_","-")+"'>"+d.id+"</a>";  		}else{  			var q = d.group+10;  			return "<a style='font-size:"+q+"px;' href='/tags/"+d.id+"'>"+d.id+"</a>";  			}        })        .attr('x', function(d) {  			if(d.group>=100) return d.group/100*(10.00/48.00)+5; //取整  			else return d.group+3;  		})        .attr('y',function(d) {  			if(d.group>=100) return d.group/100*(3.00/48.00)+5; //取整  			else return 5;  		});      node.append("title")        .text(function(d) { return d.id; });      simulation        .nodes(graph.nodes)        .on("tick", ticked);      simulation.force("link")        .links(graph.links);      function ticked() {      link          .attr("x1", function(d) { return d.source.x; })          .attr("y1", function(d) { return d.source.y; })          .attr("x2", function(d) { return d.target.x; })          .attr("y2", function(d) { return d.target.y; });        node          .attr("transform", function(d) {            return "translate(" + d.x + "," + d.y + ")";          })    }    function dragstarted(d) {    if (!d3.event.active) simulation.alphaTarget(0.3).restart();    d.fx = d.x;    d.fy = d.y;  }  function dragged(d) {    d.fx = d3.event.x;    d.fy = d3.event.y;  }  function dragended(d) {    if (!d3.event.active) simulation.alphaTarget(0);    d.fx = null;    d.fy = null;  }  </script>

構造數據格式

需要匹配示例的輸入格式,這樣才能最大化的復用程式碼。上述內容的官方示例中使用的格式是:

{    "nodes": [      {"id": "Myriel", "group": 1},      ... ...      {"id": "Mme.Hucheloup", "group": 8}    ],    "links": [      {"source": "Napoleon", "target": "Myriel", "value": 1},      ... ...      {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1}    ]  }

即,需要在可視化頁面被渲染出來之前就得到上述格式的數據,這便是要藉助Hexo的輔助函數來完成,將構造數據的程式碼封裝成一個函數,然後在適當的ejs模板中調用一下,即可在 hexo generate 之後,從Console中拿到構造好的數據。

在此,構造規則是:類別永遠單向的指向標籤,類別不互連,標籤不互連,同時,還需要計算的是類別和標籤出現的次數

hexo.extend.helper.register('getPostData', () => {    	var posts = hexo.locals.get('posts');  	var tagsMap = new Map(); //counter        // 利用posts對象獲取類名和標籤名  	for(var i = 0; i< posts.length; i++){  		var nameCS;  		posts.data[i].categories.forEach(function(k, v) {  			nameCS = k.name;  			return;  		})  		for(var j = 0; j< posts.data[i].tags.length; j++){  			var pname = posts.data[i].tags.data[j].name;  			var pval = tagsMap.get(pname);  			if(pval != null){  				// 將類名和標籤名壓制在一起  				tagsMap.set(nameCS+">"+pname, parseInt(tagsMap.get(pname))+1);  			}else{  				//  				tagsMap.set(nameCS+">"+pname, 1);  			}  		}  	}  	//由此開始,構造符合特定格式的JSON字元串  	let obj= [];  	let setss =  new Map();  	for (let[k,v] of tagsMap) {  	    var st = k.split(">");  	    var str = {};  	    str.source = st[0];  	    str.target = st[1];  	    str.value  = v;  		obj.push(str);  		if(setss.get(st[0]) != null){  			// 類節點 每次加100  			setss.set(st[0], parseInt(setss.get(st[0]))+100);  		}else{  			//  			setss.set(st[0], 100);  		}  		if(setss.get(st[1].trim()) != null){  			// 標籤節點 每次加1  			setss.set(st[1], parseInt(setss.get(st[1]))+1);  			setss.set(st[0], parseInt(setss.get(st[0]))+100);  		}else{  			//  			setss.set(st[1], 1);  			setss.set(st[0], parseInt(setss.get(st[0]))+100);  		}  	}    	let obk= [];  	for (let [k,v] of setss) {  	   var str = {};  	   str.id = k.trim();  	   str.group = v; //通過數量分類  	   obk.push(str);  	}  	let d3str = {};  	d3str.nodes = obk;  	d3str.links = obj;  	console.log(JSON.stringify(d3str).trim()); //按第三步說的,可以手動放置數據到可視化頁面  	return JSON.stringify(d3str).trim(); //或按第四步,將數據返回至ejs模板中,直接渲染出可視化頁面    });

注意上述程式碼中的注釋,這裡利用了類節點和標籤節點出現的次數,來分辨兩種節點的種類,因為繪製時類節點和標籤節點都是一視同仁的被繪製。如何分辨呢?在可視化頁面中有以下程式碼:

var circles = node.append("circle")    .attr("r", function(d) {  		if(d.group>=100) return d.group/100*(10.00/48.00)+1; //取整  		else return d.group+1;  	})

按照不同的次數計算步長,得到的類節點的次數一定是100的倍數,而標籤節點的次數一定小於100,這個值可以設的很大,從而讓兩者不可能出現交集。在判斷時「如果次數大於100」,那麼就是類節點,取整百的好處是,歸一化方便。例如上述程式碼需要給定節點的大小,類節點的次數統計可能是100-4800(1-48次),而標籤節點的次數卻是1-10(1-10次),如是,兩者應繪製的一樣大。這就需要歸一化,只需要縮放100倍再乘比例係數即可。

最終調用

上文中hexo.extend.helper.register(『getPostData』, () => {})的「getPostData」即註冊的函數名,在ejs(或其他)模板中直接調用即可。但由於我希望把這個可視化模組放在我的評論頁或者關於頁面,而這兩個頁面都不是渲染出來的,所以就只能採用先前第三步的做法,只構造出數據,再手動放入可視化頁面。

// 在 index.ejs 內添加:  <% var arr = getPostData(); %>

所以,需要做的就是找一個渲染頁面的ejs,調用下該函數即可,這裡放在index.ejs里,注意由於分頁可能該模板會構造很多次,所以就會重複輸出很多遍JSON數據。

基本上還是抓住程式碼執行的輸入輸出做文章。從待改造程式碼的輸入找格式,然後從原程式碼的框架中構造出該格式的數據(輸出),就像適配一樣,如此便可以利用Hexo可以獲得的數據,藉助D3.js等可視化庫,把自己的部落格(知識系統)做一個梳理和呈現,從而更好的幫助自己管理維護,也給了自己二次挖掘自己知識的機會。