大屏适配实战-经验总结

近期在做大屏项目,走了很多弯路,踩了很多坑,这篇文章总结了我对自适应大屏项目的经验总结 文本代码: gitee.com/hhhsir/rech…

自适应需求: 尽可能兼容所有分辨率尺寸

技术栈

名称 文档地址 说明
渲染框架
react17 zh-hans.reactjs.org/
css
styled-components styled-components.com/docs/basics 基本样式使用less写,然后全局引入,具体切图样式用
styled-components
图表
recharts recharts.org/en-US/ 适用于react的图标库,支持svg语法,很灵活
echarts-for-react git.hust.cc/echarts-for… 全网开发者下载量最高的 ECharts 的 React 组件封装
工具
decimal.js www.npmjs.com/package/dec… js数字计算库,用来计算百分比
dayjs dayjs.gitee.io/zh-CN/ 时间处理
lodash-es www.lodashjs.com/docs/lodash… 主要用了lodash的get方法来取接口的返回数据

布局方案-grid

大屏一般是网格化的布局,非常适合使用grid布局

grid入门文章: www.ruanyifeng.com/blog/2019/0… CSS Grid 网格布局教程-阮一峰

不入门也行,照我的写法,简单易懂

封装一些通用的基础组件

import styled from "styled-components";

// 页面根容器
export const PageWrap = styled.div`
    width: 100%;
    height:100%;
`
// 头部容器
export const PageHeader = styled.div`
  width: 100%;
  height: 8%;
  // 
  @media (max-width: 600px) {
    height: 80px;
    
  }
`;
// main容器
export const PageMain = styled.div`
  width: 100%;
  height: 92%;
  padding:0 24px 24px 24px;
`;
// grid的area容器,因为子容器基本只需要grid-area这个属性,所以封装起来方便使用
export const PageArea = styled.div<{area:string}>`
  grid-area: ${(p)=>p.area};
   width: 100%;
   height:100%;
`
复制代码

grid容器

const GridDemoWrap = styled.div`
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-areas:
    "t1 t2 t3"
    "t4 t4 t4"
    "t7 t7 t8";
  // 这里直接写设计稿上的px
  grid-template-rows: 277fr 333fr 320fr;
  grid-template-columns: 529fr 529fr 814fr;
  grid-gap: 24px;
`;
复制代码

可以看到,上面这段 529fr 529fr 814fr是我最喜欢的部分,可以不计算百分比(我的设计稿是1920x1080)
最早用百分比布局,算的我怀疑人生

组装起来

export default function GridDemo() {
  return (
    <PageWrap>
      <PageHeader>头部啊</PageHeader>
      <PageMain>
        <GridDemoWrap>
          <PageArea area="t1">t1t1</PageArea>
          <PageArea area="t2">t2t2t2</PageArea>
          <PageArea area="t3">t3t3t3t3t3</PageArea>
          <PageArea area="t4">t4t4t4t4t4t4</PageArea>
          <PageArea area="t7">t7t7t7t7t7t7</PageArea>
          <PageArea area="t8">t8t8t8t8t8</PageArea>
        </GridDemoWrap>
      </PageMain>
    </PageWrap>
  );
}

复制代码

看下效果

grid容器.png

非常漂亮的网格化管理

grid适配移动端

需要给GridDemoWrap加上一段媒体查询

const GridDemoWrap = styled.div`
  width: 100%;
  height: 100%;

  display: grid;
  grid-template-areas:
    "t1 t2 t3"
    "t4 t4 t4"
    "t7 t7 t8";
  // 这里直接写设计稿上的px
  grid-template-rows: 277fr 333fr 320fr;
  grid-template-columns: 529fr 529fr 814fr;
  grid-gap: 24px;
  // 移动端(小于600px) 网格变成竖向block排列
  @media (max-width: 600px) {
    grid-template-areas:
      't1'
      't2'
      't3'
      't4'
      't7'
      't8'
      ;
    grid-template-rows: 100px 100px 100px 130px 130px 100px;
    grid-template-columns: 1fr;
  }
`;
复制代码

