前面写的。微信小程序发布有一段时间了,github上很多人开源了很多项目。但是由于微信平台的限制(Canvas底层能力被称为一系列JSBridge包),图表的制作一直是个让人头疼的问题。目前成熟的图表库并不能正常工作,即使修改后,性能也存在很大问题。目前有比较好的图表库,比如xiaolin3303的wx-charts,但是只是为小程序量身定做的,不能在普通的H5页面上执行。因此在开发的时候萌生了写一个兼容多种环境的图形库的想法。这个图形库已经完成了基本的图形构建,已经支持PC浏览器、WAP、React、微信小程序等环境。并上传到github,大家可以自由使用:wx-chart,欢迎在Issue中提出BUG和修改建议。效果图无图无真相。先来一张效果图。工程结构wx-chart图形库参考了很多图形库的构建。分为三层,如下图所示:最底层是兼容层,其中最重要的是WxCanvas和WxCanvasRenderingContext2D用于兼容浏览器和微信小程序的环境。本章稍后将对实现原理进行说明。中间层是基础组件和动画库,最上层是各种图形实现。这两部分将在后续章节中详细阐述。WxCanvas&WxCanvasRenderingContext2D首先我们实现这个中间层的目的很明确,就是完成一套和W3??CCanvas一致的接口层,忽略小程序底层接口,让上层使用相同调用方式与普通Canvas相同。小程序的架构类似于ReactNative的JSBridge混合开发模式,所以它的Canvas绘图实现也非常有限。与普通浏览器相比,主要有四点不同:Node和Canvas对象的创建:小程序的CanvasNodes(严格来说小程序没有节点的概念)有一个ID属性canvas-id,它使用canvas-id创建JS对象wx.createCanvasContext方法。属性赋值不同:在W3C规范中,绘制的属性都是赋值的形式,例如:ctx.fillStyle='#ffffff';ctx.font='40px宋体';而小程序都是调用的形式:ctx.setFillStyle('#ffffff');ctx.setFontSize(20);每次绘制后都需要调用draw方法完成渲染有些Canvas的能力和原有的参数不同,比如shadowBlur,shadowOffsetX都需要调用wx.setShadow;有些能力没有实现,比如lineDash属性,裁剪能力等,其中,以上基本是其JSBridge的实现机制导致的。有兴趣的同学可以谷歌一下了解一下。那么,问题来了,我们的中间层能解决上面的问题吗?实现思路是什么?第一个问题很简单。我们声明节点同时写入id和canvas-id。其他三个问题我的解决方案是:检测小程序环境的能力和W3C标准一样,实现Canvas&CanvasRenderingContext2D这两个类:我们称之为UseObject.definePropertyforWxCanvas&WxCanvasRenderingContext2Dtoconvertcallstoassignments.自动处理对已区分且未实现的函数的调用。检测小程序环境导出函数checkWX(){return__GLOBAL__DEBUG__WX__===true||(typeofwx!='undefined'&&typeofwx==='object');}忽略__GLOBAL__DEBUG__WX__,该变量用于调试。我们只是检查wx全局是否存在。代码实现兼容类WxCanvas的主要逻辑如下:classWxCanvas{constructor(id,config,contextOptions){letme=this;//检查小程序环境me.isWeiXinAPP=checkWX();me._config=extend({},WX_CANVAS_DEFAULT_PROPERTY,me.initConfig(config));//提前获取Canvas和context(这里忽略webgl情况)let{canvas,context}=this.acquireContext(id,config);我._canvas=画布;me._ctx=上下文;//实例化WxCanvasRenderingContext2Dme.wxCanvasRenderingContext2D=newWxCanvasRenderingContext2D(canvas,context,contextOptions);还我;}/***创建画布上下文*@param{String}id*@param{Object}config*@returns{*}*/acquireContext(id,config){letme=this,canvas,context;//外部画布配置lethandlerCanvas=config.canvas;if(me.isWeiXinAPP){if(is.String(id)){canvas=context=wx.createCanva上下文(id);}else{//...}}else{if(handlerCanvas)canvas=handlerCanvas;否则canvas=is.String(id)?document.getElementById(id):(typeofHTMLCanvasElement!='undefined'&&idinstanceofHTMLCanvasElement)?编号:空;if(typeofcanvas!='undefined'){context=canvas.getContext&&canvas.getContext('2d');}}//...返回{canvas,context};}getContext(str){}setheight(){...}getheight(){...}setwidth(){...}getwidth(){...}}WxCanvas的实现比较simple是的,主要要注意的是acquireContext方法,这里判断小程序和非小程序环境,获取Canvas上下文。我们看一下WxCanvasRenderingContext2D的实现。classWxCanvasRenderingContext2D{constructor(canvas,context,options){让我=this;me.canvas=画布;me._ctx=上下文;me.isWeiXinAPP=checkWX();//声明画布属性缓存栈以优化性能me._ctxOptions=options;me._propertyCache=[扩展({},WX_CANVAS_CTX_DEFAULT_PROPERTY,选项)];me.cp=me._propertyCache[0];//属性赋值转换me.createStyleProperty();我.createShadowsProperty();//后续省略,先拿前两个例子就够了...returnme;}createStyleProperty(){让我=这个;让styleProperty=['fillStyle','strokeStyle'];styleProperty.forEach(p=>{Object.defineProperty(me,p,{get:()=>{returnme.cp[p];},set:(value)=>{if(value){返回我。_wxSetPropertyCallable(value.toLowerCase(),p)}}})});}_wxSetPropertyCallable(value,propertyName,wxSetName='set'+propertyName.replace(/(\w)/,v=>v.toUpperCase())){让我=this;if(is.Null(value)||is.Undefined(value)){返回值;}//性能if(me.cp[propertyName]===value){returnvalue;}if(me.isWeiXinAPP){me._ctx[wxSetName](value);me.cp[属性名]=值;}else{me._ctx[propertyName]=value;me.cp[属性名]=me._ctx[属性名];}返回值;}...draw(ctu=true){if(this.isWeiXinAPP){this._ctx.draw(ctu);}}}在上面的例子中,我们只给出了fillStyle和strokeStyle兼容转换的例子。可以看到,我们定义了通用方法_wxSetPropertyCallable,在小程序环境下,它会调用set[wxSetName](即setFillStyle和setStrokeStyle),而在普通环境下,它会分配普通的属性。再看看我们的draw方法,也是只在微信小程序中有效,可惜每次绘制完还是要调用一次draw方法。这是唯一与W3C方法不同的地方。不过这主要是出于性能的考虑,因为JSBridge的每一次调用都是有代价的。如果我们每次设置都自动调用draw,这个成本会大大增加。以上完成后,我们就可以在类似W3C标准的小程序中绘制了:importWxCanvas,{WxCanvasRenderingContext2D}from'wx-chart/util/wxCanvas'letwxCanvas=newWxCanvas('myCanvas',{width:'800px',height:'500px'});letwxCanvasContext=wxCanvas.getContext('2d');wxCanvasContext.fillStyle='#FF0000';wxCanvasContext.fillRect(0,0,80,100);wxCanvasContext.draw();遇到的一些问题在实现兼容库时,有几个棘手的问题。首先,小程序没有实现measureText方法,所以我们需要模拟一个:measureText(text,fontSize=this.fontSize){letme=this;if(me.isWeiXinAPP){if(!text){return0;}让textLen=text.length;lethanzi=text.match(newRegExp(REG_HANZI,'g'));让hanziNum=!!hanzi?汉字.length:0;让otherNum=textLen-hanziNum;return{'width':fontSize*(otherNum+hanziNum*2)/2+fontSize/4};}else{//...}}我们处理了汉字和非汉字。然而,需要承认的是,即便如此,也存在一些与现实不符的地方。另外textBaseline和textAlign也需要特殊处理(目前1.1版本已经支持)。由于时间和篇幅有限,文章的第一段先写到这里。如有错误或不完善之处,还请多多理解和支持。也使用wx-chart图形库,并给予意见和建议。
