如何利用 JavaScript 实现一个简单的路由 (Router) 功能?

各位观众老爷们,大家好!今天咱们来聊聊前端路由这个磨人的小妖精。别害怕,咱们的目标是把它驯服,让它乖乖听话,而不是被它搞得焦头烂额。

一、什么是路由?为啥我们需要它?

想象一下你正在浏览一个网站。你点击了“首页”,页面内容变成了首页;点击了“关于我们”,页面内容又变成了关于我们的介绍。这个切换过程,就是路由在背后默默地工作。

简单来说,路由就是根据 URL 的变化,来决定显示什么内容

在单页应用 (SPA) 中,路由尤为重要。因为 SPA 的特点是只有一个 HTML 页面,所有的页面切换都在这个页面内部完成。如果没有路由,整个应用就会像一锅粥,所有内容都堆在一起,用户体验简直是灾难。

二、手撸一个简单的路由:思路先行

要实现一个基本的路由功能,我们可以大致分为以下几个步骤:

  1. 监听 URL 的变化: 浏览器提供了 hashchangepopstate 两个事件,可以用来监听 URL 的变化。
  2. 解析 URL: 获取 URL 中的路径 (path),例如 /home/about
  3. 匹配路由: 将解析出的路径与我们预先定义的路由规则进行匹配。
  4. 渲染对应的组件: 根据匹配结果,渲染对应的页面内容。

三、代码实现:一步一个脚印

接下来,咱们就用代码来实现这个过程。

1. HTML 骨架

先来一个简单的 HTML 结构,作为我们路由的容器:

<!DOCTYPE html>
<html>
<head>
  <title>Simple Router</title>
</head>
<body>
  <nav>
    <a href="#/home">Home</a> |
    <a href="#/about">About</a> |
    <a href="#/contact">Contact</a>
  </nav>

  <div id="app">
    <!-- 这里的内容会根据路由变化 -->
  </div>

  <script src="router.js"></script>
</body>
</html>

这里使用了 hash 路由,也就是 URL 中带有 # 符号。 这种路由方式兼容性好,实现起来也简单。

2. JavaScript 路由核心

// router.js

class Router {
  constructor() {
    this.routes = {}; // 存储路由规则
    this.app = document.getElementById('app'); // 获取路由容器
    this.currentRoute = null;

    // 监听 hashchange 事件
    window.addEventListener('hashchange', this.handleRouteChange.bind(this));

    // 页面加载时,也要执行一次路由
    window.addEventListener('load', this.handleRouteChange.bind(this));
  }

  // 添加路由规则
  addRoute(path, callback) {
    this.routes[path] = callback;
  }

  // 处理路由变化
  handleRouteChange() {
    const path = this.getHashPath();

    if (this.currentRoute === path) {
      return; // 防止重复渲染
    }

    this.currentRoute = path;

    const routeHandler = this.routes[path] || this.routes['*']; // 匹配路由,如果没有匹配到,就使用默认路由
    if (routeHandler) {
      routeHandler();
    } else {
      this.app.innerHTML = '<h1>404 Not Found</h1>';
    }
  }

  // 获取 hash 中的路径
  getHashPath() {
    const hash = window.location.hash;
    return hash.slice(1); // 去掉 # 符号
  }

  // 渲染内容到容器
  render(content) {
    this.app.innerHTML = content;
  }
}

// 创建 Router 实例
const router = new Router();

// 定义路由规则
router.addRoute('/home', () => {
  router.render('<h1>Home Page</h1><p>Welcome to the home page!</p>');
});

router.addRoute('/about', () => {
  router.render('<h1>About Us</h1><p>This is the about us page.</p>');
});

router.addRoute('/contact', () => {
  router.render('<h1>Contact Us</h1><p>You can contact us at [email protected]</p>');
});

// 添加一个默认路由,处理 404 情况
router.addRoute('*', () => {
  router.render('<h1>404 Not Found</h1><p>The requested page was not found.</p>');
});

这段代码的核心在于 Router 类。它负责存储路由规则,监听 URL 变化,匹配路由,以及渲染对应的页面内容。

代码解释:

  • constructor(): 构造函数,初始化路由信息,绑定事件监听器。
  • addRoute(path, callback): 添加路由规则,path 是 URL 路径,callback 是当路径匹配时要执行的回调函数。
  • handleRouteChange(): 处理 URL 变化的核心函数。它会获取当前 URL 的路径,并根据路径匹配对应的回调函数。
  • getHashPath(): 从 URL 的 hash 中提取路径。
  • render(content): 将内容渲染到指定的容器中。

3. 运行效果

现在,打开你的 HTML 文件,点击导航链接,就可以看到页面内容随着 URL 的变化而改变了。

四、进阶:使用 History API

虽然 hash 路由简单易用,但 URL 中带有 # 符号,看起来不够优雅。 我们可以使用 History API 来实现更美观的 URL。

1. 修改 HTML