更多的样式(适配移动端)

body,html,#root {
  font-size: 14px;
  width: 100vw;
  height: 100vh;
}
// 根容器在移动端状态下,变成默认容器撑开的状态
@media (max-width: 600px) {
  body,html,#root {
    width: unset;
    height: unset;
  }
}

// 这个记得写
* {
  box-sizing: border-box;
}



复制代码

看下效果

grid容器适配移动端.png

如果用百分比布局,或者用栅格布局,我们的代码量会非常不好维护/理解,grid很轻松就把格子画好了

容器内容大小适配

适配这块的内容,,一般有百分比,rem,scale等方案,掘金上也有很多文章介绍, 我的需求是: 1. 可以复制蓝湖上的css代码 2. 一定不算百分比 3. 写完就不用管了,全自动各种分辨率 . 经过大量的调研和实战后,决定采用计算scale的方案(利用ResizeObserver.observe监听容器变化) GOGOGO

先贴张图,让大家对下面的代码有点概念~~~
image.png

  1. 封装Responsive.tsx,直接上代码
import { ReactElement, useEffect, useRef, useState } from "react";
import { useResizeDetector } from "react-resize-detector";

import styled from "styled-components";
import de from 'lodash-es/debounce'

interface ResponsiveProps {
  children: ReactElement;
  aspect?: number;
  // 设计稿尺寸
  width: number;
  height: number;
  // 预设属性,可以后期根据需求实现
  minWidth?: string | number;
  minHeight?: string | number;
  maxHeight?: number;
  debounceTime?: number;
  id?: string | number;
  className?: string | number;
}

const ResponsiveWrap = styled.div`
  width: 100%;
  height: 100%;
  position: relative;
`;

// 不能把容器撑开,所以absolute
const ResponsiveInner = styled.div<{
  scale?: number;
  left: string;
  top: string;
}>`
  position: absolute;
  left: ${(p) => p.left || 0};
  top: ${(p) => p.top || 0};

  transform: scale(${(p) => p.scale || 0});
  transform-origin: top left;
`;

export default function Responsive(props: ResponsiveProps) {
  const {
    children,
    width = 500,
    height = 500,
  } = props;

  const [mounted, setMounted] = useState<boolean>(false);
  const containerRef = useRef<HTMLDivElement>(null);
  const [scale, setScale] = useState(0);
  // 缩放之后,根据比例居中摆放
  const [position, setPosition] = useState({
    top: "0",
    left: "0",
  });

  const getContainerSize = () => {
    if (!containerRef.current) {
      return null;
    }

    return {
      rwidth: containerRef.current.clientWidth,
      rheight: containerRef.current.clientHeight,
    };
  };

  const updateDimensionsImmediate = () => {
    if (!mounted) {
      return;
    }
    const { rwidth, rheight } = getContainerSize();

    if (rwidth && rheight) {
      // 目前容器的尺寸 / 设计稿尺寸

      const w = rwidth / width;
      const h = rheight / height;

      const isLong = !!(w < h);
      const s = isLong ? w : h;
      if (s !== scale) {
        setScale(s);
      }
      const leftNum = (rwidth - width * h) / 2;
      const topNum = (rheight - height * w) / 2;
      setPosition((p) => {
        return {
          ...p,
          left: leftNum <= 0 ? "0" : leftNum + "px",
          top: topNum <= 0 ? "0" : topNum + "px",
        };
      });
    }
  };
 // TODO优化点: 套上一层debounce
  const handleResize = updateDimensionsImmediate;
  // 这里借助useResizeDetector实现监听(据说react18这个包有问题),
  //就不用自己写ResizeObserver.observe了
  useResizeDetector({
    onResize: handleResize,
    targetRef: containerRef,
  });
 // mounted之后set一次
  useEffect(() => {
    if (mounted) {
      handleResize();
    }
  }, [mounted]);

  useEffect(() => {
    setMounted(true);
  }, []);
  return (
    <ResponsiveWrap ref={containerRef}>
      <ResponsiveInner scale={scale} {...position}>
        {children}
      </ResponsiveInner>
    </ResponsiveWrap>
  );
}
复制代码
  1. 使用Responsive组件

