react-router-dom 实战(二)

385 阅读3分钟

尝试不实用React Router

import React from 'react';
import ReactDOM from 'react-dom';

const About = () => {
  return <div>About</div>;
};
const Inbox = () => {
  return <div>Inbox</div>;
};
const Home = () => {
  return <div>Home</div>;
};

const App = () => {
  const [hash, setHash] = React.useState('');

  const updateMenu = () => {
    const hashValue = window.location.hash.replace('#', '') || 'home';
    setHash(hashValue);
  };

  React.useEffect(() => {
    updateMenu();
    window.addEventListener('hashchange', updateMenu);
    return () => {
      window.removeEventListener('hashchange', updateMenu);
    };
  }, []);

  const renderChild = () => {
    let Child;
    switch (hash) {
      case '/about':
        Child = About;
        break;
      case '/inbox':
        Child = Inbox;
        break;
      default:
        Child = Home;
    }
    return <Child />;
  };

  return (
    <div>
      <h1>App</h1>
      <ul>
        <li>
          <a href="#/about">About</a>
        </li>
        <li>
          <a href="#/inbox">Inbox</a>
        </li>
      </ul>
      {renderChild()}
    </div>
  );
};

ReactDOM.render(<App />, document.body);

image.png

当 URL 的 hash 部分(指的是 # 后的部分)变化后,<App> 会根据 hash 来渲染不同的 <Child>。看起来很直接,但它会随着业务的发展很快会变得复杂起来。 为了让我们的 URL 解析变得更智能,我们需要编写很多代码来实现指定 URL 应该渲染哪一个嵌套的 UI 组件分支。

使用router改造

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, BrowserRouter, Link, Switch } from 'react-router-dom';

const About = () => {
  return <div>About</div>;
};
const Inbox = () => {
  return <div>Inbox</div>;
};
const home = () => {
  return <div>home</div>;
};

const App = () => {
  return (
    <div>
      <BrowserRouter>
        <h1>App</h1>
        <ul>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/inbox">Inbox</Link>
          </li>
        </ul>
        <Switch>
          <Route exact={true} path="/" component={home} />
          <Route exact={true} path="/about" component={About} />
          <Route exact={true} path="/inbox" component={Inbox} />
        </Switch>
      </BrowserRouter>
    </div>
  );
};

ReactDOM.render(<App />, document.body);

通过上面的配置,这个应用知道如何渲染下面三个 URL

URL组件
/home
/aboutAbout
/inboxInbox

react-router-dom 列子

URL Parameters(路由参数)

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, BrowserRouter, Link, Switch, useParams } from 'react-router-dom';

const About = () => {
  return <div>About</div>;
};
const Inbox = () => {
  return <div>Inbox</div>;
};
const home = () => {
  return <div>home</div>;
};
const Child = () => {
  console.log('useParams:', useParams());
  const { id } = useParams() as any;
  return <div>ID:{id}</div>;
};

const App = () => {
  return (
    <div>
      <BrowserRouter basename="test">
        <h1>App</h1>
        <ul>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/inbox">Inbox</Link>
          </li>
          <li>
            <Link to="/child/yahoo">Yahoo</Link>
          </li>
          <li>
            <Link to="/child/modus-create">Modus Create</Link>
          </li>
        </ul>
        <Switch>
          <Route exact={true} path="/" component={home} />
          <Route exact={true} path="/about" component={About} />
          <Route exact={true} path="/inbox" component={Inbox} />
          <Route exact={true} path="/child/:id" children={<Child />} />
        </Switch>
      </BrowserRouter>
    </div>
  );
};

ReactDOM.render(<App />, document.body);

useParams返回 URL 参数的键/值对对象。使用它来访问match.params当前<Route>.

image.png

tip:键/值对对象的key(id),与 path="/child/:id"的ID对应,如:path="/child/:slug", 则{slug: xxx}

Nesting(嵌套)

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, BrowserRouter, Link, Switch, useParams, useRouteMatch } from 'react-router-dom';

const Topics = () => {
  console.log('useRouteMatch:', useRouteMatch());
  const { path, url } = useRouteMatch();

  return (
    <div>
      <h2>Topics</h2>
      <ul>
        <li>
          <Link to={`${url}/rendering`}>Rendering with React</Link>
        </li>
        <li>
          <Link to={`${url}/components`}>Components</Link>
        </li>
        <li>
          <Link to={`${url}/props-v-state`}>Props v. State</Link>
        </li>
      </ul>

      <Switch>
        <Route exact path={path}>
          <h3>Please select a topic.</h3>
        </Route>
        <Route path={`${path}/:topicId`}>
          <Topic />
        </Route>
      </Switch>
    </div>
  );
};

const Topic = () => {
  const { topicId } = useParams() as any;

  return (
    <div>
      <h3>{topicId}</h3>
    </div>
  );
};
const home = () => {
  return <div>home</div>;
};

const App = () => {
  return (
    <div>
      <BrowserRouter basename="test">
        <h1>App</h1>
        <ul>
          <li>
            <Link to="/">home</Link>
          </li>
          <li>
            <Link to="/topics">topics</Link>
          </li>
        </ul>
        <Switch>
          <Route exact={true} path="/" component={home} />
          <Route path="/topics" component={Topics} />
        </Switch>
      </BrowserRouter>
    </div>
  );
};

ReactDOM.render(<App />, document.body);

useRouteMatch 尝试以与<Route>相同的方式匹配当前URL。它主要用于访问匹配数据,而无需实际渲染<Route>

image.png

注意exact的使用

重定向(身份验证)

