做小程序开发,大都会遇到这么一个需求:生成分享图片。
需求的复杂度各不相同,不外乎 背景图+微信头像+昵称+小程序码+其他。本文作者入坑较深,使用了在前端canvas画布来实现。
大概的总结了下遇到的一些知识点:
1、创建一个通用的图片分享组件components;
2、用drawImage绘制自适应图片;
3、获取小程序码接口真的很坑;
4、保存base64图片到本地临时文件;
5、Promise.all解决获取图片数据过程中的异步;
废话不说了 直接贴代码
组件:
canvas-share.wxml
1 <view catchtap="handleClose" class="share {{ visible ? \'show\' : \'\' }}"> 2 3 <canvas class="canvas-hide" canvas-id="share" style="width:{{canvasWidth*2}}rpx;height:{{canvasHeight*2}}rpx" /> 4 5 <view class="content" style="transform:scale({{responsiveScale}});-webkit-transform:scale({{responsiveScale}});"> 6 7 <image class="canvas" catchtap="zuzhimaopao" src="{{imageFile}}" style="width:{{canvasWidth/3*2}}rpx;height:{{canvasHeight/3*2}}rpx" /> 8 9 <view class="save" catchtap="handleSave"><text>保存图片</text></view> 10 11 </view> 12 13 </view>
canvas-share.js
1 const app = getApp(); 2 3 var base64src = require("../../utils/base64src.js"); 4 5 var ajax = require("../../common/commonAjax.js"); 6 7 var util = require("../../utils/util.js"); 8 9 function getImageInfo(url) { 10 11 return new Promise((resolve, reject) => { 12 13 wx.getImageInfo({ 14 15 src: url, 16 17 success: resolve, 18 19 fail: reject, 20 21 }) 22 23 }) 24 25 } 26 27 function getImageInfo2(search){ 28 29 let url=""; 30 31 return new Promise((resolve, reject) => { 32 33 ajax.getAccessToken(search, function (e) { 34 35 if (e.ErrorCode == 0) { 36 37 let base64data = e.Data; 38 39 base64src.base64src(base64data, res => { 40 41 //resolve(res); 42 43 url = res; 44 45 wx.getImageInfo({ 46 47 src: url, 48 49 success: resolve, 50 51 fail: reject, 52 53 }) 54 55 }); 56 57 } 58 59 }) 60 61 }) 62 63 64 65 } 66 67 function createRpx2px() { 68 69 const { windowWidth } = wx.getSystemInfoSync() 70 71 72 73 return function(rpx) { 74 75 return windowWidth / 750 * rpx 76 77 } 78 79 } 80 81 82 83 const rpx2px = createRpx2px() 84 85 86 87 function canvasToTempFilePath(option, context) { 88 89 return new Promise((resolve, reject) => { 90 91 wx.canvasToTempFilePath({ 92 93 ...option, 94 95 success: resolve, 96 97 fail: reject, 98 99 }, context) 100 101 }) 102 103 } 104 105 106 107 function saveImageToPhotosAlbum(option) { 108 109 return new Promise((resolve, reject) => { 110 111 wx.saveImageToPhotosAlbum({ 112 113 ...option, 114 115 success: resolve, 116 117 fail: reject, 118 119 }) 120 121 }) 122 123 } 124 125 126 127 Component({ 128 129 properties: { 130 131 visible: { 132 133 type: Boolean, 134 135 value: false, 136 137 observer(visible) { 138 139 if (visible && !this.beginDraw) { 140 141 this.draw() 142 143 this.beginDraw = true 144 145 } 146 147 } 148 149 }, 150 151 userInfo: { 152 153 type: Object, 154 155 value: null, 156 157 observer: function (e) { 158 159 this.setData({ 160 161 userInfo: e 162 163 }) 164 165 } 166 167 }, 168 169 courseName: { 170 171 type: String, 172 173 value: "", 174 175 observer: function (e) { 176 177 this.setData({ 178 179 courseName: e 180 181 }) 182 183 } 184 185 }, 186 187 page: { 188 189 type: String, 190 191 value: "", 192 193 observer: function (e) { 194 195 this.setData({ 196 197 page: e 198 199 }) 200 201 } 202 203 }, 204 205 scene: { 206 207 type: String, 208 209 value: "", 210 211 observer: function (e) { 212 213 this.setData({ 214 215 scene: e 216 217 }) 218 219 } 220 221 } 222 223 }, 224 225 226 227 data: { 228 229 beginDraw: false, 230 231 isDraw: false, 232 233 234 235 canvasWidth: 1000, 236 237 canvasHeight: 1200, 238 239 240 241 imageFile: \'\', 242 243 244 245 responsiveScale: 1, 246 247 userInfo: app.globalData.userInfo, 248 249 courseName:"", 250 251 page:"", 252 253 scene:"", 254 255 }, 256 257 258 259 lifetimes: { 260 261 ready() { 262 263 const designWidth = 375 264 265 const designHeight = 603 // 这是在顶部位置定义,底部无tabbar情况下的设计稿高度 266 267 268 269 // 以iphone6为设计稿,计算相应的缩放比例 270 271 const { windowWidth, windowHeight } = wx.getSystemInfoSync() 272 273 const responsiveScale = 274 275 windowHeight / ((windowWidth / designWidth) * designHeight) 276 277 if (responsiveScale < 1) { 278 279 this.setData({ 280 281 responsiveScale, 282 283 }) 284 285 } 286 287 }, 288 289 }, 290 291 methods: { 292 293 handleClose() { 294 295 this.triggerEvent(\'close\') 296 297 }, 298 299 zuzhimaopao(){ 300 301 302 303 }, 304 305 306 307 handleSave() { 308 309 const { imageFile } = this.data 310 311 312 313 if (imageFile) { 314 315 saveImageToPhotosAlbum({ 316 317 filePath: imageFile, 318 319 }).then(() => { 320 321 wx.showToast({ 322 323 icon: \'none\', 324 325 title: \'分享图片已保存至相册\', 326 327 duration: 2000, 328 329 }) 330 331 }) 332 333 } 334 335 }, 336 337 draw() { 338 339 wx.showLoading() 340 341 let cpage=this; 342 343 console.log(this.data); 344 345 const { userInfo, canvasWidth, canvasHeight,courseName } = this.data; 346 347 const { avatarUrl, nickName } = userInfo; 348 349 const avatarPromise = getImageInfo(avatarUrl); 350 351 const backgroundPromise = \'/images/share.png\'; 352 353 let search={ 354 355 page:this.data.page, 356 357 scene:this.data.scene 358 359 } 360 361 const filePath = getImageInfo2(search); 362 363 364 365 Promise.all([avatarPromise, filePath]) 366 367 .then(([avatar, filePathxcx]) => { 368 369 const ctx = wx.createCanvasContext(\'share\', this) 370 371 372 373 const canvasW = rpx2px(canvasWidth * 2) 374 375 const canvasH = rpx2px(canvasHeight * 2) 376 377 378 379 // 绘制背景 380 381 ctx.drawImage( 382 383 backgroundPromise, 384 385 //background, 386 387 1, 388 389 1, 390 391 canvasW, 392 393 canvasH 394 395 ) 396 397 398 399 // 绘制头像 400 401 const radius = rpx2px(80 * 2) 402 403 const y = rpx2px(1030 * 2) 404 405 // ctx.arc(canvasW / 2 - radius * 4, y, radius, 0, 2 * Math.PI) 406 407 // ctx.clip() 408 409 ctx.drawImage( 410 411 avatar.path, 412 413 canvasW / 2 - radius * 5 - 10, 414 415 y - radius + 15, 416 417 radius * 2, 418 419 radius * 2, 420 421 ) 422 423 // 绘制小程序码 424 425 if (!util.isNullObj(filePathxcx)){ 426 427 ctx.drawImage( 428 429 filePathxcx.path, 430 431 // \'http://usr/tmp_base64src.png\', 432 433 canvasW / 2 + radius * 2.5, 434 435 y - radius * 1.3, 436 437 radius * 3, 438 439 radius * 3, 440 441 ) 442 443 444 445 } 446 447 448 449 // 绘制用户名 450 451 ctx.setFontSize(36) 452 453 ctx.setTextAlign(\'center\') 454 455 ctx.setFillStyle(\'#000000\') 456 457 ctx.fillText( 458 459 nickName, 460 461 canvasW / 2 - radius * 1.5 - 25, 462 463 // y + rpx2px(150 * 2), 464 465 y - radius * 0.5, 466 467 ) 468 469 ctx.stroke() 470 471 472 473 // 绘制课程名称 474 475 ctx.setFontSize(48) 476 477 ctx.setTextAlign(\'center\') 478 479 ctx.setFillStyle(\'#434999\') 480 481 ctx.fillText( 482 483 "《" + courseName + "》", 484 485 canvasW / 2, 486 487 y - radius * 3.5, 488 489 ) 490 491 492 493 ctx.draw(false, () => { 494 495 canvasToTempFilePath({ 496 497 canvasId: \'share\', 498 499 }, cpage).then(({ tempFilePath }) => cpage.setData({ imageFile: tempFilePath })) 500 501 }) 502 503 504 505 wx.hideLoading() 506 507 cpage.setData({ isDraw: true }) 508 509 510 511 }) 512 513 .catch(() => { 514 515 cpage.setData({ beginDraw: false }) 516 517 wx.hideLoading() 518 519 }) 520 521 522 523 } 524 525 } 526 527 })
前端保存base64图片的引用
base64src.js
1 const fsm = wx.getFileSystemManager(); 2 3 const FILE_BASE_NAME = \'tmp_base64src\'; //自定义文件名 4 5 6 7 function base64src(base64data, cb) { 8 9 const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || []; 10 11 if (!format) { 12 13 return (new Error(\'ERROR_BASE64SRC_PARSE\')); 14 15 } 16 17 const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`; 18 19 const buffer = wx.base64ToArrayBuffer(bodyData); 20 21 fsm.writeFile({ 22 23 filePath, 24 25 data: buffer, 26 27 encoding: \'binary\', 28 29 success() { 30 31 cb(filePath); 32 33 }, 34 35 fail() { 36 37 return (new Error(\'ERROR_BASE64SRC_WRITE\')); 38 39 }, 40 41 }); 42 43 }; 44 45 46 47 export { base64src };
调用组件:
<canvas-share bindclose="close" userInfo="{{userInfo}}" visible="{{visible}}" courseName="{{aColumn.sName}}" page="/pages/course/PDFDetail" scene="iAutoID={{iAutoID}}" />
参数:
userInfo:wx.getUserInfo 获取的用户信息
visible:是否显示
courseName:可忽略,特殊需求 分享图上的显示课程名称
page:用于生成小程序码的参数 扫码跳转的页面
scene:用于生成小程序码的参数 扫码跳转的页面参数
bindclose:隐藏组件visible:false
Php后台 获取小程序码涉及的接口和方法
1 public function GetAccessTokenAction(){ 2 $formVals= json_decode($this->getParam(\'formVals\')); 3 $page=""; 4 $scene=""; 5 if ($formVals){ 6 $page=$formVals->page; 7 $scene=$formVals->scene; 8 } 9 10 // $page="pages/course/oneVideoDetail"; 11 // $scene="iAutoID=60"; 12 $appid=Yaf_G::getConf(\'appid\', \'dcr\'); 13 $secret=Yaf_G::getConf(\'appsecret\', \'dcr\'); 14 $rwm_contents=""; 15 16 $url1="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$secret}"; 17 $result = self::curl_file_get_contents($url1); 18 $access_token= isset($result)?json_decode($result)->access_token:\'\'; 19 20 if($access_token!=""){ 21 // $url2="https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={$access_token}"; 22 // $rwm_contents = self::curl_file_get_contents($url2,$page,$scene); 23 $url2="https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=".$access_token; 24 //dump($access_token); 25 $post_data = array( 26 "page"=>$page, 27 "scene"=>$scene, 28 "width"=>50 29 ); 30 $post_data=json_encode($post_data); 31 $rwm_contents=self::send_post($url2,$post_data); 32 $result=self::data_uri($rwm_contents,\'image/png\'); 33 } 34 return $this->showMgApiMsg($result); 35 } 36 37 38 39 40 41 protected function send_post( $url, $post_data ) { 42 $options = array( 43 \'http\' => array( 44 \'method\' => \'POST\', 45 \'header\' => \'Content-type:application/json\',
全部评论
请发表评论