SPA 前端路由原理与实现

1,400 阅读3分钟

SPA 是什么

SPAsingle page application 的简称(单页应用) 不同于传统的服务端渲染,简单的说,SPA 就是一个 web 项目只有一个 html 页面,一个页面一旦加载完成,SPA 不会因为用户的操作而进行页面的重新加载或者跳转,取而代之的是利用JS动态的变换 html 内容,从而来模拟多个视图间的跳转。如今的 VueReactAngular 均是如此,这里排除框架的 SSR 服务端渲染。

SPA 应用页面 URL 变了,但是并没有重新加载,提高了用户体验,同时减轻了服务器压力。但也带来了 SEO 难以优化,首屏加载事件较长等问题。

使用前端路由控制页面渲染

由于需要根据用户操作对前端页面实现动态渲染,所以需要前端路由来观察用户的页面URL的变化从而对页面有所响应。

常见路由实现有historyhash两种

Hash 路由原理与实现

早期的前端路由是通过hash来实现的。改变url的hash值并不会刷新页面。hash也就是html中a标签中的锚点

<a href="#head"></a>
<!---标签中href的属性:#head就为URL的hash---->

浏览器提供了hashchange事件来监听hash的变化从而根据hash的变化控制页面渲染

hash 路由的基础实现

image.png

class HashRouter{
    constructor(){
        this.currentPath = '/';
        this.routes = {}
    }
    init(){
        //DOMContentLoaded 使事件在html渲染后直接执行
        window.addEventListener('DOMContentLoaded', this.updateView.bind(this))
        window.addEventListener('hashchange', this.updateView.bind(this))
    }
    updateView(){
        this.currentPath = location.hash.substring(1) || '/'
        this.routes[this.currentPath] && this.routes[this.currentPath]()
    }
    route(path, callback){
        this.routes[path] = callback
    }
}

const router = new HashRouter();
router.init();

router.route('/page1', function(){
    document.getElementById('content').innerHTML = 'This is page1'
})
router.route('/page2', function(){
    document.getElementById('content').innerHTML = 'This is page2'
})
router.route('/page3', function(){
    document.getElementById('content').innerHTML = 'This is page3'
})

History 路由原理与实现

history router 稍微麻烦一点,我们先思考下,对于一个应用而言,url 的改变只能由下面三种情况引起:

点击浏览器的前进后退按钮 => 可以监听popstate事件 点击a 标签跳转 在 JS 代码中触发 history.pushState()、history.replaceState()

所以history router的实现思路是:监听页面中和路由有关的a标签点击事件,阻止默认的跳转行为,然后手动调用history.pushState()方法,让浏览器记住路由,然后手动更新相应视图。同时为了监听用户手点击浏览器的前进后退按钮,还需要监听popstate事件,动态的修改相应视图。

class HistoryRouter{
    constructor(){
        this.currentPath = '/';
        this.routes = {}
    }
    init(){
        //DOMContentLoaded事件用于刷新页面后
        window.addEventListener('DOMContentLoaded', this.updateView.bind(this, '/'))
        var that = this
        window.addEventListener('click', function (e) {
            if(e.target.tagName.toLocaleLowerCase() === 'a' && e.target.getAttribute('data-href')) {
                e.preventDefault()
                var path = e.target.getAttribute('data-href');
                history.pushState({ path: path }, '', path)
                that.updateView(path)
            }
        })
        window.addEventListener('popstate', function (e) {
            if(e.state){
                var path = e.state.path
                that.updateView(path)
            }else{
                that.updateView('/')
            }
        })
    }
    updateView(path){
        this.currentPath = path
        this.routes[this.currentPath] && this.routes[this.currentPath]()
    }
    route(path, callback){
        this.routes[path] = callback
   }
}

var router = new HistoryRouter();
router.init();
router.route('/page1', function(){
    document.getElementById('content').innerHTML = 'This is page1'
})
router.route('/page2', function(){
    document.getElementById('content').innerHTML = 'This is page2'
})
router.route('/page3', function(){
    document.getElementById('content').innerHTML = 'This is page3'
})

hash模式和history模式核心区别在于init的实现

history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,及每一个前端路由相对于后端资源有一一对应,如果服务器中没有相应的响应或者资源就会404

例如使用Nginx配置

location / {
  try_files $uri $uri/ /index.html;
}

总结

文章介绍了 SPA 原理与实现,下面还会根据ReactVue分别讲一下,React routerVue Router的实现与区别

❤️ 感谢大家

如果你觉得这篇内容对你挺有有帮助的话:

点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)

关注公众号咸鱼爱前端,我们一起学习一起进步。

觉得不错的话,也可以阅读其他文章(感谢朋友的鼓励与支持🌹🌹🌹):

Nodejs 实现定时爬虫

React-Query 让你的状态管理更优雅

前端页面布局学习神器

面试必备知识点之深浅拷贝