React 最佳实践:可拖拽侧边栏

5,221 阅读2分钟

前言

页面布局也是在实际开发中经常用到的技术。

  • 在大的方面,可以实现整个页面的布局,比如左侧导航、header、footer...
  • 在小的方面,可以是内容布局,比如文章。
  • 考虑布局时,需要兼顾桌面浏览器和移动端的情况,也就是常说的 responsive。

在 react 中实现,和经典实现其实没什么大的区别。

实现布局有这几种方式

  • 从 0 开始使用 CSS 实现,这是必须掌握的技能
  • 使用 CSS Grid 系统(网状页面),可以使用不同尺寸的屏幕
  • 使用组件库,例如 antd
    • Grid:24 栅格系统
    • Layout:页面级整体布局

CSS 实现基础布局

上中下布局

layout-1.png

.app-layout1 {
  width: 500px;
  height: 400px;
  position: relative;
  text-align: center;
}

.app-layout1 .header {
  line-height: 60px;
}
.app-layout1 .content {
  position: absolute;
  bottom: 60px;
  top: 60px;
  left: 0;
  right: 0;
}
.app-layout1 .footer {
  line-height: 60px;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
}

Sider + 上中下布局

layout-2.png

使用 left: 150px,留出 Sider 的位置

.app-layout2 {
  width: 500px;
  height: 400px;
  position: relative;
  text-align: center;
}
.app-layout2 .header {
  position: absolute;
  left: 150px;
  top: 0;
  right: 0;
}
.app-layout2 .content {
  position: absolute;
  bottom: 60px;
  top: 60px;
  left: 150px;
  right: 0;
}
.app-layout2 .footer {
  bottom: 0;
  left: 150px;
  right: 0;
  position: absolute;
}
.app-layout2 .sider {
  width: 150px;
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
}

进阶:侧边栏宽度可拖拽

样式布局

style.css

.layout {
  position: relative;
  width: 100%;
  height: 400px;
}
.sider {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  background-color: #ddd;
}
.header {
  background-color: #aaa;
  height: 60px;
}

index.js

通过状态 siderWidth 控制 layoutpaddingLeft

import { useState } from 'react';
import './style.css';

export default function ResizeLayout() {
  const [siderWidth, setSiderWidth] = useState(150);
  const pxWidth = `${siderWidth}px`;

  return (
    <div className="layout" style={{ paddingLeft: pxWidth }}>
      <div className="sider" style={{ width: pxWidth }}>
        sider
      </div>
      <div className="header">header</div>
      <div className="content">content</div>
    </div>
  );
}

拖放逻辑

视觉上,我们拖拽的是侧边栏 Sider 的右边框,其实并不是。我们会在右边框位置放置一个“隐身”的 bar -- sider-resizer

.sider-resizer {
  position: absolute;
  width: 6px;
  top: 0;
  bottom: 0;
  cursor: col-resize;
}

当鼠标在 sider-resizer 上,触发 onMouseDown 时,记录下鼠标的初始位置,同时标记 dragging 状态为 true

const handleMouseDown = (event) => {
  setStartPageX(event.pageX);
  setDragging(true);
};

伴随着 dragging 状态的改变,我们会为页面铺上一层遮罩 -- resize-mask,以便后续 事件的监听:

{
  dragging && (
    <div
      className="resize-mask"
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
    />
  );
}
.resize-mask {
  background: rgba(0, 0, 0, 0);
  position: fixed;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  cursor: col-resize;
}

onMouseMove 的过程中,实时更新 siderWidth 以及对 startPageX 的更新,就会产生拖拽的效果。

最终在 onMouseUp 时,结束拖拽状态。

const handleMouseMove = (event) => {
  const currentSiderWidth = siderWidth + event.pageX - startPageX;
  setSiderWidth(currentSiderWidth);
  setStartPageX(event.pageX);
};
const handleMouseUp = () => {
  setDragging(false);
};

localStorage 存储宽度位置

在拖拽结束时,保存 siderWidthlocalStorage;初始化 siderWidth 时,检查 localStorage 是否有值。

const [siderWidth, setSiderWidth] = useState(
  parseInt(localStorage.getItem('siderWidth')) || 150,
);
const handleMouseUp = () => {
  setDragging(false);
  localStorage.setItem('siderWidth', siderWidth);
};

完整代码

style.css

.layout {
  position: relative;
  width: 100%;
  height: 400px;
}
.sider {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  background-color: #ddd;
}
.header {
  background-color: #aaa;
  height: 60px;
}
.sider-resizer {
  position: absolute;
  width: 6px;
  top: 0;
  bottom: 0;
  cursor: col-resize;
}
.resize-mask {
  background: rgba(0, 0, 0, 0);
  position: fixed;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  cursor: col-resize;
}

index.js

import { useState } from 'react';
import './style.css';

export default function ResizeLayout() {
  const [siderWidth, setSiderWidth] = useState(
    parseInt(localStorage.getItem('siderWidth')) || 150,
  );
  const [dragging, setDragging] = useState(false);
  const [startPageX, setStartPageX] = useState(0);
  const pxWidth = `${siderWidth}px`;
  const handleMouseDown = (event) => {
    setStartPageX(event.pageX);
    setDragging(true);
  };
  const handleMouseMove = (event) => {
    const currentSiderWidth = siderWidth + event.pageX - startPageX;
    setSiderWidth(currentSiderWidth);
    setStartPageX(event.pageX);
  };
  const handleMouseUp = () => {
    setDragging(false);
    localStorage.setItem('siderWidth', siderWidth);
  };
  return (
    <div className="layout" style={{ paddingLeft: pxWidth }}>
      <div className="sider" style={{ width: pxWidth }}>
        sider
      </div>
      <div className="header">header</div>
      <div className="content">content</div>
      <div
        className="sider-resizer"
        style={{ left: pxWidth }}
        onMouseDown={handleMouseDown}
      >
        {dragging && (
          <div
            className="resize-mask"
            onMouseMove={handleMouseMove}
            onMouseUp={handleMouseUp}
          />
        )}
      </div>
    </div>
  );
}

React 最佳实践