注册

Taro开发小程序记录-海报生成

Taro开发小程序记录-海报生成

0af6059f28c3c74e4b43d5a66ef7509e.png

在公司开发的第一个项目是一个简单的Taro小程序项目,主要应用的则是微信小程序,由于是第一次使用Taro,所以在开发过程中遇到的一些问题和一些开发技巧,记录一下,方便以后进行查看,也与大家进行分享。



自定义海报


说到自定义海报可以说是很多小程序中都会进行开发的内容,比如需要进行二维码的保存,然后再对二维码进行一点文字的修饰,涉及到这方面的时候我们就需要使用canvas了。


在实际开发的过程中,遇到了一些很坑的问题,当我们需要使用离屏canvas来进行绘制时,我们可能就会遇到问题(我自己就遇到了)。


对于安卓端,我们可以正常的使用OffscreenCanvas来创建离屏canvas,然后绘制相关内容,最后在使用Taro.canvasToTempFilePath方法保存到临时文件下,Taro.canvasToTempFilePath方法会返回文件路径,我们就可以通过获取到的文件路径来进行下载。


下面是安卓端的一个🌰,大家有需要也可以直接拿去使用


  • 需要使用到的方法
 /**
  * @description 获取二维码图像
  */
 export const qrCodeImage = async (qrCodeValue: string, size: number = 128) => {
     /* NOTE: 通过创建离屏canvas承载code */
     const context = createOffscreenCanvas('2d', size, size);
     QRCode.toCanvas(context, qrCodeValue, { width: size, height: size, margin: 1 });
     return (context as unknown as HTMLCanvasElement).toDataURL();
 };
 /**
  * @description 创建离屏canvas对象,width与height单位为px
  */
 export const createOffscreenCanvas = (type: '2d' | 'webgl', width: number = 100, height: number = 100) => {
     return Taro.createOffscreenCanvas({ type, width, height });
 };
 /**
  * @description 将传入的图片url转换成一个ImageElement对象
  */
 export const loadImageByUrlToCanvasImageData = async (url: string, width: number = 100, height: number = 100) => {
     const context = createOffscreenCanvas('2d', width, height);
     const imageElement = context.createImage();
     await new Promise(resolve => {
         imageElement.onload = resolve;
         imageElement.src = url;
    });
     return imageElement;
 };
 /**
  * @description 将canvas转成图片文件并保存在临时路径下
  */
 export const changeCanvasToImageFileAndSaveToTempFilePath = async (options: Taro.canvasToTempFilePath.Option) => {
     const successCallback = await Taro.canvasToTempFilePath(options);
     return successCallback.tempFilePath;
 };
 interface SettingOptions {
     title: string;
     titleInfo: {
         dx: number;
         dy: number;
         color?: string;
         font?: string;
    };
     imageUrl: string;
     imagePos: {
         dx: number;
         dy: number;
    };
     width: number;
     height: number;
 }
 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
    } = option;
     const context = await createOffscreenCanvas('2d', width, height);
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };
 /**
  * @description 保存图片
  */
 export const saveImage = async (urls: string[], isLocal: boolean = true) => {
     let filePath = urls;
     if (!isLocal) {
         filePath = await netImageToLocal(urls);
    }
     await Promise.all(filePath.map(path => {
         return Taro.saveImageToPhotosAlbum({ filePath: path });
    }));
 
     return true;
 };
 /**
  * @description 加载在线图片,并返回临时图片文件地址
  */
 export const netImageToLocal = async (urls: string[]) => {
     const res = await Promise.all(urls.map((url:string) => {
         return Taro.downloadFile({ url });
    }));
 
     const result = res.map(data => {
         if (data.statusCode === 200) {
             return data.tempFilePath;
        }
         throw new Error(data.errMsg);
    });
 
     return result;
 };
 /**
  * @description 判断用户是否授权保存图片
  */
 export const checkHasAuthorizedSaveImagePermissions = async () => {
     const setting = await Taro.getSetting();
     const { authSetting } = setting;
     return authSetting['scope.writePhotosAlbum'];
 };
 /**
  * @description 下载图片,需要区分是本地图片还是在线图片
  */
 export const downloadImage = async (urls: string[], isLocal: boolean = true) => {
     const hasSaveImagePermissions = await checkHasAuthorizedSaveImagePermissions();
     if (hasSaveImagePermissions === undefined) {
         // NOTE: 用户未授权情况下,进行用户授权,允许保存图片
         await Taro.authorize({ scope: 'scope.writePhotosAlbum' });
         return await saveImage(urls, isLocal);
    } else if (typeof hasSaveImagePermissions === 'boolean' && !hasSaveImagePermissions) {
         return new Promise((resolve, reject) => {
             Taro.showModal({
                 title: '是否授权保存到相册',
                 content: '需要获取您的保存图片权限,请确认授权,否则图片将无法保存到相册',
                 success: (result) => {
                     if (result.confirm) {
                         Taro.openSetting({
                             success: async (data) => {
                                 if (data.authSetting['scope.writePhotosAlbum']) {
                                     showLoadingModal('正在保存...');
                                     resolve(await saveImage(urls, isLocal));
                                }
                            },
                        });
                    } else {
                         reject(new Error('未授予保存权限'));
                    }
                },
            });
        });
    }
     await saveImage(urls, isLocal);
     return true;
 };

  • 生成海报(二维码+标题头)

 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
    } = option;
     const context = await createOffscreenCanvas('2d', width, height);
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: any = await loadImageByUrlToCanvasImageData(imageUrl, width, height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage(imgElement, imagePos.dx, imagePos.dy);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };

  • 具体使用

 export const saveQrCodeImageWithTitle = async () => {
     const url = await qrCodeImage(enterAiyongShopUrl(), 160);
     const imgUrl: string = await generateQrCodeWithTitle({
         title: 'adsionli菜鸡前端',
         titleInfo: {
             dx: 95,
             dy: 20,
             font: '600 14px PingFang SC',
             color: 'black',
        },
         imageUrl: url,
         imagePos: {
             dx: 15,
             dy: 34,
        },
         width: 190,
         height: 204,
    });
     await downloadImage([imgUrl]);
 }


