基于缩放方案的类大屏项目问题集

346 阅读5分钟

为什么是类大屏项目? 大屏项目适用吗?

  • 因为笔者做的项目,采用的大屏项目的缩放方案,但又不是纯粹的大屏项目,所以就叫类大屏项目
  • 文中提到的问题和解决方案,在大屏项目也适用

什么是基于缩放方案的大屏项目

通过css的transform:scale(x)方案实现自适应不同屏幕的大屏项目

问题集合和解决方案

不同scale方案实现, 以及特点

铺满: 视口比例与设计稿比例不一致时,宽高分别按照实际的缩放比,进行分别设置,从而达到强制缩放铺满的效果. 特点: 不会留白,但内容可能会因为缩放导致变形。

使用最小缩放比缩放: 视口比例与设计稿比例不一致时,使用宽高缩放比较小的一个进行统一缩放. 特点: 内容不会因为缩放导致变形,但会留白

使用最大缩放比缩放: 视口比例与设计稿比例不一致时,使用宽高缩放比较大的一个进行统一缩放. 特点: 内容不会因为缩放导致变形, 但会有滚动条

scale-view.ts

import { debounce } from 'lodash-unified'

/**
 * 缩放模式
 *
 * full: 视口比例与设计稿比例不一致时,强制拉伸. 特点: 不会留白,但内容可能会因为缩放导致变形
 * minScale: 视口比例与设计稿比例不一致时,使用宽高缩放比较小的一个. 特点: 内容不会因为缩放导致变形,但会留白
 * maxScale: 视口比例与设计稿比例不一致时,使用宽高缩放比较大的一个. 特点: 内容不会因为缩放导致变形, 但会有滚动条
 */
type ScaleMode = 'full' | 'minScale' | 'maxScale'
const autoScaleStyleDomId = 'auto-scale-style'
export interface ScaleViewOption {
  /**
   * 将哪个html的dom元素设置为缩放元素. 默认:body
   */
  el?: string | HTMLElement
  /**
   * body内容溢出时, 是否隐藏滚动条. 默认: true
   */
  bodyOverflowHidden?: boolean
  /**
   * el的定位方式. 默认: absolte
   */
  position?: 'absolute' | 'fixed'
  /**
   * 设计稿尺寸
   * 默认: width: 1920 height: 1080
   */
  designSize?: {
    /**
     * 设计稿宽度
     */
    width: number
    /**
     * 设计稿高度
     */
    height: number
  }
  /**
   * 缩放模式. 默认: full
   */
  scaleMode?: ScaleMode
}
export class ScaleView {
  private el: HTMLElement = document.body
  private bodyOverflowHidden = true
  private designSize = { width: 1920, height: 1080 }
  private scaleMode: ScaleMode = 'full'
  private position: 'absolute' | 'fixed' = 'absolute'

