my-react-admin-fe
react+antd+mobx+postcss+react-router-dom+webpack
虚拟列表
需求:大数据量渲染。但同时渲染大量DOM会造成页面卡顿。
解决方案:
- 虚拟列表(也叫按需渲染或可视区域渲染)
- 监听scroll事件,通过slice 方法对数据进行分割展示
- 延迟渲染(即懒渲染)
- 监听scroll事件,当子项的 offsetTop(偏移高度)<innerHeight(视窗高度)+ scrollTop(滚动高度)说明已经滚到下方
- IntersectionObserver API 是异步的,不随目标元素的滚动同步触发,性能消耗小
- getBoundingClientRect 方法返回元素的大小机器相对于视窗的位置
- 时间分片
- 定时器或requestAnimationFrame
- DocumentFragment
最优选是虚拟列表,DOM 树上只挂载有限的DOM;懒加载和时间分片的缺点在于插入大量的DOM,占内存运行时会造成卡顿。
虚拟列表通过仅渲染大型数据集的一部分(刚好足以填充视口)来工作。这有助于解决一些常见的性能瓶颈:
- 它减少了渲染初始视图和处理更新所需的工作量(和时间)。
- 它通过避免过度分配 DOM 节点来减少内存占用。
自动滚动
css3 animation
功能:鼠标移上去停止滚动,会聚焦相应列表项。鼠标移开恢复。
@keyframes rowup {
0% {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
100% {
-webkit-transform: translate3d(0, -307px, 0);
transform: translate3d(0, -307px, 0);
}
}
.list {
border: 1px solid #999;
margin: 20px auto;
height: 200px;
overflow: hidden;
}
.list :hover {
animation: none;
}
.rowup {
-webkit-animation: 10s rowup linear infinite normal;
animation: 10s rowup linear infinite normal;
}
.rowup :hover {
background-color: red;
}
<div className="list">
<div className="rowup">
{new Array(1000).fill(
<div className="item">
1. 自动滚动⚽️
</div>
)}
</div>
</div>
js控制滚动条
可以实现虚拟列表自动滚动
react
useEffect(() => {
// TODO:fix
const child = document.querySelector('.list') as HTMLElement;
let timer: string | number | NodeJS.Timeout | undefined;
if (isScroll&&child) {
timer = setInterval(() => {
const isBottom =
Math.abs(child.scrollHeight - child.clientHeight - child.scrollTop) < 1;
isBottom ? (child.scrollTop = 0) : (child.scrollTop += speed);
}, 1000);
}
return () => clearInterval(timer);
}, [isScroll]);
tailwind干扰antD样式
@tailwind base;
@tailwind components;
@tailwind utilities;
会使Image组件预览图片位于左下角。
解决
如果您想完全禁用 Preflight - 可能是因为您要将 Tailwind 集成到现有项目中,或者是因为您想提供自己的基本样式 - 您所需要做的就是在 tailwind.config.js 文件的 corePlugins 部分,设置 preflight 为 false:
// tailwind.config.js
module.exports = {
corePlugins: {
preflight: false,
}
}
动态换肤
src/index.js
import 'antd/dist/antd.variable.min.css';
import { ConfigProvider } from 'antd';
ConfigProvider.config({
theme: {
primaryColor: '#25b864',
},
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ConfigProvider>
<App />
</ConfigProvider>
);
src/components/Dashboard/MyToolbar.tsx
const initColor={
primaryColor: '#1890ff',
errorColor: '#ff4d4f',
warningColor: '#faad14',
successColor: '#52c41a',
infoColor: '#1890ff',
}
const ColorChange: FC = () => {
const [color, setColor] = useState(()=>initColor);
const [isModalOpen, setIsModalOpen] = useState(false);
const {globalStore}=useStores()
// 配置选项卡内容
const primary = (
<SketchPicker
presetColors={['#1890ff', '#25b864', '#ff6f00']}
color={color.primaryColor}
onChange={({ hex }) => {
onColorChange({
primaryColor: hex,
});
}}
/>
);
const error = (
<SketchPicker
presetColors={['#ff4d4f']}
color={color.errorColor}
onChange={({ hex }) => {
onColorChange({
errorColor: hex,
});
}}
/>
);
const warning = (
<SketchPicker
presetColors={['#faad14']}
color={color.warningColor}
onChange={({ hex }) => {
onColorChange({
warningColor: hex,
});
}}
/>
);
const success = (
<SketchPicker
presetColors={['#52c41a']}
color={color.successColor}
onChange={({ hex }) => {
onColorChange({
successColor: hex,
});
}}
/>
);
const info = (
<SketchPicker
presetColors={['#1890ff']}
color={color.infoColor}
onChange={({ hex }) => {
onColorChange({
infoColor: hex,
});
}}
/>
);
const items = Object.entries({ primary, success, error, warning, info }).map((item) => {
const [label, children] = item;
return {
label,
children,
key: label,
};
});
const onColorChange = (nextColor: Partial<typeof color>) => {
const mergedNextColor = {
...color,
...nextColor,
};
setColor(mergedNextColor);
// 组件共享theme
globalStore.setTheme(mergedNextColor);
ConfigProvider.config({
theme: mergedNextColor,
});
};
return (
<Tabs animated={true} items={items} />
);
};
非antd组件的动态换肤 src/hooks/useTheme.ts
import {useStores} from './useStores'
const useTheme=()=>{
const {globalStore} =useStores()
const [theme,setTheme]=useState(globalStore.theme)
const themeRef=useRef(theme)
themeRef.current=theme
useEffect(()=>{
setTheme(globalStore.theme)
})
return {theme:themeRef.current}
}
export {useTheme}
富文本编辑器
import { Editor, Toolbar } from '@wangeditor/editor-for-react';
export const MyEditor = () => {
const [editor, setEditor] = (useState < IDomEditor) | (null > null);
const toolbarConfig: Partial<IToolbarConfig> = {};
const editorConfig: Partial<IEditorConfig> = {
placeholder: '请输入内容。。。',
};
// 及时销毁editor
useEffect(() => {
return () => {
if (editor == null) return;
editor.destroy();
setEditor(null);
};
}, [editor]);
return (
<>
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
style={{ borderBottom: '1px solid #ccc' }}
data-w-e-toolbar={true}
/>
<Editor
defaultConfig={editorConfig}
value={html.val}
// onCreated属性有问题会使toolbar变成一条线
onCreated={setEditor}
onChange={(editor) => {
setHTML({ val: editor.getHtml() });
setTEXT({ val: editor.getText() });
setJSON({ val: editor.children });
}}
style={{ minHeight: '300px' }}
mode="default"
/>
</>
);
};
前端水印
原理:水印是一个或多个元素,通过z-index将其设置在上层覆盖所有元素;pointer-events设置为none使元素永远不会成为鼠标事件的target。但是,当其后代元素的pointer-events属性指定其他值时,鼠标事件可以指向后代元素,在这种情况下,鼠标事件将在捕获或冒泡阶段触发父元素的事件侦听器。
- dom方案需要生成多个dom元素,不优雅也影响性能;
- svg方案可以隐藏文字样式去掉文字;
- background方案(使用canvas或svg生成base64url做背景图片)
前端生成dom元素覆盖到页面上的,对于有些前端知识的人来说,可以在开发者工具中找到水印所在的元素,将元素整个删掉,以达到删除页面上的水印的目的。 - 用定时器或MutationObserver观测水印,但可以通过禁用js跳过。
多页签
src/components/Dashboard/MyMenu.tsx
点击 MenuItem时将当前菜单子项的标签和key传给状态管理库
import { useStores } from '@/hooks';
const MyMenu = () => {
const navigate = useNavigate();
const { globalStore } = useStores();
const onHandleClick = (e: any) => {
let path = e.keyPath.reverse().join('/');
navigate(path);
globalStore.setTab({ label: e.key, key: path });
};
return <Menu onClick={onHandleClick} items={dragItems} mode="inline" theme="dark" />;
};
src/stores/global.ts
const globalStore = makeAutoObservable({
tab: { label: '', key: '' },
setTab(tab: { label: string; key: string }) {
this.tab = tab;
},
});
src/components/Dashboard/MyTabs.tsx
状态库的当前标签信息改变时,添加标签页;
export const MyTabs: React.FC = () => {
const { globalStore } = useStores();
const navigate = useNavigate();
useEffect(() => {
if (globalStore.tab.label !== '' && globalStore.tab.key !== '') {
add(globalStore.tab);
}
}, [globalStore.tab]);
const onTabClick = (key: string) => {
const regexp = /.+(?=\*\*)/;
const res = regexp.exec(key) || [];
navigate(res[0] || '');
};
return (
<>
<Tabs
type="editable-card"
onChange={onChange}
activeKey={activeKey}
onEdit={onEdit}
onTabClick={onTabClick}
items={items}
/>
</>
);
};