资讯专栏INFORMATION COLUMN

开发适用于微信小程序的跨平台图表库:part1

mingde / 1714人阅读

摘要:写在前面微信小程序出来已经有一段时间了,上也有很多人开源了很多项目。但是由于微信平台的限制底层能力调用为一系列封装,图表的制作一直是个比较头疼的问题。这个图形库已经完成基本的图形建设,已经支持浏览器,,以及微信小程序等环境。

写在前面

微信小程序出来已经有一段时间了,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一下具体了解。

那么,问题来了,我们的中间层能够解决上面那些问题?实现思路是什么?
第一个问题很简单,我们声明节点同时书写idcanvas-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 图形库并提出意见和建议。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88916.html

相关文章

  • 2017-10-10 前端日报

    摘要:前端日报精选第期写给前端应届生的职业规划建议应用编译优化之路进阶篇命名空间模式解析源码解析之任务管理入门教程快速上手聊聊改变历史中文正式发布,带来种新的图表类型关系图解好好写代码吧使用手册掘金发布在即将全面支持掘金仿懂球帝 2017-10-10 前端日报 精选 【第1074期】写给前端应届生的职业规划建议webpack 应用编译优化之路JS进阶篇--命名空间模式解析gulp源码解析之任...

    myshell 评论0 收藏0
  • 在微信小程序中绘制图表part1

    摘要:微信小程序中提供了的支持,利用自行绘制图表。关注我的项目查看完整代码。 微信小程序中图表现状 由于微信小程序本身框架的限制,很难集成目前已有的图表工具,显示图表目前有两种方案:1、服务器端渲染图表,输出图片,微信小程序中直接显示渲染好的图片,比如highcharts提供了服务端渲染的能力hightcharts server render,这种方式需要后台有一套渲染服务,并且有一定的网络...

    chaosx110 评论0 收藏0
  • 在微信小程序中绘制图表(part2)

    摘要:本期大纲确定纵坐标的范围并绘制根据真实数据绘制折线相关阅读在微信小程序中绘制图表在微信小程序中绘制图表关注我的项目查看完整代码。相关阅读在微信小程序中绘制图表在微信小程序中绘制图表 本期大纲 1、确定纵坐标的范围并绘制 2、根据真实数据绘制折线 相关阅读:在微信小程序中绘制图表(part1)在微信小程序中绘制图表(part3) 关注我的 github 项目 查看完整代码。 确定纵坐...

    canopus4u 评论0 收藏0

发表评论

0条评论

mingde

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<