近期在做大屏项目,走了很多弯路,踩了很多坑,这篇文章总结了我对自适应大屏项目的经验总结 文本代码: 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入门文章: 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%;
`
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 (
头部啊
t1t1
t2t2t2
t3t3t3t3t3
t4t4t4t4t4t4
t7t7t7t7t7t7
t8t8t8t8t8
);
}
非常漂亮的网格化管理
需要给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很轻松就把格子画好了
适配这块的内容,,一般有百分比,rem,scale等方案,掘金上也有很多文章介绍, 我的需求是: 1. 可以复制蓝湖上的css代码 2. 一定不算百分比 3. 写完就不用管了,全自动各种分辨率 . 经过大量的调研和实战后,决定采用计算scale的方案(利用ResizeObserver.observe监听容器变化) GOGOGO
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(false);
const containerRef = useRef(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 (
{children}
);
}
模拟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 (
{title}
{value}
);
}
export default function T1Block() {
return (
// 引用Responsive 传入设计稿尺寸
{[
{
title: "1",
value: "123",
},
{
title: "2",
value: "998",
},
{
title: "3",
value: "123",
},
{
title: "4",
value: "998",
},
].map((item, index) => {
return ;
})}
);
}
ResponsiveInner
组件已经根据容器尺寸算出对应的scale
resize时候的效果
4k分辨下依然很好的运行
当一些图表组件/地图组件,可能会在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%的容器内,监听到容器尺寸变化,就强制重新渲染