前端路由介绍
什么前端路由
路由这个概念最早出现在后端,通过⽤户请求的 url 导航到具体的 html ⻚⾯。现在的前端路由不同
于传统路由,它不需要服务器解析,⽽是可以通过 hash 函数或者 history API 来实现。在前端开发中,我
们可以使⽤路由设置访问路径,并根据路径与组件的映射关系切换组件的显示,⽽这整个过程都是在同
⼀个⻚⾯中实现的,不涉及⻚⾯间的跳转,这也就是我们常说的单⻚应⽤(spa)。
前端路由带来了什么
相⽐多⻚应⽤(mpa)来说,spa 有以下优点:
- 不涉及 html ⻚⾯跳转,内容改变不需要重新加载⻚⾯,对服务器压⼒⼩。
- 只涉及组件之间的切换,因此跳转流畅,⽤户体验好。
- ⻚⾯效果会⽐较炫酷(⽐如切换⻚⾯内容时的转场动画)。
- 组件化开发便捷。
但是同时 spa 也有以下缺点:
- ⾸屏加载过慢。
- 不利于 seo。
- ⻚⾯复杂度提⾼很多。
⽤原⽣ js 实现前端路由
什么前端路由
路由这个概念最早出现在后端,通过⽤户请求的 url 导航到具体的 html ⻚⾯。现在的前端路由不同于
传统路由,它不需要服务器解析,⽽是可以通过 hash 函数或者 h5 history API 来实现。在前端开发
中,我们可以使⽤路由设置访问路径,并根据路径与组件的映射关系切换组件的显示,⽽这整个过程都
是在同⼀个⻚⾯中实现的,不涉及⻚⾯间的跳转,这也就是我们常说的单⻚应⽤(spa)。
原⽣ js 实现前端路由
<html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>lesson2</title> </head> <body> <ul> <li><a href="#/home">首页</a></li> <li><a href="#/user">用户中心</a></li> <li><a href="#/login">登录</a></li> </ul>
<div id="view"></div> </body>
<script> let view = null; window.addEventListener('DOMContentLoaded', onLoad);
window.addEventListener('hashchange', onHashChange);
function onLoad() { view = document.getElementById('view'); onHashChange(); }
function onHashChange() { switch (location.hash) { case '#/home': view.innerHTML = '首页'; break; case '#/user': view.innerHTML = '用户中心'; break; case '#/login': view.innerHTML = '登录'; break; } } </script> </html>
|
环境配置与 react-router 简介
资源
- React 官网
- react-router
目标
- 掌握 cra 环境
- 掌握 react-router 的基本使用
知识点
快速开始
npx create-react-app router-nut cd router-nut yarn start
|
配置 less 与装饰器
yarn add @craco/craco craco-less @babel/plugin-proposal-decorators
|
根目录下添加 craco.config.js 文件
const CracoLessPlugin = require('craco-less');
module.exports = { babel: { plugins: [['@babel/plugin-proposal-decorators', { legacy: true }]], }, plugins: [ { plugin: CracoLessPlugin, }, ], };
|
修改 package.json
"scripts": { "start": "craco start", "build": "craco build", "test": "craco test" },
|
react-router 简介
react-router 包含 3 个库,react-router、react-router-dom 和 react-router-native。react-router 提供最基本的路由功能,实际使用的时候我们不会直接安装 react-router,而是根据应用运行的环境选择安装 react-router-dom(在浏览器中使用)或 react-router-native(在 rn 中使用)。react-router-dom 和 react-router-native 都依赖 react-router,所以在安装时,react-router 也会自动安装,创建 web 应用。
安装
yarn add react-router-dom
|
BrowserRouter 与 HashRouter 对比
- HashRouter 最简单,不需要服务器端渲染,靠浏览器的#的来区分 path 就可以,BrowserRouter 需要服务器端对不同的 URL 返回不同的 HTML,后端配置可参考。
- BrowserRouter 使用 HTML5 history API( pushState,replaceState 和 popstate 事件),让页面的 UI 同步与 URL。
- HashRouter 不支持 location.key 和 location.state,动态路由跳转需要通过?传递参数。
- Hash history 不需要服务器任何配置就可以运行,如果你刚刚入门,那就使用它吧。但是我们不推荐在实际线上环境中用到它,因为每一个 web 应用都应该渴望使用
browserHistory
。
MemoryRouter
把 URL 的历史记录保存在内存中的 <Router>
(不读取、不写入地址栏)。在测试和非浏览器环境中很有用,如 React Native。
基本使用
react-router 中奉行一切皆组件的思想,路由器-Router、链接-Link、路由-Route、独占-Switch、重定向-Redirect都以组件形式存在
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'; import HomePage from './pages/HomePage'; import UserPage from './pages/UserPage'; import LoginPage from './pages/LoginPage';
function App() { return ( <div className='App'> <Router> <Link to='/'>首页</Link> <Link to='/user'>用户中心</Link> <Link to='/login'>登录</Link>
{} <Route exact path='/' component={HomePage} /> <Route path='/user' component={UserPage} /> <Route path='/login' component={LoginPage} /> </Router> </div> ); }
export default App;
|
Route 渲染内容的三种方式
资源
- React 官网
- react-router
目标
- 掌握 Route 渲染内容的三种方式
- 掌握 404 路由
知识点
Route 渲染优先级:children>component>render。
三者能接收到同样的[route props],包括 match, location and history,但是当不匹配的时候,children 的 match 为 null。
这三种方式互斥,你只能用一种。
import React, { useState } from 'react'; import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom'; import HomePage from './pages/HomePage'; import UserPage from './pages/UserPage'; import LoginPage from './pages/LoginPage'; import _404Page from './pages/_404Page';
function App() { const [count, setCount] = useState(0); return ( <div className='App'> <button onClick={() => { setCount(count + 1); }} > add: {count} </button> <Router> <Link to='/'>首页</Link> <Link to='/user'>用户中心</Link> <Link to='/login'>登录</Link> {} <Switch> <Route path='/' exact component={HomePage}
/> <Route path='/user' component={UserPage} /> <Route path='/login' component={LoginPage} /> <Route component={_404Page} /> </Switch> </Router> </div> ); }
export default App;
function children(props) { console.log('children props', props); return <div>children</div>; }
function render(props) { console.log('props props', props); return <div>render</div>; }
|
children:func
有时候,不管 location 是否匹配,你都需要渲染一些内容,这时候你可以用 children。
除了不管 location 是否匹配都会被渲染之外,其它工作方法与 render 完全一样。
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Link, Route } from 'react-router-dom';
function ListItemLink({ to, name, ...rest }) { return ( <Route path={to} children={({ match }) => ( <li className={match ? 'active' : ''}> <Link to={to} {...rest}> {name} </Link> </li> )} /> ); }
export default class RouteChildren extends Component { render() { return ( <div> <h3>RouteChildren</h3> <Router> <ul> <ListItemLink to='/somewhere' name='链接1' /> <ListItemLink to='/somewhere-else' name='链接2' /> </ul> </Router> </div> ); } }
|
render:func
但是当你用 render 的时候,你调用的只是个函数。但是它和 component 一样,能访问到所有的[route props]。
import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Route } from 'react-router-dom';
ReactDOM.render( <Router> <Route path='/home' render={() => <div>Home</div>} /> </Router>, node );
function FadingRoute({ component: Component, ...rest }) { return ( <Route {...rest} render={(routeProps) => <Component {...routeProps} />} /> ); }
ReactDOM.render( <Router> <FadingRoute path='/cool' component={Something} /> </Router>, node );
|
component: component
只在当 location 匹配的时候渲染。
注意
当你用component
的时候,Route 会用你指定的组件和 React.createElement 创建一个新的[React element]。这意味着当你提供的是一个内联函数的时候,每次 render 都会创建一个新的组件。这会导致不再更新已经现有组件,而是直接卸载然后再去挂载一个新的组件。因此,当用到内联函数的内联渲染时,请使用 render 或者 children。
Route 核心渲染代码如下:

