【二】:使用 Express-Validator進行後端校驗react
最近接到了一個小需求,須要畫一個數據漏斗圖,本來也沒什麼,由於原本項目裏就有 chart 相關需求,無非就是用現成的漏斗圖改巴改巴就完事了。github
產品的PRD大概長這個樣子:canvas
我想像的而且 chart 官網給的示例是這樣的:後端
UI設計出來的是這個樣子的:api
文本內容涉及到內部項目,我就馬賽克了,雖然放出來也沒啥問題😄數組
通過一番激烈的討論,觀點以下:瀏覽器
我:人家有現成的漏斗圖,我直接拿來用十分鐘就搞定了,節約項目時間
UI:我這設計屬於原創,嘔心瀝血設計出來的,最好按照這個實現
產品:我以爲UI設計的挺好看,我們不能和其餘家同樣
我:能夠😊(心中一萬匹不知名動物奔騰,一個B端項目要什麼原創)
複製代碼
好了,結論出來了,按照UI來吧,那麼就開始想方案:bash
第一種 - 純Dom實現:看起來結構並非很複雜,若是純用dom來實現我以爲是可能的,層疊方案設計好一層一層放置元素應該是能夠實現的。
第二種 - Canvas繪製:,雖然說看起來很簡單,可是裏面涉及到了不少多邊形和箭頭線段,若是使用 Dom + CSS 可能能實現,可是必定是大費周章,因此可能 Canvas 相比來講更容易一些。
第三種 - SVG繪製:SVG和Canvas同樣,都是進行繪製,只不過方式不一樣而已。而這裏爲何選擇SVG。一方面,由於SVG不依賴於像素,放大縮小不會失真,更適合處理圖表類;另外一方面,Canvas 多多少少使用過,而 SVG 還沒真正使用過(基於SVG的ICON就不算了)。既然 UI 選擇了原創,那麼我也以爲這個小需求也值得我原創成長一下。所以選擇了SVG。
SVG | Canvas | |
---|---|---|
優勢 | 矢量圖,不依賴於像素,放大縮小不會失真。以 Dom 的形式表示,事件綁定由瀏覽器直接分發到節點上。 | 定製型更強,能夠繪製繪製任何本身想要的東西。非 Dom 結構形式,用 JavaScript 進行繪製,涉及到動畫性能較高。 |
缺點 | Dom 形式,涉及到大量更新 Dom 以及動畫的時候,性能較低。 | 事件分發由canvas處理,繪製的內容的事件須要本身作處理。依賴於像素,沒法高效保真,畫布較大時候性能較低。 |
本文記錄了一天時間內,SVG學習實踐系列,僅供你們參考,最後我實現出來的效果是這樣的:
看起來基本一致哈~😄
這裏我就介紹幾個特別經常使用的吧,簡單介紹一下使用方法,由於官方文檔寫的十分詳細,各個API的用法。惟一的缺點就是是英文的,因此在這裏我就簡單的把幾個經常使用的介紹一下,其餘的你們業務場景使用到了再去翻API就好了。
yarn add svg.js
複製代碼
import React, { Component } from 'react';
import SVG from 'svg.js';
class API extends Component {
componentDidMount() {
// 初始化,獲取svg document
const draw = SVG('api_container').size('100%', '400px');
....
}
render() {
return (
<div id='api_container' />
)
}
}
export default API;
複製代碼
獲取到 SVG Document 以後,咱們就可使用它進行繪製,獲取它須要的是元素 id,因此通常咱們在componentDidMount
這個生命週期進行初始化獲取。
首先,咱們來使用最簡單的 API 來畫一條直線,兩點肯定一條直線,依次輸入兩點的(x,y)座標便可。
const line = draw.line(0, 100, 100, 0);
line.stroke({ color: '#f06', width: 10 });
複製代碼
效果如上圖所示,而且,全部的 API 均爲鏈式調用(若是你熟悉jQuery),那麼必定不會陌生,因此上面代碼也能夠直接寫成。
draw.line(0, 100, 100, 0)
.stroke({ color: '#f06', width: 10 });
複製代碼
上面直線,很容易就畫出來了,也就是從(0, 100) -> (100, 0),不過呢,我畫的時候不必定非要在起始位置畫吧,有可能我就想在畫布中間,那怎麼辦?沒錯,有設置起始位置的API —— move
。
// 從(100, 100)開始畫
draw.line(0, 100, 100, 0)
.move(100, 100)
.stroke({ color: '#f06', width: 10 });
複製代碼
如圖,能夠看到,起始位置也能夠設置,而且代碼的位置由於是鏈式,因此放在哪裏都是能夠的。
畫矩形就更簡單了,只須要傳入你想要畫的長和寬就好了,固然你也能夠從任何地方開始畫。
// 好比,我也想從(100, 100)畫
draw.rect(140, 140).move(100, 100);
複製代碼
畫出來是黑色,也就是默認填充是黑色,我但願變成藍色,那麼可使用fill('blue')
。
通常來講,與顏色相關的 API 有兩種,
fill
和stroke
,fill
通常是填充色,stroke
通常是描邊色。
不知道注意到沒有,上面咱們的矩形把直線覆蓋住了,嗯沒錯,若是位置重疊,它的順序是後畫的會覆蓋在先畫的上面,這與 CSS 層疊樣式規則很像。咱們將兩者順序調換一下再看一下。
componentDidMount() {
const draw = SVG('api_container').size('100%', '400px');
// 畫矩形
draw.rect(140, 140).move(100, 100).fill('blue');
// 畫直線
const line = draw.line(0, 100, 100, 0).move(100, 100);
line.stroke({ color: '#f06', width: 10 });
}
複製代碼
這裏想說明的就是,當你須要在繪製的圖形內部畫文字的時候,繪製的前後順序必定不能亂,必定是先畫圖形,再畫文本。
畫圓就更簡單了,只須要給出半徑就行。
// 從(200, 200)開始畫一個半徑是100的紅邊橘色圓
draw.circle(100)
.move(200, 200)
.fill('orange')
.stroke({ color: 'red', width: 4 });
複製代碼
繪製多邊形也很簡單,只不過畫的過程須要開發者設計好,參數接受兩種類型第一種是用逗號隔開的座標,座標x y用空格間隔,第二種是點二維數組。
// 字符串參數繪製三角形
draw.polygon('300 300, 360 240, 360 360');
// 點座標繪製矩形
draw.polygon([[400, 400], [440, 400], [500, 300], [400, 300]])
.fill('green');
複製代碼
繪製多邊曲線與上面多邊形類似,只不過就是用線段展現,能夠理解爲中空。
// 繪製多邊曲線
draw.polyline('0,0 100,50 50,100').fill('none').stroke({ width: 1 })
複製代碼
draw.image('https://cdn.img42.com/4b6f5e63ac50c95fe147052d8a4db676.jpeg')
.size(60, 60) // 設置繪製的長寬
.move(500, 100);
複製代碼
繪製文本就是,直接繪製的文字放進來就行
draw.text('我是被繪製的文本')
.move(600, 200)
.stroke({ color: 'yellow' })
.font({ size: 20 });
複製代碼
到如今爲止,基本簡單的都介紹完了,繪製一些基本需求沒啥問題了,剩下的就是,咱們繪製上去的是 dom,既然是 dom 那麼確定就有事件。svg.js
支持綁定各類 dom 事件,寫法也是多種多樣,這裏咱們就介紹最通用的on
。
// 爲圖片綁定click事件
const draw_image = draw.image('https://cdn.img42.com/4b6f5e63ac50c95fe147052d8a4db676.jpeg')
.size(60, 60) // 設置繪製的長寬
.move(500, 100);
draw_image.on('click', function(){
alert(this.node.getAttribute('href'));
});
複製代碼
其餘更多 API 及其使用方法,建議你們去啃官方文檔。
上面基本把我這裏該用到的API都介紹完了,下面開始言歸正傳,正式進行產品需求自定義圖表的開發工做。下面將從思路到實現逐步講解。
大概就是上面的設計,分爲左中右,上中下三等分。左側部分,繪製文字;中間部分,繪製圖表以及對應的文字;右側部分,繪製背景板以及箭頭文字部分。
function drawStepText(draw) {
const stepTextGroup = draw.group().move(0, 0);
const text_step1_label = draw.text('第一步').move(0, 43);
stepTextGroup.add(text_step1_label);
const text_step1_content = draw.text('AAAA').move(60, 43).font({ fill: '#5F6369' });
stepTextGroup.add(text_step1_content);
const text_step2_label = draw.text('第二步').move(0, 143);
stepTextGroup.add(text_step2_label);
const text_step2_content = draw.text('BBBB').move(60, 143).font({ fill: '#5F6369' });
stepTextGroup.add(text_step2_content);
const text_step3_label = draw.text('第三步').move(0, 243);
stepTextGroup.add(text_step3_label);
const text_step3_content = draw.text('CCCC').move(60, 243).font({ fill: '#5F6369' });
stepTextGroup.add(text_step3_content);
}
componentDidMount() {
const draw = SVG('statistics_draw').size('100%', '100%');
drawStepText(draw);
}
render() {
return (
<div id='statistics_draw' className='chart-container'></div>
)
}
複製代碼
文字內容繪製完成了,這裏使用到了draw.group()
,其實也沒什麼,就是對所畫內容分一下組,既然是分爲三部分,因此這邊也就分爲三組,方便劃分。
繪製多變形漏斗區域,其實也挺簡單的,只不過複雜之處在於要計算好位置,位置的計算比較費心。代碼部分與下方 hover 提示一塊兒講解。
上面按照計算,漏斗圖以及對應的文字已經繪製完成,接下來,最複雜的地方就在這裏了。每個文字前面都有一個 hvoer 提示,鼠標懸浮上去會有個小彈窗。這裏的思路很明顯要加上on('mouseover', funciton())
事件。不過彈窗要怎麼展現就是難點了。總不能每一個 hover 都事先畫出來而後 hover 的時候再顯示吧。雖然這樣也行,可是一是我以爲複雜,二是如何繪製隱藏而且還有複雜定位的彈窗。
因此,最後個人實現思路:
<div id='statistics_draw' className='chart-container'>
<div id='hover_container' className='tooltip-container' />
</div>
複製代碼
const hoverDom = document.getElementById('hover_container');
const { left, top, width } = dom.getBoundingClientRect();
hoverDom.style.left = `${left + width / 2}px`;
hoverDom.style.top = `${top - 6}px`;
hoverDom.innerHTML = `${TYPE_TEXT[type]}`;
const arrowDom = document.createElement('div');
arrowDom.classList = `tooltip-arrow`;
hoverDom.appendChild(arrowDom);
hoverDom.style.display = 'block';
複製代碼
這裏用到了一個很重要的 DOM API —— getBoundingClientRect()
,用於獲取 dom 的絕對位置座標以及長寬各類參數。最後實現的效果:
image_referral_per.on('mouseover', function() {
const dom = this.node;
showHoverDom(dom, 'referral_per');
});
image_referral_per.on('mouseleave', function() {
document.getElementById('hover_container').style.display = 'none';
});
複製代碼
繪製矩形區域代碼:
function drawPolygonArea(draw, data) {
/* 第一個多邊形及文字 */
const polygonGroup = draw.group().move(140, 0);
const polygon_transform = draw.polygon('0 290, 120 290, 180 210,0 210').fill('#ACD3FA');
polygonGroup.add(polygon_transform);
const text_transform1 = draw.text(`付費線索數: ${data.paidNumberTotal}`)
.move(30, 260).fill(color_white).font({ size: 12 });
polygonGroup.add(text_transform1);
const image_transform1 = draw.image('/static/imgs/question-circle.png', 14, 14).move(10, 256);
image_transform1.on('mouseover', function() {
const dom = this.node;
showHoverDom(dom, 'transform1');
});
image_transform1.on('mouseleave', function() {
document.getElementById('hover_container').style.display = 'none';
});
polygonGroup.add(image_transform1);
/* 第二個多邊形及文字 */
...
/* 第三個多邊形及文字 */
...
}
複製代碼
從上面的 UI 圖,咱們能夠看出來,右側除了箭頭文字,還有一個漸變的背景色。所以,咱們在繪製文字以前,須要先繪製三塊漸變背景色。
這裏還要多介紹一個漸變色 API —— gradient
, 嗯,沒錯,就是處理相應的 CSS 漸變色操做的。
// 繪製右側背景色
function drawShadowArea(draw) {
const { width } = document.getElementById('statistics_draw').getBoundingClientRect();
// 由於是自動填滿右邊界,須要手動計算右邊界位置
const shadow_right = width - 260;
const shadowGroup = draw.group().move(260, 0);
const gradient = draw.gradient('linear', function(stop) {
stop.at(0, 'rgba(255,255,255,0)');
stop.at(1, 'rgba(247,247,247,1)');
});
const shadow_area1 = draw.polygon(`0 290, ${shadow_right} 290, ${shadow_right} 210, 60 210`).fill(gradient);
shadowGroup.add(shadow_area1);
const shadow_area2 = draw.polygon(`70 190, ${shadow_right} 190, ${shadow_right} 110, 130 110`).fill(gradient);
shadowGroup.add(shadow_area2);
const shadow_area3 = draw.polygon(`140 90, ${shadow_right} 90, ${shadow_right} 10, 200 10`).fill(gradient);
shadowGroup.add(shadow_area3);
}
複製代碼
能夠看到,背景色塊出來了,這裏使用的依然是多邊形繪製,由於要與左側多邊互補才行。
最後只剩下右側的箭頭以及文字了,思路以下:
polyline
進行繪製polygon
繪製三角形剩下的就是複雜的定位計算了。
function drawArrowLine(draw, data) {
/* 第一條線 + 文字 */
const arrowTextGroup = draw.group().move(260, 0);
const polyline_paid_rate = draw.polyline([[40, 250], [340, 250], [340, 270], [30, 270]])
.fill('none').stroke({ color: '#e2e2e4', width: 1 });
const polygon_paid_rate = draw.polygon([[22, 270], [30, 265], [30, 275]]).fill('#e2e2e4');
const image_paid_transform = draw.image('/static/imgs/question-circle-black.png', 12, 12).move(350, 254);
image_paid_transform.on('mouseover', function() {
const dom = this.node;
showHoverDom(dom, 'paid_transform');
});
image_paid_transform.on('mouseleave', function() {
document.getElementById('hover_container').style.display = 'none';
});
const text_paid_transform = draw.text(`付費轉化率:${(data.paidConversionRateTotal * 100).toFixed(2)}%`)
.move(364, 256).fill('#5F6369').font({ size: 12 });
arrowTextGroup.add(polyline_paid_rate)
.add(polygon_paid_rate)
.add(image_paid_transform)
.add(text_paid_transform);
...
}
複製代碼
最後效果,上面也看到了:
本文從實際需求觸發,從零開始學習SVG基礎並實踐使用開發,期間所歷時間較短,可能理解不是很深,各位看官不喜勿噴。
在多邊形區域內繪製文字的時候,我在想一件事,正常來講,若是多邊形區域做爲父元素,在內部繪製文本節點 text,這樣的畫豈不是會簡單不少?那麼出來的svg元素應該就是下面這段代碼的樣子:
<svg>
<rect> // 矩形
<text></text> // 矩形內的文本
</rect>
<text>矩形外的文本</text>
</svg>
複製代碼
而我在使用svg.js
以及閱讀文檔,並無發現這種使用方式。都是經過繪製的前後順序來肯定文本位置的,也就是下面這樣。
<svg>
<rect></rect> // 矩形
<text></text> // 矩形內的文本,經過x y 肯定位置
<text></text> // 矩形外的文本,經過x y肯定位置
</svg>
複製代碼
固然,由於只是簡單看看就直接上手了,因此多是我不知道如何使用,若是你們有用過而且知道的,能夠留言給個提示~萬分感謝。