返回首页

说说 React Router 中有几种模式?实现原理?

问题解析(面试官考察点)

面试官通过此问题主要考察:

  • 对 React Router 两种路由模式的掌握程度
  • 对前端路由实现原理的深入理解
  • 对浏览器 History API 和 hash 机制的了解
  • 能够根据场景选择合适的路由模式

核心概念(基础知识点)

前端路由的核心需求

在单页应用(SPA)中,前端路由需要满足:

  1. 改变 URL 且不让浏览器向服务器发送请求
  2. 在不刷新页面的前提下动态改变浏览器地址栏中的 URL
  3. 能够监听 URL 变化并做出响应

两种路由模式

React Router 主要提供了两种路由模式:

  1. Hash 模式: URL 中包含 #,如 http://example.com/#/page
  2. History 模式: 使用 HTML5 History API,URL 如 http://example.com/page

详细解答(代码示例)

Hash 模式

Hash 模式利用 URL 中的 hash(#)部分来实现路由:

import { HashRouter as Router, Route, Link } from "react-router-dom";

function App() {
  return (
    <Router>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
      </nav>

      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
    </Router>
  );
}

特点:

  • URL 示例: http://example.com/#/home
  • hash 值的改变不会触发页面刷新
  • hash 值改变会触发 hashchange 事件
  • 兼容性好(IE8+)

History 模式

History 模式使用 HTML5 的 History API:

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

function App() {
  return (
    <Router>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
      </nav>

      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
    </Router>
  );
}

特点:

  • URL 示例: http://example.com/home
  • 使用 pushState/replaceState 方法改变 URL
  • 需要服务端配置支持(处理 404)
  • URL 更美观

手动实现简易 HashRouter

import React, { Component } from "react";

// 创建 Context
const RouterContext = React.createContext();

class HashRouter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      location: {
        pathname: window.location.hash.slice(1) || "/"
      }
    };
  }

  componentDidMount() {
    // 设置默认 hash
    window.location.hash = window.location.hash || "/";

    // 监听 hash 变化
    window.addEventListener("hashchange", () => {
      this.setState({
        location: {
          pathname: window.location.hash.slice(1) || "/"
        }
      });
    });
  }

  render() {
    const value = {
      location: this.state.location,
      history: {
        push: (path) => {
          window.location.hash = path;
        }
      }
    };

    return (
      <RouterContext.Provider value={value}>
        {this.props.children}
      </RouterContext.Provider>
    );
  }
}

// Route 组件实现
class Route extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {(context) => {
          const { path, component: Component } = this.props;
          const pathname = context.location.pathname;

          // 简单匹配
          const match = pathname === path ||
                       (path !== "/" && pathname.startsWith(path));

          return match ? <Component /> : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

// Link 组件实现
class Link extends Component {
  handleClick = (e) => {
    e.preventDefault();
    window.location.hash = this.props.to;
  };

  render() {
    return (
      <a href={`#${this.props.to}`} onClick={this.handleClick}>
        {this.props.children}
      </a>
    );
  }
}

export { HashRouter, Route, Link };

手动实现简易 BrowserRouter

import React, { Component } from "react";

const RouterContext = React.createContext();

class BrowserRouter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      location: {
        pathname: window.location.pathname
      }
    };
  }

  componentDidMount() {
    // 监听 popstate 事件(浏览器前进/后退)
    window.addEventListener("popstate", () => {
      this.setState({
        location: {
          pathname: window.location.pathname
        }
      });
    });
  }

  render() {
    const value = {
      location: this.state.location,
      history: {
        push: (path) => {
          window.history.pushState({}, "", path);
          // 手动触发更新
          this.setState({
            location: { pathname: path }
          });
        },
        replace: (path) => {
          window.history.replaceState({}, "", path);
          this.setState({
            location: { pathname: path }
          });
        }
      }
    };

    return (
      <RouterContext.Provider value={value}>
        {this.props.children}
      </RouterContext.Provider>
    );
  }
}

// Link 组件(阻止默认行为,使用 history.push)
class Link extends Component {
  static contextType = RouterContext;

  handleClick = (e) => {
    e.preventDefault();
    this.context.history.push(this.props.to);
  };

  render() {
    return (
      <a href={this.props.to} onClick={this.handleClick}>
        {this.props.children}
      </a>
    );
  }
}

export { BrowserRouter, Link };

深入理解(原理剖析)

Hash 模式原理

┌─────────────────────────────────────────────────────────────┐
│                     Hash 模式工作流程                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. URL 格式: http://example.com/#/path                     │
│                                                              │
│  2. hash 值改变不会发送 HTTP 请求到服务器                    │
│                                                              │
│  3. 监听 hashchange 事件:                                    │
│     window.addEventListener('hashchange', callback)         │
│                                                              │
│  4. 根据新的 hash 值渲染对应组件                             │
│                                                              │
│  5. 浏览器前进/后退同样触发 hashchange                       │
│                                                              │
└─────────────────────────────────────────────────────────────┘