上面三块内容就可以组成我们的海报生成了,这里面的主要步骤不是很难,包括了几个方面:


  1. 用户授权鉴定,主要是是否允许保存,这里做了一点处理,就是可以在用户第一次授权不允许时,进行二次授权调起,这个可以看一下上面的downloadImage这个函数,以及用于判断用户是否授权的checkHasAuthorizedSaveImagePermissions这个函数
  2. 创建OffscreenCanvas并进行绘制,这里其实没有太多的难点,主要就是需要知道,如果我们使用image的内容的话,或者是一个图片的url时,我们需要先将其绘制到一个canvas上(这里可以获取imageElement对象,也可以直接使用canvas),这样方便我们后面进行drawImage时进行使用
  3. 图片保存,这里也有一个需要注意的点,如果图片(或二维码)是网络图片的话,我们需要处理以下,先将其转成本地图片,也就是通过netImageToLocal这个方法,然后再还给对应的将图片画在canvas上的方法。最后的保存很简单,我们可以直接使用Taro.canvasToTempFilePath这个方法转到临时地址,再通过downloadImage就可以搞定了。

感觉好像很麻烦,其实就四步:图片加载转化—>canvas绘制—>用户鉴权—>图片保存。


安卓端实现起来还是很简单的,但是这些方法对于ios端就出现了问题,如果按照上面的路线进行海报绘制保存的话,在ios端就会报一个错误(在本地开发的时候并不会抛出): canvasToTempFilePath:fail invalid viewId


