做小程序开发,大都会遇到这么一个需求:生成分享图片。
需求的复杂度各不相同,不外乎 背景图+微信头像+昵称+小程序码+其他。本文作者入坑较深,使用了在前端canvas画布来实现。
大概的总结了下遇到的一些知识点:
1、创建一个通用的图片分享组件components;
2、用drawImage绘制自适应图片;
3、获取小程序码接口真的很坑;
4、保存base64图片到本地临时文件;
5、Promise.all解决获取图片数据过程中的异步;
废话不说了 直接贴代码
组件:
canvas-share.wxml
<view catchtap="handleClose" class="share {{ visible ? 'show' : '' }}">
<canvas class="canvas-hide" canvas-id="share" style="width:{{canvasWidth*2}}rpx;height:{{canvasHeight*2}}rpx" />
<view class="content" style="transform:scale({{responsiveScale}});-webkit-transform:scale({{responsiveScale}});">
<image class="canvas" catchtap="zuzhimaopao" src="{{imageFile}}" style="width:{{canvasWidth/3*2}}rpx;height:{{canvasHeight/3*2}}rpx" />
<view class="save" catchtap="handleSave"><text>保存图片</text></view>
</view>
</view>
canvas-share.js
const app = getApp();
var base64src = require("../../utils/base64src.js");
var ajax = require("../../common/commonAjax.js");
var util = require("../../utils/util.js");
function getImageInfo(url) {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src: url,
success: resolve,
fail: reject,
})
})
}
function getImageInfo2(search){
let url="";
return new Promise((resolve, reject) => {
ajax.getAccessToken(search, function (e) {
if (e.ErrorCode == 0) {
let base64data = e.Data;
base64src.base64src(base64data, res => {
//resolve(res);
url = res;
wx.getImageInfo({
src: url,
success: resolve,
fail: reject,
})
});
}
})
})
}
function createRpx2px() {
const { windowWidth } = wx.getSystemInfoSync()
return function(rpx) {
return windowWidth / 750 * rpx
}
}
const rpx2px = createRpx2px()
function canvasToTempFilePath(option, context) {
return new Promise((resolve, reject) => {
wx.canvasToTempFilePath({
...option,
success: resolve,
fail: reject,
}, context)
})
}
function saveImageToPhotosAlbum(option) {
return new Promise((resolve, reject) => {
wx.saveImageToPhotosAlbum({
...option,
success: resolve,
fail: reject,
})
})
}
Component({
properties: {
visible: {
type: Boolean,
value: false,
observer(visible) {
if (visible && !this.beginDraw) {
this.draw()
this.beginDraw = true
}
}
},
userInfo: {
type: Object,
value: null,
observer: function (e) {
this.setData({
userInfo: e
})
}
},
courseName: {
type: String,
value: "",
observer: function (e) {
this.setData({
courseName: e
})
}
},
page: {
type: String,
value: "",
observer: function (e) {
this.setData({
page: e
})
}
},
scene: {
type: String,
value: "",
observer: function (e) {
this.setData({
scene: e
})
}
}
},
data: {
beginDraw: false,
isDraw: false,
canvasWidth: 1000,
canvasHeight: 1200,
imageFile: '',
responsiveScale: 1,
userInfo: app.globalData.userInfo,
courseName:"",
page:"",
scene:"",
},
lifetimes: {
ready() {
const designWidth = 375
const designHeight = 603 // 这是在顶部位置定义,底部无tabbar情况下的设计稿高度
// 以iphone6为设计稿,计算相应的缩放比例
const { windowWidth, windowHeight } = wx.getSystemInfoSync()
const responsiveScale =
windowHeight / ((windowWidth / designWidth) * designHeight)
if (responsiveScale < 1) {
this.setData({
responsiveScale,
})
}
},
},
methods: {
handleClose() {
this.triggerEvent('close')
},
zuzhimaopao(){
},
handleSave() {
const { imageFile } = this.data
if (imageFile) {
saveImageToPhotosAlbum({
filePath: imageFile,
}).then(() => {
wx.showToast({
icon: 'none',
title: '分享图片已保存至相册',
duration: 2000,
})
})
}
},
draw() {
wx.showLoading()
let cpage=this;
console.log(this.data);
const { userInfo, canvasWidth, canvasHeight,courseName } = this.data;
const { avatarUrl, nickName } = userInfo;
const avatarPromise = getImageInfo(avatarUrl);
const backgroundPromise = '/images/share.png';
let search={
page:this.data.page,
scene:this.data.scene
}
const filePath = getImageInfo2(search);
Promise.all([avatarPromise, filePath])
.then(([avatar, filePathxcx]) => {
const ctx = wx.createCanvasContext('share', this)
const canvasW = rpx2px(canvasWidth * 2)
const canvasH = rpx2px(canvasHeight * 2)
// 绘制背景
ctx.drawImage(
backgroundPromise,
//background,
1,
1,
canvasW,
canvasH
)
// 绘制头像
const radius = rpx2px(80 * 2)
const y = rpx2px(1030 * 2)
// ctx.arc(canvasW / 2 - radius * 4, y, radius, 0, 2 * Math.PI)
// ctx.clip()
ctx.drawImage(
avatar.path,
canvasW / 2 - radius * 5 - 10,
y - radius + 15,
radius * 2,
radius * 2,
)
// 绘制小程序码
if (!util.isNullObj(filePathxcx)){
ctx.drawImage(
filePathxcx.path,
// 'http://usr/tmp_base64src.png',
canvasW / 2 + radius * 2.5,
y - radius * 1.3,
radius * 3,
radius * 3,
)
}
// 绘制用户名
ctx.setFontSize(36)
ctx.setTextAlign('center')
ctx.setFillStyle('#000000')
ctx.fillText(
nickName,
canvasW / 2 - radius * 1.5 - 25,
// y + rpx2px(150 * 2),
y - radius * 0.5,
)
ctx.stroke()
// 绘制课程名称
ctx.setFontSize(48)
ctx.setTextAlign('center')
ctx.setFillStyle('#434999')
ctx.fillText(
"《" + courseName + "》",
canvasW / 2,
y - radius * 3.5,
)
ctx.draw(false, () => {
canvasToTempFilePath({
canvasId: 'share',
}, cpage).then(({ tempFilePath }) => cpage.setData({ imageFile: tempFilePath }))
})
wx.hideLoading()
cpage.setData({ isDraw: true })
})
.catch(() => {
cpage.setData({ beginDraw: false })
wx.hideLoading()
})
}
}
})
前端保存base64图片的引用
base64src.js
const fsm = wx.getFileSystemManager();
const FILE_BASE_NAME = 'tmp_base64src'; //自定义文件名
function base64src(base64data, cb) {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
if (!format) {
return (new Error('ERROR_BASE64SRC_PARSE'));
}
const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
const buffer = wx.base64ToArrayBuffer(bodyData);
fsm.writeFile({
filePath,
data: buffer,
encoding: 'binary',
success() {
cb(filePath);
},
fail() {
return (new Error('ERROR_BASE64SRC_WRITE'));
},
});
};
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后台 获取小程序码涉及的接口和方法
public function GetAccessTokenAction(){
$formVals= json_decode($this->getParam('formVals'));
$page="";
$scene="";
if ($formVals){
$page=$formVals->page;
$scene=$formVals->scene;
}
// $page="pages/course/oneVideoDetail";
// $scene="iAutoID=60";
$appid=Yaf_G::getConf('appid', 'dcr');
$secret=Yaf_G::getConf('appsecret', 'dcr');
$rwm_contents="";
$url1="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$secret}";
$result = self::curl_file_get_contents($url1);
$access_token= isset($result)?json_decode($result)->access_token:'';
if($access_token!=""){
// $url2="https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={$access_token}";
// $rwm_contents = self::curl_file_get_contents($url2,$page,$scene);
$url2="https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=".$access_token;
//dump($access_token);
$post_data = array(
"page"=>$page,
"scene"=>$scene,
"width"=>50
);
$post_data=json_encode($post_data);
$rwm_contents=self::send_post($url2,$post_data);
$result=self::data_uri($rwm_contents,'image/png');
}
return $this->showMgApiMsg($result);
}
protected function send_post( $url, $post_data ) {
$options = array(
'http' => array(
'method' => 'POST',
'header' => 'Content-type:application/json',
//header 需要设置为 JSON
'content' => $post_data,
'timeout' => 60
//超时时间
)
);
$context = stream_context_create( $options );
$result = file_get_contents( $url, false, $context );
return $result;
}
public function data_uri($contents, $mime)
{
$base64 = base64_encode($contents);
return ('data:' . $mime . ';base64,' . $base64);
}
public function curl_file_get_contents($durl){
// header传送格式
$headers = array(
"token:1111111111111",
"over_time:22222222222",
);
// 初始化
$curl = curl_init();
// 设置url路径
curl_setopt($curl, CURLOPT_URL, $durl);
// 将 curl_exec()获取的信息以文件流的形式返回,而不是直接输出。
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true) ;
// 在启用 CURLOPT_RETURNTRANSFER 时候将获取数据返回
curl_setopt($curl, CURLOPT_BINARYTRANSFER, true) ;
// 添加头信息
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
// CURLINFO_HEADER_OUT选项可以拿到请求头信息
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
// 不验证SSL
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
// 执行
$data = curl_exec($curl);
// 打印请求头信息
// echo curl_getinfo($curl, CURLINFO_HEADER_OUT);
// 关闭连接
curl_close($curl);
// 返回数据
return $data;
}
思考:
1、后端绘图更便于调试;
2、生成二维码可存入图片服务器 在根据page+scene 值和图片服务器返回的地址 存入数据库,可提高复用性,也可以简化前端的一些操作。

