【前端面试小册】浏览器-第10节:前端路由原理与实战,Hash与History模式

一、现实世界类比 🗺️

想象你在一个大型购物中心

  • 传统多页面应用:就像每个店铺都是独立的建筑

    • 从 A 店到 B 店要走出去,重新进入(刷新页面)
    • 每次都要重新过安检(重新加载资源)
    • 速度慢,体验差
  • 单页面应用(SPA):就像所有店铺在同一栋楼里

    • 从 A 店到 B 店只需换楼层(切换组件)
    • 不用走出大楼(不刷新页面)
    • 速度快,体验好
  • 前端路由:就像电梯和楼层指示牌

    • Hash 模式:像贴在墙上的地图(#/floor1)
    • History 模式:像真实的楼层编号(/floor1)

二、为什么需要前端路由?

💡 传统多页面 vs 单页面应用

// ===== 传统多页面应用(MPA)=====
const TraditionalMPA = {
  特点: {
    页面结构: '每个URL对应一个完整的HTML页面',
    跳转方式: '浏览器请求新页面,刷新整个页面',
    资源加载: '每次跳转都重新加载HTML、CSS、JS'
  },
  
  缺点: [
    '页面跳转慢(重新加载资源)',
    '白屏时间长',
    '用户体验差(不流畅)',
    '无法保留页面状态'
  ],
  
  例子: `
    点击链接:/page1 → /page2
    1. 浏览器发起HTTP请求到服务器
    2. 服务器返回完整的page2.html
    3. 浏览器刷新,重新加载所有资源
    4. 页面显示(白屏时间长)❌
  `
};

// ===== 单页面应用(SPA)=====
const ModernSPA = {
  特点: {
    页面结构: '只有一个HTML页面,通过JS渲染不同组件',
    跳转方式: '改变URL,不刷新页面,只切换组件',
    资源加载: '首次加载所有资源,后续无需重新加载'
  },
  
  优点: [
    '页面切换快(无需重新加载)',
    '用户体验好(流畅)',
    '可以保留页面状态',
    '前后端分离(API驱动)'
  ],
  
  例子: `
    点击链接:/page1 → /page2
    1. 前端路由拦截,改变URL
    2. 根据新URL匹配对应组件
    3. 卸载旧组件,渲染新组件
    4. 页面更新(无刷新,快速)✅
  `
};

三、前端路由的两种模式

模式1:Hash 模式(#)⭐⭐⭐

// ===== Hash 模式原理 =====
const HashMode = {
  URL格式: 'https://example.com/#/page1',
  
  特点: {
    标识符: 'URL中的 # 符号',
    监听事件: 'hashchange',
    兼容性: '所有浏览器都支持(IE8+)',
    服务器配置: '无需配置(# 后内容不会发送到服务器)'
  },
  
  优点: [
    '兼容性好',
    '无需服务器配置',
    '实现简单'
  ],
  
  缺点: [
    'URL不美观(有#)',
    'SEO不友好',
    '无法使用锚点定位'
  ]
};

// ===== 实现 Hash 路由 =====
class HashRouter {
  constructor() {
    this.routes = {};  // 存储路由配置
    this.currentUrl = '';  // 当前路径
    
    // 监听hashchange事件
    window.addEventListener('hashchange', this.handleHashChange.bind(this));
    
    // 监听页面加载
    window.addEventListener('load', this.handleHashChange.bind(this));
  }
  
  // 注册路由
  route(path, callback) {
    this.routes[path] = callback;
  }
  
  // 处理 hash 变化
  handleHashChange() {
    // 获取当前 hash(去掉 #)
    this.currentUrl = location.hash.slice(1) || '/';
    
    console.log('路由变化:', this.currentUrl);
    
    // 执行对应的回调
    if (this.routes[this.currentUrl]) {
      this.routes[this.currentUrl]();
    } else {
      // 404
      this.routes['/404'] && this.routes['/404']();
    }
  }
  
  // 编程式导航
  push(path) {
    location.hash = path;
  }
  
  // 替换当前路由(不产生历史记录)
  replace(path) {
    location.replace(location.origin + location.pathname + '#' + path);
  }
  
  // 后退
  back() {
    history.back();
  }
  
  // 前进
  forward() {
    history.forward();
  }
}

// ===== 使用示例 =====
const router = new HashRouter();

// 注册路由
router.route('/', function() {
  document.getElementById('app').innerHTML = '<h1>首页</h1>';
});

router.route('/about', function() {
  document.getElementById('app').innerHTML = '<h1>关于页面</h1>';
});

router.route('/user/:id', function() {
  const id = router.currentUrl.match(/\/user\/(\d+)/)[1];
  document.getElementById('app').innerHTML = `<h1>用户${id}</h1>`;
});

router.route('/404', function() {
  document.getElementById('app').innerHTML = '<h1>404 Not Found</h1>';
});

// 编程式导航
// router.push('/about');
// router.back();
<!-- ===== HTML 示例 ===== -->
<!DOCTYPE html>
<html>
<head>
  <title>Hash Router</title>
</head>
<body>
  <nav>
    <a href="#/">首页</a>
    <a href="#/about">关于</a>
    <a href="#/user/123">用户123</a>
  </nav>
  
  <div id="app"></div>
  
  <script src="router.js"></script>
</body>
</html>

<!-- 
URL 变化示例:
1. 点击"首页" → https://example.com/#/
2. 点击"关于" → https://example.com/#/about
3. 点击"用户123" → https://example.com/#/user/123

特点:# 后面的内容不会发送到服务器 ✅
-->

模式2:History 模式(HTML5)⭐⭐⭐⭐⭐

// ===== History 模式原理 =====
const HistoryMode = {
  URL格式: 'https://example.com/page1',
  
  特点: {
    标识符: '正常的URL路径',
    监听事件: 'popstate(仅监听浏览器前进/后退)',
    兼容性: 'IE10+',
    服务器配置: '必须配置(所有路径返回index.html)'
  },
  
  优点: [
    'URL美观(无#)',
    'SEO友好',
    '可以使用锚点定位'
  ],
  
  缺点: [
    '需要服务器配置',
    '兼容性稍差',
    '实现稍复杂'
  ]
};

// ===== 实现 History 路由 =====
class HistoryRouter {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
    
    // 监听popstate(浏览器前进/后退)
    window.addEventListener('popstate', this.handlePopState.bind(this));
    
    // 监听页面加载
    window.addEventListener('load', this.handlePopState.bind(this));
    
    // 拦截所有链接点击
    this.bindLinks();
  }
  
  // 注册路由
  route(path, callback) {
    this.routes[path] = callback;
  }
  
  // 处理 popstate 事件
  handlePopState() {
    this.currentUrl = location.pathname;
    console.log('路由变化:', this.currentUrl);
    this.render();
  }
  
  // 渲染页面
  render() {
    if (this.routes[this.currentUrl]) {
      this.routes[this.currentUrl]();
    } else {
      // 404
      this.routes['/404'] && this.routes['/404']();
    }
  }
  
  // 拦截所有 a 标签
  bindLinks() {
    document.addEventListener('click', (e) => {
      if (e.target.tagName === 'A') {
        e.preventDefault();  // 阻止默认跳转
        
        const href = e.target.getAttribute('href');
        this.push(href);
      }
    });
  }
  
  // 编程式导航
  push(path) {
    // pushState 不会触发 popstate 事件
    history.pushState(null, '', path);
    this.currentUrl = path;
    this.render();
  }
  
  // 替换当前路由
  replace(path) {
    history.replaceState(null, '', path);
    this.currentUrl = path;
    this.render();
  }
  
  // 后退
  back() {
    history.back();
  }
  
  // 前进
  forward() {
    history.forward();
  }
  
  // 跳转指定步数
  go(n) {
    history.go(n);
  }
}

// ===== 使用示例 =====
const router = new HistoryRouter();

router.route('/', function() {
  document.getElementById('app').innerHTML = '<h1>首页</h1>';
});

router.route('/about', function() {
  document.getElementById('app').innerHTML = '<h1>关于页面</h1>';
});

router.route('/user/123', function() {
  document.getElementById('app').innerHTML = '<h1>用户123</h1>';
});

router.route('/404', function() {
  document.getElementById('app').innerHTML = '<h1>404 Not Found</h1>';
});
<!-- ===== HTML 示例 ===== -->
<!DOCTYPE html>
<html>
<head>
  <title>History Router</title>
</head>
<body>
  <nav>
    <a href="/">首页</a>
    <a href="/about">关于</a>
    <a href="/user/123">用户123</a>
  </nav>
  
  <div id="app"></div>
  
  <script src="router.js"></script>
</body>
</html>

<!--
URL 变化示例:
1. 点击"首页" → https://example.com/
2. 点击"关于" → https://example.com/about
3. 点击"用户123" → https://example.com/user/123

特点:URL 美观,无 # ✅
-->

四、服务器配置(History 模式必需)

Nginx 配置

server {
    listen 80;
    server_name example.com;
    root /usr/share/nginx/html;
    index index.html;
    
    location / {
        # 尝试访问文件,不存在则返回 index.html
        try_files $uri $uri/ /index.html;
        
        # 或者使用 if 判断
        # if (!-e $request_filename) {
        #     rewrite ^/(.*) /index.html last;
        # }
    }
    
    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Node.js (Express) 配置

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

// 静态资源
app.use(express.static(path.join(__dirname, 'dist')));

// 所有路由都返回 index.html
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

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>

五、Vue Router 实战

Vue Router 基础使用

// ===== 安装 =====
// npm install vue-router@4

// ===== router/index.js =====
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';

// 路由配置
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { title: '首页' }
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    meta: { title: '关于' }
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue'),  // 懒加载
    props: true,  // 路由参数作为组件 props
    meta: { requiresAuth: true }  // 需要登录
  },
  {
    path: '/404',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  },
  {
    path: '/:pathMatch(.*)*',  // 捕获所有路由
    redirect: '/404'
  }
];