核心代码逻辑:

// HashRouter 核心实现
class HashRouter {
  constructor() {
    this.listeners = [];

    // 监听 hash 变化
    window.addEventListener('hashchange', () => {
      this.notify();
    });
  }

  // 获取当前路径(去掉 #)
  get location() {
    return window.location.hash.slice(1) || '/';
  }

  // 订阅变化
  listen(callback) {
    this.listeners.push(callback);
    return () => {
      this.listeners = this.listeners.filter(fn => fn !== callback);
    };
  }

  // 通知所有订阅者
  notify() {
    this.listeners.forEach(fn => fn(this.location));
  }

  // 导航到指定路径
  push(path) {
    window.location.hash = path;
  }
}

History 模式原理

┌─────────────────────────────────────────────────────────────┐
│                   History 模式工作流程                       │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. URL 格式: http://example.com/path                       │
│                                                              │
│  2. 使用 History API:                                        │
│     - history.pushState(state, title, url)                  │
│     - history.replaceState(state, title, url)               │
│                                                              │
│  3. 监听 popstate 事件(前进/后退):                         │
│     window.addEventListener('popstate', callback)           │
│                                                              │
│  4. 注意: pushState/replaceState 不会触发 popstate          │
│     需要手动触发组件更新                                     │
│                                                              │
│  5. 服务端需要配置 fallback 到 index.html                   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

核心代码逻辑:

// BrowserRouter 核心实现
class BrowserRouter {
  constructor() {
    this.listeners = [];

    // 监听浏览器前进/后退
    window.addEventListener('popstate', () => {
      this.notify();
    });
  }

  get location() {
    return window.location.pathname;
  }

  listen(callback) {
    this.listeners.push(callback);
    return () => {
      this.listeners = this.listeners.filter(fn => fn !== callback);
    };
  }

  notify() {
    this.listeners.forEach(fn => fn(this.location));
  }

  // 使用 pushState 导航
  push(path) {
    window.history.pushState({}, '', path);
    // pushState 不会触发 popstate,需要手动通知
    this.notify();
  }

  // 使用 replaceState 导航
  replace(path) {
    window.history.replaceState({}, '', path);
    this.notify();
  }

  // 前进/后退
  go(n) {
    window.history.go(n);
  }

  back() {
    this.go(-1);
  }

  forward() {
    this.go(1);
  }
}

两种模式对比

特性 Hash 模式 History 模式
URL 美观度 #,不够美观 #,美观
兼容性 IE8+ IE10+
服务端配置 不需要 需要配置 fallback
SEO 较差 较好
实现原理 hashchange 事件 History API
刷新 404 不会出现 需要服务端处理

最佳实践

1. 根据场景选择模式

// 内部系统、后台管理 - 可以使用 HashRouter
import { HashRouter } from "react-router-dom";

// 面向用户的网站 - 推荐使用 BrowserRouter
import { BrowserRouter } from "react-router-dom";

2. 服务端配置(History 模式)

Nginx 配置:

server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Apache 配置:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Node.js/Express:

const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'build')));

app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

3. 路由匹配原理

// 使用 path-to-regexp 进行路径匹配
const pathToRegexp = require('path-to-regexp');

// 编译路径为正则
const keys = [];
const regexp = pathToRegexp('/user/:id', keys);

// 匹配
const match = regexp.exec('/user/123');
// match = ['/user/123', '123']

// 提取参数
const params = {};
keys.forEach((key, index) => {
  params[key.name] = match[index + 1];
});
// params = { id: '123' }

4. 路由守卫实现

import { Route, Redirect } from "react-router-dom";

// 权限路由守卫
function AuthRoute({ component: Component, requiresAuth, ...rest }) {
  const isAuthenticated = checkAuth(); // 检查登录状态

  return (
    <Route
      {...rest}
      render={(props) => {
        if (!requiresAuth) {
          return <Component {...props} />;
        }

        return isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: props.location }
            }}
          />
        );
      }}
    />
  );
}

面试要点

  1. 两种模式: Hash 模式和 History 模式

  2. Hash 模式原理:

    • 利用 URL 中的 # 部分
    • hash 变化不会发送请求到服务器
    • 通过 hashchange 事件监听变化
  3. History 模式原理:

    • 使用 HTML5 History API (pushState/replaceState)
    • 通过 popstate 事件监听前进/后退
    • 需要服务端配置支持
  4. 选择建议:

    • 需要兼容旧浏览器 -> Hash 模式
    • 需要更好的 SEO -> History 模式
    • 内部系统 -> Hash 模式
    • 面向用户的网站 -> History 模式
  5. 注意事项:

    • History 模式刷新页面会 404,需要服务端配置
    • Hash 模式的锚点功能会冲突
    • Hash 模式不利于 SEO