React第八章 react-router (版本4.x)

333 阅读5分钟

第一节    react-router安装&介绍

1、安装
npm install react-router-dom --save
2、学习

react-router

react-router中文

3、特点
3.1、秉承react一切皆组件,路由也是组件。

路由器 - Router

链接 - Link

路由 - Route

独占 - Switch

重定向 - Redirect

3.2、分布式的配置,分布在你页面的每个角落。
3.3、包含式配置,可匹配多个路由。
3.4、渲染优先级:

children>component>render 这三种⽅式互斥,你只能⽤用一种。

3.4.1children:func

有时候,不管location是否匹配,你都需要渲染一些内容,这时候你可以⽤用children。

除了不管location是否匹配都会被渲染之外,其它⼯作⽅法与render完全⼀样。

****

3.4.2render:func

但是当你用render的时候,你调用的只是个函数。 只在当location匹配的时候渲染。

3.4.3component: component****

只在当location匹配的时渲染。

3.5、404页面

设定⼀个没有path 的路由在路由列表最后面,表示⼀定匹配

{/* 添加Switch表示仅匹配⼀一个*/} 
<Switch>
    {/* 根路路由要添加exact,实现精确匹配 */} 
    <Route
    exact
    path="/"
    component={HomePage}
    />
    <Route path="/user" component={UserPage} />
    <Route component={EmptyPage} />
</Switch>

第二节    react-router基本使用

1、使用步骤

step1:引入顶层路由组件包裹根组件。

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom';
import RtouerSamples from './react-router/RouterSamples';
ReactDOM.render(
    <div>
        <h4>使用react-router</h4>
        <BrowserRouter>
            <RtouerSamples></RtouerSamples>
        </BrowserRouter>
    </div>, document.getElementById('root')
)

step2:引入Link组件编写路由导航。

<Link to="/">首页</Link>

to: 指定要到的路径

step3:引入Route组件编写导航配置。

<Route exact path="/" component={Home}></Route>

// path: 配置路径

// component: 配置对应显示的组件,也可以使用render函数,优先级低于component

// exact 精准完全匹配,避免包含匹配,到多个组件

step4:引入Switch组件,默认只匹配上一个Route即可返回。

2、完整代码

RouterSamples.js

import React from 'react'

import { Switch, Link, Route } from 'react-router-dom';
function Home() {
    return <div>首页</div>
}
function Classes() {
    return <div>课程列表</div>
}
function Mine() {
    return <div>个人中心</div>
}
function NoFound() {
    return <div>404</div>
}
export default function routerSamples() {
    return (
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="classes">课程</Link></li>
                <li><Link to="mine">我的</Link></li>
                <li><Link to="sssssssssss">其他找不到的页面</Link></li>
            </ul>
            <div>
                <Switch>
                    <Route exact path="/" component={Home}></Route>
                    <Route path="/classes" component={Classes}></Route>
                    <Route path="/mine" component={Mine}></Route>
                    <Route component={NoFound}></Route>
                </Switch>
            </div>
        </div>
    )
}

第三节    react-router路由传参取参

1、方式一:声明式导航路由配置时-添加路由参数

配置:

<Route path="/detail/:goodsId" component={GoodsDetail}></Route>

传递:

<Link to="/detail/g100986">查看商品详情</Link>

获取:

解构路由器对象里的match出来(match获取参数信息)。

{match.params.goodId}。

2、方式二:编程式导航路由传参和获取

解构路由器对象获取到导航对象 history (用作命令行导航)

通过事件执行history.push({pathname:'/', state:{backId:'g100986'}}) 传递的参数装载在state里面

从路由信息解构出location(当前的url信息)对象location.state进行获取

3、完整代码

RouterSamples.js

...
function Home({ location }) {
    return <div>
        <p>首页</p>
        <p>{location.state && `返回的课程是:${location.state.backId}`}</p>
    </div>
}
function Classes() {
    return <div>
        <ul className="course">
            <li>
                <Link to="/detail/vue">vue.js</Link>
            </li>
            <li>
                <Link to="/detail/react">react.js</Link>
            </li>
        </ul>
    </div>
}
function Detail({ match, history }) {
    return <div>
        <p>{match.params.goodsId}详细课程</p>
        <p onClick={() => { history.push({ pathname: '/', state: { backId: match.params.goodsId } }) }}>返回</p>
    </div>
}
export default function routerSamples() {
    return (
        <Switch>
          <Route exact path="/" component={Home}></Route>
          <Route path="/classes" component={Classes}></Route>
          <Route path="/detail/:goodsId" component={Detail}></Route>
        </Switch>
    )
}

第四节    react-router路由嵌套&重定向

1、组件内再次使用做路由结构。

2、使用做路由默认选中。

// 一级路由
<Route path="/mine" component={Mine}></Route>

