写在前面
微信小程序出来已经有一段时间了,github上也有很多人开源了很多项目。但是由于微信平台的限制(底层Canvas能力调用为一系列JSBridge封装),图表的制作一直是个比较头疼的问题。当前的成熟图表库无法正常运行,即使经过改造,性能也有很大问题。
当前已经有做的比较好的图表库,如xiaolin3303的 wx-charts,但是只是为小程序量身定做的,在普通的H5页面就无法执行。因此,在开发中萌生了写一个多环境下兼容的图形库的念头。
这个图形库已经完成基本的图形建设,已经支持PC浏览器,WAP,React以及微信小程序等环境。并上传至github,大家可以自由使用:wx-chart,并欢迎在Issues中提出BUG以及修改建议。
效果图
无图无真相,首先上个效果图吧
工程结构
wx-chart 图形库参考了众多图形库的构建,分为三层,如下图所示:
最下方一层为兼容层,其中最主要的是 WxCanvas 和 WxCanvasRenderingContext2D
用于兼容浏览器以及微信小程序的环境。在本章节中,一会儿会展开讲解实现原理。
中间层为基础组件以及动画库,最上层为实现的各种图形。这两部分会在后续的章节中再另行阐述。
WxCanvas & WxCanvasRenderingContext2D
首先,我们实现这个中间层的目的很明确,就是完成一套与W3C Canvas一致的接口层,忽略小程序的底层接口,并使得上层调用时与普通Canvas使用方式一致。
小程序的架构是类似于React Native的JSBridge混合开发模式,因此其Canvas绘图实现也很有局限性,与普通的浏览器对比,主要有四个不同点:
- 节点和Canvas对象创建:小程序的Canvas节点(严格意义上小程序没有节点的概念)id的标识属性为
canvas-id
,其创建JS对象使用wx.createCanvasContext
方法 -
属性赋值不同:在W3C的规范中,绘制的属性都是赋值形式的,例如:
- ctx.fillStyle = '#ffffff';
- ctx.font = '40px Arial';
而小程序全部是调用形式的:
- ctx.setFillStyle('#ffffff');
- ctx.setFontSize(20);
- 每次绘图后需要调用
draw
方法完成渲染 - 有些Canvas的能力与原生的参数有区别,如
shadowBlur
,shadowOffsetX
均要调用wx.setShadow
;有些能力并没有实现,例如lineDash
属性,clip
剪切能力等。
其中,以上这些基本也都是由于其JSBridge的实现机理导致的,有兴趣的同学可以Google一下具体了解。
那么,问题来了,我们的中间层能够解决上面那些问题?实现思路是什么?
第一个问题很简单,我们声明节点同时书写id
与canvas-id
,而其他三个问题我的解决思路是:
- 能够检测小程序环境
- 与W3C标准一样,实现Canvas & CanvasRenderingContext2D 两个类:我们称其为WxCanvas & WxCanvasRenderingContext2D
- 使用
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 图形库并提出意见和建议。