用按需缩放的方式, 实现一个通用且高效的前端适配方案.

1,517 阅读6分钟

前言

又来摸鱼了, 适配这个问题确实有点老生常谈了, 这里主要分享下我开发中常用的两种适配方式. 也算回馈下社区. 新手可学习, 老手随缘~

这边文章主要介绍如何高效率完成适配部分的代码编写, 追求的是效率, 看情况使用~

文末另付 vue 版本实现一份, 随缘使用~

常规的适配方案分析

通常来说, 适配的手段是绕不过这些的 (也有其他方式, 这里只简单介绍). 我印象里好像jQuery时代还有个适配方式, 忘了具体是啥了.

方案实现内容就不细说了, 分析下采用这些方案的常规操作和优缺点 (有一定的个人偏见, 随缘采纳吧~)

  • viewport (vw/vh) 方案
    • 一般用 vw/vh 需要借助设计工具(如: 蓝湖, figma)的转换功能, 单靠前端自己转(要疯~), 主要突出就一点, 还原度好, 但是开发(主要是调样式)麻烦.
  • rem/em 方案
    • 基本同上, 尤其是使用 px2rem 工具的时候, 以及 1px 问题无法解决.
  • 媒体查询 + 通用样式
    • 完美的网站适配方案, 还原度, 兼容性啥啥都好, 就一点 写起来麻烦, 需要花点功夫.
    • 另外, 本质上来说 媒体查询是为每一个不同的设备重新实现一套样式. 此中花费的时间一言难尽 (time = n * 0.5, t: 开发1份的时间, n: 适配的场景)
  • 缩放 (zoom/scale)
    • 这篇文章主要介绍下这个
    • 效率高, 还原度高, 能解决一些适配的问题(如: 1px 边框)
    • 适用场景: 大屏、官网等
    • 单纯用缩放其实坑很多的, 比如定位偏移、滚动条错位等. 而且, 适用场景上其实相较于上面是有些局限性的,但架不住他写的快啊~

详细介绍下为什么用缩放以及怎么用的问题

简介

借助css提供的缩放能力(zoom / scale), 对原始的网页进行缩放, 适应当前屏幕的宽度达到缩放效果.

在这个基础上, 再抽象成组件, 针对指定的block进行缩放, 增强一下适用场景. (毕竟一些场景下, 网页的某些部分是需要保持原始的尺寸或单独写适配的)

最终, 得出了一个支持按需使用的自动缩放组件 (见文末)

主要有几个特点,

  • 开发效率高, 如果有原有代码, 改动范围比较小, 调试也方便. 毕竟不管屏幕尺寸如何, 始终保持与设计稿相同的比例. (再也不用担心UI走查了, 明确告诉他们写的跟他们设计稿的像素是一毛一样的, 有证据)
  • 支持按需使用, 针对需要适配的部分组件使用即可.
  • 牺牲了一定的用户体验 (比如: 用户没法再放大、缩小窗口。但也影响不大, 毕竟只有追求效率才会用这个方案, 要不早用 media 一个一个扣了)

为啥说这种方式效率高

我想应该没有别的方式能比无脑贴样式开发网页来的快吧(低代码, 设计稿转的那种除外), 另外, 就是调试方面, chrome devTool 里面的样式表跟你的原始样式是一样的, 不会像 vw / rem 这些需要找计算后的属性.

怎么用

Tips: 其实还有一种方式是, 直接修改 meta 标签进行缩放操作. 但这种方式, resize 需要刷新窗口才能重渲染, 暂且不提.

建议用我封装的 <AutoZoom> 组件, 主要是用缩放有一些坑, 直接贴代码了


<template>
    <AutoZoom :design-width="isPortrait ? 360:920">
           <your component />
           ...
    </AutoZoom>
</template>

坑在哪

  1. zoom chrome 支持, firefox 不支持
  2. zoom 会导致 canvas 焦点错位, 主要表现在 echarts 图表类canvas组件光标焦点错误.
  3. translate:scale 在firefox上会产生额外的滚动条 (我搞的组件里面捣鼓了下, 但不确定别人用会不会有问题, 用的时候注意下吧)

适用场景

大屏、官网等强UI还原度要求, 且弱交互场景.

就先写到这吧, 累了 .

VUE 实现 (vue3.x 按需自取)

前面废话了太多, 直接看代码吧

