小程序云开发+puppeteer+天地图生成地图分享海报

小程序云开发+puppeteer+天地图生成地图分享海报

前情提要

在开发微信小程序地图应用的过程中遇到了许多限制和问题

  1. 微信小程序地图无法截取地图内容到图片
  2. 个人认证小程序不能使用webview
  3. 微信小程序没有截屏api

最近在完成我的骑行运动小程序的路线分享功能的过程中被上面两个问题所困扰

如果地图可以截取图片就可以直接拿到图片进行分享

如果可以使用webview 就可以使用html2img库进行截取屏幕

如果小程序可以直接截取屏幕,就可以截取整张屏幕后再选取可用部分

可是这三项都不能做到 o(╥﹏╥)o

既然官方为我们关上了一扇窗,那我们只能剑走偏锋把门撬开了

puppeteer

中文网 https://puppeteer.bootcss.com/

GitHub https://github.com/puppeteer/puppeteer ttps://github.com/puppeteer/puppeteer

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。

无头浏览器(Headless Browser)是一种浏览器程序,没有图形用户界面(GUI),但能够执行与普通浏览器相似的功能。无头浏览器能够加载和解析网页,执行JavaScript代码,处理网页事件,并提供对DOM(文档对象模型)的访问和操作能力。

与传统浏览器相比,无头浏览器的主要区别在于其没有可见的窗口或用户界面。这使得它在后台运行时,不会显示实际的浏览器窗口,从而节省了系统资源,并且可以更高效地执行自动化任务。

常见的无头浏览器包括Headless Chrome(Chrome的无头模式)、PhantomJS、Puppeteer(基于Chrome的无头浏览器库)等。它们提供了编程接口,使开发者能够通过代码自动化控制和操作浏览器行为。

Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式

天地图+小程序云开发+Puppeteer

利用Puppeteer的截图功能,我们可用把任何的网页内容导出为图片进行保存,这也就达到了我们的目的,微信不给的api我们自己来造

实现原理:

  1. 写一个静态的html用于加载天地图底图用于底图显示
  2. 使用node环境加载puppeteer浏览器打开这个静态页面并传入数据调用方法进行地图数据的渲染
  3. 使用puppeteer的截图api进行截图并保存或者直接返回图片

优缺点

优点: 打破了以上微信小程序的限制,可以获取地图截图可以渲染数据信息

缺点: 1. puppeteer渲染较慢,接口效率不高 2 天地图瓦片地图加载速度感人

为什么选用天地图?

  1. 腾讯地图,高德地图,百度地图最新的js api 都使用webgl进行渲染,在没有显卡的服务器上直接出了兼容性问题,目前尝试只有天地图兼容性最好,可以正常显示地图内容,

第一步:写渲染html

reader.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="http://api.tianditu.gov.cn/api?v=4.0&tk=9699754b0cfb58cb75xxxxxxxxx" type="text/javascript"></script>
<!--    天地图key -->
    <style>
        *{
            margin: 0;
            padding: 0;

        }
        html,body{
            width: 100%;
            height: 100%;
            position: relative;
        }

        #mapDiv{
            position:absolute;
            width:100%;
            height:100%
        }
    </style>
</head>
<body onload="onLoad()">

<div id="mapDiv" style=""></div>

