React路由原理
- 不同的路径渲染不同的组件
- 有两种实现方式
- HashRouter: 利用hash实现路由切换
- BrowserRouter:实现h5 Api实现路由的切换
HashRouter
- 利用hash实现路由切换
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>React App</title>
</head>
<body>
<div id="root"></div>
<a href="#/a">去a</a>
<a href="#/b">去b</a>
<script>
window.addEventListener('hashchange', () => {
console.log(window.location.hash);
});
</script>
</body>
</html>
BrowserRouter
- 利用h5 Api实现路由的切换
history
- history对象提供了操作浏览器历史会话的接口
- historylength属性声明了浏览器历史列表中的元素数量
- pushState HTML5引进了history.pushState() 和history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate配合使用
- onpopstate window.onpopstate是popstate事件在window对象上的事件处理程序
pushState
- pushState会往history中写入一个对象,他造成的结果便是,History length +1 、url改变、该索引History对应有一个State对象,这个是会若是点击浏览器的后腿,便会触发popstate事件,将刚刚的存入数据对象读出
- poshState会改变History
- 每次使用时候会为该索引的State加入我们自定义数据
- 每次我们会根据State的信息还原当前的view,于是用户点击后退便有了与浏览器后退前进一致的感受
- pushState()需要三个参数:一个状态对象,一个标题(目前被忽略),和可选的一个URL
- 调用history.pushState()或者history.replaceState()不会触发popState事件,popState事件只会在浏览器某些行为下触发,比如点击后退、前进按钮(或者在Javascript中调用history.back()、history.forward()、history.go()方法)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script>
window.onpopstate = (event) => {
console.log({ state: event.state, pathname: window.location.pathname, type: 'popstate' });
};
window.onpushstate = (event) => {
console.log(event);
};
(function (history) {
var pushState = history.pushState;
history.pushState = function (state, title, pathname) {
if (typeof window.onpushstate == "function") {
window.onpushstate({ state, pathname, type: 'pushstate' });
}
return pushState.apply(history, arguments);
};
})(window.history);
//绑定事件处理函数.
setTimeout(() => {
history.pushState({ page: 1 }, "title 1", "/page1");
}, 1000);
setTimeout(() => {
history.pushState({ page: 2 }, "title 2", "/page2");
}, 2000);
setTimeout(() => {
history.replaceState({ page: 3 }, "title 3", "/page3");
}, 3000);
setTimeout(() => {
history.back();
}, 4000);
setTimeout(() => {
history.go(1);//向前进一步
}, 5000);
// page1 => page2 => page3 => page2 => page3
</script>
</body>
</html>
跑通路由
src\index.tsx
cnpm i react-router-dom @types/react-router-dom path-to-regexp -S
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
<Router>
<div>
<Route path="/" component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</div>
</Router>
, document.getElementById('root'));
src\components\Home.tsx
import React, { Component } from 'react';
export default class Home extends Component {
render() {
return (
<div>Home</div>
)
}
}
src\components\User.tsx
import React, { Component } from 'react';
import { RouteComponentProps } from '../react-router-dom';
interface Params { }
type Props = RouteComponentProps<Params> & {
}
export default class User extends Component {
render() {
console.log(this.props);
return (
<div>User</div>
)
}
}
实现路由
src\history\index.tsx
export * from './types';
src\history\types.tsx
export interface Location {
pathname: string;
state?: any;
}
export interface History {
location: Location;
}
src\react-router-dom\index.tsx
import HashRouter from './HashRouter';
import Route from './Route';
export {
HashRouter,
Route
}
export * from './types';
src\react-router-dom\types.tsx
import { History } from '../history';
export type Location = History['location'];
export interface ContextValue {
location?: Location
}
export interface match<Params = {}> {
params: Params;
isExact: boolean;
path: string;
url: string;
}
export interface RouteComponentProps<Params = {}> {
history: History;
location: Location;
match: match<Params>;
}
src\react-router-dom\context.tsx
import { ContextValue } from './types';
import { createContext } from 'react';
export default createContext<ContextValue>({});
src\react-router-dom\HashRouter.tsx
import React, { Component } from 'react'
import Context from './context';
import { ContextValue, Location } from './types';
interface Props { }
interface State {
location: Location;
}
export default class HashRouter extends Component<Props, State> {
state = {
location: {
pathname: window.location.hash.slice(1),
state: null
}
}
componentWillMount() {
window.addEventListener('hashchange', (event: HashChangeEvent) => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
});
});
window.location.hash = window.location.hash || '/';
}
render(): React.ReactNode {
let value: ContextValue = {
location: this.state.location
}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
)
}
}
src\react-router-dom\Route.tsx
import React, { Component,ComponentType } from 'react';
import RouterContext from './context';
interface Props {
path: string;
component: ComponentType<any>
}
export default class Route extends Component<Props> {
static contextType = RouterContext;
render() {
let { path, component: Component } = this.props;
let pathname = this.context.location.pathname;
if (pathname.startsWith(path)) {
return <Component />
} else {
return null;
}
}
}
实现Link
src\index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
+import { HashRouter as Router, Route, Link } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
<Router>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/user">User</Link></li>
<li><Link to="/profile">Profile</Link></li>
</ul>
<Route path="/" component={Home} exact />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</div>
</Router>
, document.getElementById('root'));
src\history\types.tsx
export interface Location {
pathname: string;
state?: any;
}
export interface History {
location: Location;
push(path: string, state?: any): void;
}
src\react-router-dom\types.tsx
import { History } from '../history';
export type Location = History['location'];
export interface ContextValue {
location?: Location;
+ history?: History
}
export interface match<Params = {}> {
params: Params;
isExact: boolean;
path: string;
url: string;
}
export interface RouteComponentProps<Params = {}> {
history: History;
location: Location;
match?: match<Params>;
}
src\react-router-dom\Link.tsx
import React, { Component } from 'react'
import RouterContext from './context';
import { LocationDescriptor } from '../history';
export interface LinkProps {
to: LocationDescriptor;
}
export default class Link extends Component<LinkProps> {
static contextType = RouterContext;
render() {
return (
<a {...this.props} onClick={() => this.context.history.push(this.props.to)}>{this.props.children}</a>
)
}
}
src\react-router-dom\HashRouter.tsx
import React, { Component } from 'react'
import Context from './context';
import { ContextValue } from './types';
+import { LocationDescriptor, Location } from '../history';
interface Props { }
interface State {
location: Location;
}
export default class HashRouter extends Component<Props, State> {
locationState: any
state = {
location: {
+ pathname: window.location.hash.slice(1),
+ state: null
}
}
componentWillMount() {
window.addEventListener('hashchange', (event: HashChangeEvent) => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/',
+ state: this.locationState
}
});
});
window.location.hash = window.location.hash || '/';
}
render(): React.ReactNode {
+ let that = this;
let value: ContextValue = {
location: this.state.location,
history: {
location: this.state.location,
+ push(to: LocationDescriptor) {
+ if (typeof to === 'object') {
+ let { pathname, state } = to;
+ that.locationState = state;
+ window.location.hash = pathname!;
+ } else {
+ window.location.hash = to;
+ }
}
}
}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
)
}
}
Redirect&Switch
src\index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
+import { HashRouter as Router, Route, Link, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
<Router>
<>
<div className="navbar navbar-inverse">
<div className="container-fluid">
<div className="navbar-heading">
<div className="navbar-brand">珠峰架构</div>
</div>
<ul className="nav navbar-nav">
<li><Link to="/">Home</Link></li>
<li><Link to="/user">User</Link></li>
<li><Link to="/profile">Profile</Link></li>
</ul>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-md-12">
+ <Switch>
+ <Route path="/" exact component={Home} />
+ <Route path="/user" component={User} />
+ <Route path="/profile" component={Profile} />
+ <Redirect to="/" />
+ </Switch>
</div>
</div>
</div>
</>
</Router>
, document.getElementById('root'));
src\react-router-dom\index.tsx
import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
+import Switch from './Switch';
+import Redirect from './Redirect';
export {
HashRouter,
Route,
Link,
+ Switch,
+ Redirect
}
export * from './types';
src\react-router-dom\Switch.tsx
import React, { Component } from 'react'
import Context from './context';
import { pathToRegexp } from 'path-to-regexp';
interface Props {
children: Array<JSX.Element>
}
export default class Switch extends Component<Props> {
static contextType = Context;
render() {
let pathname = this.context.location.pathname;
if (this.props.children) {
for (let i = 0; i < this.props.children.length; i++) {
let child: JSX.Element = this.props.children[i];
let { path = '/', component: Component, exact = false } = child.props;
let regxp = pathToRegexp(path, [], { end: exact });
let result = pathname.match(regxp);
if (result) {
return child;
}
}
}
return null;
}
}
src\react-router-dom\Redirect.tsx
import React, { Component } from 'react'
import { LocationDescriptor } from '../history';
interface Props {
to: LocationDescriptor;
}
export default class extends Component<Props> {
static contextType = Context;
render() {
this.context.history.push(this.props.to);
return null;
}
}
受保护的路由
src\index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route, Link, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
+import Protected from './components/Protected';
+import Login from './components/Login';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
<Router>
<>
<div className="navbar navbar-inverse">
<div className="container-fluid">
<div className="navbar-heading">
<div className="navbar-brand">珠峰架构</div>
</div>
<ul className="nav navbar-nav">
<li><Link to="/">Home</Link></li>
<li><Link to="/user">User</Link></li>
<li><Link to="/profile">Profile</Link></li>
</ul>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-md-12">
<Switch>
<Route path="/" exact component={Home} />
<Route path="/user" component={User} />
+ <Route path="/login" component={Login} />
+ <Protected path="/profile" component={Profile} />
<Redirect to="/" />
</Switch>
</div>
</div>
</div>
</>
</Router>
, document.getElementById('root'));
src\components\Protected.tsx
import React from 'react'
import { Route, Redirect } from '../react-router-dom';
interface Props extends Record<string, any> {
path: string;
component: React.ComponentType<any>;
}
export default (props: Props) => {
let { component: RouteComponent, path } = props;
return (
<Route path={path} render={
(props: any) => (
localStorage.getItem('logined') ? <RouteComponent {...props} /> : <Redirect to={{ pathname: '/login', state: { from: props.location.pathname } }} />
)
} />
)
}
withRouter
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route, MenuLink, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
import Protected from './components/Protected';
import Login from './components/Login';
+import NavHeader from './components/NavHeader';
import 'bootstrap/dist/css/bootstrap.css';
ReactDOM.render(
<Router>
<>
<div className="navbar navbar-inverse">
<div className="container-fluid">
+ <NavHeader title="欢迎来到珠峰架构" />
<ul className="nav navbar-nav">
<li><MenuLink exact to="/">Home</MenuLink></li>
<li><MenuLink exact to="/user">User</MenuLink></li>
<li><MenuLink exact to="/profile">Profile</MenuLink></li>
</ul>
</div>
</div>
<div className="container">
<div className="row">
<div className="col-md-12">
<Switch>
<Route path="/" exact component={Home} />
<Route path="/user" component={User} />
<Route path="/login" component={Login} />
<Protected path="/profile" component={Profile} />
<Redirect to="/" />
</Switch>
</div>
</div>
</div>
</>
</Router>
, document.getElementById('root'));
src\components\NavHeader.tsx
import React from 'react';
import { RouteComponentProps } from '../react-router-dom';
import { withRouter } from '../react-router-dom';
//只有当一个组件是通过路由Route渲染出来的话才会有RouteComponentProps里的属性
interface NavHeaderProps {
title: string;
}
class NavHeader extends React.Component<RouteComponentProps & NavHeaderProps> {
render() {
return (
<div className="navbar-header">
<div
onClick={(event: React.MouseEvent) => this.props.history.push('/')}
className="navbar-brand">{this.props.title}</div>
</div>
)
}
}
export default withRouter<NavHeaderProps>(NavHeader);
src\react-router-dom\withRouter.tsx
import React from 'react';
import { Route, RouteComponentProps } from './';
export default function <NavHeaderProps>(OldComponent: React.ComponentType<NavHeaderProps & RouteComponentProps>) {
return (props: NavHeaderProps) => (
<Route render={
(routeProps: RouteComponentProps) => <OldComponent {...props} {...routeProps} />
} />
)
}
BrowserRouter
public\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>React App</title>
+ <script>
+ (function (history) {
+ var pushState = history.pushState;
+ history.pushState = function (state, title, pathname) {
+ if (typeof window.onpushstate == "function") {
+ window.onpushstate(state, pathname);
+ }
+ return pushState.apply(history, arguments);
+ };
+ })(window.history);
+ </script>
</head>
<body>
<div id="root"></div>
</body>
</html>
src\index.tsx
import ReactDOM from 'react-dom';
+import { BrowserRouter as Router, Route, MenuLink, Redirect, Switch } from './react-router-dom';
import Home from './components/Home';
src\react-router-dom\BrowserRouter.tsx
import React, { Component } from 'react'
import Context from './context';
import { Message } from './';
import { LocationDescriptor, Location } from '../history';
declare global {
interface Window {
onpushstate: (state: any, pathname: string) => void;
}
}
export default class BrowserRouter extends Component {
state = {
location: { pathname: '/' }
}
message: Message | null
componentDidMount() {
window.onpopstate = (event: PopStateEvent) => {
this.setState({
location: {
...this.state.location,
pathname: document.location.pathname,
state: event.state
}
});
};
window.onpushstate = (state: any, pathname: string) => {
this.setState({
location: {
...this.state.location,
pathname,
state
}
});
};
}
render() {
let that = this;
let value = {
location: that.state.location,
history: {
push(to: LocationDescriptor) {
if (that.message) {
let allow = window.confirm(that.message(typeof to == 'object' ? to : { pathname: to }));
if (!allow) return;
}
if (typeof to === 'object') {
let { pathname, state } = to;
window.history.pushState(state, '', pathname);
} else {
window.history.pushState('', '', to);
}
},
block(message: Message) {
that.message = message;
}
}
}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
)
}
}