什么是前端路由?
路由的概念来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系。
在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。
如何实现前端路由?
要实现前端路由,需要解决两个核心问题:
- 如何改变 URL 却不引起页面刷新?
- 如何检测 URL 变化了?
下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。
hash 实现
- hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新
- 通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:通过浏览器前进后退改变 URL、通过<a>标签改变 URL、通过window.location改变URL,这几种情况改变 URL 都会触发 hashchange 事件
history 实现
- history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
- history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进后退改变 URL 时会触发 popstate 事件,通过pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
原生路由实现
hash
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Native Router Hash</title> </head> <body> <ul> <!-- 定义路由 --> <li><a href="#/home">home</a></li> <li><a href="#/about">about</a></li> <!-- 渲染路由对应的 UI --> <div id="routeView"></div> </ul> </body> <script> // 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件 window.addEventListener('DOMContentLoaded', onLoad) // 监听路由变化 window.addEventListener('hashchange', onHashChange) // 路由视图 var routerView = null function onLoad() { routerView = document.querySelector('#routeView') onHashChange() } // 路由变化时,根据路由渲染对应 UI function onHashChange() { console.log(window.location) switch (location.hash) { case '#/home': routerView.innerHTML = 'Home' return case '#/about': routerView.innerHTML = 'About' return default: return } } </script> </html>
history
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Native Router History</title> </head> <body> <ul> <li><a href='/home'>home</a></li> <li><a href='/about' class='GGHHJJ'>about</a></li> <div id="routeView"></div> </ul> </body> <script> // 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件 window.addEventListener('DOMContentLoaded', onLoad) // 监听路由变化 window.addEventListener('popstate', onPopState) // 路由视图 var routerView = null function onLoad() { routerView = document.querySelector('#routeView') onPopState() // 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动更新 UI,从而实现点击链接更新 URL 和 UI 的效果。 var linkList = document.querySelectorAll('a[href]') linkList.forEach(el => el.addEventListener('click', function (e) { e.preventDefault() history.pushState(null, '', el.getAttribute('href')) onPopState() })) } // 路由变化时,根据路由渲染对应 UI function onPopState() { switch (location.pathname) { case '/home': routerView.innerHTML = 'Home' return case '/about': routerView.innerHTML = 'About' return default: return } } </script> </html>
angular UI-router
// 所需插件版本 "@uirouter/angular": "^9.1.0", "@uirouter/core": "^6.0.8", "@uirouter/rx": "^1.0.0",
app.module.ts
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent, Page1, Page3, Page2 } from './app.component';// 导入组件模块 import { UIRouterModule } from "@uirouter/angular";// 导入 uirouter UIRouterModule 方法 import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; // UI router state // const Page1State = { name: "Page1", url: "/Page1", component: Page1 }; const Page2State = { name: "Page2", url: "/Page2", component: Page2 }; const Page3State = { name: "Page3", url: "/Page3", component: Page3 }; // imports: [ BrowserModule, UIRouterModule.forRoot({ ... // 允许您应用的模块使用来自另一个模块的代码。在此示例中,UIRouterModule.forRoot导入 UI-Router 模块,并注册列出的状态。 // declarations: [ Page1, Page2, Page3 ] // 声明根模块中使用的所有组件。 // bootstrap: [ AppComponent ] // 告诉 Angular 引导App组件作为应用程序的根。 @NgModule({ imports: [ BrowserModule, UIRouterModule.forRoot({ states: [Page1State, Page2State, Page3State], useHash: true }) ], declarations: [ AppComponent, Page1, Page2, Page3 ], bootstrap: [AppComponent] }) export class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule).then(ref => { // 确保 Angular 在热重载时自行销毁。 if (window['ngRef']) { window['ngRef'].destroy(); } window['ngRef'] = ref; }).catch(err => console.error(err));
app.component.html
// 每个标记都包含一个uiSref指令。uiSref指令是链接,类似于锚标记的href. 不是像 a href那样链接到 URL,而是uiSref链接到状态。 // 单击时,链接状态被激活。该指令会根据您所在州的 urluiSref自动为您构建一个href属性 ( )。<a href=...></a> // 当uiSref链接到的状态为活动时,uiSrefActive会将activeCSS 类添加到链接 <a uiSref="Page1" uiSrefActive="active">Page-1</a> <a uiSref="Page2" uiSrefActive="active">Page-2</a> <a uiSref="Page3" uiSrefActive="active">Page-3</a> // 此视口将填充当前活动状态的组件 <ui-view></ui-view>
app.component.ts
import { Component } from '@angular/core'; // root app @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'uiRouterDemo'; } // component 1 @Component({ template: "<h3>Page-1</h3>" }) export class Page1 {} // component 2 @Component({ template: "<h3>Page-2</h3>" }) export class Page2 {} //component 3 @Component({ template: "<h3>Page-3</h3>" }) export class Page3 {}