前端路由原理及实现

在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂。想要搞懂路由内部实现还是有些困难的,但是如果只想了解路由实现基本原理还是比较简单的。本文针对前端路由主流的实现方式 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、通过<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 那么方便。

原生JS版前端路由实现

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

基于 hash 实现

运行效果:

HTML 部分:

 
  1. <body>

  2.   <ul>

  3.     <!-- 定义路由 -->

  4.     <li><a href="#/home">home</a></li>

  5.     <li><a href="#/about">about</a></li>

  6.  
  7.     <!-- 渲染路由对应的 UI -->

  8.     <div id="routeView"></div>

  9.   </ul>

  10. </body>

JavaScript 部分:

 
  1. // 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件

  2. window.addEventListener('DOMContentLoaded', onLoad)

  3. // 监听路由变化

  4. window.addEventListener('hashchange', onHashChange)

  5.  
  6. // 路由视图

  7. var routerView = null

  8.  
  9. function onLoad () {

  10.   routerView = document.querySelector('#routeView')

  11.   onHashChange()

  12. }

  13.  
  14. // 路由变化时,根据路由渲染对应 UI

  15. function onHashChange () {

  16.   switch (location.hash) {

  17.     case '#/home':

  18.       routerView.innerHTML = 'Home'

  19.       return

  20.     case '#/about':

  21.       routerView.innerHTML = 'About'

  22.       return

  23.     default:

  24.       return

  25.   }

  26. }

基于 history 实现

运行效果:

HTML 部分:

 
  1. <body>

  2.   <ul>

  3.     <li><a href='/home'>home</a></li>

  4.     <li><a href='/about'>about</a></li>

  5.  
  6.     <div id="routeView"></div>

  7.   </ul>

  8. </body>

 

JavaScript 部分:

 
  1. // 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件

  2. window.addEventListener('DOMContentLoaded', onLoad)

  3. // 监听路由变化

  4. window.addEventListener('popstate', onPopState)

  5.  
  6. // 路由视图

  7. var routerView = null

  8.  
  9. function onLoad () {

  10.   routerView = document.querySelector('#routeView')

  11.   onPopState()

  12.  
  13.   // 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。

  14.   var linkList = document.querySelectorAll('a[href]')

  15.   linkList.forEach(el => el.addEventListener('click', function (e) {

  16.     e.preventDefault()

  17.     history.pushState(null, '', el.getAttribute('href'))

  18.     onPopState()

  19.   }))

  20. }

  21.  
  22. // 路由变化时,根据路由渲染对应 UI

  23. function onPopState () {

  24.   switch (location.pathname) {

  25.     case '/home':

  26.       routerView.innerHTML = 'Home'

  27.       return

  28.     case '/about':

  29.       routerView.innerHTML = 'About'

  30.       return

  31.     default:

  32.       return

  33.   }

  34. }

React 版前端路由实现

基于 hash 实现

运行效果:

使用方式和 react-router 类似:

 
  1.   <BrowserRouter>

  2.     <ul>

  3.       <li>

  4.         <Link to="/home">home</Link>

  5.       </li>

  6.       <li>

  7.         <Link to="/about">about</Link>

  8.       </li>

  9.     </ul>

  10.  
  11.     <Route path="/home" render={() => <h2>Home</h2>} />

  12.     <Route path="/about" render={() => <h2>About</h2>} />

  13.   </BrowserRouter>

 

BrowserRouter 实现

 
  1. export default class BrowserRouter extends React.Component {

  2.   state = {

  3.     currentPath: utils.extractHashPath(window.location.href)

  4.   };

  5.  
  6.   onHashChange = e => {

  7.     const currentPath = utils.extractHashPath(e.newURL);

  8.     console.log("onHashChange:", currentPath);

  9.     this.setState({ currentPath });

  10.   };

  11.  
  12.   componentDidMount() {

  13.     window.addEventListener("hashchange", this.onHashChange);

  14.   }

  15.  
  16.   componentWillUnmount() {

  17.     window.removeEventListener("hashchange", this.onHashChange);

  18.   }

  19.  
  20.   render() {

  21.     return (

  22.       <RouteContext.Provider value={{currentPath: this.state.currentPath}}>

  23.         {this.props.children}

  24.       </RouteContext.Provider>

  25.     );

  26.   }

  27. }

 