import React, { useContext, createContext, useState } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Switch, Route, Link, Redirect, useHistory, useLocation } from 'react-router-dom';

/*
 * 使用createContext进行跨组件间数据传递方法
 *首先需要在父组件中定义一个容器和需要传递的默认值,
 *然后通过Provider(生产者,简单来说就是定义数据的东西),
 *定义共享的数据,然后通过Consumer(消费者,就是子组件或孙子组件来使用),具体实现代码如下:
 */
const authContext = createContext({});

const ProvideAuth = (props: any) => {
  // 返回一个权限管理对象
  const auth = useProvideAuth();
  return <authContext.Provider value={auth}>{props.children}</authContext.Provider>;
};

// react16.8版本之后增加了hooks,可以使用hooks中的useContext来获取消费者
const useAuth = () => {
  return useContext(authContext);
};

// 用于管理权限下发数据
const useProvideAuth = () => {
  const [user, setUser] = useState(null as null | string);

  const signin = (cb: () => void) => {
    // 登录操作
    setUser('user');
    // 登录成功后的操作
    setTimeout(cb, 100);
  };

  const signout = (cb: () => void) => {
    // 退出操作
    setUser(null);
    setTimeout(cb, 100);
  };

  return {
    user,
    signin,
    signout,
  };
};

const AuthButton = () => {
  let history = useHistory();
  let auth = useAuth() as any;

  return auth.user ? (
    <p>
      欢迎!
      <br />
      用户:{auth.user}
      <br />
      <button
        onClick={() => {
          auth.signout(() => history.push('/'));
        }}
      >
        Sign out
      </button>
    </p>
  ) : (
    <p>您没有登录</p>
  );
};

// 重定向到登录
// 如果您尚未通过身份验证,请选登录
const PrivateRoute = (props: any) => {
  // 获取权限数据
  let auth = useAuth() as any;
  return (
    <Route
      {...props.rest}
      // 存在用户则渲染否则重定向login
      render={({ location }) =>
        auth.user ? (
          props.children
        ) : (
          <Redirect
            to={{
              pathname: '/login',
              state: { from: location },
            }}
          />
        )
      }
    />
  );
};

const PublicPage = () => {
  return <h3>公开页面</h3>;
};

const ProtectedPage = () => {
  return <h3>保护页面</h3>;
};

function LoginPage() {
  let history = useHistory();
  let location = useLocation() as any;
  let auth = useAuth() as any;

  let { from } = location.state || { from: { pathname: '/' } };
  let login = () => {
    auth.signin(() => {
      history.replace(from);
    });
  };

  return (
    <div>
      <p>您必须登录才能查看该页面 {from.pathname}</p>
      <button onClick={login}>Log in</button>
    </div>
  );
}
export default function AuthExample() {
  return (
    <ProvideAuth>
      <Router>
        <div>
          <AuthButton />
          <ul>
            <li>
              <Link to="/public">公共页</Link>
            </li>
            <li>
              <Link to="/protected">保护页</Link>
            </li>
          </ul>

          <Switch>
            <Route path="/public">
              <PublicPage />
            </Route>
            <Route path="/login">
              <LoginPage />
            </Route>
            <PrivateRoute path="/protected">
              <ProtectedPage />
            </PrivateRoute>
          </Switch>
        </div>
      </Router>
    </ProvideAuth>
  );
}
ReactDOM.render(<AuthExample />, document.body);

Redirect 重定向, 如果使用了Switch,请把Redirect放后面

<Route path="/home" 
render={() => (
  <Switch>
    <Route path="/home/page1" />
    <Route path="/home/page2" />
    <Redirect to="/home/page1" />
  </Switch>
)} 
/>

无匹配 (404)

const NoMatch = () => {
  let location = useLocation();

  return (
    <div>
      <h3>
        没有与之 <code>{location.pathname}</code> 对应的页面,404
      </h3>
    </div>
  );
};
<Switch>
  ...
  <Route path="*" component={NoMatch} />
</Switch>

path="*",无匹配时候渲染。如有兜底页面,则可以重定向到404页面

    <Route render={() => <Redirect to="/404" />} />

侧边栏(sidebar)

import React from "react";
import ReactDOM from "react-dom";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

const routes = [
  {
    path: "/",
    exact: true,
    sidebar: () => <div>home!</div>,
    main: () => <h2>Home</h2>
  },
  {
    path: "/bubblegum",
    sidebar: () => <div>bubblegum!</div>,
    main: () => <h2>Bubblegum</h2>
  },
  {
    path: "/shoelaces",
    sidebar: () => <div>shoelaces!</div>,
    main: () => <h2>Shoelaces</h2>
  }
];
const SidebarExample = () => {
  return (
    <Router>
      <div style={{ display: "flex" }}>
        <div
          style={{
            padding: "10px",
            width: "40%",
            background: "#f0f0f0"
          }}
        >
          <ul style={{ listStyleType: "none", padding: 0 }}>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/bubblegum">Bubblegum</Link>
            </li>
            <li>
              <Link to="/shoelaces">Shoelaces</Link>
            </li>
          </ul>

          <Switch>
            {routes.map((route, index) => (
              <Route
                key={index}
                path={route.path}
                exact={route.exact}
                children={<route.sidebar />}
              />
            ))}
          </Switch>
        </div>

        <div style={{ flex: 1, padding: "10px" }}>
          <Switch>
            {routes.map((route, index) => (
              <Route
                key={index}
                path={route.path}
                exact={route.exact}
                children={<route.main />}
              />
            ))}
          </Switch>
        </div>
      </div>
    </Router>
  );
}
ReactDOM.render(<SidebarExample />, document.body);

效果:

image.png