在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂。想要搞懂路由内部实现还是有些困难的,但是如果只想了解路由实现基本原理还是比较简单的。本文针对前端路由主流的实现方式 hash 和 history,提供了原生JS/React/Vue 共计六个版本供参考,每个版本的实现代码约 25~40 行左右(含空行)。

什么是前端路由?

路由的概念来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系。

在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。

如何实现前端路由?

要实现前端路由,需要解决两个核心问题:

  • 如何改变 URL 却不引起页面刷新?

  • 如何检测 URL 变化了?

下面分别使用 hash 和 history 两种实现方式回答上面的两个核心问题。

hash 实现

  • hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新

  • 通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:通过浏览器前进后退改变 URL、通过标签改变 URL、通过window.location改变URL,这几种情况改变 URL 都会触发 hashchange 事件

history 实现

  • history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新

  • history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进后退改变 URL 时会触发 popstate 事件,通过pushState/replaceState或标签改变 URL 不会触发 popstate 事件。好在我们可以拦截 pushState/replaceState的调用和标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。

原生JS版前端路由实现

基于上节讨论的两种实现方式,分别实现 hash 版本和 history 版本的路由,示例使用原生 HTML/JS 实现,不依赖任何框架。

基于 hash 实现

运行效果:


HTML 部分:

<body><ul><!-- 定义路由 --><li><a href="#/home">home</a></li><li><a href="#/about">about</a></li><!-- 渲染路由对应的 UI --><div id="routeView"></div></ul>
</body>

JavaScript 部分:

// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('hashchange', onHashChange)// 路由视图
var routerView = nullfunction onLoad () {routerView = document.querySelector('#routeView')onHashChange()
}// 路由变化时,根据路由渲染对应 UI
function onHashChange () {switch (location.hash) {case '#/home':routerView.innerHTML = 'Home'returncase '#/about':routerView.innerHTML = 'About'returndefault:return}
}

基于 history 实现

运行效果:


HTML 部分:

<body><ul><li><a href='/home'>home</a></li><li><a href='/about'>about</a></li><div id="routeView"></div></ul>
</body>

JavaScript 部分:

// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('popstate', onPopState)// 路由视图
var routerView = nullfunction 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'returncase '/about':routerView.innerHTML = 'About'returndefault:return}
}

React 版前端路由实现

基于 hash 实现

运行效果:

使用方式和 react-router 类似:

  <BrowserRouter><ul><li><Link to="/home">home</Link></li><li><Link to="/about">about</Link></li></ul><Route path="/home" render={() => <h2>Home</h2>} /><Route path="/about" render={() => <h2>About</h2>} /></BrowserRouter>

BrowserRouter 实现

export default class BrowserRouter extends React.Component {state = {currentPath: utils.extractHashPath(window.location.href)};onHashChange = e => {const currentPath = utils.extractHashPath(e.newURL);console.log("onHashChange:", currentPath);this.setState({ currentPath });};componentDidMount() {window.addEventListener("hashchange", this.onHashChange);}componentWillUnmount() {window.removeEventListener("hashchange", this.onHashChange);}render() {return (<RouteContext.Provider value={{currentPath: this.state.currentPath}}>{this.props.children}</RouteContext.Provider>);}
}

Route 实现

export default ({ path, render }) => (<RouteContext.Consumer>{({currentPath}) => currentPath === path && render()}</RouteContext.Consumer>
);

Link 实现

export default ({ to, ...props }) => <a {...props} href={"#" + to} />;

基于 history 实现

运行效果:

使用方式和 react-router 类似:

  <HistoryRouter><ul><li><Link to="/home">home</Link></li><li><Link to="/about">about</Link></li></ul><Route path="/home" render={() => <h2>Home</h2>} /><Route path="/about" render={() => <h2>About</h2>} /></HistoryRouter>

HistoryRouter 实现:

export default class HistoryRouter extends React.Component {state = {currentPath: utils.extractUrlPath(window.location.href)};onPopState = e => {const currentPath = utils.extractUrlPath(window.location.href);console.log("onPopState:", currentPath);this.setState({ currentPath });};componentDidMount() {window.addEventListener("popstate", this.onPopState);}componentWillUnmount() {window.removeEventListener("popstate", this.onPopState);}render() {return (<RouteContext.Provider value={{currentPath: this.state.currentPath, onPopState: this.onPopState}}>{this.props.children}</RouteContext.Provider>);}
}

