背景
为了适配多端小程序,通用的功能一般采用webview的方式引入小程序,这次的移动管理端也不例外。
需求
h5表单的地图选择功能。如下所示:
可行性分析
方案:
- 引入第三方地图
- 使用微信sdk的地图选择功能
- 采用跳转至小程序,使用小程序中的地图
分析:
- 方案一采用第三方地图在浏览器中没有问题,但是通过小程序的webview打开会出现提示无法打开该页面,大概率和小程序安全业务域名相关,先不考虑该做法。
- 方案二采用微信sdk直接拉起地图,但是经查询没有选择位置的方法,只有定位自身位置和打开地图的方法,也不考虑该做法。
- 方案三在小程序中单独写一个地图页面用来选择地图,选择完后进行和h5通信,实现该功能。(适合)
方案定为第三种,主要的难点在小程序和h5如何通信以及保留通信完成后原先填写的表单数据。
具体实现
- webviewH5 --> miniProgram
通过js调用微信JSSDK的wx.miniProgram.navigateTo方法跳转到小程序的指定页面并且把参数当做query带给小程序原生页面。
- miniProgram --> webviewH5
webview组件存在一个src属性, 用于展示指定的h5页面,通过操作当前src的hash值,当前组件上的页面是不会刷新的,webview的h5页面利用hashchange监听事件获取小程序传过来的值。
注意点:
- webview 中的 console 并不会再微信开发者工具的控制台打印,alert才可以,打开了微信开发者工具中的 webview 的调试工具,那么 console和 alert 都不会执行。(大坑,开发效率特别低,建议不使用webview开发调试工具,只通过alert调试)
- hash改变虽然当前组件上的页面是不会刷新的,但是js需要重新调用,也就是说useState会清空,form表单会清空。需要在跳转到小程序页面前保存当前的form所有的状态数据,可以保存在redux或者浏览器本地存储,在小程序跳转回h5的时候取出重新setState。
- h5页面在跳转到小程序的时候将当前页面的完整url传递至小程序,实测获取当前h5页面完整url传递至小程序,query并不会成功,需要单独处理。
具体难点代码层面(Taro+react)
- map页面具体实现
import Taro, { getCurrentPages, useRouter } from '@tarojs/taro';
const Map = () => {
const { params } = useRouter();
const { src, isParty, type, id } = useRouter().params; // 分别获取path以及所有的query
Taro.getLocation({
type: 'gcj02', //返回可以用于 Taro.openLocation的经纬度
success: ({ latitude, longitude }) => {
Taro.chooseLocation({
latitude,
longitude,
success: (res) => {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2]; // 上一个页面,也就是页面A
prevPage.setData({ ...res, src, isParty, type, id }); // 将信息set到上个页面也就是webview的Data中
},
fail: () => {},
complete: () => {
Taro.navigateBack(); // 确定取消按钮统一返回上个webview页面
},
});
},
});
};
export default Map;
- webview具体实现
import { useRouter, useDidShow, getCurrentPages } from '@tarojs/taro';
import { View, WebView } from '@tarojs/components';
import { connect } from 'react-redux';
import { useState } from 'react';
import { setRegionId } from '@/store/actions/index';
import { formatQuery } from '@/utils/formatQuery'; // 将对象转换为query模板 如:{a=1,b=2} => '?a=1&b=2'
const mapStateToProps = (state) => ({
token: state.token,
regionId: state.regionInfo.regionId,
});
type WebviewProps = {
token: string;
regionId: number;
setRegionId: Function;
};
const mapDispatchToProps = (dispatch: (params: any) => void) => ({
setRegionId: (regionId: number) => {
dispatch(setRegionId(regionId));
},
});
const Webview = (props: WebviewProps) => {
const router = useRouter();
const [src, setSrc] = useState('');
// 添加用户token和regionId
const getSrc = (src: string) => {
const url = `${src}pages/index/index?token=${props.token}®ionId=${props.regionId}`;
updataUrl(url);
};
const updataUrl = (url: string) => {
setSrc(url);
};
const handleMessage = (res) => {
// 目前只处理regionId,保持webview的地域选择和小程序一致
const messageArr = res.detail.data;
const lastMessage = messageArr[messageArr.length - 1];
props.setRegionId(lastMessage.regionId);
};
useDidShow(() => {
const pages = getCurrentPages();
let currPage = pages[pages.length - 1]; // 获取当前页面
const { address, errMsg, latitude, longitude, name, src, isParty, type, id } = currPage.data;
if (errMsg === 'chooseLocation:ok') {
let newSrc = String(`${router.params.src}`); // 添加活动地址
newSrc += src.substring(1);
newSrc += `${formatQuery({ isParty, type, id })}`;
newSrc +=
'&__callback=1&addressData=' + JSON.stringify({ address, latitude, longitude, name });
newSrc += '&' + Math.random(); // 当需要多次通信时,避免src没有变化导致hashchange失效
updataUrl(encodeURI(newSrc));
} else {
getSrc(router.params.src as string); // 初始化的操作
}
});
return (
<View>
<WebView src={src} onMessage={handleMessage} />
</View>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(Webview);
总结
总的来说思路清晰实现不难,注意一些细节以及一些小坑,实现起来还是比较容易的。