前情提要
在开发微信小程序地图应用的过程中遇到了许多限制和问题
- 微信小程序地图无法截取地图内容到图片
- 个人认证小程序不能使用webview
- 微信小程序没有截屏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我们自己来造
实现原理:
- 写一个静态的html用于加载天地图底图用于底图显示
- 使用node环境加载puppeteer浏览器打开这个静态页面并传入数据调用方法进行地图数据的渲染
- 使用puppeteer的截图api进行截图并保存或者直接返回图片
优缺点
优点: 打破了以上微信小程序的限制,可以获取地图截图可以渲染数据信息
缺点: 1. puppeteer渲染较慢,接口效率不高 2 天地图瓦片地图加载速度感人
为什么选用天地图?
腾讯图,高德地图,百度地图最新的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=9699754b0cfb58cb7xxxxxxxxxx" type="text/javascript"></script>
<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
})
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(startPointMarker);
map.addOverLay(endPointMarker);
map.addOverLay(line);
map.setViewport(points)
return data.wayPath
}
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=9699754b0cfxxxxxxxxxxxxxxxx"
// 影 像底图urlconst
const lay = new T.TileLayer(imageURL, { minZoom: 6, maxZoom: 18 });
// lay
map = new T.Map('mapDiv',{
layers: [lay]
});
map.disableContinuousZoom()
// map.disableDrag()
// map.disableScrollWheelZoom()
// map.disableDoubleClickZoom()
map.disableKeyboard()
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
})
程序执行步骤
- 小程序调用云函数 share 进入 getImg 方法的main
- getImg 中 查询地图信息相关数据 查询是否有缓存图片信息
- 如果没有缓存图片信息 则进入渲染流程
- 实例化浏览器 并新建一个页面跳转到reader.html 使用file协议 (如果是已经上线的项目可以使用http)
- 调用html的drawMap() 此方法中阻塞等待 onLaunched 状态完成 保证地图实例化完成后再执行操作
- 地图渲染
- 等待一段时间后截图 这里是唯一的遗憾 不能确定全部瓦片都加载完成的时机
- 处理图片 保存云存储 设置缓存 返回数据到小程序
生成效果
ps: 因为这个接口阻塞等待的时间过长 大概要五秒以上才可以返回内容
如果用户刚打开页面就直接点击右上角分享的话会加载不出图片
解决方案(可能有更好的)
页面.js定义两个变量 在onLoad的时候再进行初始化
一定要在onLoad初始化 因为只要是打开当前页面 在page对象外的变量是共享的 第二次打开不一样内容的当前页面会有问题
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 最后查看一下效果 (非静止画面)
温馨提醒 Skyline 已经支持截图功能 只需要为地图组件截图即可
好强大
请问一下大佬有代码片段吗