Route 实现

export default ({ path, render }) => (<RouteContext.Consumer>{({currentPath}) => currentPath === path && render()}</RouteContext.Consumer>
);

Link 实现

export default ({ to, ...props }) => (<RouteContext.Consumer>{({ onPopState }) => (<ahref=""{...props}onClick={e => {e.preventDefault();window.history.pushState(null, "", to);onPopState();}}/>)}</RouteContext.Consumer>
);

Vue 版本前端路由实现

基于 hash 实现

运行效果:

使用方式和 vue-router 类似(vue-router 通过插件机制注入路由,但是这样隐藏了实现细节,为了保持代码直观,这里没有使用 Vue 插件封装):

    <div><ul><li><router-link to="/home">home</router-link></li><li><router-link to="/about">about</router-link></li></ul><router-view></router-view></div>
const routes = {'/home': {template: '<h2>Home</h2>'},'/about': {template: '<h2>About</h2>'}
}const app = new Vue({el: '.vue.hash',components: {'router-view': RouterView,'router-link': RouterLink},beforeCreate () {this.$routes = routes}
})

router-view 实现:

<template><component :is="routeView" />
</template><script>
import utils from '~/utils.js'
export default {data () {return {routeView: null}},created () {this.boundHashChange = this.onHashChange.bind(this)},beforeMount () {window.addEventListener('hashchange', this.boundHashChange)},mounted () {this.onHashChange()},beforeDestroy() {window.removeEventListener('hashchange', this.boundHashChange)},methods: {onHashChange () {const path = utils.extractHashPath(window.location.href)this.routeView = this.$root.$routes[path] || nullconsole.log('vue:hashchange:', path)}}
}
</script>

router-link 实现:

<template><a @click.prevent="onClick" href=''><slot></slot></a>
</template><script>
export default {props: {to: String},methods: {onClick () {window.location.hash = '#' + this.to}}
}
</script>

基于 history 实现

运行效果:

使用方式和 vue-router 类似:

    <div><ul><li><router-link to="/home">home</router-link></li><li><router-link to="/about">about</router-link></li></ul><router-view></router-view></div>
const routes = {'/home': {template: '<h2>Home</h2>'},'/about': {template: '<h2>About</h2>'}
}const app = new Vue({el: '.vue.history',components: {'router-view': RouterView,'router-link': RouterLink},created () {this.$routes = routesthis.boundPopState = this.onPopState.bind(this)},beforeMount () {window.addEventListener('popstate', this.boundPopState) },beforeDestroy () {window.removeEventListener('popstate', this.boundPopState) },methods: {onPopState (...args) {this.$emit('popstate', ...args)}}
})

router-view 实现:

<template><component :is="routeView" />
</template><script>
import utils from '~/utils.js'
export default {data () {return {routeView: null}},created () {this.boundPopState = this.onPopState.bind(this)},beforeMount () {this.$root.$on('popstate', this.boundPopState)},beforeDestroy() {this.$root.$off('popstate', this.boundPopState)},methods: {onPopState (e) {const path = utils.extractUrlPath(window.location.href)this.routeView = this.$root.$routes[path] || nullconsole.log('[Vue] popstate:', path)}}
}
</script>

router-link 实现:

<template><a @click.prevent="onClick" href=''><slot></slot></a>
</template><script>
export default {props: {to: String},methods: {onClick () {history.pushState(null, '', this.to)this.$root.$emit('popstate')}}
}
</script>

小结

前端路由的核心实现原理很简单,但是结合具体框架后,框架增加了很多特性,如动态路由、路由参数、路由动画等等,这些导致路由实现变的复杂。本文去粗取精只针对前端路由最核心部分的实现进行分析,并基于 hash 和 history 两种模式,分别提供原生JS/React/Vue 三种实现,共计六个实现版本供参考,希望对你有所帮助。

所有的示例的代码放在 Github 仓库:

https://github.com/whinc/web-router-principle

参考

  • 详解单页面路由的几种实现原理

  • 单页面应用路由实现原理:以 React-Router 为例

文中链接如打不开,可以点击下方知乎链接查看:

