在ASCF元服务开发中,开发者常需要通过web-view组件内嵌外部H5页面以复用现有业务逻辑(如表单录入、问卷调查)。
在此场景下,普遍存在以下交互诉求:
在ASCF框架下,web-view的通信机制与ArkTS环境有显著差异,需特别注意以下三点:
在ASCF页面(如首页)获取所需参数,通过路由将其传递给承载web-view的容器页。容器页接收参数并拼接到H5的URL中。
// 在URL后面拼接参数传给webview页面
// 使用encodeURIComponent防止中文乱码或特殊字符导致解析错误
const encodedType = encodeURIComponent(this.taskType || '默认');
has.navigateTo({
url: `/pages/index/webview?type=${encodedType}`,
success: () => console.info('跳转webview容器页成功')
}); props: ['type'], // 显式声明以接收路由参数
data() {
return {
// 基础URL
// 赋一个合法初始值防止web-view瞬间白屏
dynamicUrl: '此处输入html的地址'
}
},
onShow() {
console.info('[webview容器onShow触发]');
// 1.获取参数
let rawType = this.type || (this.$page && this.$page.query && this.$page.query.type) || '通用';
// 2.对拿到的参数进行解码
// 因为首页用了encodeURIComponent,这里先解码还原成中文
try {
rawType = decodeURIComponent(rawType);
} catch (e) {
console.error('解码失败:', e);
}
console.info('容器页成功提取参数: ', rawType);
// 3.拼接新的带参数的URL(拼接到URL里时,为了安全再次编码)
const targetUrl = `${this.baseUrl}?taskType=${encodeURIComponent(rawType)}`;
// 4.如果URL变了,才重新赋值,防止web-view陷入无限刷新死循环
if (this.dynamicUrl !== targetUrl) {
this.dynamicUrl = targetUrl;
console.info('动态更新H5地址为: ', this.dynamicUrl);
}
}, // 页面加载时解析 URL 参数
window.onload = function () {
// 使用浏览器的 URLSearchParams API
const urlParams = new URLSearchParams(window.location.search);
const taskType = urlParams.get('taskType');
if (taskType) {
// 如果拿到了参数,动态修改 DOM
// decodeURIComponent 会自动将 URL 编码转换回中文
const decodedType = decodeURIComponent(taskType);
document.getElementById('dynamicTitle').innerText = ` ${decodedType}信息录入`;
console.log("成功接收ASCF端传值: " + decodedType);
}
}; H5侧需引入UMD格式的SDK,利用has.ascfweb.postMessage将数据推入队列,并立即调用has.ascfweb.navigateBack触发ASCF端结算。
function submitAndBack() {
if (typeof has === 'undefined' || !has.ascfweb) {
return alert("环境未就绪,请在元服务中打开");
}
// 1. 获取输入框的值
const nameValue = document.getElementById('userName').value;
const dateValue = document.getElementById('bookDate').value;
// 2. 构造数据 Payload
const payload = {
action: 'form_submit',
data: {
name: nameValue || '未填写姓名',
date: dateValue || '未选择日期'
}
};
// 3. 存入消息队列
has.ascfweb.postMessage({ data: payload });
// 4. 延迟触发返回(保证消息入队,并销毁当前容器触发宿主结算)
setTimeout(() => {
has.ascfweb.navigateBack();
}, 300);
} 容器页在@message中拦截数据,借助has.setStorageSync写入本地缓存,最终在首页的onShow中读取。
handleH5Message(e) {
const messageQueue = e.detail.data;
if (messageQueue && messageQueue.length > 0) {
const lastMessage = messageQueue[messageQueue.length - 1];
if (lastMessage.action === 'form_submit') {
console.info('接收到 H5 数据: ' + JSON.stringify(lastMessage.data));
try {
has.setStorageSync('h5_form_result', JSON.stringify(lastMessage.data));
console.info('数据已成功写入本地缓存');
} catch (err) {
console.error('数据写入缓存失败: ', err);
}
}
}
} const res = has.getStorageSync('h5_form_result');
// 确保 res 存在且不为空字符串
if (res && res !== '') {
console.info('原始缓存读取内容:', res);
// 防止底层双重 stringify
let resultData = res;
if (typeof resultData === 'string') {
resultData = JSON.parse(resultData);
}
// 如果解析一次后还是字符串(说明被双重转义了),再解一次
if (typeof resultData === 'string') {
resultData = JSON.parse(resultData);
}
console.info('最终解析到的对象:', JSON.stringify(resultData));
if (resultData && resultData.name) {
// 赋值给 Vue 变量
this.displayName = resultData.name;
this.displayDate = resultData.date;
console.info(`成功赋值 -> 姓名: ${this.displayName}, 日期: ${this.displayDate}`);
// 防止 ASCF 在部分机型上丢失响应式
if (typeof this.$forceUpdate === 'function') {
this.$forceUpdate();
}
// 等页面彻底渲染稳定后,再清理缓存,避免 onShow 连击导致的错乱
setTimeout(() => {
has.removeStorageSync('h5_form_result');
console.info('缓存已安全清理');
}, 1000);
}
} else {
console.debug('暂无回传数据 (缓存为空)');
} pages/index/index.vue:
<template>
<div class="container">
<text class="page-title">数据展示</text>
<div class="input-card">
<text class="label">请输入要办理的业务类型:</text>
<input type="text" class="input-box" placeholder="例如:会议、体检" :value="taskType" @input="onTypeChange" />
</div>
<div class="data-card">
<text class="label">收到 H5 传回的数据:</text>
<div class="info-group">
<div class="info-row">
<text class="info-text">姓名: {{ displayName || '读取失败或为空' }}</text>
</div>
<div class="info-row">
<text class="info-text">日期: {{ displayDate || '读取失败或为空' }}</text>
</div>
</div>
</div>
<div class="btn" @click="goToH5">
<text class="btn-text">去 H5 填写信息</text>
</div>
</div>
</template>
<script>
export default {
data() {
return {
displayName: '暂无数据',
displayDate: '暂无数据',
taskType: '' // 默认值
}
},
onShow() {
console.info('--- onShow 触发 ---');
this.checkH5Data();
},
methods: {
checkH5Data() {
try {
// Start solution2
const res = has.getStorageSync('h5_form_result');
// 确保 res 存在且不为空字符串
if (res && res !== '') {
console.info('原始缓存读取内容:', res);
// 防止底层双重 stringify
let resultData = res;
if (typeof resultData === 'string') {
resultData = JSON.parse(resultData);
}
// 如果解析一次后还是字符串(说明被双重转义了),再解一次
if (typeof resultData === 'string') {
resultData = JSON.parse(resultData);
}
console.info('最终解析到的对象:', JSON.stringify(resultData));
if (resultData && resultData.name) {
// 赋值给 Vue 变量
this.displayName = resultData.name;
this.displayDate = resultData.date;
console.info(`成功赋值 -> 姓名: ${this.displayName}, 日期: ${this.displayDate}`);
// 防止 ASCF 在部分机型上丢失响应式
if (typeof this.$forceUpdate === 'function') {
this.$forceUpdate();
}
// 等页面彻底渲染稳定后,再清理缓存,避免 onShow 连击导致的错乱
setTimeout(() => {
has.removeStorageSync('h5_form_result');
console.info('缓存已安全清理');
}, 1000);
}
} else {
console.debug('暂无回传数据 (缓存为空)');
}
// End solution2
} catch (err) {
console.error('缓存读取或解析异常:', err);
}
},
// 监听输入框内容变化
onTypeChange(e) {
this.taskType = e.value || e.detail.value || '';
console.info('正在输入,当前变量已被实时更新为:', this.taskType);
},
goToH5() {
// 每次进去前,主动清空一下上次的残留
has.removeStorageSync('h5_form_result');
this.displayName = '暂无数据';
this.displayDate = '暂无数据';
// Start solution1
// 在 URL 后面拼接参数传给 webview 页面
// 使用 encodeURIComponent 防止中文乱码或特殊字符导致解析错误
const encodedType = encodeURIComponent(this.taskType || '默认');
has.navigateTo({
url: `/pages/index/webview?type=${encodedType}`,
success: () => console.info('跳转 webview 容器页成功')
});
// End solution1
}
}
}
</script>
<style>
.container {
flex-direction: column;
align-items: center;
padding: 20px;
background-color: #fff;
height: 100%;
}
.page-title {
font-size: 22px;
font-weight: bold;
margin-bottom: 30px;
margin-top: 20px;
}
.data-card {
flex-direction: column;
align-items: flex-start;
width: 100%;
background-color: #fff;
padding: 20px;
border-radius: 12px;
margin-bottom: 40px;
}
.label {
font-size: 14px;
color: #888;
}
.info-group {
flex-direction: column;
margin-top: 15px;
width: 100%;
}
.info-row {
width: 100%;
margin-bottom: 10px;
}
.info-text {
font-size: 18px;
color: #333;
font-weight: bold;
}
.btn {
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
background-color: #007DFF;
border-radius: 8px;
justify-content: center;
align-items: center;
}
.btn-text {
color: #fff;
font-size: 16px;
font-weight: bold;
}
.input-card {
width: 100%;
background-color: #fff;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
flex-direction: column;
align-items: flex-start;
}
.input-box {
width: 150px;
height: 45px;
border: 1px solid #ccc;
border-radius: 6px;
padding: 0 10px;
margin-top: 10px;
font-size: 16px;
}
</style> pages/index/webview.vue:
<template>
<div class="container">
<web-view class="web-content" :src="dynamicUrl" @message="handleH5Message">
</web-view>
</div>
</template>
<script>
export default {
// Start solution1
props: ['type'], // 显式声明以接收路由参数
data() {
return {
// 基础 URL
// 赋一个合法初始值防止 web-view 瞬间白屏
dynamicUrl: '此处输入html的地址'
}
},
onShow() {
console.info('--- webview 容器 onShow 触发 ---');
// 1. 获取参数
let rawType = this.type || (this.$page && this.$page.query && this.$page.query.type) || '通用';
// 2. 对拿到的参数进行解码
// 因为首页用了 encodeURIComponent,这里先解码还原成中文
try {
rawType = decodeURIComponent(rawType);
} catch (e) {
console.error('解码失败:', e);
}
console.info('容器页成功提取参数: ', rawType);
// 3. 拼接新的带参数的 URL (拼接到 URL 里时,为了安全再次编码)
const targetUrl = `${this.baseUrl}?taskType=${encodeURIComponent(rawType)}`;
// 4. 如果 URL 变了,才重新赋值,防止 web-view 陷入无限刷新死循环
if (this.dynamicUrl !== targetUrl) {
this.dynamicUrl = targetUrl;
console.info('动态更新 H5 地址为: ', this.dynamicUrl);
}
},
// End solution1
methods: {
// Start solution2
handleH5Message(e) {
const messageQueue = e.detail.data;
if (messageQueue && messageQueue.length > 0) {
const lastMessage = messageQueue[messageQueue.length - 1];
if (lastMessage.action === 'form_submit') {
console.info('接收到 H5 数据: ' + JSON.stringify(lastMessage.data));
try {
has.setStorageSync('h5_form_result', JSON.stringify(lastMessage.data));
console.info('数据已成功写入本地缓存');
} catch (err) {
console.error('数据写入缓存失败: ', err);
}
}
}
}
// End solution2
}
}
</script>
<style>
.container {
width: 100%;
height: 100%;
}
.web-content {
flex: 1;
width: 100%;
}
</style> index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>H5 信息录入</title>
<script src="node_modules/@atomicservice/ascf-web-sdk/dist/ascf-web-sdk.umd.js"></script>
<style>
body {
font-family: sans-serif;
padding: 20px;
background-color: #fff;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #333;
font-size: 14px;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
box-sizing: border-box;
}
.btn-submit {
width: 100%;
padding: 15px;
background-color: #007DFF;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
margin-top: 20px;
font-weight: bold;
}
</style>
</head>
<body>
<h3 id="dynamicTitle"> 请填写您的信息</h3>
<div class="form-group">
<label>姓名</label>
<input type="text" id="userName" placeholder="请输入姓名">
</div>
<div class="form-group">
<label>预约日期</label>
<input type="date" id="bookDate">
</div>
<button class="btn-submit" onclick="submitAndBack()">提交并返回首页</button>
<script>
// Start solution1
// 页面加载时解析 URL 参数
window.onload = function () {
// 使用浏览器的 URLSearchParams API
const urlParams = new URLSearchParams(window.location.search);
const taskType = urlParams.get('taskType');
if (taskType) {
// 如果拿到了参数,动态修改 DOM
// decodeURIComponent 会自动将 URL 编码转换回中文
const decodedType = decodeURIComponent(taskType);
document.getElementById('dynamicTitle').innerText = ` ${decodedType}信息录入`;
console.log("成功接收ASCF端传值: " + decodedType);
}
};
// End solution1
// Start solution2
function submitAndBack() {
if (typeof has === 'undefined' || !has.ascfweb) {
return alert("环境未就绪,请在元服务中打开");
}
// 1. 获取输入框的值
const nameValue = document.getElementById('userName').value;
const dateValue = document.getElementById('bookDate').value;
// 2. 构造数据 Payload
const payload = {
action: 'form_submit',
data: {
name: nameValue || '未填写姓名',
date: dateValue || '未选择日期'
}
};
// 3. 存入消息队列
has.ascfweb.postMessage({ data: payload });
// 4. 延迟触发返回(保证消息入队,并销毁当前容器触发宿主结算)
setTimeout(() => {
has.ascfweb.navigateBack();
}, 300);
}
// End solution2
</script>
</body>
</html> Q:H5侧调用has.ascfweb.postMessage后,ASCF端的@message无反应?
A:ASCF采用消息暂存机制。单纯调用postMessage不会触发实时回调,必须在H5侧紧接着调用has.ascfweb.navigateBack(),或者由用户物理点击返回键销毁当前H5页面时,暂存的消息队列才会一并结算并触发宿主的@message。
Q:从容器页返回首页时,遇到输入法软键盘异常弹起怎么办?
A:若首页触发跳转的按钮使用的是<input type="button">标签,底层在页面重新聚焦时会尝试恢复输入法状态。应将按钮标签替换为<div>或<text>,并绑定@click事件。
Q:H5页面如何获取底部导航栏高度以进行底部内容规避?
A:推荐通过URL参数传递方式,在ASCF页面加载web-view前调用has.getSystemInfoSync()获取系统信息,将tabBar高度拼接到web-view的src URL参数中,H5页面解析URL参数后设置底部元素的paddingBottom或bottom值。另外,应优先检查web-view所在页面的布局配置,确保web-view组件高度不超过页面安全区,此时H5页面使用height: 100%或100vh即可正确布局,无需手动规避。