前端路由介绍

什么前端路由

路由这个概念最早出现在后端,通过⽤户请求的 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);

// 监听hash变化
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 简介

资源

  1. React 官网
  2. react-router

目标

  1. 掌握 cra 环境
  2. 掌握 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 对比

  1. HashRouter 最简单,不需要服务器端渲染,靠浏览器的#的来区分 path 就可以,BrowserRouter 需要服务器端对不同的 URL 返回不同的 HTML,后端配置可参考
  2. BrowserRouter 使用 HTML5 history API( pushState,replaceState 和 popstate 事件),让页面的 UI 同步与 URL。
  3. HashRouter 不支持 location.key 和 location.state,动态路由跳转需要通过?传递参数。
  4. 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>

{/* 根路由要添加exact,实现精确匹配 */}
<Route exact path='/' component={HomePage} />
<Route path='/user' component={UserPage} />
<Route path='/login' component={LoginPage} />
</Router>
</div>
);
}

export default App;

Route 渲染内容的三种方式

资源

  1. React 官网
  2. react-router

目标

  1. 掌握 Route 渲染内容的三种方式
  2. 掌握 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
//children={children}
component={HomePage}

// ! 渲染component的时候会调用React.createElement,如果使用下面这种匿名函数的形式,每次都会生成一个新的匿名的函数,
// ! 导致生成的组件的type总是不相同,这个时候会产生重复的卸载和挂载
//! 错误举例 课下自己尝试下 观察下HomePage的didMount和willUnmount函数 */}
//component={() => <HomePage />}

// render={render}
/>
<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); //sy-log
return <div>children</div>;
}

function render(props) {
console.log('props props', props); //sy-log
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
);

// wrapping/composing
//把route参数传递给你的组件
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 核心渲染代码如下:

image20200224174023810

404 页面

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

<Switch>
<Route path='/' exact component={HomePage} />
<Route path='/user' component={UserPage} />
<Route path='/login' component={LoginPage} />
<Route component={_404Page} />
</Switch>

动态路由

资源

  1. React 官网
  2. react-router

目标

  1. 动态路由

知识点

动态路由

使用:id 的形式定义动态路由

定义路由:

<Route path='/product/:id' component={Product} />

添加导航链接:

<Link to={'/product/123'}>搜索</Link>

创建 Search 组件并获取参数:

function Product({ location, match }) {
console.log('match', match); //sy-log
const { id } = match.params;
return <h1>Product-{id}</h1>;
}

嵌套路由

资源

  1. React 官网
  2. react-router

⽬标

  • 掌握嵌套路由

知识点

嵌套路由

Route 组件嵌套在其他⻚⾯组件中就产⽣了嵌套关系

修改 Product,添加新增和详情

<Route path={url + "/detail"} component={Detail} />
function Product({ match }) {
console.log('match', match); //sy-log

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>
);
}

⼿写实现BrowserRouterRouteLink

资源

  1. React 官网
  2. react-router

⽬标

  • react-router 初步实现

知识点

跨层级传输数据 Context

import React from 'react';

// 使用Context做数据跨层级传递
// step1: 创建context对象
export const RouterContext = React.createContext();

// step2: 使用context对象的Provider传递value

// step3: 子组件消费value: Consumer、useContext、contextType

实现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

路由配置,匹配检测,内容渲染

// match 按照互斥规则 优先渲染顺序为children component render null,children如果是function执⾏function,是节点直接渲染
// 不match children 或者null (只渲染function)

export default class Route extends Component {
render() {
return (
<RouterContext.Consumer>
{(context) => {
// 优先从props中取值
const location = this.props.location || context.location;
// 优先从props中取值计算
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;
// match 渲染这三者之⼀:children component render或者null
// 不match,渲染children 或者 null
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;
// children element | array
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

持续分享技术博文,关注微信公众号 👇🏻

img