令人惊叹的前端路由原理解析和实现方式相关推荐

  1. reactrouter监听路由变化_一篇文章搞懂前端路由原理解析和实现方式

    在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂. 想要搞懂路由内部实现还是有些困难的,但是如果只想了解路由实现基本原理 ...

  2. react router 级联路由_前端路由原理解析和实现

    作者:@whinc链接:https://github.com/whinc/blog/issues/13 在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大 ...

  3. return view前端怎么获取_Web 前端路由原理解析和功能实现

    ↑ 点击上面 "时代Java"关注我们, 关注新技术,学习新知识! 什么是前端路由? 路由的概念来源于服务端,在服务端中路由描述的是 URL 与处理函数之间的映射关系. 在 Web ...

  4. 【前端路由原理--原生JS实现方式】--前后端路由的区别、关于单页面应用与多页面应用、什么是CSR、SSR、SSG、ISP

    前言 本来只是想学习 React-Router v6 ,没有想到,带出了这么多东西.前后端路由有什么区别?SPA与MPA的是什么?在了解到前端路之后又发现单页面于应用与多页面应用的不同之处,以及 .n ...

  5. 单页面应用的前端路由原理是什么?

    前置知识 在了解单页面应用的前端路由原理之前,我们先了解下什么事单页面应用,什么是多页面应用,他们之间的区别又是什么? 什么是单页面应用? 单页面应用指的是第一次进入页面的时候会请求一个html文件, ...

  6. 浅谈前端路由原理hash和history

    浅谈前端路由原理hash和history

  7. c语言实现路由功能,前端路由的两种实现方式,内附详细代码

    一.前端路由介绍 前端路由主要应用在SPA(单页面开发)项目中.在无刷新的情况下,根据不同的URL来显示不同的组件或者内容. 前端路由的实现原理 : hash值 + onhashchange事件 hi ...

  8. navigator工具_推荐!11个令人惊叹的前端开发工具,值得拥有

    全文共2080字,预计学习时长6分钟 来源:Pexels 作为前端开发人员,互联网有很多令人惊叹的工具,这些工具为我们的生活提供了极大的便利. 本文将快速回顾开发工作中经常使用的 11 个工具. 大家 ...

  9. 父页面监听iframe路由变化_前端路由原理

    对于前端路由应该都很熟悉了,开发过spa应用的应该都用过,只是很少人去查一下前端路由实现的原理. 前端路由的实现核心问题有两个,一个是改变url不刷新,另一个是监听url变化.主要靠的就是hash和h ...

最新文章

  1. 原创 | 工业场景中的预测性维护
  2. 关于计算机和人物的英语短文,人脑和电脑英语作文
  3. bootstrap --- 表单
  4. error: failed to push some refs to ‘github.com:English.git‘ hint: Updates w
  5. Bootstrap 异常
  6. 近些年很火的Java,就业前景到底怎么样?
  7. 金格pdf打开服务器文件,金格插件解决方法.pdf
  8. snmp trap配置
  9. 解决手机邮箱登录学生邮箱时服务器无法连接的问题
  10. oracle字段Varchar2长度问题
  11. 计算机程序有哪些性质,程序的特性有哪些
  12. itext linux 中文乱码_itext linux 中文
  13. https://mp.weixin.qq.com/s?__biz=MzkxNTIzODIxNQ==tempkey=MTEwOV9qbGlub091aVlKb1NtOVZKV3lTcHgwd3U0NG
  14. 2 SAP SCC1同一服务器传请求号
  15. 教大家如何修改Mac电脑上的DNS
  16. Android微信SDK实现分享
  17. Markdown KaTex 积分符号
  18. oracle怎么退出,[转载]Oracle 11g SQL*Plus的几种启动、退出方法
  19. ar vr mr 计算机技术,VR、AR、MR、还有云计算,他们到底是什么?
  20. iOS录音麦克风和系统震动的冲突

热门文章

  1. 20170626_oracle_数据库设计
  2. 【SpringMVC】SpringMVC基础-SpringMVC项目快速搭建、日志框架为logback
  3. TYVJ P1051 选课 Label:多叉转二叉树形dp(虐心♥)
  4. 牛逼站是怎样炼成的?-推荐系统篇
  5. 【Programming Clip】06、07年清华计算机考研上机试题解答(个别测试用例无法通过)...
  6. SSIS中的容器和数据流—调试工具数据视图
  7. Linux系统检测工具 三(Free,Top,ps,Vmstat,Sysstat)
  8. 如何将本地代码使用Git上传至Github
  9. CodeForces - 1076D Edge Deletion(最短路+贪心/最短路树+bfs)
  10. CodeForces - 1417E XOR Inverse(字典树求逆序对+分治)