Skip to content
霞露小伙 — HfWang
On this page

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,客户端需要拦截并修改为app
  • user-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

javascript
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 高度 + 实时行情高度

javascript
// 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 滑动控制

javascript
// 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 页面滑动控制

javascript
// 获取 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

javascript
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;
    }

}

客户端协议

javascript
/**
 * @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 的方式可以解决这个问题

代码修改如下:

javascript
if (isIos) {
  iframeHtmlEle.style.position = tabOfffsetY > headerHeight ? 'fixed' : 'static'
}

到这,ios 端的滚动优化也基本完成了

F10 完整代码

javascript
/*
 * @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 后面的数值,在去裁切按位比较即可

代码比较简单就忽略不写

本站中引用到的其他资料,如有侵权,请联系本人删除