// 一级路由对应的组件
function Mine() {
    return <div>
        <ul className="course">
            <li>
                <Link to="/mine/order">个人订单</Link>
            </li>
            <li>
                <Link to="/mine/userInfo">个人中心</Link>
            </li>
        </ul>
      <Route path="/mine/order" component={()=><div>个人订单的页面</div>}></Route>
      <Route path="/mine/userInfo" component={()=><div>个人中心的页面</div>}></Route>
      <Redirect to="/mine/userInfo"></Redirect>
    </div>
}

第五节    react-router路由守卫

1、路由守卫:路由拦截(保证串行),当我们有些页面需要登录后才能有权限访问的时候,就要启用路由守卫。

区分:Svip 和 vip 和 普通用户 要用路由守卫。

2、路由守卫:类似于Axios 的 前面req请求拦截和res的返回拦截。但是有区别,关系到组件的生命周期触发。

3、路由守卫:React中其实也是一个组件, 最终返回的还是Route组件


一、包装组件

实现效果:点击个人中心判断是否登录,没有登录、去登录;登录后,从哪里来回哪里去。

...
 {/* 路由配置 */}
<Switch>
  <Route exact path="/login" component={Login}></Route>
  {/* <Route path="/mine" component={Mine}></Route> */}
  <RouteGuard path="/mine" component={Mine}></RouteGuard>
</Switch>

// 接口状态
const auth = {
    isLogin: false,
    login(callback) {
        this.isLogin = true;
        setTimeout(callback, 1000);
    }
}
// 编写路由守卫进行权限控制
class RouteGuard extends Component {
    render() {
        const { component: Component, ...otherProps } = this.props;
        return (
            <Route {...otherProps} render={(props) => (
                auth.isLogin ? <Component {...props}></Component> : (<Redirect to={
                    { pathname: '/login', state: { from: props.location.pathname } }
                }></Redirect>)
            )}></Route>
        )
    }
}
// 登录组件
class Login extends Component {
    login = () => {
        // 方法1、可以用setDate()触发render函数,在render函数中<Redirect>根据this.state.isLogin做跳转。
        // 方法2、简便写法使用history.push。
        // 回调地址
        const pathname = (this.props.location.state && this.props.location.state.from) || '/'
        auth.login(() => {
            this.props.history.push({pathname})
        })
    }
    render() {
        return <div>
            <p>请先登录</p>
            <button onClick={this.login}>登录</button>
        </div>
    }
}

整体代码

import React, { Component } from 'react'

import { Switch, Link, Route, Redirect } from 'react-router-dom';
function Home({ location }) {
    return <div>
        <p>首页</p>
        <p>{location.state && `返回的课程是:${location.state.backId}`}</p>
    </div>
}
function Classes() {
    return <div>
        <h5>课程列表</h5>
        <ul className="course">
            <li>
                <Link to="/detail/vue">vue.js</Link>
            </li>
            <li>
                <Link to="/detail/react">react.js</Link>
            </li>
            <li>
                <Link to="/detail/angular">angular.js</Link>
            </li>
            <li>
                <Link to="/detail/node">node.js</Link>
            </li>
        </ul>
    </div>
}
function Mine() {
    return <div>
        <h5>个人中心</h5>
        <ul className="course">
            <li>
                <Link to="/mine/order">个人订单</Link>
            </li>
            <li>
                <Link to="/mine/userInfo">个人中心</Link>
            </li>
        </ul>
        {/* 子路由配置 */}
        <Route path="/mine/order" component={() => <div>个人订单的页面</div>}></Route>
        <Route path="/mine/userInfo" component={() => <div>个人中心的页面</div>}></Route>
        <Redirect to="/mine/userInfo"></Redirect>
    </div>
}
function Detail({ match, history }) {
    // 没有解构
    // console.log(props);
    return <div>
        <p>{match.params.goodsId}详细课程</p>
        <p onClick={() => { history.push({ pathname: '/', state: { backId: match.params.goodsId } }) }}>返回</p>
    </div>
}
function NoFound() {
    return <div>404</div>
}