</body>
<script>
		// 存储地图实例
    var map;
    // var zoom = 15;
		
	
    var onReadied = null
    var onLaunched = new Promise((rec,rej)=>{
        onReadied = rec
    })
		

		// 渲染方法
		// 首先这个方法会被服务器执行 传入data:{wayPath} wayPath 为经纬度坐标数组
    async function drawMap(data){
				// 等待地图实例化完成
        await onLaunched

        // map.centerAndZoom(new T.LngLat(103.113298, 35.717981),zoom);
	      
				// 绘制一条路线
				//  这是路线的点  
				let points = data.wayPath.map(({coordinates},index)=>{
            return new T.LngLat(coordinates[0], coordinates[1])
        })
				// 构建一条折线
        let line = new T.Polyline(points,{
            color: '#00c173',
            weight: 10,
            lineStyle: 'solid',
            opacity: 1
        });
				// 绘制路径的起点和终点
				// 图标大小
        let iconSize = new T.Point(64, 64)
		     // 图标的定位点 底部居中
				let iconAnchor = new T.Point(32, 64)
	       
				// 开始坐标点 图标
				let startPointIcon = new T.Icon({
            iconUrl:  "./startPointIcon.png",
            iconSize: iconSize,
            iconAnchor: iconAnchor
        })
				// 结束坐标图标
        let endPointIcon = new T.Icon({
            iconUrl: "./endPointIcon.png",
            iconSize: iconSize,
            iconAnchor: iconAnchor
        });
				// 开始 结束坐标
        let startPoint = data.wayPath[0]
        let endPoint = data.wayPath[data.wayPath.length - 1]
       
				// 开始的坐标点
				let startPointMarker = new T.Marker(new T.LngLat(startPoint.coordinates[0], startPoint.coordinates[1]), {icon: startPointIcon});
				// 结束的坐标点
        let endPointMarker = new T.Marker(new T.LngLat(endPoint.coordinates[0], endPoint.coordinates[1]), {icon: endPointIcon});
				// 折线加入地图中
				map.addOverLay(line);
				// 终点起点加入地图中
        map.addOverLay(startPointMarker);
        map.addOverLay(endPointMarker);

        // 调整地图视野可以包含全部的坐标点
        map.setViewport(points)
		    
    }

    function onLoad() {
				// 页面加载 加载天地图地图实例
        let m = 0
        let n = 7
        let e = parseInt(Math.random()*(m-n)+n)
				// 随机挑选一个地图瓦片服务地址
        let  imageURL = "http://t" + e +".tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=9699754b0cfb58xxxxxxxx"
        // 构图层
        const lay = new T.TileLayer(imageURL);
	      
				// 实例化map 传入 layers 
				// ps 官网的类参考没有 layers 参数 但是js示例里面有 不知道还有多少隐藏参数等着我们探索
        
				// 为什么要用自定义瓦片?  因为默认的底图是带有pol点信息,渲染出来显得页面比较混乱,所以使用矢量底图就可以隐藏这些信息
				map = new T.Map('mapDiv',{
            layers: [lay]
        });
				// 加载完成调用 Readied 此时 onLaunched 这个 Promise的状态就会变成完成态
				
        onReadied()
    }

</script>
</html>
  • map 地图实例 onLoad函数执行完成后该对象有值
  • onLoad() body对象的加载完成 用于构建地图实例
  • onLaunched 一个Promise 用于等待onLoad执行完成
  • onReadied 值为onLaunched这个Promise resolve 执行完这个方法后 onLaunched 会变成完成状态
  • drawMap 服务端调用脚本执行该函数 传入地图绘制时用的参数并绘制地图

云函数js

// 云函数入口文件
const cloud = require('wx-server-sdk')
const path = require('path');
const uuid = require("uuid");

// 寻找 reader.html 的绝对路径 浏览器使用file协议打开文件时需要
const filePath = path.join(__dirname, 'reader.html');

cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 使用当前云环境

// 数据库查询相关初始化
const db = cloud.database()
const _ = db.command
const runningCollection = db.collection('running')
const shareImgCacheCollection = db.collection('share-img-cache')

// 延时函数
function delay(time) {
    return new Promise(function(resolve) { 
        setTimeout(resolve, time)
    });
}