<script lang="ts" setup>
/**
 * 自适应缩放视图容器
 *
 * @description 通过缩放方式处理不同设备的适配问题, 不同于rem方案, 这个方案使用 px 像素填充样式, 相对来说更加高效.
 * @support 适用于 pc 端少交互类的看板应用, 适配条件遵循宽度优先原则, 保证与设计稿的宽度一致.
 *
 * @author libin<libin@persagy.com>
 * @date 2023年10月27日 11:14:30
 */
import { ComputedRef, Ref, computed, onMounted, provide, ref } from 'vue'
import { useResizeObserver } from '@vueuse/core'

const props = defineProps<{
    /**
     * 设计稿宽度
     *
     * @default 1920 (pc)
     * @default 750 (mobile, 2倍图)
     */
    designWidth?: number

    /**
     * 最小缩放倍数
     *
     * @default 0.2
     */
    minScaleSize?: number

    /** 容器左右间距 (计算 AutoZoom 组件 未铺满页面x轴的情况) */
    margin?: number
    /**
     * 容器顶部其他元素占用高度 (用来处理纵向滚动条过长问题)
     */
    padding?: number
}>()

/** zoom 的缩放性能更好, 能用zoom就用zoom */
const zoomSupport: ComputedRef<boolean> = computed(() => {
    const testEl = document.createElement('div')
    const support: boolean = 'zoom' in testEl.style
    return support
})

/** 初始属性 */
const initialValue: ComputedRef<{ designWidth: number; minScaleSize: number }> = computed(() => {
    const inMobile: boolean = /mobile/.test(navigator.userAgent.toLowerCase())
    const defaultDesignWidth: number = inMobile ? 750 : 1920
    return {
        designWidth: props.designWidth ?? defaultDesignWidth,
        minScaleSize: props.minScaleSize ?? 0.2
    }
})

/** 缩放率 */
const scale: Ref<number> = ref(1)
/** 容器实际渲染高度 */
const height: Ref<number> = ref(0)
/** 容器高度 */
const containerHeight: ComputedRef<string> = computed(() => {
    return height.value ? height.value + 'px' : '100%'
})
/** 滚动区间高度 */
const scrollHeight: ComputedRef<string> = computed(() => {
    return height.value ? height.value / scale.value + 'px' : 'auto'
})

/** 监听到浏览器尺寸变化时, 实时调整缩放倍率 */
const resize = (): void => {
    console.log('重计算缩放率')
    // @ 当前浏览器宽高 (如果距标准设计稿差2px以内, 使用设计稿像素)
    const innerWidth: number =
        Math.abs(window.innerWidth - initialValue.value.designWidth) < 2
            ? initialValue.value.designWidth
            : window.innerWidth
    // > 计算缩放比例
    const scaleValue: number = (innerWidth - (props.margin ?? 0)) / initialValue.value.designWidth
    // > 赋值
    scale.value = scaleValue > initialValue.value.minScaleSize ? scaleValue : initialValue.value.minScaleSize
    // > 计算容器高度
    height.value = window.innerHeight - (props.padding ?? 0)
}
const el: Ref<HTMLElement | null> = ref(null)
/** 挂载监听事件 & 触发初次重绘 */
onMounted((): void => resize())
useResizeObserver([el, document.body], (): void => resize())

/** 暴露 patchResize 主动刷新缩放率方法 */
provide('patchResize', resize)
</script>

<template>
    <!-- 自适应容器 -->
    <div
        :class="zoomSupport ? 'auto-zoom' : 'auto-scale'"
        :style="{
            '--design-width--': initialValue.designWidth + 'px',
            '--scale--': scale,
            height: containerHeight
        }"
        @resize="resize"
    >
        <div class="scroll" :style="{ height: scrollHeight }">
            <slot />
        </div>
    </div>
</template>

<style lang="less" scoped>
.auto-zoom {
    width: var(--webkit-fill-available, 100%);
    flex-shrink: 1;
    overflow: hidden;
    :deep(*, :after, :before) {
        box-sizing: content-box;
    }
    .scroll {
        width: var(--design-width--, 1920px);
        height: auto;
        zoom: var(--scale--, 1);
        transform-origin: left top;
        overflow-y: auto;
    }

    :deep(canvas) {
        zoom: calc(1 / var(--scale--, 1)) !important;
        transform: scale(var(--scale--, 1)) !important;
        transform-origin: left top;
    }
}
.auto-scale {
    width: var(--webkit-fill-available, 100%);
    flex-shrink: 1;
    overflow: hidden;
    :deep(*, :after, :before) {
        box-sizing: content-box;
    }
    .scroll {
        width: var(--design-width--, 1920px);
        height: auto;
        transform: scale(var(--scale--, 1));
        transform-origin: left top;
        overflow-y: auto;
    }
}
</style>