微信小程序出来已经有一段时间了,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 图形库并提出意见和建议。