// 创建路由实例
const router = createRouter({
  // History 模式
  history: createWebHistory(),
  
  // 或者 Hash 模式
  // history: createWebHashHistory(),
  
  routes,
  
  // 滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition;
    } else {
      return { top: 0 };
    }
  }
});

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 设置标题
  document.title = to.meta.title || 'Vue App';
  
  // 权限验证
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next('/login');
  } else {
    next();
  }
});

// 全局后置钩子
router.afterEach((to, from) => {
  console.log(`从 ${from.path} 跳转到 ${to.path}`);
});

export default router;

// ===== main.js =====
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App)
  .use(router)
  .mount('#app');

// ===== App.vue =====
<template>
  <div id="app">
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
      <router-link :to="{ name: 'User', params: { id: 123 } }">
        用户123
      </router-link>
    </nav>
    
    <!-- 路由出口 -->
    <router-view />
  </div>
</template>

// ===== 组件中使用 =====
<script setup>
import { useRouter, useRoute } from 'vue-router';

const router = useRouter();
const route = useRoute();

// 获取路由参数
console.log('用户ID:', route.params.id);

// 获取查询参数
console.log('查询参数:', route.query);

// 编程式导航
function goToAbout() {
  router.push('/about');
  // 或
  // router.push({ name: 'About' });
}

