Loading...
墨滴

莽夫xiaohuoni

2021/05/27  阅读:42  主题:默认主题

如何处理 h5 使用 iframe 嵌套页面,内外 viewport 不一致导致的缩放问题?

问题

相信不少的朋友都遇到这样的场景,在我们的系统中嵌入别人的页面,和在别人的系统中嵌入我们的页面。 这里的第一原则是,不管是我们嵌入别人还是别人嵌入我们,我们都只能修改我们自己的页面,而其他别人的页面,我们无权修改,毕竟会有“我们嵌入其他系统的时候,都是好好的啊”和“其他人嵌入我们的系统的时候,都是可以的。”

解法

这里需要分开讨论。

我们的页面嵌入其他系统

这里有一个 iframe 的特性需要事先知晓,当内外 viewport 不一致时,将会使用主页面的 viewport。

比如,在 alita 项目中,我们使用的是现在比较稳定的高清方案。 在项目中使用rem单位,通过获取设备的 dpr 即 window.devicePixelRatio,将项目中的 rem 值设置成真实的 px 值。

doc.documentElement.style.fontSize = 100 / 2 * dpr  + 'px';

然后通过设置 viewport 的 initial-scale 来进行缩放页面达到高清显示页面的效果。

比如 iphone6 中 dpr 是2,我们就会将 px 设置成 @2x,然后通过设置 initial-scale=0.5,来实现页面的适配。

通常出现内外 viewport 不一致的情况,是主系统没有使用类似的高清方案,viewport 的 initial-scale 一般是设置为 1。其实就算设置的是其他的值也没有关系,我们都可以通过在代码中获取到主页面真实的 viewport。

if (window != top ) {
    const meta = document.querySelector('meta[name="viewport"]');
    const metaStr = meta.getAttribute('content') || '';
    const viewport = getViewPort(metaStr);
   if (viewport['initial-scale']) {
          const dpr = window.devicePixelRatio || 1;
          const baseScale = 10 / dpr;
          window.alitaFontScale = baseScale / parseInt(`${parseFloat(viewport['initial-scale']) * 10}`10);
    }
}

先判断一下,我们的页面是在 iframe 中打开,然后用 js 的方法获取到当前页面的 viewport。这里需要注意我们前面提到的“当内外 viewport 不一致时,将会使用主页面的 viewport”。所以在这里获取到的是主页面的 viewport。

(这里的几个类型转换,纯属是我个人的强迫症,其实没有什么意义。)

到此为止,我们就获得了我们也需要的缩放比例,将这个值放入我们最开始的 base fontsize 的计算中,就可以处理缩放问题了。

doc.documentElement.style.fontSize = 100 / 2 * dpr * window.alitaFontScale  + 'px';

其他的页面嵌入我们的页面中。

理解了上面的逻辑,那要处理这个问题就比较容易了。如果其他页面也使用类似的高清方案或者其他适配方案,理论上他们会处理这样的异常情况。在我们实际交互场景中,我们只遇到子系统 initial-scale=1 的情况,因此我这里也仅仅针对这个情况来解决问题。

(这也仅仅是我个人的设计理念问题,做的少就是做的多。)

比如在iphone 6 上,我们使用的是 @2x 的 px 值,但是子系统用的是 @1x 。因此我们只需要将 iframe 的大小设置成我们需要的 1 半,然后通过 transform scale 放大一倍就可以解决。

const Iframe: FC<IframeProps> = (props) => {
  const stt = {
    border'none',
    width`50vw`,
    height`50vh`,
    transformOrigin'0 0',
    transform`scale(2)`,
  };
  return <iframe style={stt} src={url}></iframe>;
};

套娃模式

在有其他系统嵌入在我们的系统的情况下,我们的系统嵌入到其他系统中。(这里有点绕,反正就是嵌套里面又嵌套) 不知道你有没有留意到,在前面的实现中我们使用了一个全局变量,window.alitaFontScale。 可以通过它来判断是否是套娃,和我们项目当前已经做出的修改情况。

import React from 'react';
import type { FC, IframeHTMLAttributes } from 'react';

interface AlitaIframeProps extends IframeHTMLAttributes<HTMLIFrameElement> {
  /**
   * 0~100
   */

  width?: number;

  /**
   * 0~100
   */

  height?: number;
}

const AlitaIframe: FC<AlitaIframeProps> = (props) => {
  const { width = 100, height = 100, style, ...rest } = props;
  (window as any).alitaFontScale = (window as any).alitaFontScale ?? 1;
  const defaultStyle = {
    border'none',
    ...style,
    width`${1 / (window as any).alitaFontScale * 50 * width / 100}vw`,
    height`${1 / (window as any).alitaFontScale * 50 * height / 100}vh`,
    transformOrigin'0 0',
    transform`scale(${(window as any).alitaFontScale / 0.5})`,
  };
  return <iframe style={defaultStyle} {...rest}></i
frame>;
}
;
export default AlitaIframe;

其他

本着 alita 体系的设计理念,框架中做的越多,实际交付开发中就写的越少。

我将第一种情况内收到了 hd 的插件中,适用于 alita 项目和其他使用了 @alitajs/hd 插件的 umi 项目。框架自己兼容,用户无需理会差异。

将第二种情况,发布到了 @alitajs/iframe 组件。在项目中可以直接引入使用,用户无需理会过多的逻辑。

$ yarn add @alitajs/iframe
or
$ npm i @alitajs/iframe
import React from 'react';
import Iframe  from '@alitajs/iframe';
const otherUrl = 'http://localhost:8000/#/';
export default () => <Iframe src={otherUrl} />;

总结

解法固然重要,更有趣的是过程。其实我们在实际开发中,都会遇到一些问题,或简单或复杂,或通用或局限,但是如果能够把问题梳理清楚,将代码保留在更高一个层级的位置,服务自己,服务他人,甚至能让整个团队受益。

想象一下,以后在 React 项目中,只要使用 Iframe 打开其他人的页面,都不会再遇到缩放问题了,是不是很帅。

当然这只是理想的情况,毕竟这是我昨天写的代码,我只能保证它在昨天能够正确运行。

莽夫xiaohuoni

2021/05/27  阅读:42  主题:默认主题

作者介绍

莽夫xiaohuoni