前端路由的秘密:手写一个迷你路由,看懂Hash和History的较量

你是不是也遇到过这样的场景?开发单页面应用时,页面跳转后刷新一下就404,或者URL里带着难看的#号,被产品经理吐槽不够优雅?

别担心,今天我就带你彻底搞懂前端路由的两种模式,手把手教你实现一个迷你路由,并告诉你什么场景该用哪种方案。

读完本文,你能获得一套完整的前端路由知识体系,从原理到实战,再到生产环境配置,一次性全搞定!

为什么需要前端路由?

想象一下,你正在开发一个后台管理系统。传统做法是每个页面都对应一个HTML文件,切换页面就要重新加载,体验特别差。

而前端路由让你可以在一个页面内实现不同视图的切换,URL变化了但页面不刷新,用户体验流畅得像原生APP一样。

手写迷你路由:50行代码看懂原理

我们先来实现一个最简单的路由,这样你就能彻底明白路由是怎么工作的了。

javascript 复制代码
// 定义我们的迷你路由类
class MiniRouter {
  constructor() {
    // 保存路由配置
    this.routes = {};
    // 当前URL的hash
    this.currentUrl = '';
    
    // 监听hashchange事件
    window.addEventListener('hashchange', this.refresh.bind(this));
  }
  
  // 添加路由配置
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }
  
  // 路由刷新
  refresh() {
    // 获取当前hash,去掉#号
    this.currentUrl = location.hash.slice(1) || '/';
    
    // 执行对应的回调函数
    if (this.routes[this.currentUrl]) {
      this.routes[this.currentUrl]();
    }
  }
  
  // 初始化
  init() {
    window.addEventListener('load', this.refresh.bind(this), false);
  }
}

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

router.init();

// 配置路由
router.route('/', function() {
  document.body.innerHTML = '这是首页';
});

router.route('/about', function() {
  document.body.innerHTML = '这是关于页面';
});

router.route('/contact', function() {
  document.body.innerHTML = '这是联系我们页面';
});

这段代码虽然简单,但包含了路由的核心逻辑:监听URL变化,然后执行对应的函数来更新页面内容。

现在你可以在浏览器里试试,访问http://your-domain.com/#/about就能看到效果了!

Hash模式:简单粗暴的解决方案

Hash模式是利用URL中#号后面的部分来实现的。比如http://example.com/#/user#/user就是hash部分。

Hash模式的实现原理

javascript 复制代码
// 监听hash变化
window.addEventListener('hashchange', function() {
  const hash = location.hash.slice(1); // 去掉#号
  console.log('当前hash:', hash);
  
  // 根据不同的hash显示不同内容
  switch(hash) {
    case '/home':
      showHomePage();
      break;
    case '/about':
      showAboutPage();
      break;
    default:
      showNotFound();
  }
});

// 手动改变hash
function navigateTo(path) {
  location.hash = path;
}

// 使用示例
navigateTo('/about'); // URL变成 http://example.com/#/about

Hash模式的优点

兼容性极好,能支持到IE8。不需要服务器端任何配置,因为#号后面的内容不会发给服务器。

部署简单,直接扔到静态服务器就能用。

Hash模式的缺点

URL中带着#号,看起来不够优雅。SEO支持不好,搜索引擎对#后面的内容理解有限。

History模式:优雅的专业选择

History模式利用了HTML5的History API,让URL看起来和正常页面一样,比如http://example.com/user

History API的核心方法

javascript 复制代码
// 跳转到新URL,但不刷新页面
history.pushState({}, '', '/user');

// 替换当前URL
history.replaceState({}, '', '/settings');

// 监听前进后退
window.addEventListener('popstate', function() {
  // 这里处理路由变化
  handleRouteChange(location.pathname);
});

手写History路由

javascript 复制代码
class HistoryRouter {
  constructor() {
    this.routes = {};
    
    // 监听popstate事件(浏览器前进后退)
    window.addEventListener('popstate', (e) => {
      const path = location.pathname;
      this.routes[path] && this.routes[path]();
    });
  }
  
  // 添加路由
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }
  
  // 跳转
  push(path) {
    history.pushState({}, '', path);
    this.routes[path] && this.routes[path]();
  }
  
  // 初始化
  init() {
    // 页面加载时执行当前路由
    const path = location.pathname;
    this.routes[path] && this.routes[path]();
  }
}

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

router.route('/', function() {
  document.body.innerHTML = 'History模式首页';
});

router.route('/about', function() {
  document.body.innerHTML = 'History模式关于页面';
});

// 初始化
router.init();

// 编程式导航
document.getElementById('about-btn').addEventListener('click', () => {
  router.push('/about');
});

History模式的优点

URL美观,没有#号。SEO友好,搜索引擎能正常抓取。

state对象可以保存页面状态。

History模式的缺点

需要服务器端配合,否则刷新会404。兼容性稍差,IE10+支持。

两种模式的深度对比

在实际项目中,我们该怎么选择呢?来看几个关键维度的对比:

兼容性方面 Hash模式几乎支持所有浏览器,包括老旧的IE。History模式需要IE10+,对于需要支持老浏览器的项目,Hash是更安全的选择。

SEO优化 如果你的网站需要搜索引擎收录,History模式是更好的选择。虽然现代搜索引擎已经能解析JavaScript,但History模式的URL结构更受搜索引擎欢迎。

开发体验 History模式的URL更简洁,在分享链接时用户体验更好。但开发阶段需要配置服务器,稍微麻烦一些。

