大屏适配实战-经验总结

标签: 经验 | 发表时间:2022-05-21 18:38 | 作者:Boris在掘金
出处:https://juejin.cn/frontend

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

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

技术栈

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

布局方案-grid

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

grid入门文章: https://www.ruanyifeng.com/blog/2019/03/grid-layout-tutorial.html 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

相关 [经验] 推荐:

scrum经验

- - CSDN博客研发管理推荐文章
Scrum是基于过程控制理论的经验方法,倡导自组织团队;其运行框架核心是迭代增量型并行开发,也是“适应性”的软件开发方法. Scrum提供了高度可视化的用于管理软件开发复杂性管理的敏捷项目管理的实践框架或敏捷过程,可以用于对现存软件工程实践的包装,提高软件生产率,改善沟通和合作的方法,使人们协作并注重业务目标.

Scrum 实施经验

- bluesnail - 新浪UED
Scrum是一种迭代式增量软件开发过程,通常用于敏捷软件开发. Scrum在英语的意思是橄榄球里的争球. 虽然Scrum是为管理软件开发项目而开发的,它同样可以用于运行软件维护团队,或者作为计划管理方法:Scrum of Scrums. Scrum定义了许多角色,根据猪和鸡的笑话分为两组,猪和鸡:.

减肥小经验

- 超群 - 中文热文榜|最新
kergee 在 GoogleReader 说. 还有 可可, scavin, 推荐,查看全部 17 个推荐. UnIndexed发表于2010-08-20 17:12:58. Shared by 南闲. 腹肌也是我高中之后一直消失了的东西……不过我不爱腹肌……. 我实在是太懒了,这篇东西三个月前就想写了,竟然能拖到现在.

SQLAlchemy 使用经验

- - keakon的涂鸦馆
上篇文章提到了,最近在用 Python 做一个网站. 除了 Tornado ,主要还用到了 SQLAlchemy. 这篇就是介绍我在使用 SQLAlchemy 的过程中,学到的一些知识. 首先说下,由于最新的 0.8 版还是开发版本,因此我使用的是 0.79 版,API 也许会有些不同. 因为我是搭配 MySQL InnoDB 使用,所以使用其他数据库的也不能完全照搬本文.

ZooKeeper运维经验

- - Juven Xu
ZooKeeper 是分布式环境下非常重要的一个中间件,可以完成动态配置推送、分布式 Leader 选举、分布式锁等功能. 在运维 AliExpress ZooKeeper 服务的一年多来,积累如下经验:. 3台起,如果是虚拟机,必须分散在不同的宿主机上,以实现容灾的目的. 如果长远来看(如2-3年)需求会持续增长,可以直接部署5台.

Google Chrome使用经验谈

- sylvia - 月光博客
  尽管笔者对于Google Chrome(谷歌浏览器)有着这样那样的偏爱,但是笔者仍然需要诚实告诉你它并不是对所有人都是一个好选择. 当然它有着启动快速、界面简洁的特点,但是对于习惯了IE、Firefox界面的朋友来说也许这并不是一个好选择,除此之外它还是一个挥霍无度的家伙,所以2G内存是它的基础配备,因为就连笔者的4G内存有时都力有不逮,痛并快乐着的确是一个很好的形容.

留英经验分享

- 木頭 - ss1271的奋斗
据说今年申请英国留学的人数再创新高,我来英国也快一年了,最近看到街头很多眼神中充满期待的新生,分享些经验:. 本经验分两大类:行前准备和到英国之后的衣食住行介绍. 全部例子以本人所处的Newcastle upon Tyne为准. 在签证拿到前购买机票还是在签证拿到之后购买各有利弊. 签证拿到前购买,最后可能导致机票改签,请斟酌.

面试的几点经验

- - 曉生
有个癖好,翻看简历,特别是附带作品集的简历,然后找其中的规律. 对于在校生,一份精致的纸质作品集可以展现出设计师对待作品的态度,当翻着每一页向你讲诉想法时,是一个非常让人享受的过程,有时候可以从中获取灵感. 纵然紧张,表达能力欠缺,这都无关紧要,沉默的设计师会炫耀自己的得意之作. 在校生需要考察对待设计的热情、态度和潜力,专业知识可以后期培养.