做小程序开发,大都会遇到这么一个需求:生成分享图片。
需求的复杂度各不相同,不外乎 背景图+微信头像+昵称+小程序码+其他。本文作者入坑较深,使用了在前端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 值和图片服务器返回的地址 存入数据库,可提高复用性,也可以简化前端的一些操作。