  constructor(option: ScaleViewOption = {}) {
    const {
      el,
      bodyOverflowHidden = true,
      designSize = { width: 1920, height: 1080 },
      scaleMode = 'full',
      position = 'absolute'
    } = option
    this.bodyOverflowHidden = bodyOverflowHidden
    if (typeof el === 'string') {
      const dom = document.querySelector<HTMLElement>(el)
      if (!dom) {
        throw `通过 【${el}】 未匹配到dom元素`
      }
      this.el = dom
    } else if (el) {
      this.el = el
    }
    this.position = position
    this.designSize = designSize
    this.scaleMode = scaleMode
    this.init()
  }
  destory() {
    window.removeEventListener('resize', this.updateScale)
    this.el.style.width = ''
    this.el.style.height = ''
    this.el.style.left = ''
    this.el.style.top = ''
    this.el.style.position = ''
    this.el.style.transform = ''
    this.el.style.transformOrigin = ''
  }
  private getOrCreateStyleDom(id: string) {
    let styleDom = document.getElementById(id)
    if (!styleDom) {
      styleDom = document.createElement('style')
      styleDom.lang = 'text/css'
      styleDom.id = id
      document.body.appendChild(styleDom)
    }
    return styleDom
  }
  private init() {
    // 必须设置body的overflow = 'hidden',否则,body会因为临时的窗口大小变化而出现滚动条(最终实际不应该出现,但中间过程出现了),导致后续document.documentElement.clientWidth,document.documentElement.clientHeight不包含滚动条的大小
    if (this.bodyOverflowHidden) {
      document.body.style.overflow = 'hidden'
    }
    const dynScaleDomWrapperDom = document.createElement('div')
    dynScaleDomWrapperDom.id = 'dynRevertScaleDomWrapperDom'
    dynScaleDomWrapperDom.classList.add('revertAutoScaleView')
    this.el.appendChild(dynScaleDomWrapperDom)
    // 将元素的大小定义为设计稿的大小
    this.el.style.height = `${this.designSize.height}px`
    this.el.style.width = `${this.designSize.width}px`
    // 将transfrom变化的源点设置为左上顶点(默认为:中心点)
    this.el.style.transformOrigin = '0 0'
    this.el.style.position = this.position
    // this.el.style.overflow = 'hidden'

    this.updateScale()
    const updateScaleDebounce = debounce(this.updateScale, 300, { trailing: true })
    window.addEventListener('resize', updateScaleDebounce.bind(this))
  }
  private updateScale() {
    const autoScaleStyleDom = this.getOrCreateStyleDom(autoScaleStyleDomId)
    const { widthScale, heightScale, minScale, maxScale } = this.computeScale()
    let transformScale = ''
    let revertTransformScale = ''
    if (this.scaleMode === 'full') {
      // 为了使内容充满视口, 需要分别对宽高进行缩放
      transformScale = `scale(${widthScale},${heightScale})`
      this.el.style.transform = transformScale
      revertTransformScale = `scale(${1 / widthScale},${1 / heightScale})`
      // 因为是拉伸铺满模式,所以将元素的起始位置设置为视口的左上顶点
      this.el.style.left = '0'
      this.el.style.top = '0'
    } else if (this.scaleMode === 'minScale') {
      // 因为是等比缩放模式,所以将元素的位置设置为上下都居中的位置
      this.el.style.left = '50%'
      this.el.style.top = '50%'
      // 等比缩放则只要使用宽度缩放比和高度缩放比中较小的那个设置为宽高的公共缩放比即可,translate3d(-50%, -50%, 0) 用于实现元素居中
      transformScale = `scale(${minScale})`
      this.el.style.transform = `${transformScale} translate3d(-50%, -50%, 0)`
      revertTransformScale = `scale(${1 / minScale})`
    } else if (this.scaleMode === 'maxScale') {
      transformScale = `scale(${maxScale})`
      this.el.style.transform = `${transformScale}`
      revertTransformScale = `scale(${1 / maxScale})`
    }
    autoScaleStyleDom.innerHTML = `.autoScaleView{transform:${transformScale};translateZ(0);will-change: transform;backface-visibility: hidden;} .revertAutoScaleView{transform:${revertTransformScale};translateZ(0);will-change: transform;backface-visibility: hidden;}`
  }
  computeScale() {
    const { width: viewPortWidth, height: viewPortHeight } = this.getViewPortSize()
    // 计算宽度缩放比
    const widthScale = this.computedScale(viewPortWidth, this.designSize.width)
    // 计算高度缩放比
    const heightScale = this.computedScale(viewPortHeight, this.designSize.height)
    // 如果是等比缩放,则需要使用最小的缩放比,避免内容被拉伸
    const minScale = Math.min(widthScale, heightScale)
    const maxScale = Math.max(widthScale, heightScale)
    return { widthScale, heightScale, minScale, maxScale }
  }
  private computedScale(realSize: number, designSize: number) {
    return realSize / designSize
  }
  private getViewPortSize() {
    // 获取浏览器视口宽/高
    const width = document.documentElement.clientWidth
    const height = document.documentElement.clientHeight
    console.log('视口大小', width, height)
    return { width, height }
  }
}

scale方案导致的, antdv的a-dropdown未被缩放问题

原因: a-dropdown之类的组件的下拉菜单,默认是在body节点之下,可能部分同学的scale并不是加在了body节点,所以a-dropdown之类的下拉菜单并未被缩放

解决方案: 一般这类a-dropdown之类的组件,支持单独对下拉菜单的dom节点设置class,通过这个class对下拉菜单的dom节点设置相同的缩放比即可

可能细心的同学会有疑问:

既然是因为a-dropdown的下拉菜单dom不在,缩放dom元素导致的未被缩放,为什么不将该元素移到缩放元素之下呢?

这是个好问题,在下个问题中解答

scale方案导致的, antdv的a-dropdown组件错位问题

原因: scale()改变了元素的坐标系统,使得原本基于父元素或视口的定位计算不再准确。

解决方案:a-dropdown组件的下拉元素创建在,缩放元素之外,此时可以解决定位不准确问题。但会导致a-dropdown组件的下拉元素未被缩放,此时再使用上个问题的解决方案,解决a-dropdown未被缩放问题

其他解决方案: 在缩放元素下创建一个反缩放元素,将a-dropdown组件的下拉元素创建在这个反缩放元素中,也能解决该问题,但反缩放,是将元素又恢复到未被缩放前的大小,所以定位问题解决了,缩放问题又出现了,因此非首选解决方案

什么是反缩放元素?

假设你原本的缩放是0.8, 那么反缩放比的计算就是: 1 / 0.8 = 1.25, 将一个dom的缩放比设置为1.25, 就会恢复原本的大小

scale方案导致的, antdv的a-dropdown组件错位问题解决后,发现a-dropdown的下拉菜单总是停顿了一下才进行缩放

原因: 可能是由于antdv的a-dropdown组件的动效是基于transfrom实现的,而scale的变化也是transfrom, 所以,scale也使用了antdv的transfrom动效, 所以感觉停顿了一下才进行缩放

解决方案: 停用a-dropdown的下拉动效

<a-dropdown :transitionName="''">
</a-dropdown>

transitionName设置为空,会清除a-dropdown的动效className(从而达到清除动效的目的)