首先,修改 HTML 中的链接,去掉 # 符号:

<!DOCTYPE html>
<html>
<head>
  <title>Simple Router</title>
</head>
<body>
  <nav>
    <a href="/home">Home</a> |
    <a href="/about">About</a> |
    <a href="/contact">Contact</a>
  </nav>

  <div id="app">
    <!-- 这里的内容会根据路由变化 -->
  </div>

  <script src="router.js"></script>
</body>
</html>

2. 修改 JavaScript

然后,修改 JavaScript 代码,使用 pushStatepopstate

// router.js

class Router {
  constructor() {
    this.routes = {};
    this.app = document.getElementById('app');
    this.currentRoute = null;

    // 监听 popstate 事件
    window.addEventListener('popstate', this.handleRouteChange.bind(this));

    // 拦截链接点击事件
    document.addEventListener('click', (event) => {
      if (event.target.tagName === 'A') {
        event.preventDefault(); // 阻止默认的链接跳转行为
        const path = event.target.getAttribute('href');
        this.navigateTo(path);
      }
    });

    // 页面加载时,也要执行一次路由
    this.handleRouteChange();
  }

  addRoute(path, callback) {
    this.routes[path] = callback;
  }

  handleRouteChange() {
    const path = this.getPath();

    if (this.currentRoute === path) {
      return; // 防止重复渲染
    }

    this.currentRoute = path;

    const routeHandler = this.routes[path] || this.routes['*'];
    if (routeHandler) {
      routeHandler();
    } else {
      this.app.innerHTML = '<h1>404 Not Found</h1>';
    }
  }

  // 获取当前路径
  getPath() {
    return window.location.pathname;
  }

  // 导航到指定路径
  navigateTo(path) {
    history.pushState(null, null, path); // 修改 URL,但不刷新页面
    this.handleRouteChange(); // 手动触发路由处理
  }

  render(content) {
    this.app.innerHTML = content;
  }
}

// 创建 Router 实例
const router = new Router();

// 定义路由规则
router.addRoute('/home', () => {
  router.render('<h1>Home Page</h1><p>Welcome to the home page!</p>');
});

router.addRoute('/about', () => {
  router.render('<h1>About Us</h1><p>This is the about us page.</p>');
});

router.addRoute('/contact', () => {
  router.render('<h1>Contact Us</h1><p>You can contact us at [email protected]</p>');
});

// 添加一个默认路由,处理 404 情况
router.addRoute('*', () => {
  router.render('<h1>404 Not Found</h1><p>The requested page was not found.</p>');
});

代码解释:

  • popstate 事件: 当用户点击浏览器的前进/后退按钮时,会触发 popstate 事件。
  • history.pushState(state, title, url) 修改 URL,但不刷新页面。state 可以用来存储一些状态数据,title 是页面的标题(通常浏览器会忽略),url 是新的 URL。
  • navigateTo(path) 导航到指定路径的函数,它会调用 pushState 修改 URL,并手动触发 handleRouteChange 函数。
  • 拦截链接点击事件: 监听 click 事件,如果点击的是 <a> 标签,就阻止默认的链接跳转行为,然后调用 navigateTo 函数。

3. 运行效果

现在,打开你的 HTML 文件,点击导航链接,就可以看到 URL 变成了 /home/about 等,而且页面内容也会相应地变化。

五、总结:路由的本质与扩展

咱们今天手撸了一个简单的路由,虽然功能还比较基础,但已经足以理解路由的本质:监听 URL 变化,匹配路由规则,渲染对应内容

总结一下,我们今天涉及到的知识点:

知识点 描述 示例代码
hashchange 事件 监听 URL 中 hash 的变化 window.addEventListener('hashchange', this.handleRouteChange.bind(this));
popstate 事件 监听浏览器的前进/后退按钮点击事件 window.addEventListener('popstate', this.handleRouteChange.bind(this));
history.pushState() 修改 URL,但不刷新页面 history.pushState(null, null, path);
window.location.hash 获取 URL 中的 hash 部分 const hash = window.location.hash;
window.location.pathname 获取 URL 中的路径部分 return window.location.pathname;
event.preventDefault() 阻止事件的默认行为 event.preventDefault();

扩展方向:

  • 路由参数: 可以支持带参数的路由,例如 /user/:id,其中 :id 是一个参数。
  • 嵌套路由: 可以支持嵌套的路由结构,例如 /admin/users/admin/products
  • 路由守卫: 可以在路由切换前后执行一些逻辑,例如权限验证、数据加载等。
  • 更灵活的匹配规则: 可以使用正则表达式进行路由匹配,提供更强大的灵活性。

当然,实际开发中,我们通常会使用现成的路由库,例如 react-routervue-router 等。 这些库提供了更丰富的功能和更好的性能。 但是,理解路由的原理,才能更好地使用这些库,并在遇到问题时能够快速定位和解决。

好了,今天的讲座就到这里。希望大家有所收获,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注