给React路由加上Tabs页签效果
很多后台管理系统中都有多Tab窗口切换多效果比如 jQadmin 中的的效果:
jQadmin中实现的tabs切换的效果以及很多中台后端系统中这种类似的功能大部分是通过 iframe 标签来实现的。
如果要走在 React 框架中实现这种效果,我们不能使用 iframe 来实现,react构建到大型常规应用中,一般都会有一个全局到 state 状态树,而网页到多个 frame 中 js 到执行环境是隔离的,所以没办法统一使用 Redux 这样就工具做到统一的状态管理了,这个方案pass、掉。
那么如何在React中实现Tabs效果,你可能想到到第一个方案是通过 Antd 提供到 Tabs 组件实现这个需求,但是还是会有一个问题,在 Tabs 下面切换页面是并不是通过 React-Router 切换页面到,所以这个方案 pass 掉。
解决思路
在 React-Router中 有一个 history 对象,history这个为React Router提供核心功能的包。它能轻松地在客户端为项目添加基于location的导航,这种对于单页应用至关重要的功能。
通过 history 我们可以:
- 获取当前页面路由
- 监听页面路由变化
- 设置当前页面切换到另外一个页面
所以我们只要拿到history对象,我们完全可以实现 Tabs 页面切换这个功能
在 React-Router 4 中还提供一张根简单到方式获取 history 对象: withRouter高阶组件,提供了history让你使用~
import React from "react";
import {withRouter} from "react-router-dom";
class MyComponent extends React.Component {
...
myFunction() {
this.props.history.push("/some/Path");
}
...
}
export default withRouter(MyComponent);
为了实现这个功能,我们可以定义一个组件 RouterTabs
。
在 RouterTabs
中可以使用 @withRouter
注入 history 。
在组件中 通过 history 监听 路由到变化,当切换到新到页面时,在RouterTabs
的state中增加一个页签到数据,并把当前到路由参数也保存下来 ,并设置当前路由对于的页签为激活状态,切换到其他路由时,根据 history 的事件变更组件到状态,让组件和页面保持同步。
点击 RouterTabs 上面到标签,通过 history
控制页面路由的切换。
具体实现代码如下:
import React, { Component } from 'react';
import ReactDom from 'react-dom';
import classNames from 'classnames';
import { Tag, Dropdown, Icon, Tooltip, Menu } from 'antd';
import styles from './index.less';
import {withRouter} from "react-router-dom";
const { SubMenu } = Menu;
// 模拟全局路由配置对象,
const routerCcnfig = [
'/a': {name: 'A页面'},
'/b': {name: 'B页面'}
];
// 通过 pathname 获取 pathname 对应到路由描述信息对象
const getTitleByPathname = (pathname) => {
return routerCcnfig[pathname]:pathname;
}
@withRouter
export class RouterTabs extends Component {
static unListen = null;
static defaultProps = {
initialValue: [],
};
constructor(props) {
super(props);
const { pathname } = this.props.location;
this.state = {
currentPageName: [], // 当前路由对应到 pathname
refsTag: [], // tabs 所有到所有页签
searchMap: {}, // 每个 页签对应的路由参数
};
this.handleMenuClick = this.handleMenuClick.bind(this);
}
componentDidMount() {
if (this.unListen) {
this.unListen();
this.unListen = null;
}
// 监听路由切换事件
this.unListen = this.props.history.listen((_location) => {
if (this.didUnMounted) {
return;
}
if (this.notListenOnce) {
this.notListenOnce = false;
return;
}
const { pathname } = _location;
if (pathname === '/' || !getTitleByPathname(pathname)) {
this.setState({
currentPageName: '',
});
return;
}
const newRefsTag = [...this.state.refsTag];
const currentPathname = pathname;
if (newRefsTag.indexOf(currentPathname) === -1) {
newRefsTag.push(currentPathname);
}
this.state.searchMap[pathname] = _location.search;
this.setState({
currentPageName: pathname,
refsTag: newRefsTag,
});
// 假如是新的 导航item 添加进来,需要在 添加完成后调用 scrollIntoTags
clearTimeout(this.tagChangeTimerId);
this.tagChangeTimerId = setTimeout(() => {
this.scrollIntoTags(pathname);
}, 100);
});
const { pathname } = this.props.location;
this.scrollIntoTags(pathname);
}
componentWillUnmount() {
this.didUnMounted = true;
if (this.unListen) {
this.unListen();
this.unListen = null;
}
}
scrollIntoTags(pathname) {
let dom;
try {
// eslint-disable-next-line react/no-find-dom-node
dom = ReactDom.findDOMNode(this)
.querySelector(`[data-key="${pathname}"]`);
if (dom === null) {
// 菜单 还没有假如 导航条(横)
} else {
// 菜单 已经加入 导航条(横)
dom.scrollIntoView(false);
}
} catch (e) {
// console.error(e);
}
}
handleClose = (tag) => {
const { pathname } = this.props.location;
const { history } = this.props;
let { currentPageName } = this.state;
const { searchMap } = this.state;
const newRefsTag = [...this.state.refsTag.filter(t => t !== tag)];
if (currentPageName === tag) {
currentPageName = this.state.refsTag[this.state.refsTag.indexOf(tag) - 1];
}
this.setState({
currentPageName,
refsTag: newRefsTag,
});
if (pathname !== currentPageName) {
this.notListenOnce = true;
history.push({
pathname: currentPageName,
search: searchMap[currentPageName],
});
}
};
handleClickTag = (tag, e) => {
if (e && e.target.tagName.toLowerCase() === 'i') {
return;
}
if (tag !== this.state.currentPageName) {
this.props.history.push({
pathname: tag,
search: this.state.searchMap[tag] ? this.state.searchMap[tag].replace(/from=[^&]+&?/, '') : undefined,
});
}
}
handleMenuClick = (e) => {
const eKey = e.key;
let currentPathname = this.props.location.pathname;
const { refsTag } = this.state;
let newRefsTag;
if (eKey === '1') {
newRefsTag = '/';
currentPathname = '首页';
} else if (eKey === '2') {
newRefsTag = [webConfig.indexPath];
if (currentPathname !== webConfig.indexPath) {
newRefsTag.push(currentPathname);
}
} else {
this.handleClickTag(eKey);
return;
}
if (currentPathname !== this.state.currentPageName) {
this.props.history.push({
pathname: currentPathname,
search: this.state.searchMap[currentPathname],
});
}
this.setState({
refsTag: newRefsTag,
});
}
render() {
const { currentPageName, refsTag } = this.state;
const { className, style } = this.props;
const cls = classNames(styles['router-tabs'], className);
const tags = refsTag.map((pathname, index) => {
const routeInfo = getTitleByPathname(pathname); // 这里假设每个pathname都能获取到指定到页面名称
let title = routeInfo ? routeInfo.name : '404';
const isLongTag = title.length > 30;
const tagElem = (
<Tag
key={pathname}
data-key={pathname}
className={classNames(styles.tag,
{ [styles.active]: pathname === currentPageName })}
onClick={e => this.handleClickTag(pathname, e)}
closable={index !== 0}
afterClose={() => this.handleClose(pathname)}
>
<span className={styles.icon} />{isLongTag ? `${title.slice(
0, 30)}...` : title}
</Tag>
);
return isLongTag
? <Tooltip title={title} key={`tooltip_${pathname}`}>{tagElem}</Tooltip>
: tagElem;
});
this.tags = tags;
/* eslint-disable */
return (
<div className={cls} style={{
...style,
height: '40px',
maxHeight: '40px',
lineHeight: '40px',
marginRight: '-12px',
}}>
<div style={{
flex: '1',
height: '40px',
position: 'relative',
overflow: 'hidden',
background: '#f0f0f0',
padding: '0px 0px',
}}>
<div style={{
position: 'absolute',
whiteSpace: 'nowrap',
width: '100%',
top: '0px',
padding: '0px 10px 0px 10px',
overflowX: 'auto',
}}>
{tags}
</div>
</div>
<div style={{
width: '96px',
height: '100%',
background: '#fff',
boxShadow: '-3px 0 15px 3px rgba(0,0,0,.1)',
}}>
<Dropdown overlay={<Menu onClick={this.handleMenuClick}>
<Menu.Item key="1">关闭所有</Menu.Item>
<Menu.Item key="2">关闭其他</Menu.Item>
<SubMenu title="切换标签">
{
tags.map(item => (<Menu.Item key={item.key}>{item.props.children}</Menu.Item>))
}
</SubMenu>
</Menu>}
>
<Tag size={'small'} color="#2d8cf0"
style={{ marginLeft: 12 }}>
标签选项 <Icon type="down" />
</Tag>
</Dropdown>
</div>
</div>
);
}
}
样式定义:
.router-tabs {
user-select: none;
display: flex;
position: relative;
overflow: hidden;
transition: all .3s;
box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px 0, inset rgba(0, 0, 0, 0.52) 0 5px 2px;
clear: both;
.tag {
margin-top: 4px;
height: 32px;
line-height: 32px;
border: 1px solid #e9eaec!important;
color: #495060!important;
background: #fff!important;
padding: 0 12px;
.icon {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 8px;
border-radius: 50%;
background: #e9eaec;
position: relative;
top: 1px;
}
&.active {
.icon {
background: #2d8cf0;
}
}
}
}
使用方法
只要把组件放到页面可以实现 Tabs 页面切换到效果了
render() {
return (
<Layout>
<Header className={styles.header}>
<RouterTabs />
</Header>
<Content>
<Switch>
{ routers }
<Redirect exact from="/" to={webConfig.indexPath} />
<Route component={NotFound} />
</Switch>
</Content>
</Layout>
);
}
预览效果
不做前端好多年,跟不上节凑了
但是tab之间的切换 怎么保存状态呢
使用新版antd 有个bug onClose={(e) => this.handleClose(pathname,e)} 需要在handleClose 里阻止冒泡