k线页嵌入手炒 F10 页面开发复盘
背景
老板希望引入手炒 F10 页面到 app 中,用于提高 app 的 k 线页日活。由于 F10 功能手炒那边的项目已经更新迭代了 5 年多了,所以我们这边是希望直接使用他们的 H5 页面,而不是自己这边再开发一套类似功能的页面
最终实现效果如下:
实现过程
直接 iframe 嵌入
我们第一个阶段是打算直接使用,再预研阶段,我们直接再 k 线页使用 iframe 嵌入了 F10 的页面
但是在本地测试阶段就发现问题了
问题描述
- F10 的 url 上需要拼接手炒的市场代码,但是我们这边目前是小财神的市场代码
- F10 原本就是采用 iframe 嵌入到手炒里面的,后面功能迭代,废弃了这个方案,改为 webview 嵌入,但是代码中遗留了很多对 iframe 环境的判断代码,导致我们出现各种奇奇怪怪的 bug
- 跨域问题,导致很多接口请求失败,页面无法正常显示
- 如图,橙色框是手机屏幕大小,黄色框是 k 线 H5 页面,绿色是嵌进去的 iframe 页面,高度和手机屏幕高度一致。这时就会很容易发现,F10 页面的高度如果大于屏幕高度,那么在左侧的时候就会出现手指在屏幕下半部滑动,F10 内部发生滚动,但是整个k线页并没有滚动
新方案讨论
- 方案一:F10 不使用 iframe,使用客户端提供的新的 webview
成本巨大
,如下图,底部原本就有很多的功能点,如分享、历史分时、记账
等,都是使用 fixed 布局
固定在底部的
如果使用新的 webview 容器来放置,那么底部的这部分就必须单独在使用一个 webview 来包裹,那么整个k线页就被分割成 3个webview,一个顶部的k线图、中间的交易记录和F10、顶部的分享、历史分时、记账 3个webview,且需要安卓和 ios 两端同时支持,开发成本巨大
- 方案二:F10 改为本地化部署,解决跨域问题
本地化部署,成本低,可以解决跨域等问题
,但是由于 F10 项目本身内部遗留对父级容器时 iframe 时的部分代码并没有删除,导致有些功能还是无法正常使用。而且 F10 项目的功能迭代,即一些新的功能我们需要同步升级新的版本才能支持
在和客户端讨论后,决定先使用方案二看下效果,对于不支持的一些功能点提前和产品确认。针对功能迭代的延迟,和产品沟通后确认,除非 F10 又大的功能点的升级,否则我们这边没必要同步跟进版本升级
确认好开发方案后,H5 和客户端就可以正式开始开发了
客户端协议设配
协议设配主要涉及以下几点:
用户问题反馈
,F10 的用户问题回馈会调用手炒协议打开手炒问题反馈页,我们客户端这边需要做下拦截,即点击跳转的路径给修改为的问题反馈页面股票跳转拦截
,F10 内部有很多地方,点击后会调用手炒的客户端协议打开对应的股票页面,客户端这里也需要拦截并重定向到的 H5 k线页分享到第三方app
,分享链接显示的是手炒 app,客户端需要拦截并修改为appuser-agent 设配
,F10 对于不同的 ua 有不同的显示内容,ua 为手炒时显示完整的内容,否则只显示部分功能板块,所以针对 k线页,客户端需要修改 ua 为 手炒ua- 还有其他 F10 内部调用的比较常规的客户端协议,总计有20多个,都需要客户端设配
这部分主要是客户端的工作,设配难度有多大我也不清楚,只知道大概的改动点,所以这部分也就不细说了
F10 滚动优化
终于到我了,先列下 H5 需要优化的点:
- frame 容器高度确定
- touch事件和滚动属性修改时机
- ios 端滚动优化
绿色:nav-bar、橙色:tab-bar、紫色:bottom-bar
目标优化效果
- tab-bar 没有滑动到顶部,F10 内部禁止滚动,只有 tab-bar 到达顶部后,F10内部才能滚动
- F10 内部发生滚动,整个k线页禁止滚动,知道 F10 内部滚动到顶部后才能触发 k线页的滚动
iframe 容器高度确定
从最终效果图我们可以知道:
- iframe 高度 = 屏幕高度 - 顶部 navbar 高度 - 底部操作栏高度 - tab控制栏高度
- iframe宽度 = F10功能模块 tab 的宽度 || 屏幕宽度
不直接使用屏幕宽度是因为 k 线页支持分享到外部,如果使用 PC 浏览器打开的话,页面会固定宽度最外层盒子宽度为 440px
const diffy =
$('.nav-bar').height() + $('controll-bar').height() + $('.tab-bar').height()
const iframeHeight = window.outerHeight - diffy
const iframeWidth = $('.tab-bar').width() || window.outerWidth
touch 事件和滚动属性修改时机
这个是啥意思呢,如上图,当 tab-bar 滑动到顶部时,如果这时 F10 内部发生滚动了,再使用手指去滑动 tab-bar,就会出现 F10 内容还没有滑动到顶部,但是 F10 容器已经开始往下滑了
所以我们需要做个判断
- tab-bar 没有滑动到顶部,F10 禁止滑动
- tab-bar 滑动到顶部,禁止 tab-bar 的 touchmove 事件
滑动顶部的判断:html 滚动的距离 = k线图的高度 + navbar 高度 + 实时行情高度
// hqCharts.js
import { stopTabScrollEvent, getThsPageTabDom } from './utils'
class HqCharts extends Component {
componentDidMount() {
// ...
const tabsDom = getThsPageTabDom();
tabsDom?.addEventListener?.('touchmove', stopTabScrollEvent);
}
componentWillUnMount() {
// ...
const tabsDom = getThsPageTabDom();
tabsDom?.removeEventListener?.('touchmove', stopTabScrollEvent);
}
render() {
// ...
}
}
tab-bar 滑动控制
// utils.js
/** tab 滚动到顶部的时候禁止 tab 的滚动事件 */
export const stopTabScrollEvent = e => {
// html 的滚动距离
const htmlEleScrollTop = document.querySelector('html')?.scrollTop;
const contain_height = getDomHeight('.kline_render_contain');
const tab_height = getDomHeight(extendModuleTabSelector);
const navHeight = getDomHeight('.nav-bar');
// 最大滚动距离
const maxScrollTop = contain_height - tab_height - navHeight;
if (!htmlEleScrollTop || !maxScrollTop) {
return;
}
if (htmlEleScrollTop >= maxScrollTop) {
e?.preventDefault?.();
}
};
export const getThsPageTabDom = () => document.querySelector('.tab-bar')
F10 页面滑动控制
// 获取 iframe 内嵌 F10 页面的 html 元素
const iframeHtmlEle =
DOMS.F10Iframe()?.contentWindow?.document?.querySelector?.('html');
// 设置 F10 页面的高度,通过控制 overflow 的行为控制 F10 h5 是否能滚动
iframeHtmlEle.style.height = `${iframe_height}px`;
iframeHtmlEle.style.overflowY = tabOfffsetY > headerHeight ? 'hidden' : 'auto';
ios 滚动优化
经过上述的优化手段, 安卓端的滑动效果已经基本符合产品要求,但是在 ios 端发现了两个问题
- ios webview 的弹性效果
- 上述设置在 ios 端无效
我们项目中处理 IOS 弹性效果的方式有以下几种处理方法:
- 使用公司内部封装的 js 库 js-scroll_puncture
- 客户端通过 jsBright 提供协议用于开启或禁用
js-scroll_puncture
var nowScrollTop = 0//初始化
//禁止滚轮
function touchStart_js_scroll_puncture() {
event.preventDefault();
}
export function clickPop(type) {
var _type = type || "1"
switch (_type) {
case "1":
document.addEventListener(
'touchmove',
touchStart_js_scroll_puncture,
{ passive: false }
);
//适配pc滚动事件
document.addEventListener(
'wheel',
touchStart_js_scroll_puncture,
{ passive: false }
);
break;
case "2":
//将页面的scroolTop赋值给变量
nowScrollTop = document.body.scrollTop || document.documentElement.scrollTop
document.body.style.position = "fixed"
document.body.style.top = -nowScrollTop + 'px'
break;
case '3':
//仅禁止pc滚动事件
document.addEventListener(
'wheel',
touchStart_js_scroll_puncture,
{ passive: false }
);
break;
default:
break;
}
}
//激活滚轮
export function closePop(type) {
var _type = type || "1"
switch (_type) {
case "1":
document.removeEventListener(
'touchmove',
touchStart_js_scroll_puncture,
{ passive: false }
);
//适配pc滚动事件
document.removeEventListener(
'wheel',
touchStart_js_scroll_puncture,
{ passive: false }
);
break;
case "2":
document.body.style.position = "static"
document.body.scrollTop = document.documentElement.scrollTop = nowScrollTop
break;
case '3':
//仅禁止pc滚动事件
document.removeEventListener(
'wheel',
touchStart_js_scroll_puncture,
{ passive: false }
);
break;
default:
break;
}
}
客户端协议
/**
* @desc 开启关闭 webview 弹性滚动
* @param {0|1} data 0-关闭 1-开启
*/
export const setWebViewConfig = function (data) {
return new Promise(function (resolve) {
if (bridgeIns) {
bridgeIns.callHandler('setWebViewConfig', data);
} else {
initBridge().then(bridge => {
bridgeIns = bridge;
bridgeIns.callHandler('setWebViewConfig', data);
});
}
resolve('1');
});
}
这里我们k线页面时已经通过客户端协议关闭了这个弹窗滚动的效果,但是在 iframe 内部不生效
在真机尝试后,发现使用 fixed 布局即 js-scroll_puncture 库 type 等于 2 的方式可以解决这个问题
代码修改如下:
if (isIos) {
iframeHtmlEle.style.position = tabOfffsetY > headerHeight ? 'fixed' : 'static'
}
到这,ios 端的滚动优化也基本完成了
F10 完整代码
/*
* @Author: wanghaofeng
* @Date: 2023-10-12 13:43:53
* @LastEditors: wanghaofeng
*/
import { useMemo, useEffect, useLayoutEffect } from 'react';
import { useDebounceFn } from 'ahooks';
import { getPlatformUtil } from '../../../utils/utils';
const isIos = getPlatformUtil().versions.ios;
const getTzzbAppVersion = () => {
const u = navigator.userAgent;
const index = u.indexOf('Hexin_xcs');
const TEN = 10;
if (index > -1) {
return u.substr(index + TEN);
} else {
return '';
}
};
const DOMS = {
hdPageTabs: () => document.querySelector('.hd_page_tabs'),
navBarBlock: () => document.querySelector('.navBar-block'),
F10Iframe: () => document.querySelector('#F10_iframe'),
};
const getIframeHtmlEle = () =>
DOMS.F10Iframe()?.contentWindow?.document?.querySelector?.('html');
const getScrollInfo = () => {
const tabOfffsetY = parseInt(
DOMS.hdPageTabs()?.getBoundingClientRect?.()?.top || 0
);
const headerHeight = parseInt(
DOMS.navBarBlock()?.getBoundingClientRect?.()?.height || 0
);
return {
tabOfffsetY,
headerHeight,
};
};
const scrollDebounceTime = 200;
export default function F10Page(props) {
const { iframe_width, iframe_height, show, selectedStockItem } = props;
const { code = '', market = '' } = selectedStockItem;
const iframeUrl = useMemo(() => {
const query = 'query--a-a-a-a-a';
const url = 'F10-URL' + query;
return url;
}, [code, market]);
// iframe 滚动行为修改新增防抖处理,避免在到达临界值附近出现多次抖动
const { run: iframeScrollActionControl } = useDebounceFn(
() => {
const { tabOfffsetY, headerHeight } = getScrollInfo();
if (!tabOfffsetY || !headerHeight) {
return;
}
const iframeHtmlEle = getIframeHtmlEle();
if (iframeHtmlEle?.style) {
iframeHtmlEle.style.height = `${iframe_height}px`;
iframeHtmlEle.style.overflowY = tabOfffsetY > headerHeight
? 'hidden'
: 'auto';
if (isIos) {
iframeHtmlEle.style.position = tabOfffsetY > headerHeight
? 'fixed'
: 'static'
}
}
},
{
wait: scrollDebounceTime,
}
);
const iframeOneLoadAction = () => {
const iframeHtmlEle = getIframeHtmlEle();
if (iframeHtmlEle?.style) {
iframeHtmlEle.style.overflowY = 'hidden';
if (isIos) {
iframeHtmlEle.style.position = 'fixed';
}
}
};
useLayoutEffect(() => {
const iframeDom = DOMS.F10Iframe();
if (iframeDom) {
// iframe 加载时默认禁止滚动
iframeDom.onload = () => iframeOneLoadAction();
}
document?.addEventListener?.('scroll', iframeScrollActionControl);
return () => {
document?.removeEventListener?.('scroll', iframeScrollActionControl);
};
}, [code]);
return (
<iframe
style={{ display: show ? 'block' : 'none' }}
id='F10_iframe'
frameBorder={0}
src={iframeUrl}
width={iframe_width}
height={iframe_height}
/>
);
}
股票跳转支持
我们项目内部跳转的时候,为了避免错误,我们会在 url 上拼接上至少这几个参数
- code 股票代码
- market 股票市场代码
- name 股票名称
- from_page 从那个页面进入 k 线页
从前面客户端协议设配部分我们知道,客户端会拦截原本的跳转k线操作,给替换成跳转到的 k线页
但是有个问题就是,客户端只是做了个中转,无法保证 url 上携带了我们需要的这几个参数
实际测试发现有以下几种情况
- url 只有 code
- url 上有 code、market,但是没有 name
- url 的 market 时同花顺市场代码,不是小财神市场代码
- 板块、指数股票的支持(我们使用了两套 k 线组件,新版的只支持 A 股),需要判断使用那个组件
由于上述问题关前端无法解决,所以这里时通过服务端获取相关信息
但是这里目前还有个坑
就是针对只有 code 的情况,服务端这边查询时可能存在相同的股票代码的多只股票(市场不一样)
由于没有市场代码,服务端页无法判断具体时那一只股票,所以这里目前是去查到的第一只股票
客户端版本控制
由于 F10 需要用到很多的客户端协议,即 F10 的功能必须强依赖于客户端的版本
但是有的用户可能并不会使用最新版本的 app,但是加载的 k线 H5 页面却是最新的,这时就会出现异常
所以 H5 需要对 app 的版本做一个限制,只有大于某个版本的才支持显示 F10 模块,否则显示空的占位图即可
版本控制的思路
app 的 user-agent 上会凭借上类似这样的字段
- xxxxxxxxx-ios-version1.1.11-xxxxxxxxxxxx
- xxxxxxxxx-and-version1.1.11-xxxxxxxxxxxx
所以只要获取 ua 里的 version 后面的数值,在去裁切按位比较即可
代码比较简单就忽略不写