模拟T1Block组件

import styled from "styled-components";
import Responsive from "./Responsive";
const T1BlockWrap = styled.div`
font-size:16px;
  width:350px;
  height: 268px;
  padding:12px 16px;
  display:grid;
  grid-gap:18px;
  grid-template-rows:repeat(2,1fr);
  grid-template-columns:repeat(2,1fr);
  // 子容器居中摆放
  align-items:center;
  justify-content:center;
`;

const T1BlockItemWrap = styled.div`
  width:100%;
  height:70px;
  background-color: rgba(12, 46, 93, 0.5);
  padding: 8px 16px;
  display: flex;
  flex-direction: column;
  justify-content:space-between;
`;

function T1BlockItem(props: { title: string; value: string }) {
  const { title, value } = props;
  return (
    <T1BlockItemWrap>
      <div>{title}</div>
      <div>{value}</div>
    </T1BlockItemWrap>
  );
}

export default function T1Block() {
  return (
    // 引用Responsive  传入设计稿尺寸
    <Responsive width={350} height={268}>
      <T1BlockWrap>
        {[
          {
            title: "1",
            value: "123",
          },
          {
            title: "2",
            value: "998",
          },
          {
            title: "3",
            value: "123",
          },
          {
            title: "4",
            value: "998",
          },
        ].map((item, index) => {
          return <T1BlockItem key={index} {...item}></T1BlockItem>;
        })}
      </T1BlockWrap>
    </Responsive>
  );
}

复制代码

看下效果

ResponsiveInner组件已经根据容器尺寸算出对应的scale

image.png

resize时候的效果

Untitled_ May 21, 2022 5_45 PM.gif

4k分辨下依然很好的运行

image.png

Responsive组件可能遇到的问题

当一些图表组件/地图组件,可能会在scale下表现异常,所以不得不考虑第二种方案

useResize.ts

import { useCallback, useEffect, useRef, useState } from 'react';
import { useDebounceFn } from 'ahooks';
/**

原理: 监听容器变化,改变vnode的key,强制重新渲染
*/
export function useResize() {
  const [IndexKey, setIndexKey] = useState(Math.random());
  const hasMount = useRef(0)
 
  const getIndexKey = useCallback((block:string|number)=>{
    return `${IndexKey}${block}`
  },[IndexKey])
  const { run } = useDebounceFn(
    () => {
     
      setIndexKey(Math.random());
      hasMount.current=hasMount.current+1;
    },
    {
      wait: 200,
    },
  );

  const setRootHeight = useCallback(() => {
    // ... 
    // 额外的逻辑
  },[])

  useEffect(() => {
    // setRootHeight()
    const resizeObserver = new ResizeObserver((entries) => {
     
      if(hasMount.current>0){
        run();
      }
       
    });
    resizeObserver.observe(document.getElementById('root')!);
    // 预设 Mount一秒后才run
    setTimeout(() => {
      hasMount.current=hasMount.current+1;
    }, 1000);
    () => {
      hasMount.current=0;
      resizeObserver.unobserve(document.getElementById('root')!);
    };
  }, []);

  return {
    IndexKey,
    getIndexKey
  };
}

复制代码

useResize用法

// 上伪代码了

const { IndexKey ,getIndexKey } = useResize();

// 组件放在一个100%的容器内,监听到容器尺寸变化,就强制重新渲染

 <TechnologyPercentChart
            key={getIndexKey('TechnologyPercent')}
             
            />
复制代码

今天先写到这里

下一篇预告:

recharts实现复杂图表(基于svg)

柱子图

image.png

猜你喜欢

转载自juejin.im/post/7100137233281187871
今日推荐