前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[探索]前端路由Router原理

[探索]前端路由Router原理

作者头像
前端LeBron
发布2021-12-08 16:40:31
2.6K0
发布2021-12-08 16:40:31
举报
文章被收录于专栏:前端LeBron前端LeBron

前端路由介绍

什么前端路由

路由这个概念最早出现在后端,通过⽤户请求的url导航到具体的html⻚⾯。现在的前端路由不同 于传统路由,它不需要服务器解析,⽽是可以通过hash函数或者history API来实现。在前端开发中,我 们可以使⽤路由设置访问路径,并根据路径与组件的映射关系切换组件的显示,⽽这整个过程都是在同 ⼀个⻚⾯中实现的,不涉及⻚⾯间的跳转,这也就是我们常说的单⻚应⽤(spa)。

前端路由带来了什么

相⽐多⻚应⽤(mpa)来说,spa有以下优点:

  • 不涉及html⻚⾯跳转,内容改变不需要重新加载⻚⾯,对服务器压⼒⼩。
  • 只涉及组件之间的切换,因此跳转流畅,⽤户体验好。
  • ⻚⾯效果会⽐较炫酷(⽐如切换⻚⾯内容时的转场动画)。
  • 组件化开发便捷。

但是同时spa也有以下缺点:

  • ⾸屏加载过慢。
  • 不利于seo。
  • ⻚⾯复杂度提⾼很多。

⽤原⽣ 「js」 实现前端路由

什么前端路由

路由这个概念最早出现在后端,通过⽤户请求的 url 导航到具体的 html ⻚⾯。现在的前端路由不同于

传统路由,它不需要服务器解析,⽽是可以通过 hash 函数或者 h5 history API 来实现。在前端开发

中,我们可以使⽤路由设置访问路径,并根据路径与组件的映射关系切换组件的显示,⽽这整个过程都

是在同⼀个⻚⾯中实现的,不涉及⻚⾯间的跳转,这也就是我们常说的单⻚应⽤(spa)。

原⽣ 「js」 实现前端路由

代码语言:javascript
复制
<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 简介

快速开始

代码语言:javascript
复制
npx create-react-app router-nut
cd router-nut
yarn start

配置 less 与装饰器

代码语言:javascript
复制
yarn add @craco/craco craco-less @babel/plugin-proposal-decorators

根目录下添加 craco.config.js 文件

代码语言:javascript
复制
// * 配置完成后记得重启下
const CracoLessPlugin = require("craco-less");

module.exports = {
  babel: {
    //用来支持装饰器
    plugins: [["@babel/plugin-proposal-decorators", {legacy: true}]]
  },
  plugins: [
    {
      plugin: CracoLessPlugin
    }
  ]
};

修改 package.json

代码语言:javascript
复制
 "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 应用。

安装
代码语言:javascript
复制
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」都以组件形式存在

代码语言:javascript
复制
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 渲染内容的三种方式

Route 渲染优先级:children > component > render。

三者能接收到同样的[route props],包括 match, location and history,但是当不匹配的时候,children 的 match 为 null。

这三种方式互斥,你只能用一种。

代码语言:javascript
复制
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总是不相同,这个时候会产生重复的卸载和挂载
            //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 完全一样。

代码语言:javascript
复制
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]。

代码语言:javascript
复制
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 的路由在路由列表最后面,表示一定匹配

代码语言:javascript
复制
<Switch>
  <Route path="/" exact component={HomePage} />
  <Route path="/user" component={UserPage} />
  <Route path="/login" component={LoginPage} />
  <Route component={_404Page} />
</Switch>

动态路由

动态路由

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

定义路由:

代码语言:javascript
复制
<Route path="/product/:id" component={Product} />

添加导航链接:

代码语言:javascript
复制
<Link to={"/product/123"}>搜索</Link>

创建Search组件并获取参数:

代码语言:javascript
复制
function Product({location, match}) {
  console.log("match", match); //sy-log
  const {id} = match.params;
  return <h1>Product-{id}</h1>;
}

嵌套路由

嵌套路由

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

修改Product,添加新增和详情

代码语言:javascript
复制
<Route path={url + "/detail"} component={Detail} />
代码语言:javascript
复制
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> );

}

⼿写实现「BrowserRouter」、「Route」、「Link」

跨层级传输数据 Context

代码语言:javascript
复制
import React from "react";

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

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

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

实现「Router」

代码语言:javascript
复制
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变更监听

代码语言:javascript
复制
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」

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

代码语言:javascript
复制
// 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: 跳转链接,处理点击事件

代码语言:javascript
复制
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」

渲染与该地址匹配的第⼀个⼦节点或者。

代码语言:javascript
复制
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>
        );
    }
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端LeBron 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前端路由介绍
    • 什么前端路由
      • 前端路由带来了什么
        • 相⽐多⻚应⽤(mpa)来说,spa有以下优点:
        • 但是同时spa也有以下缺点:
    • ⽤原⽣ 「js」 实现前端路由
      • 什么前端路由
        • 原⽣ 「js」 实现前端路由
          • 快速开始
          • 配置 less 与装饰器
          • react-router 简介
          • children:func
          • render:func
          • component: component
          • 注意
          • 404 页面
          • 动态路由
          • 嵌套路由
          • 跨层级传输数据 Context
          • 实现「Router」
          • 实现「BrowserRouter」
          • 实现「Route」
          • 实现「Link」
          • 实现「Switch」
      • 环境配置与 react-router 简介
      • Route 渲染内容的三种方式
      • 动态路由
      • 嵌套路由
      • ⼿写实现「BrowserRouter」、「Route」、「Link」
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档