微信小程序出來已經有一段時間了,github上也有不少人開源了不少項目。可是因爲微信平臺的限制(底層Canvas能力調用爲一系列JSBridge封裝),圖表的製做一直是個比較頭疼的問題。當前的成熟圖表庫沒法正常運行,即便通過改造,性能也有很大問題。git
當前已經有作的比較好的圖表庫,如xiaolin3303的 wx-charts,可是隻是爲小程序量身定作的,在普通的H5頁面就沒法執行。所以,在開發中萌生了寫一個多環境下兼容的圖形庫的念頭。github
這個圖形庫已經完成基本的圖形建設,已經支持PC瀏覽器,WAP,React以及微信小程序等環境。並上傳至github,你們能夠自由使用:wx-chart,並歡迎在Issues中提出BUG以及修改建議。web
先作個推廣,貼一個當前的實現效果
canvas
wx-chart 圖形庫參考了衆多圖形庫的構建,分爲三層,以下圖所示:小程序
最下方一層爲兼容層,其中最主要的是 WxCanvas 和 WxCanvasRenderingContext2D
用於兼容瀏覽器以及微信小程序的環境。在本章節中,一下子會展開講解實現原理。
中間層爲基礎組件以及動畫庫,最上層爲實現的各類圖形。這兩部分會在後續的章節中再另行闡述。微信小程序
首先,咱們實現這個中間層的目的很明確,就是完成一套與W3C Canvas一致的接口層,忽略小程序的底層接口,並使得上層調用時與普通Canvas使用方式一致。瀏覽器
小程序的架構是相似於React Native的JSBridge混合開發模式,所以其Canvas繪圖實現也頗有侷限性,與普通的瀏覽器對比,主要有四個不一樣點:緩存
canvas-id
,其建立JS對象使用wx.createCanvasContext
方法ctx.fillStyle = '#ffffff';
ctx.font = '40px Arial';複製代碼
而小程序所有是調用形式的:ctx.setFillStyle('#ffffff');
ctx.setFontSize(20);複製代碼
draw
方法完成渲染shadowBlur
,shadowOffsetX
均要調用wx.setShadow
;有些能力並無實現,例如lineDash
屬性,clip
剪切能力等。其中,以上這些基本也都是因爲其JSBridge的實現機理致使的,有興趣的同窗能夠Google一下具體瞭解。bash
那麼,問題來了,咱們的中間層可以解決上面那些問題?實現思路是什麼?
第一個問題很簡單,咱們聲明節點同時書寫id
與canvas-id
,而其餘三個問題個人解決思路是:微信
Object.defineProperty
將調用轉換爲賦值export function checkWX() {
return __GLOBAL__DEBUG__WX__ === true || (typeof wx != 'undefined' && typeof wx === 'object');
}複製代碼
忽略__GLOBAL__DEBUG__WX__
,此變量用於調試。咱們簡單檢測 wx
全局是否存在。代碼
WxCanvas主要邏輯以下:
class WxCanvas {
constructor(id, config, contextOptions) {
let me = this;
// 檢查小程序環境
me.isWeiXinAPP = checkWX();
me._config = extend({}, WX_CANVAS_DEFAULT_PROPERTY, me.initConfig(config));
// 提早獲取Canvas與context(這裏忽略了webgl的狀況)
let {canvas, context} = this.acquireContext(id, config);
me._canvas = canvas;
me._ctx = context;
// 實例化WxCanvasRenderingContext2D
me.wxCanvasRenderingContext2D = new WxCanvasRenderingContext2D(canvas, context, contextOptions);
return me;
}
/**
* Create Canvas context
* @param {String} id
* @param {Object} config
* @returns {*}
*/
acquireContext(id, config) {
let me = this,
canvas,
context;
// Outer canvas config
let handlerCanvas = config.canvas;
if (me.isWeiXinAPP) {
if (is.String(id)) {
canvas = context = wx.createCanvasContext(id);
} else { //... }
} else {
if (handlerCanvas) canvas = handlerCanvas;
else
canvas = is.String(id) ? document.getElementById(id) :
(typeof HTMLCanvasElement != 'undefined' && id instanceof HTMLCanvasElement) ?
id:
null;
if (typeof canvas != 'undefined') {
context = canvas.getContext && canvas.getContext('2d');
}
}
// ...
return {canvas, context};
}
getContext(str){
}
set height() {...}
get height() {...}
set width() {...}
get width() {...}
}複製代碼
WxCanvas的實現仍是比較簡單的,主要注意的就是acquireContext
方法,在這裏咱們判斷了小程序與非小程序環境,並得到Canvas上下文。
下面咱們看下WxCanvasRenderingContext2D的實現。
class WxCanvasRenderingContext2D {
constructor(canvas, context, options) {
let me = this;
me.canvas = canvas;
me._ctx = context;
me.isWeiXinAPP = checkWX();
// 聲明canvas屬性緩存棧,用於優化性能
me._ctxOptions = options;
me._propertyCache = [extend({}, WX_CANVAS_CTX_DEFAULT_PROPERTY, options)];
me.cp = me._propertyCache[0];
// 屬性賦值轉化
me.createStyleProperty();
me.createShadowsProperty();
// 後續省略,咱們就拿前兩個舉例足以...
return me;
}
createStyleProperty() {
let me = this;
let styleProperty = ['fillStyle', 'strokeStyle'];
styleProperty.forEach(p => {
Object.defineProperty(me, p, {
get: () => {
return me.cp[p];
},
set: (value) => {
if (value) {
return me._wxSetPropertyCallable(value.toLowerCase(), p)
}
}
})
});
}
_wxSetPropertyCallable(value, propertyName, wxSetName = 'set' + propertyName.replace(/(\w)/, v => v.toUpperCase())) {
let me = this;
if (is.Null(value) || is.Undefined(value)) {
return value;
}
// performance
if (me.cp[propertyName] === value) {
return value;
}
if (me.isWeiXinAPP) {
me._ctx[wxSetName](value);
me.cp[propertyName] = value;
} else {
me._ctx[propertyName] = value;
me.cp[propertyName] = me._ctx[propertyName];
}
return value;
}
...
draw(ctu = true) {
if (this.isWeiXinAPP) {
this._ctx.draw(ctu);
}
}
}複製代碼
上面的例子,咱們只舉例 fillStyle
, strokeStyle
兩個賦值屬性的兼容轉換。能夠看到,咱們定義了通用方法_wxSetPropertyCallable
,在小程序環境下,它會調用set[wxSetName]
(也就是setFillStyle和setStrokeStyle),而在普通環境下會正常的屬性賦值。
再來看咱們的draw
方法,一樣只在微信小程序中有效,可是遺憾的是每次繪製之後仍是要調用draw
方法一次,這是惟一的與W3C方法有區別的事情。不過,這主要是爲了性能考慮,由於JSBridge每一次的調用都是有成本的,若是每次設置咱們都自動的調用draw
會大幅增長這個成本。
以上完成之後,咱們就能夠作到相似於W3C標準,在小程序中繪圖了:
import WxCanvas, { WxCanvasRenderingContext2D } from 'wx-chart/util/wxCanvas'
let wxCanvas = new WxCanvas('myCanvas', {width: '800px', height: '500px'});
let wxCanvasContext = wxCanvas.getContext('2d');
wxCanvasContext.fillStyle='#FF0000';
wxCanvasContext.fillRect(0,0,80,100);
wxCanvasContext.draw();複製代碼
在實現兼容庫時,有幾個比較棘手的問題。
首先是小程序並無實現 measureText
方法,所以咱們須要模擬一個:
measureText(text, fontSize = this.fontSize) {
let me = this;
if (me.isWeiXinAPP) {
if (!text) { return 0; }
let textLen = text.length;
let hanzi = text.match(new RegExp(REG_HANZI, 'g'));
let hanziNum = !!hanzi ? hanzi.length: 0;
let otherNum = textLen - hanziNum;
return {'width': fontSize * (otherNum + hanziNum * 2) / 2 + fontSize / 4};
} else {//...}
}複製代碼
咱們對漢字和非漢字進行了處理。不過,須要認可的是,即使是這樣,也與實際有一些不許。
另外,textBaseline
,textAlign
也須要特殊處理(當前1.1版本已經支持)
因爲時間和和篇幅有限,第一段文章就先寫到這裏。有錯誤或者不完善的狀況請多多諒解和支持。也換用你們使用wx-chart 圖形庫並提出意見和建議。