404 页面
设定一个没有 path 的路由在路由列表最后面,表示一定匹配
<Switch> <Route path='/' exact component={HomePage} /> <Route path='/user' component={UserPage} /> <Route path='/login' component={LoginPage} /> <Route component={_404Page} /> </Switch>
|
动态路由
资源
- React 官网
- react-router
目标
- 动态路由
知识点
动态路由
使用:id 的形式定义动态路由
定义路由:
<Route path='/product/:id' component={Product} />
|
添加导航链接:
<Link to={'/product/123'}>搜索</Link>
|
创建 Search 组件并获取参数:
function Product({ location, match }) { console.log('match', match); const { id } = match.params; return <h1>Product-{id}</h1>; }
|
嵌套路由
资源
- React 官网
- react-router
⽬标
知识点
嵌套路由
Route 组件嵌套在其他⻚⾯组件中就产⽣了嵌套关系
修改 Product,添加新增和详情
<Route path={url + "/detail"} component={Detail} />
|
function Product({ match }) { console.log('match', match);
const { params, url } = match;
const { id } = params;
return ( <div> <h1>Search-{id}</h1>
<Link to={url + '/detail'}>详情</Link>
<Route path={url + '/detail'} component={Detail} /> </div> ); }
|
⼿写实现BrowserRouter、Route、Link
资源
- React 官网
- react-router
⽬标
知识点
跨层级传输数据 Context
import React from 'react';
export const RouterContext = React.createContext();
|
实现Router
import React, { Component } from 'react'; import { RouterContext } from './Context';
export default class Router extends Component { static computeRootMatch(pathname) { return { path: '/', url: '/', params: {}, isExact: pathname === '/' }; }
constructor(props) { super(props); this.state = { location: props.history.location, };
this.unlisten = props.history.listen((location) => { this.setState({ location, }); }); }
componentWillUnmount() { if (this.unlisten) { this.unlisten(); } }
render() { return ( <RouterContext.Provider value={{ history: this.props.history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname), }} > {this.props.children} </RouterContext.Provider> ); } }
|
实现BrowserRouter
BrowserRouter:历史记录管理对象 history 初始化及向下传递,location 变更监听
import React, { Component } from 'react'; import { createBrowserHistory } from 'history'; import Router from './Router';
export default class BrowserRouter extends Component { constructor(props) { super(props); this.history = createBrowserHistory(); }
render() { return <Router history={this.history}>{this.props.children}</Router>; } }
|
实现Route
路由配置,匹配检测,内容渲染
export default class Route extends Component { render() { return ( <RouterContext.Consumer> {(context) => { const location = this.props.location || context.location; const match = this.props.computedMatch ? this.props.computedMatch : this.props.path ? matchPath(location.pathname, this.props) : context.match; const props = { ...context, location, match, }; let { path, children, component, render } = this.props; return ( <RouterContext.Provider value={props}> {match ? children ? typeof children === 'function' ? children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === 'function' ? children(props) : null} </RouterContext.Provider> ); }} </RouterContext.Consumer> ); } }
|
实现Link
Link.js: 跳转链接,处理点击事件
import React from 'react'; import { RouterContext } from './RouterContext';
export default function Link({ to, children, ...restProps }) { const context = React.useContext(RouterContext); const handleClick = (e) => { e.preventDefault(); context.history.push(to); }; return ( <a href={to} {...restProps} onClick={handleClick}> {children} </a> ); }
|
实现Switch
渲染与该地址匹配的第⼀个⼦节点 或者 。
import React, { Component, isValidElement } from 'react'; import { RouterContext } from './Context'; import matchPath from './matchPath';
export default class Switch extends Component { render() { return ( <RouterContext.Consumer> {(context) => { const { location } = context; let match, element; React.Children.forEach(this.props.children, (child) => { if (match == null && React.isValidElement(child)) { element = child; const { path } = child.props; match = path ? matchPath(location.pathname, child.props) : context.match; } }); return match ? React.cloneElement(element, { computedMatch: match }) : null; }} </RouterContext.Consumer> ); } }
|
掘金:前端 LeBron
知乎:前端 LeBron
持续分享技术博文,关注微信公众号 👇🏻