// 接口状态
const auth = {
    isLogin: false,
    login(callback) {
        this.isLogin = true;
        setTimeout(callback, 1000);
    }
}
// 编写路由守卫进行权限控制
class RouteGuard extends Component {
    render() {
        const { component: Component, ...otherProps } = this.props;
        return (
            <Route {...otherProps} render={(props) => (
                auth.isLogin ? <Component {...props}></Component> : (<Redirect to={
                    { pathname: '/login', state: { from: props.location.pathname } }
                }></Redirect>)
            )}></Route>
        )
    }
}
// 登录组件
class Login extends Component {
    login = () => {
        // 方法1、可以用setDate()触发render函数,在render函数中<Redirect>根据this.state.isLogin做跳转。
        // 方法2、简便写法使用history.push。
        // 回调地址
        const pathname = (this.props.location.state && this.props.location.state.from) || '/'
        auth.login(() => {
            this.props.history.push({pathname})
        })
    }
    render() {
        return <div>
            <p>请先登录</p>
            <button onClick={this.login}>登录</button>
        </div>
    }
}
export default function routerSamples() {
    return (
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="classes">课程</Link></li>
                <li><Link to="mine">我的</Link></li>
                <li><Link to="sssssssssss">其他找不到的页面</Link></li>
            </ul>
            <div>
                {/* 路由配置 */}
                <Switch>
                    <Route exact path="/" component={Home}></Route>
                    <Route exact path="/login" component={Login}></Route>
                    <Route path="/classes" component={Classes}></Route>
                    {/* <Route path="/mine" component={Mine}></Route> */}
                    <RouteGuard path="/mine" component={Mine}></RouteGuard>
                    <Route path="/detail/:goodsId" component={Detail}></Route>
                    <Route component={NoFound}></Route>
                </Switch>
            </div>
        </div>
    )
}

二、嵌套路由

step1、 入口文件

index.js

ReactDOM.render(
  <Provider {...stores}>
    <Page />
  </Provider>,
  document.getElementById('root')
);

step2、通过Page.tsx,做路由区分:/app:业务,/login:登录页面,/login-guide:过期, /404

...
export default function Page() {
    const urlParms = getUrlAllParams();
    useEffect(() => {
        let key = '/app/shop/index';
        async function loginApp(code: any, plat: any) {
            history.push(key);
        }
      	// 根路径去登陆
        if (history.location.pathname === '/') {
            // 优先使用code
            if (code && state) {
                loginApp(code, state);
                return;
            }
            // 其他有无token;
            let userInfo: any = {};
            if (localStorage.userInfo) {
                userInfo = JSON.parse(localStorage.userInfo);
            }
            if (userInfo.dsfToken) {
                store.userStore.selectMenu(key);
                history.push(key);
            } else {
                history.push('/login-guide');
            }
        }
      	// 非根路径,自动走路由,ajax调用,统一处理接口失败,后自动跳转到登录页。
    },[]);
    return (
        <Router>
            <ConfigProvider locale={zhCN}>
                <Switch>
                    <Route exact path="/" />
                    <Route path="/app" component={App} />
                    <Route path="/login-guide" component={LoginGuide} />
                    <Route component={NotFound} />
                </Switch>
            </ConfigProvider>
        </Router>
    )
};

step3、业务组件全部是/app子路由

App.tsx

....
 render() {
        const { collapsed } = this.state;
        const { isTopMenu, completeProductGuide } = this.props.userStore;
        return <div className="app">
            <Layout className="app-section">
                {isTopMenu ? '' : <SiderCustom collapsed={collapsed} {...this.props}></SiderCustom>}
                <Layout className="site-layout">
                    <Header className={isTopMenu ? 'top-layout-background' : 'site-layout-background'}>
                        {isTopMenu ? '' : React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
                            className: 'trigger',
                            onClick: this.toggle,
                        })}
                        <UserInfo></UserInfo>
                    </Header>
                    <PersonGuide></PersonGuide>
                    <Content style={{ position: 'relative' }}>
                        <Routes />
                    </Content>
                </Layout>
                {completeProductGuide ? '' : <ProductGuide></ProductGuide>}
            </Layout>
        </div >
    }

routes/index.tsx

/*
 * @Author: lxq
 * @Date: 2020-06-11 21:32:55
 */
import React, { Component } from 'react';
import { Route, HashRouter as Router, Redirect, Switch } from 'react-router-dom';
import queryString from 'query-string';
import DocumentTitle from 'react-document-title';
import AllComponents from '../components';
import routesConfig, { IFMenuBase, IFMenu } from './config';

type BaseRouterProps = {
    auth?: any
};
type BaseRouterState = {};

class BaseRouter extends Component<BaseRouterProps, BaseRouterState> {
    // requireLogin = (component: React.ReactElement, permit = false) => {
    //     if (permit) {
    //         return <Redirect to={'/login-guide'} />;
    //     }
    //     return component;
    // };
  
    mergeQueryToProps = (props: any) => {
        const queryReg = /?\S*/g;

        const matchQuery = (reg: RegExp) => {
            const queryParams = window.location.hash.match(reg);
            return queryParams ? queryParams[0] : '{}';
        };
        const removeQueryInRouter = (props: any, reg: RegExp) => {
            const { params } = props.match;
            Object.keys(params).forEach((key) => {
                params[key] = params[key] && params[key].replace(reg, '');
            });
            props.match.params = { ...params };
        };

        props = removeQueryInRouter(props, queryReg);

        const merge = {
            ...props,
            query: queryString.parse(matchQuery(queryReg)),
        };
        return merge;
    };
  