这一步错误就是发生在Taro.canvasToTempFilePath这里,保存到临时文件时会触发,然后这一切的原因就是使用了OffscreenCanvas离屏canvas造成的。


所以为了能够兼容ios端的这个问题,有了以下的修改:


首先需要在我们要下载海报的pages中,添加一个Canvas,帮助我们可以获取CanvasElement

 <Canvas
     type='2d'
     id='qrCodeOut'
     className='aiyong-shop__qrCode'
 />

这里需要注意一下,我们需要添加一个type='2d'的属性,这是为了能够使用官方提供的获取Canvas2dContext的属性,这样就可以不使用createCanvasContext这个方法来获取了(毕竟已经被官方停止维护了)。


然后我们就可以获取一下CanvasElement对象了

 /**
  * @description 获取canvas标签对象
  */
 export const getCanvasElement = (canvasId: string): Promise<Taro.NodesRef> => {
     return new Promise(resolve => {
         const canvasSelect: Taro.NodesRef = selectQuery().select(`#${canvasId}`);
         canvasSelect.node().exec((res: Taro.NodesRef) => {
             resolve(res);
        });
    });
 };

注:这里又有一个小坑,我们在获取CanvasElement之后,如果直接进行绘制的话,这里存在一个问题,就是这个CanvasElementwidth:300、height:150被限制死了,所以我们需要自己在拿到CanvasElement之后,在设置一下width、height

 const canvasNodeRef = await getCanvasElement(canvas);
 let context;
 if (canvasNodeRef && canvasNodeRef[0].node !== null) {
     context = canvasNodeRef[0].node;
    (context as Taro.Canvas).width = width;
    (context as Taro.Canvas).height = height;
 }

好了,改造完成,这样就可以兼容ios端的内容了,实际我们只需要修改generateQrCodeWithTitle这个方法和page新增Canvas用于获取CanvasElement就可以了,其他可以不要动。修改后的generateQrCodeWithTitle方法如下:

 /**
  * @description 获取二维码图像并设置标题
  */
 export const generateQrCodeWithTitle = async (option: SettingOptions): Promise<string> => {
     const {
         title,
         titleInfo,
         imageUrl,
         imagePos,
         width,
         height,
         qrCodeSize,
         canvas,
    } = option;
     const canvasNodeRef = await getCanvasElement(canvas);
     let context;
     if (canvasNodeRef && canvasNodeRef[0].node !== null) {
         context = canvasNodeRef[0].node;
        (context as Taro.Canvas).width = width;
        (context as Taro.Canvas).height = height;
    }
     const ctx = (context.getContext('2d') as CanvasRenderingContext2D);
     const imgElement: Taro.Image = await loadImageByUrlToCanvasImageData(imageUrl, qrCodeSize.width, qrCodeSize.height);
     ctx.fillStyle = 'white';
     ctx.fillRect(0, 0, width, height);
     ctx.fillStyle = titleInfo.color || 'black';
     ctx.font = titleInfo.font || '';
     ctx.textAlign = 'center';
     ctx.fillText(title, titleInfo.dx || 0, titleInfo.dy || 0);
     ctx.drawImage((imgElement as HTMLImageElement), imagePos.dx, imagePos.dy, imgElement.width, qrCodeSize.height);
 
     const filePath = await changeCanvasToImageFileAndSaveToTempFilePath({
         canvas: (context as Canvas),
         width,
         height,
         fileType: 'png',
         destWidth: width,
         destHeight: height,
    });
     return filePath;
 };


如果大家不想让海报被人看到,那可以设置一下css

 .qrCode {
     position: fixed;
     left: 100%;
 }

这样就可以啦



突然发现内容可能有点多了,所以打算分成两篇进行Taro使用过程中的总结,开发完之后进行总结,总是可以让自己回顾在开发过程中遇到的问题的进一步进行思考,这是一个很好的进步过程,加油加油!!!


作者:Adsionli
链接:https://juejin.cn/post/7244452419458498616
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册