// 云函数入口函数
exports.main = async (event, context) => {
		 // 初始化 puppeteer
    const puppeteer = require('puppeteer');
    const wxContext = cloud.getWXContext()
		
		// 接收参数查询地图数据
    const runningId = event.data.runningId
    const docRunning = runningCollection.doc(runningId)
    
		// 查询结果
    const {data:running} = await docRunning.get()
    if(!running){ // 结果不存在代表该id的数据已经删除
        return null
    }
		// 一个渲染缓存的记录 用于防止重复调用渲染相同内容
    const {data:cache} = await shareImgCacheCollection.where({
        runningId: _.eq(runningId)
    }).get()
		// 存在则返回缓存内容
    if(cache && cache.length > 0){
        return cache[0]
    }
		// 实例化一个浏览器
    const browser = await puppeteer.launch({
        args: ['--no-sandbox'], // 关闭Chrome的沙箱 节省资源
        // headless: 'new',
        headless: true, // 无头模式 云开发没有窗口相关资源无法使用有头模式
        ignoreHTTPSErrors:true, // 忽略https报错
        devtools:false,  // 启东时自动打开调试面板F12
        defaultViewport :{ // 默认视口大小
            width: 1024,
            height: 1024,
            deviceScaleFactor: 2.0, // 像素比 看不出区别好像没有什么效果
            isMobile :true, // 是否移动端
            isLandscape :true,
            timeout :5000 // 启动超时时间 超时则抛出异常
        }
    });
		// 一个页面
    const page = await browser.newPage();
		// 跳转到一个路径
    await page.goto("file://" + filePath);
		// 传入一个function到页面去执行 henshenqi
		// evaluate(function,data)
		// ps 无法从传入的函数访问服务器上的变量 应为不在一个内存区域 甚至不在一个程序当中
		// 传入数据使用第二个参数data 可以传递json全部类型
    await page.evaluate(async (data) => {
				// 这里的返回值如果使用 async 关键字则外面拿不到 不适用可以拿到 我这里无所谓所以没有改
       return drawMap(data)
    },{
        wayPath: running.wayPath
    });
		 // 由于网络原因 目前看来延时四秒以上才可以保证天地图全部的瓦片渲染完成再进行截图
		// 小小的遗憾
    await delay(4000);
		// 截图 传入path会保存到绝对或者相对路径
		// 传入encodeing:base64 返回base64编码
	// 什么都不传返回buffer 
		// png 格式不支持quality 1- 100
    const bf =  await page.screenshot({
        // path : new Date().getTime() + '.png',
        type: 'jpeg',
        quality: 100
    })
    // 关闭浏览器
    await browser.close();

 
//   // 上传云存储

    const uploadRes = await cloud.uploadFile({
        cloudPath: "share-img" + "/"+ uuid.v4() + '.jpg',
        fileContent: bf,
    })
		// 换取临时文件路径

    const tempFileRes = await cloud.getTempFileURL({
        fileList: [uploadRes.fileID]
    })
    // console.log("tempFileRes", tempFileRes)
    
		// 保存缓存后返回
const data = {
        runningId: runningId,
        fileID: uploadRes.fileID,
        fileURL: tempFileRes.fileList[0].tempFileURL
    }
    await shareImgCacheCollection.add({
        data: data
    })
	
    return data

}

小程序调用

const {result: {fileURL}} =  await wx.cloud.callFunction({
      name: 'share',
      data: {
          action: 'getImg',
          data: {
              runningId: this.data.runningId
          }
      }
})

this.setData({
    shareImg: fileURL
})

程序执行步骤

  1. 小程序调用云函数 share 进入 getImg 方法的main
  2. getImg 中 查询地图信息相关数据 查询是否有缓存图片信息
  3. 如果没有缓存图片信息 则进入渲染流程
  4. 实例化浏览器 并新建一个页面跳转到reader.html 使用file协议 (如果是已经上线的项目可以使用http)
  5. 调用html的drawMap() 此方法中阻塞等待 onLaunched 状态完成 保证地图实例化完成后再执行操作
  6. 地图渲染
  7. 等待一段时间后截图 这里是唯一的遗憾 不能确定全部瓦片都加载完成的时机
  8. 处理图片 保存云存储 设置缓存 返回数据到小程序

生成效果
请添加图片描述
请添加图片描述

ps: 因为这个接口阻塞等待的时间过长 大概要五秒以上才可以返回内容

如果用户刚打开页面就直接点击右上角分享的话会加载不出图片

解决方案()

页面.js定义两个变量 在onLoad的时候再进行初始化

let MessageShareLoad = null
let MessageShareReady = null

// onLoad
MessageShareReady = new Promise((rev,rej) => {
      MessageShareLoad = rev
})

onShareAppMessage 中使用 await MessageShareReady 进行阻塞等待显示loading

可以达到想要的效果

如果没有自定义标题的需求还可以把MessageShareReady 直接传递到返回值的 promise 属性中,根据微信小程序的规则 该promise三秒内进行 resolve 会已resolve 返回的参数作为实际结果 但是我这边因为加载图片时还可能拿不到自定义标题的信息所以就没有采用这种方式

注意事项:

puppeteer 小程序云函数中不需要npm依赖 直接可以引入 npm安装会有找不到Chrome的bug

总结:

最终实现了效果但是留有遗憾,天地图底图的加载速度确实有一些些的慢导致接口的返回速度有一些感人

如果服务器端有图形计算能力,使用高德地图或者腾讯地图的矢量渲染应该可以达到更好的效果,个人电脑上使用高德地图可以在三秒内返回

over 最后查看请添加图片描述
一下效果 (非静止画面)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值