image


前言


简化了代码逻辑和代码量,重写了一遍,执行逻辑和上个版本有所差异;


效果图


image


功能点


在上个版本的功能的基础上梳理,剔除一些BUG,基本都会触发联动


  • 重定向
  • 关闭单一标签/关闭其他标签
  • 动态追加标签
  • 浏览器的前进后退功能
  • 同子域的,菜单会保持展开


依赖 : antd / styled-components / mobx / mobx-react / react


实现思路


  • 把遍历匹配的扔到状态里面去匹配,可以减少挺多代码量
  • 从布局容器触发匹配(这样初始化就能让动态菜单正常)
  • 借助 getDerivedStateFromProps getSnapshotBeforeUpdate 这类 React 16.3+ 的特性实现侧边栏联动
  • 动态菜单只操作mobx共享状态


代码


布局缓存活动路由的关键代码


// 路由容器那个组件
    // 注入mobx状态,这样活动路由每次都能正确响应
    // 减少一些不必要的渲染,update需要做一些判断..同样的路由不作处理
    componentDidMount = () => {
        this.props.rstat.searchRoute(location.pathname);
    componentDidUpdate(prevProps, prevState) {
            this.props.rstat.activeRoute.pathname !==
            this.props.location.pathname
            this.props.rstat.searchRoute(this.props.location.pathname);

侧边栏(Sidebar.js)


import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { observer, inject } from 'mobx-react';
// antd
import { Layout, Menu, Icon } from 'antd';
const { Sider } = Layout;
const { SubMenu, Item } = Menu;
import RouterTree, { groupKey } from 'router';
// Logo组件
import Logo from 'pages/Layout/Logo';
@inject('rstat')
@withRouter
@observer
class Sidebar extends Component {
    constructor(props) {
        super(props);
        this.state = {
            openKeys: [''],
            selectedKeys: [''],
            rootSubmenuKeys: groupKey
    static getDerivedStateFromProps(nextProps, prevState) {
        const { groupKey, childKey } = nextProps.rstat.activeRoute;
            !prevState.openKeys[0] ||
            (groupKey !== prevState.openKeys[0] &&
                childKey !== prevState.selectedKeys[0])
            return {
                openKeys: [groupKey],
                selectedKeys: [childKey]
        return null;
    getSnapshotBeforeUpdate(prevProps, prevState) {
        const { openKeys, selectedKeys } = prevState;
        const { groupKey, childKey } = prevProps.rstat.activeRoute;
        if (openKeys[0] !== groupKey || selectedKeys[0] !== childKey) {
            return {
                openKeys: [groupKey],
                selectedKeys: [childKey]
        return null;
    componentDidUpdate = (prevProps, prevState, snapshot) => {
        if (snapshot) {
            this.setState(snapshot);
    OpenChange = openKeys => {
        const latestOpenKey = openKeys.find(
            key => this.state.openKeys.indexOf(key) === -1
        if (this.state.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
            this.setState({ openKeys });
        } else {
            this.setState({
                openKeys: latestOpenKey ? [latestOpenKey] : [...openKeys]
    // 路由跳转
    gotoUrl = itemurl => {
        const { history, location } = this.props;
        if (location.pathname === itemurl) {
            return;
        } else {
            this.props.rstat.searchRoute(itemurl)
            history.push(itemurl);
    render() {
        const { openKeys, selectedKeys } = this.state;
        const { collapsed, onCollapse, rstat, history } = this.props;
        const SiderTree = RouterTree.map(item => (
            <SubMenu
                key={item.key}
                title={
                        <Icon type={item.title.icon} />
                        <span>{item.title.text}</span>
                    </span>
                {item.children &&
                    item.children.map(menuItem => (
                            key={menuItem.key}
                            onClick={() => this.gotoUrl(menuItem.path)}>
                            {menuItem.text}
                        </Item>
            </SubMenu>
        return (
            <Sider
                collapsible
                breakpoint="lg"
                collapsed={collapsed}
                onCollapse={onCollapse}
                trigger={collapsed}>
                <Logo collapsed={collapsed} />
                    subMenuOpenDelay={0.3}
                    theme="dark"
                    openKeys={openKeys}
                    selectedKeys={selectedKeys}
                    mode="inline"
                    onOpenChange={this.OpenChange}>
                    {SiderTree}
                </Menu>
            </Sider>
export default Sidebar;

Mobx Model(联动共享状态)


import { observable, action, computed, toJS } from 'mobx';
import RouterTree from 'router'; // 这个是自己维护的静态路由表
function findObj(array, obj) {
    for (let i = 0, j = array.length; i < j; i++) {
        if (array[i].childKey === obj.childKey) {
            return true;
    return false;
class RouterStateModel {
    @observable
    currentRouteInfo; // 当前访问的信息
    @observable
    routerCollection; // 访问过的路由信息
    constructor() {
        this.currentRouteInfo = {};
        this.routerCollection = [];
    // 当前访问的信息
    @action
    addRoute = values => {
        // 赋值
        this.currentRouteInfo = values;
        // 若是数组为0
        if (this.routerCollection.length === 0) {
            // 则追加到数组中
            this.routerCollection.push(this.currentRouteInfo);
        } else {
            findObj(this.routerCollection, values)
                ? null
                : this.routerCollection.push(this.currentRouteInfo);
    // 设置index为高亮路由
    @action
    setIndex = index => {
        this.currentRouteInfo = this.routerCollection[index];
    // 查询路由匹配
    @action
    searchRoute(path) {
        RouterTree.map(item => {
            if (item.pathname) {
                // 做一些事情,这里只有二级菜单
            // 因为菜单只有二级,简单的做个遍历就可以了
            if (item.children && item.children.length > 0) {
                item.children.map(childitem => {
                    // 为什么要用match是因为 url有可能带参数等,全等就不可以了
                    // 若是match不到会返回null
                    if (path.match(childitem.path)) {
                        // 设置title
                        document.title = childitem.text;
                        this.addRoute({
                            groupKey: item.key,
                            childKey: childitem.key,
                            childText: childitem.text,
                            pathname: childitem.path
    // 关闭单一路由
    @action
    closeCurrentTag = index => {
        this.routerCollection.splice(index, 1);
        this.currentRouteInfo = this.routerCollection[
            this.routerCollection.length - 1
    // 关闭除了当前url的其他所有路由
    @action
    closeOtherTag = route => {
        if (this.routerCollection.length > 1) {
            this.routerCollection = [this.currentRouteInfo];
        } else {
            return false;
    // 获取当前激活的item,也就是访问的路由信息
    @computed
    get activeRoute() {
        return toJS(this.currentRouteInfo);
    // 获取当前的访问历史集合
    @computed
    get historyCollection() {
        return toJS(this.routerCollection);
const RouterState = new RouterStateModel();
export default RouterState;

静态路由表(router/index.js)


import asyncComponent from 'components/asyncComponent/asyncComponent';
// 数据分析
const Monitor = asyncComponent(() => import('pages/DashBoard/Monitor'));
const Analyze = asyncComponent(() => import('pages/DashBoard/Analyze'));
// 音频管理
const VoiceList = asyncComponent(() => import('pages/AudioManage/VoiceList'));
const CallVoice = asyncComponent(() => import('pages/AudioManage/CallVoice'));
const PrivateChat = asyncComponent(() =>
    import('pages/AudioManage/PrivateChat')
const Topic = asyncComponent(() => import('pages/AudioManage/Topic'));
// 活动中心
const ActiveList = asyncComponent(() =>
    import('pages/ActivityCenter/ActiveList')
// APP 管理
const USERLIST = asyncComponent(() => import('pages/AppManage/UserList'));
const ApkSetting = asyncComponent(() => import('pages/AppManage/ApkSetting'));
const LicenseList = asyncComponent(() => import('pages/AppManage/LicenseList'));
const QaList = asyncComponent(() => import('pages/AppManage/QaList'));
// 安全中心
const REPORT = asyncComponent(() => import('pages/Safety/Report'));
const BroadCast = asyncComponent(() => import('pages/Safety/BroadCast'));
// 电影频道
const MovieList = asyncComponent(() => import('pages/Movie/MovieList'));
// 后台管理
const UserSetting = asyncComponent(() =>
    import('pages/AdminSetting/UserSetting')
const RouterTree = [
        key: 'g0',
        title: {
            icon: 'dashboard',
            text: '数据分析'
        exact: true,
        path: '/dashboard',
        children: [
                key: '1',
                text: '数据监控',
                path: '/dashboard/monitor',
                component: Monitor
                key: '2',
                text: '数据分析',
                path: '/dashboard/analyze',
                component: Analyze
        key: 'g1',
        title: {
            icon: 'play-circle',
            text: '音频管理'
        exact: true,
        path: '/voice',
        children: [
                key: '8',
                text: '声兮列表',
                path: '/voice/sxlist',
                component: VoiceList
                key: '9',
                text: '回声列表',
                path: '/voice/calllist',
                component: CallVoice
                key: '10',
                text: '私聊列表',
                path: '/voice/privatechat',
                component: PrivateChat
            //     key: '11',
            //     text: '热门话题',
            //     path: '/voice/topcis',
            //     component: Topic
        key: 'g2',
        title: {
            icon: 'schedule',
            text: '活动中心'
        exact: true,
        path: '/active',
        children: [
                key: '17',
                text: '活动列表',
                path: '/active/list',
                component: ActiveList
        key: 'g3',
        title: {
            icon: 'scan',
            text: '电影专栏'
        exact: true,
        path: '/active',
        children: [
                key: '22',
                text: '电影大全',
                path: '/movie/list',
                component: MovieList
        key: 'g4',
        title: {
            icon: 'apple-o',
            text: 'APP管理'
        exact: true,
        path: '/appmanage',
        children: [
                key: '29',
                text: 'Apk设置',
                path: '/appmanage/apksetting',
                component: ApkSetting
                key: '30',
                text: '用户列表',
                path: '/appmanage/userlist',
                component: USERLIST
                key: '31',
                text: '用户协议',
                path: '/platform/license',
                component: LicenseList
                key: '32',
                text: '帮助中心',
                path: '/platform/help',
                component: QaList
        key: 'g5',
        title: {
            icon: 'safety',
            text: '安全中心'
        exact: true,
        path: '/safety',
        children: [
                key: '36',
                text: '举报处理',
                path: '/safety/report',
                component: REPORT
                key: '37',
                text: '广播中心',
                path: '/safety/broadcast',
                component: BroadCast
        key: 'g6',
        title: {
            icon: 'user',
            text: '后台设置'
        exact: true,
        path: '/user',
        children: [
                key: '43',
                text: '个人设置',
                path: '/admin/setting',
                component: UserSetting
export const groupKey = RouterTree.map(item => item.key);
export default RouterTree;

动态菜单


DynamicTabMenu.js


import React, { Component } from 'react';
import styled from 'styled-components';
import { withRouter } from 'react-router-dom';
import { observer, inject } from 'mobx-react';
import { Button, Popover } from 'antd';
import TagList from './TagList';
const DynamicTabMenuCSS = styled.div`
    box-shadow: 0px 1px 1px -1px rgba(0, 0, 0, 0.2),
        0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12);
    width: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    background-color: #fff;
    .tag-menu {
        flex: 1;
    .operator {
        padding: 0 15px;
        flex-shrink: 1;
@inject('rstat')
@withRouter
@observer
class DynamicTabMenu extends Component {
    constructor(props) {
        super(props);
        this.state = {
            closeTagIcon: false // 控制关闭所有标签的状态
    // 关闭其他标签
    closeOtherTagFunc = () => {
        this.props.rstat.closeOtherTag();
    render() {
        return (
            <DynamicTabMenuCSS>
                <div className="tag-menu">
                    <TagList />
                    className="operator"
                    onClick={this.closeOtherTagFunc}
                    onMouseEnter={() => {
                        this.setState({
                            closeTagIcon: true
                    onMouseLeave={() => {
                        this.setState({
                            closeTagIcon: false
                    <Popover
                        placement="bottom"
                        title="关闭标签"
                        content={'只会保留当前访问的标签'}
                        trigger="hover">
                        <Button type="dashed" shape="circle" icon="close" />
                    </Popover>
            </DynamicTabMenuCSS>
export default DynamicTabMenu;

TagList.js


import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { observer, inject } from 'mobx-react';
import { Icon, Menu } from 'antd';
@inject('rstat')
@withRouter
@observer
class TagList extends Component {
    constructor(props) {
        super(props);
        this.state = {
            showCloseIcon: false, //  控制自身关闭icon
            currentIndex: '' // 当前的索引
    render() {
        const { rstat, history, location } = this.props;
        const { showCloseIcon, currentIndex } = this.state;
        return (
            <Menu selectedKeys={[rstat.activeRoute.childKey]} mode="horizontal">
                {rstat.historyCollection &&
                    rstat.historyCollection.map((tag, index) => (
                        <Menu.Item
                            key={tag.childKey}
                            onMouseEnter={() => {
                                this.setState({
                                    showCloseIcon: true,
                                    currentIndex: tag.childKey
                            onMouseLeave={() => {
                                this.setState({
                                    showCloseIcon: false
                            onClick={() => {
                                if (tag.pathname === location.pathname) {
                                    return;
                                } else {
                                    rstat.setIndex(index)
                                    history.push(tag.pathname);
                                    type="tag-o"
                                    style={{ padding: '0 0 0 10px' }}
                                {tag.childText}
                            </span>
                            {showCloseIcon &&
                            rstat.historyCollection.length > 1 &&
                            currentIndex === tag.childKey ? (
                                    type="close-circle"
                                    style={{
                                        position: 'absolute',
                                        top: 0,
                                        right: -20,
                                        fontSize: 24
                                    onClick={event => {
                                        event.stopPropagation();
                                        rstat.closeCurrentTag(index);
                                        history.push(
                                            rstat.activeRoute.pathname
                            ) : null}
                        </Menu.Item>
            </Menu>
export default TagList;
基于React、Mobx、Webpack 和 React-Router的项目模板。 #88
基于React、Mobx、Webpack 和 React-Router的项目模板。 #88
从hr口中了解react的状态管理库(mobx, recoil), 立马过来学习之mobx
从hr口中了解react的状态管理库(mobx, recoil), 立马过来学习之mobx
从hr口中了解react的状态管理库(mobx, recoil), 立马过来学习之recoil
从hr口中了解react的状态管理库(mobx, recoil), 立马过来学习之recoil
[react-native]mobx (react中全局数据管理库, 可以简单的实现数据的跨组件共享,类似于vue中的vuex)
[react-native]mobx (react中全局数据管理库, 可以简单的实现数据的跨组件共享,类似于vue中的vuex)
随着侧边栏的东东越来越多..本来不考虑的三级菜单,也需要考虑进去了; 一开始都是手动map去遍历对应的组件, 相关的的组id这些也是简单的判断下children就返回一个值; 有兴趣的瞧瞧
React 16.x折腾记 - (10) UmiJS 2.x + antd 重写后台管理系统记录的问题及解决姿势
用的是umi 2.x ,写起来挺舒服;顺带完善了上一版本后台的一些细节问题,功能等 umijs类似create-react-app, 也是一套方案的集合体,亮点很多.可以具体官网去看 • 声明式的路由(nuxtjs既视感) • dva(基于redux+redux-saga的封装方案):写起来有vuex的感觉; 主要记录我在过程中遇到的问题及解决的姿势,技术栈 antd 3.11.x + umi 2.x + react 16.7
React 16.x折腾记 - (9) 基于Antd+react-router-breadcrumbs-hoc封装一个小巧的面包屑组件
没有什么技术难度,只是比官方的文档多了一丢丢的判断和改造; 用了react-router-breadcrumbs-hoc,约定式和配置式路由路由皆可用, 只要传入的符合规格的数据格式即可
React 16.x折腾记 - (8) 基于React+Antd封装选择单个文章分类(从构建到获取)
随着管理的文章数量增多,默认的几个分类满足不了现状了,趁着重构的过程把相关的功能考虑进去 本来想自己从头写过一个,看了下Antd有内置该类型的控件了,就没必要自己造了 一般自己写,肯定优先考虑数组对象格式[{tagName:'a',value:1}]; Antd提供的是纯数组,[string,string],那如何不改变它提供的格式情况下拿到我们想要的! 拓展部分我们需要的东东,有兴趣的瞧瞧,没兴趣的止步.. React 16.x折腾记 - (7) 基于React+Antd封装聊天记录(用到React的memo,lazy, Suspense这些)
在重构的路上,总能写点什么东西出来 , 这组件并不复杂,放出来的总觉得有点用处 一方面当做笔记,一方面可以给有需要的人; 有兴趣的小伙伴可以瞅瞅。
React 16.x折腾记 - (6) 基于React 16.x+ Antd 3.x封装的一个声明式的查询组件(实用强大)
最近把新的后台系统写好了..用的是上篇文章的技术栈(mobx+react16); 但是感觉mobx没有想象中的好用,看到umi 2.x了,就着手又开始重构了。 仔细梳理了下上个系统,发现可以抽离的东西不少 此篇文章是我针对我们的搜索条件抽离的一个组件,仅供参考。
自己搭的脚手架,坑都是一步一步踩完的; 技术栈: react@16.6.0/ react-router-dom@v4 / webpack^4.23.1(babel7+) 闲话不多说,直入主题,有兴趣的可以瞧瞧,没兴趣的止步,节约您的时间.
React 16.x折腾记 - (1) React Router V4 和antd侧边栏的正确关联及动态title的实现
一如既往,实战出真理,有兴趣的可以瞧瞧,没兴趣的大佬请止步于此。