    route = (r: IFMenuBase) => {
        const Component = r.component && AllComponents[r.component];
        return (
            <Route
                key={r.key}
                exact
                path={r.key}
                render={(props: any) => {
                    // 重新包装组件
                    const wrappedComponent = (
                        <DocumentTitle title={r.title}>
                            <Component {...this.mergeQueryToProps(props)} />
                        </DocumentTitle>
                    );
                    // return this.requireLogin(wrappedComponent, r.requireAuth);
                    return wrappedComponent;
                }}
            />
        );
    };

    iterteMenu = (r: IFMenu) => {
        const subRoute = (r: IFMenu): any => {
            return (r.subs && r.subs.map((subR: IFMenu) => (subR.subs ? subRoute(subR) : this.route(subR))));
        };
        return r.component ? this.route(r) : subRoute(r);
    };

    createRoute = (key: string) => {
        return routesConfig[key].map(this.iterteMenu);
    };

    register = () => {
        return Object.keys(routesConfig).map((key) => this.createRoute(key))
    }
    
    render() {
      	const routeList = [
            <Route key="/app" exact path="/app" render={() => <Redirect to="/app/shop/index" push />} />,
            this.register(),
            <Route key="/404" render={() => <Redirect to="/404" />} />
        ]
        return <div className="app-content">
            <Router>
                <Switch>{routelist}</Switch>
            </Router>
        </div>
    }
}

export default BaseRouter;

routes/config.ts

/*
 * @Author: lxq
 * @Date: 2021-01-04 17:14:31
 * @LastEditors: lxq
 * @LastEditTime: 2021-05-19 14:40:05
 */
/**
 * Created by lxq on 2020/4/23.
 */
export interface IFMenuBase {
    key: string;
    title: string;
    icon?: string;
    component?: string;
    query?: string;
    requireAuth?: boolean;
    route?: string;
    type?: string // 'outLink' 外部链接
}

export interface IFMenu extends IFMenuBase {
    subs?: IFMenu[];
}

const routesConfig: {
    menus: IFMenu[];
    others: IFMenu[] | [];
    [index: string]: any;
} = {
    // 菜单路由
    menus: [
        // 一级菜单
        { key: '/app/shop/index', title: '店铺利润', icon: 'dianpulirun', component: 'ShopIndex' },
        { key: '/app/shop/ProfitAnalysis', title: '利润分析', icon: 'lirunfenxi', component: 'ProfitAnalysis' },
        { key: '/app/trans/index', title: '店铺交易', icon: 'dianpujiaoyi', component: 'TransIndex' },
        { key: '/app/cost/index', title: '成本费用', icon: 'chengbenfeiyong', component: 'CostIndex' },
        // 二级菜单
        {
            key: '/app/demo', title: 'demo案例', icon: 'shezhi',
            subs: [
                { key: '/app/demo/router-demo', title: '1、动态路由示例', icon: 'shezhi',component: 'DemoRouter' },
                { key: '/app/demo/store-demo', title: '2、mbox数据流示例', icon: 'shezhi',component: 'DemoStore' },
                { key: '/app/demo/axios-demo', title: '3、axios示例', icon: 'shezhi',component: 'DemoAxios' },
                { key: '/app/demo/lodash-demo', title: '4、lodash工具函数示例', icon: 'shezhi',component: 'DemoLodash' },
                { key: '/app/demo/iconfont-demo', title: '5、主题切换、图标字体', icon: 'shezhi',component: 'DemoIconfont' },
                { key: '/app/demo/img-demo', title: '6、样式、图片引入示例', icon: 'shezhi',component: 'DemoImg' },
                { key: '/app/demo/recharts-demo', title: '7、recharts示例', icon: 'shezhi',component: 'DemoRecharts' },
                { key: '/app/demo/ants-demo', title: '8、ants示例', icon: 'shezhi',component: 'DemoAnts' },
                { key: '/app/demo/ants-animate', title: '9、animate示例', icon: 'shezhi',component: 'DemoAnimate' },
            ],
        },
        { key: '/app/shop/ShopSetting', title: '设置', icon: 'shezhi', component: 'ShopSetting' },
        { key: 'https://doc-qdd.static.chanjet.com', title: '帮助中心', type: 'outLink' },
        { key: 'https://app.1688.com', title: '应用市场', type: 'outLink' },
    ],
    // 非菜单相关路由
    others: [
        // 动态路由示例
        { key: '/app/demo/icons/:id', title: '路由返回', icon: '', component: 'DemoIcon' },
    ],
};

export default routesConfig;

step4、业务组件中统一处理接口返回的登录过期、跳转到/login-guide

//request拦截器
instance.interceptors.request.use()

//respone拦截器
instance.interceptors.response.use()