Route 实现

 
  1. export default ({ path, render }) => (

  2.   <RouteContext.Consumer>

  3.     {({currentPath}) => currentPath === path && render()}

  4.   </RouteContext.Consumer>

  5. );

 

Link 实现

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

基于 history 实现

运行效果:

使用方式和 react-router 类似:

 
  1.   <HistoryRouter>

  2.     <ul>

  3.       <li>

  4.         <Link to="/home">home</Link>

  5.       </li>

  6.       <li>

  7.         <Link to="/about">about</Link>

  8.       </li>

  9.     </ul>

  10.  
  11.     <Route path="/home" render={() => <h2>Home</h2>} />

  12.     <Route path="/about" render={() => <h2>About</h2>} />

  13.   </HistoryRouter>

HistoryRouter 实现

 
  1. export default class HistoryRouter extends React.Component {

  2.   state = {

  3.     currentPath: utils.extractUrlPath(window.location.href)

  4.   };

  5.  
  6.   onPopState = e => {

  7.     const currentPath = utils.extractUrlPath(window.location.href);

  8.     console.log("onPopState:", currentPath);

  9.     this.setState({ currentPath });

  10.   };

  11.  
  12.   componentDidMount() {

  13.     window.addEventListener("popstate", this.onPopState);

  14.   }

  15.  
  16.   componentWillUnmount() {

  17.     window.removeEventListener("popstate", this.onPopState);

  18.   }

  19.  
  20.   render() {

  21.     return (

  22.       <RouteContext.Provider value={{currentPath: this.state.currentPath, onPopState: this.onPopState}}>

  23.         {this.props.children}

  24.       </RouteContext.Provider>

  25.     );

  26.   }

  27. }

Route 实现

 
  1. export default ({ path, render }) => (

  2.   <RouteContext.Consumer>

  3.     {({currentPath}) => currentPath === path && render()}

  4.   </RouteContext.Consumer>

  5. );

Link 实现

 
  1. export default ({ to, ...props }) => (

  2.   <RouteContext.Consumer>

  3.     {({ onPopState }) => (

  4.       <a

  5.         href=""

  6.         {...props}

  7.         onClick={e => {

  8.           e.preventDefault();

  9.           window.history.pushState(null, "", to);

  10.           onPopState();

  11.         }}

  12.       />

  13.     )}

  14.   </RouteContext.Consumer>

  15. );

Vue 版本前端路由实现

基于 hash 实现

运行效果:

 

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

 
  1.     <div>

  2.       <ul>

  3.         <li><router-link to="/home">home</router-link></li>

  4.         <li><router-link to="/about">about</router-link></li>

  5.       </ul>

  6.       <router-view></router-view>

  7.     </div>

 

 
  1. const routes = {

  2.   '/home': {

  3.     template: '<h2>Home</h2>'

  4.   },

  5.   '/about': {

  6.     template: '<h2>About</h2>'

  7.   }

  8. }

  9.  
  10. const app = new Vue({

  11.   el: '.vue.hash',

  12.   components: {

  13.     'router-view': RouterView,

  14.     'router-link': RouterLink

  15.   },

  16.   beforeCreate () {

  17.     this.$routes = routes

  18.   }

  19. })

 