// 带查询参数
function goToUser() {
  router.push({
    path: '/user/123',
    query: { tab: 'profile' }
  });
  // URL: /user/123?tab=profile
}

// 后退
function goBack() {
  router.back();
}
</script>

六、React Router 实战

React Router 基础使用

// ===== 安装 =====
// npm install react-router-dom

// ===== App.jsx =====
import {
  BrowserRouter,
  // HashRouter,  // Hash 模式
  Routes,
  Route,
  Link,
  Navigate,
  useNavigate,
  useParams,
  useLocation
} from 'react-router-dom';

import Home from './pages/Home';
import About from './pages/About';
import User from './pages/User';
import NotFound from './pages/NotFound';

function App() {
  return (
    <BrowserRouter>
      {/* 或使用 HashRouter */}
      {/* <HashRouter> */}
      
      <nav>
        <Link to="/">首页</Link>
        <Link to="/about">关于</Link>
        <Link to="/user/123">用户123</Link>
      </nav>
      
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/user/:id" element={<User />} />
        <Route path="/404" element={<NotFound />} />
        <Route path="*" element={<Navigate to="/404" replace />} />
      </Routes>
    </BrowserRouter>
  );
}

// ===== 组件中使用 =====
// pages/User.jsx
import { useParams, useNavigate, useLocation } from 'react-router-dom';

function User() {
  const navigate = useNavigate();
  const { id } = useParams();  // 获取路由参数
  const location = useLocation();  // 获取当前路径
  
  // 获取查询参数
  const searchParams = new URLSearchParams(location.search);
  const tab = searchParams.get('tab');
  
  // 编程式导航
  function goToAbout() {
    navigate('/about');
  }
  
  // 带查询参数
  function goToHome() {
    navigate('/?page=1');
  }
  
  // 后退
  function goBack() {
    navigate(-1);
  }
  
  return (
    <div>
      <h1>用户 {id}</h1>
      <p>Tab: {tab}</p>
      <button onClick={goToAbout}>去关于页面</button>
      <button onClick={goBack}>返回</button>
    </div>
  );
}

七、Hash vs History 对比表

特性 Hash 模式 History 模式
URL 格式 example.com/#/page example.com/page
美观度 ⭐⭐ ⭐⭐⭐⭐⭐
兼容性 IE8+ IE10+
SEO ❌ 不友好 ✅ 友好
服务器配置 ✅ 无需配置 ❌ 必须配置
锚点定位 ❌ 不能用 ✅ 可以用
监听事件 hashchange popstate
原理 location.hash history.pushState
推荐场景 快速开发、无后端 正式项目、需要SEO

八、总结与记忆口诀 📝

核心记忆

两种模式

  • Hash 模式:URL 有 #,无需服务器配置
  • History 模式:URL 美观,需要服务器配置

实现原理

  • Hash:监听 hashchange,改变 location.hash
  • History:监听 popstate,使用 history.pushState

记忆口诀

Hash 有井号,History 无
Hash 简单配,History 要服务器
Hash 兼容好,History 更美观

九、面试加分项 🌟

前端面试提升点

  • ✅ 能清晰讲解 Hash 和 History 的区别
  • ✅ 理解前端路由的实现原理
  • ✅ 知道 History 模式需要服务器配置的原因
  • ✅ 能手写简单的路由系统

业务代码提升点

  • ✅ 使用 History 模式(更专业)
  • ✅ 合理使用路由懒加载(减少首屏时间)
  • ✅ 使用路由守卫控制权限
  • ✅ 处理 404 和路由重定向

架构能力增强点

  • ✅ 设计路由权限系统
  • ✅ 实现路由级别的代码分割
  • ✅ 优化路由切换动画
  • ✅ 实现嵌套路由和动态路由

记住:前端路由是 SPA 的核心,掌握好路由才能做好单页应用! 🗺️

#前端面试小册##前端##银行##滴滴##阿里#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务