部署复杂度 Hash模式部署简单,直接上传到任何静态托管服务就行。History模式需要服务器配置,下面我们会详细讲。

服务端配置:解决History模式的404问题

History模式最大的坑就是:如果你直接访问http://example.com/user或者刷新页面,服务器会返回404,因为这个路径在服务器上并不存在。

解决方案是让服务器对所有路径都返回同一个HTML文件:

Nginx配置

nginx 复制代码
server {
  listen 80;
  server_name example.com;
  root /path/to/your/app;
  
  location / {
    try_files $uri $uri/ /index.html;
  }
}

这个配置的意思是:先尝试找对应的文件,如果找不到就返回index.html。

Node.js Express配置

javascript 复制代码
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);

Apache配置

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>

实战:完整的前端路由库

现在我们把两种模式整合起来,实现一个完整的前端路由:

javascript 复制代码
class AdvancedRouter {
  constructor(mode = 'hash') {
    this.mode = mode;
    this.routes = {};
    this.current = '';
    
    this.init();
  }
  
  init() {
    if (this.mode === 'hash') {
      // Hash模式监听
      window.addEventListener('hashchange', () => {
        this.handleChange(location.hash.slice(1));
      });
      // 初始化
      this.handleChange(location.hash.slice(1) || '/');
    } else {
      // History模式监听
      window.addEventListener('popstate', () => {
        this.handleChange(location.pathname);
      });
      // 初始化
      this.handleChange(location.pathname);
    }
  }
  
  handleChange(path) {
    this.current = path;
    const callback = this.routes[path] || this.routes['*'];
    
    if (callback) {
      callback();
    }
  }
  
  // 添加路由
  on(path, callback) {
    this.routes[path] = callback;
  }
  
  // 跳转
  go(path) {
    if (this.mode === 'hash') {
      location.hash = path;
    } else {
      history.pushState({}, '', path);
      this.handleChange(path);
    }
  }
  
  // 替换
  replace(path) {
    if (this.mode === 'hash') {
      location.replace(location.pathname + '#' + path);
    } else {
      history.replaceState({}, '', path);
      this.handleChange(path);
    }
  }
}

// 使用示例
const router = new AdvancedRouter('history');

router.on('/', () => {
  showPage('home');
});

router.on('/about', () => {
  showPage('about');
});

router.on('*', () => {
  showPage('not-found');
});

function showPage(pageName) {
  document.body.innerHTML = `当前页面:${pageName}`;
}

性能优化技巧

路由用得不好会影响性能,这里分享几个实用技巧:

路由懒加载

javascript 复制代码
// 动态导入,只有访问时才加载
router.on('/heavy-page', async () => {
  const { HeavyComponent } = await import('./HeavyComponent.js');
  render(HeavyComponent);
});

路由缓存

javascript 复制代码
const cache = new Map();

router.on('/expensive-page', () => {
  if (cache.has('/expensive-page')) {
    // 使用缓存
    showContent(cache.get('/expensive-page'));
    return;
  }
  
  // 首次加载
  fetchData().then(data => {
    cache.set('/expensive-page', data);
    showContent(data);
  });
});

常见坑点及解决方案

1. 路由循环跳转

javascript 复制代码
// 错误示例:会导致无限循环
router.on('/login', () => {
  if (!isLoggedIn) {
    router.go('/login'); // 循环了!
  }
});

// 正确做法
router.on('/login', () => {
  if (!isLoggedIn) {
    return; // 停留在登录页
  }
  router.go('/dashboard');
});

2. 路由权限控制

javascript 复制代码
// 路由守卫
router.beforeEach = (to, from, next) => {
  if (to.path === '/admin' && !isAdmin()) {
    next('/login');
  } else {
    next();
  }
};

该用Hash还是History?

看到这里,你可能还是有点纠结。我给你一个简单的决策流程:

如果你的项目是内部系统,不需要SEO,或者要支持IE8/9,选Hash模式。

如果是面向公众的网站,需要SEO,且能控制服务器配置,选History模式。

如果是微前端架构的子应用,建议用Hash模式,避免与主应用冲突。

写在最后

前端路由看似简单,但里面有很多细节值得深究。无论是Hash的兼容性优势,还是History的优雅体验,都有各自的适用场景。

现在主流框架的路由库(Vue Router、React Router)都同时支持两种模式,原理和我们今天手写的迷你路由大同小异。

理解了底层原理,你再使用这些高级路由库时,就能更得心应手,遇到问题也知道如何调试和解决。

你在项目中用的是Hash模式还是History模式?遇到过什么有趣的问题吗?欢迎在评论区分享你的经验!

相关推荐
偷光4 小时前
浏览器中的隐藏IDE: Elements (元素) 面板
开发语言·前端·ide·php
江拥羡橙9 小时前
Vue和React怎么选?全面比对
前端·vue.js·react.js
千码君20169 小时前
React Native:快速熟悉react 语法和企业级开发
javascript·react native·react.js·vite·hook
楼田莉子10 小时前
Qt开发学习——QtCreator深度介绍/程序运行/开发规范/对象树
开发语言·前端·c++·qt·学习
暮之沧蓝10 小时前
Vue总结
前端·javascript·vue.js
木易 士心11 小时前
Promise深度解析:前端异步编程的核心
前端·javascript
im_AMBER11 小时前
Web 开发 21
前端·学习
又是忙碌的一天11 小时前
前端学习day01
前端·学习·html
Joker Zxc11 小时前
【前端基础】20、CSS属性——transform、translate、transition
前端·css