router-view 实现

 
  1. <template>

  2.   <component :is="routeView" />

  3. </template>

  4.  
  5. <script>

  6. import utils from '~/utils.js'

  7. export default {

  8.   data () {

  9.     return {

  10.       routeView: null

  11.     }

  12.   },

  13.   created () {

  14.     this.boundHashChange = this.onHashChange.bind(this)

  15.   },

  16.   beforeMount () {

  17.     window.addEventListener('hashchange', this.boundHashChange)

  18.   },

  19.   mounted () {

  20.     this.onHashChange()

  21.   },

  22.   beforeDestroy() {

  23.     window.removeEventListener('hashchange', this.boundHashChange)

  24.   },

  25.   methods: {

  26.     onHashChange () {

  27.       const path = utils.extractHashPath(window.location.href)

  28.       this.routeView = this.$root.$routes[path] || null

  29.       console.log('vue:hashchange:', path)

  30.     }

  31.   }

  32. }

  33. </script>

 

router-link 实现

 
  1. <template>

  2.   <a @click.prevent="onClick" href=''><slot></slot></a>

  3. </template>

  4.  
  5. <script>

  6. export default {

  7.   props: {

  8.     to: String

  9.   },

  10.   methods: {

  11.     onClick () {

  12.       window.location.hash = '#' + this.to

  13.     }

  14.   }

  15. }

  16. </script>

基于 history 实现

运行效果:

 

使用方式和 vue-router 类似:

 
  1.     <div>

  2.       <ul>

  3.         <li><router-link to="/home">home</router-link></li>

  4.         <li><router-link to="/about">about</router-link></li>

  5.       </ul>

  6.       <router-view></router-view>

  7.     </div>

 

 
  1. const routes = {

  2.   '/home': {

  3.     template: '<h2>Home</h2>'

  4.   },

  5.   '/about': {

  6.     template: '<h2>About</h2>'

  7.   }

  8. }

  9.  
  10. const app = new Vue({

  11.   el: '.vue.history',

  12.   components: {

  13.     'router-view': RouterView,

  14.     'router-link': RouterLink

  15.   },

  16.   created () {

  17.     this.$routes = routes

  18.     this.boundPopState = this.onPopState.bind(this)

  19.   },

  20.   beforeMount () {

  21.     window.addEventListener('popstate', this.boundPopState) 

  22.   },

  23.   beforeDestroy () {

  24.     window.removeEventListener('popstate', this.boundPopState) 

  25.   },

  26.   methods: {

  27.     onPopState (...args) {

  28.       this.$emit('popstate', ...args)

  29.     }

  30.   }

  31. })

 

router-view 实现:

 
  1. <template>

  2.   <component :is="routeView" />

  3. </template>

  4.  
  5. <script>

  6. import utils from '~/utils.js'

  7. export default {

  8.   data () {

  9.     return {

  10.       routeView: null

  11.     }

  12.   },

  13.   created () {

  14.     this.boundPopState = this.onPopState.bind(this)

  15.   },

  16.   beforeMount () {

  17.     this.$root.$on('popstate', this.boundPopState)

  18.   },

  19.   beforeDestroy() {

  20.     this.$root.$off('popstate', this.boundPopState)

  21.   },

  22.   methods: {

  23.     onPopState (e) {

  24.       const path = utils.extractUrlPath(window.location.href)

  25.       this.routeView = this.$root.$routes[path] || null

  26.       console.log('[Vue] popstate:', path)

  27.     }

  28.   }

  29. }

  30. </script>

 

router-link 实现

 
  1. <template>

  2.   <a @click.prevent="onClick" href=''><slot></slot></a>

  3. </template>

  4.  
  5. <script>

  6. export default {

  7.   props: {

  8.     to: String

  9.   },

  10.   methods: {

  11.     onClick () {

  12.       history.pushState(null, '', this.to)

  13.       this.$root.$emit('popstate')

  14.     }

  15.   }

  16. }

  17. </script>

小结

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

所有的示例的代码放在 Github 仓库:https://github.com/whinc/web-router-principle

参考

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

    http://www.cnblogs.com/xiaobie123/p/6357724.html

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

    https://github.com/youngwind/blog/issues/109


作者:@whinc
链接:https://github.com/whinc/blog/issues/13
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值