Open willson-wang opened 4 years ago
在web开发中,“route”是指根据url分配到对应的处理程序。更通俗一点就是route就是URL到函数的映射
在早期的web应用中,每个url都对应一个后端的路由,这意味着,跳转到不同的页面都需要与服务端进行一次交互,也就是说一个web应用一般都是多页面的;
随着ajax的发展,慢慢有了spa,spa的出现大大提高了WEB应用的交互体验。在与用户的交互过程中,不再需要重新刷新页面,获取数据也是通过Ajax 异步获取,页面显示变的更加流畅
但由于SPA中用户的交互是通过JS改变HTML内容来实现的,页面本身的url并没有变化,这导致了两个问题
为了解决上述的两个问题,前端路由出现了
浏览器提供了可以改变url但不会请求服务端的方式,同时提供了事件用于监听浏览器url的变化,方便我们在url变化之后能够更新视图;也就说在同一个html页面内,通过浏览器提供的方式改变url,然后根据不同的url来进行不同的视图,且url的变化不会去请求服务端;
为此浏览器提供了两种改变url,但是不会去请求服务器的方式,分别是hash值的变化及history对象提供的pushState及replaceState方法
最早的前端路由方案,可以看成是一种hack方案,因为hash值最早就是用来做锚标记的,只是后面spa流行之后才被用作前端路由
https://github.com/willson-wang/Blog?a=1#test
主要用到的API
const currentHash = window.location.hash // 获取当前hash值 window.location.hash = '#test' // 设置新的hash值 window.replace(window.location.origin + window.location.pathname + '#test') // 替换当前的url window.addEventListener('hashchange', () => {}) // 监听hash值的变化
通过上面的api,我们就已经可以通过改变不同的hash值,然后通过监听hashchange事件,来改变后的hash值,最后根据获取后的hash值,来更新对应的视图,达到前端路由的目的
具体例子如下所示
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="nav"> <a href="#/page1">page1</a> <a href="#/page2">page2</a> <a href="#/page3">page3</a> <a href="#/page4">page4</a> </div> <div> <button id="btn1">push page2</button> <button id="btn2">replace page3</button> </div> <div id="content"></div> <script> class HashRouter { constructor({routes}) { this.routes = routes this.render() this.bindEvent() } push(path) { window.location.hash = `#${path}` } replace(path) { window.location.replace(window.location.origin + window.location.pathname + '#' + path) } render() { const currentHash = window.location.hash const content = document.querySelector('#content') const hashValue = currentHash.slice(1) console.log('currentHash', currentHash, hashValue) let index = 0 for (let i = 0; i < this.routes.length; i++) { if (this.routes[i].path === hashValue) { index = i break } } const component = this.routes[index] ? this.routes[index].component : this.routes[0].component content.innerHTML = component; } bindEvent() { window.addEventListener('hashchange', this.render.bind(this)) } } const routes = [ { path: '/page1', component: '<div>page1</div>' }, { path: '/page2', component: '<div>page2</div>' }, { path: '/page3', component: '<div>page3</div>' }, { path: '/page4', component: '<div>page4</div>' }, ] const router = new HashRouter({ routes }) const btn1 = document.querySelector('#btn1') const btn2 = document.querySelector('#btn2') btn1.addEventListener('click', function () { router.push('/page2') }) btn2.addEventListener('click', function () { router.replace('/page3') }) </script> </body> </html>
随着前端的spa越来越流行之后,开发者们已经不满足于通过hash的这种前端路由方式,因为url上的#无法去掉,导致url看起来很丑,会导致锚点功能失效,相同 hash 值不会触发动作将记录加入到历史栈中,为了提供更好的体验,html5拓展了history对象,提供了新的api history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目
// state:合法的 Javascript 对象,可以用在 popstate 事件中 // title:现在大多浏览器忽略这个参数,可以直接用 null 代替 // url:任意有效的 URL,用于更新浏览器的地址栏 history.pushState(state, title[, url]) // history.pushState({ 'page_id': 1, 'user_id': 5 }, null, 'hello-world.html') history.replaceState(state, title[, url]) // / history.replaceState({ 'page_id': 1, 'user_id': 5 }, null, 'hello-world.html')
pushState与replaceState方法的唯一区别就是,pushState是向history记录内新增一条url记录,而replaceState是用新的url替换当前旧的当前url记录
监听url的变化,通过onpopstate事件,但是需要注意的是调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法),此外,a 标签的锚点也会触发该事件.
所以跟hashChange事件不同的是,我们不能直接通过onpopstate监听所有url改变的场景,所以我们需要进行拦截操作,以便url变化之后可以达到视图更新的目的
拦截的场景有:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="nav"> <a href="/page1">page1</a> <a href="/page2">page2</a> <a href="/page3">page3</a> <a href="/page4">page4</a> </div> <div> <button id="btn1">push page2</button> <button id="btn2">replace page3</button> </div> <div id="content"></div> <script> class HistoryRouter { constructor({routes}) { this.routes = routes this.render() this.bindEvent() this.attachA() } push(path) { window.history.pushState({}, null, path) this.render() } replace(path) { window.history.replaceState({}, null, path) this.render() } attachA() { const nav = document.querySelector('#nav') const aEles = nav.children const _this = this Array.from(aEles).forEach((ele) => { ele.addEventListener('click', function(e) { e.preventDefault() _this.push(e.target.href) }) }) } render() { const pathname = window.location.pathname const content = document.querySelector('#content') console.log('currenPath', pathname) let index = 0 for (let i = 0; i < this.routes.length; i++) { if (this.routes[i].path === pathname) { index = i break } } const component = this.routes[index] ? this.routes[index].component : this.routes[0].component content.innerHTML = component; } bindEvent() { window.addEventListener('popstate', this.render.bind(this)) } } const routes = [ { path: '/page1', component: '<div>page1</div>' }, { path: '/page2', component: '<div>page2</div>' }, { path: '/page3', component: '<div>page3</div>' }, { path: '/page4', component: '<div>page4</div>' }, ] const router = new HistoryRouter({ routes }) const btn1 = document.querySelector('#btn1') const btn2 = document.querySelector('#btn2') btn1.addEventListener('click', function () { router.push('/page2') }) btn2.addEventListener('click', function () { router.replace('/page3') }) </script> </body> </html>
需要具备以下功能
整个例子只是一个思路,如果需要用于生产,需要去完善各种边界条件及支持更多的场景
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="nav"> <a href="/page1">page1</a> <a href="/page2">page2</a> <a href="/page3">page3</a> <a href="/page4">page4</a> </div> <div> <button id="btn1">push page2</button> <button id="btn2">replace page3</button> </div> <div id="content"></div> <script> // 执行路由钩子,保证钩子能够按顺序执行 function runQueue(queue, iterator, callback) { const step = (index) => { if (index >= queue.length) { callback() } else { if (queue[index]) { iterator(queue[index], () => { step(index + 1) }) } else { step(index + 1) } } } step(0) } function nomalizeRoute(location) { if (typeof location === 'string') { const path = location.indexOf('http') > -1 ? location.slice(window.location.origin.length) : location return { path } } return location } class Base { constructor(router) { this.attachA() this.router = router } push() {} replace() {} go() {} render() {} hasRouteInRoutes(route) { const index = this.router.routes.findIndex((it) => { return it.path === route.path }) const idx = this.router.routes.findIndex((it) => { return it.path === '/' }) if (index === -1 && idx !== -1) { route.path = '/' } return index === -1 && idx === -1 } transitionTo(location, onComplete, onAbort) { const route = nomalizeRoute(location) this.comfirmTransition(route, (newRoute) => { this.router.afterHooks.forEach((cb) => { cb && cb(newRoute, this.router.route) }) this.updateRoute(newRoute) onComplete && onComplete(newRoute) }, (err) => { onAbort && onAbort(err) }) } comfirmTransition(route, onComplete, onAbort) { let queue = [].concat( this.router.beforeHooks ) if (this.router.route.path === route.path) { onAbort('跳转到相同地址') return } if (this.hasRouteInRoutes(route)) { onAbort('找不到当前route') return } const iterator = (hook, next) => { try { // 执行注册的钩子,传一个next参数给钩子,把控制权交个钩子 hook(route, this.router.route, (to) => { if (to === false) { onAbort() } else if (typeof to === 'string' || (typeof to === 'object' && typeof to.path === 'string')) { onAbort() if (typeof to === 'object' && to.isReplace) { this.replace(to) } else { this.push(to) } } else { next() } }) } catch (error) { onAbort(error) } } runQueue(queue, iterator, () => { onComplete(route) }) } // 阻止a标签的默认事件 attachA() { const nav = document.querySelector('#nav') const aEles = nav.children const _this = this Array.from(aEles).forEach((ele) => { ele.addEventListener('click', function(e) { e.preventDefault() _this.push(e.target.href) }) }) } updateRoute(route) { this.router.route = route } } function pushHash(path) { window.location.hash = `#${path}` } function replaceHash() { window.replace(window.location.origin + window.location.pathname + '#' + path) } class HashRouter extends Base { constructor(router) { super(router) // 监听hashchange事件,保证浏览器前进后台的时候,能够触发钩子及更新视图 window.addEventListener('hashchange', () => { this.transitionTo({ path: window.location.hash.slice(1) }, () => { this.render() }) }) } push(location, onComplete, onAbort) { this.transitionTo(location, (route) => { pushHash(route.path) onComplete && onComplete() this.render() }, onAbort) } replace(location, onComplete, onAbort) { this.transitionTo(location, (route) => { replaceHash(route.path) onComplete && onComplete() this.render() }, onAbort) } go(n) { window.history.go(n) } render() { const { path } = this.router.route // const hashValue = currentHash.slice(1) console.log('currentHash', path) let index = 0 for (let i = 0; i < this.router.routes.length; i++) { if (this.router.routes[i].path === path) { index = i break } } const component = this.router.routes[index] ? this.router.routes[index].component : this.router.routes[0].component this.router.routeViewEle.innerHTML = component; } } function pushState(path) { window.history.pushState({}, null, path) } function replaceState(path) { window.history.replaceState({}, null, path) } class HistoryRouter extends Base { constructor(router) { super(router) window.addEventListener('popstate', () => { this.transitionTo({ path: window.location.pathname }, () => { this.render() }) }) } push(location, onComplete, onAbort) { this.transitionTo(location, (route) => { pushState(route.path) onComplete && onComplete() this.render() }, onAbort) } replace(location, onComplete, onAbort) { this.transitionTo(location, (route) => { replaceState(route.path) onComplete && onComplete() this.render() }, onAbort) } go(n) { window.history.go(n) } render() { const { path } = this.router.route console.log('currenPath', path) let index = 0 for (let i = 0; i < this.router.routes.length; i++) { if (this.router.routes[i].path === path) { index = i break } } const component = this.router.routes[index] ? this.router.routes[index].component : this.router.routes[0].component this.router.routeViewEle.innerHTML = component; } } function getCurrentRoute (mode) { if (mode === 'history') { return window.location.pathname } else { return window.location.hash } } function registerHook(hooks, cb) { hooks.push(cb) return () => { const index = hooks.indexOf(cb) if (index > -1) hooks.splite(index, 1) } } class Router { constructor(options) { this.beforeHooks = [] this.afterHooks = [] this.options = options this.routeViewEle = document.querySelector('#content') this.routes = options.routes this.route = {} if (options.mode === 'history') { this.history = new HistoryRouter(this) } else { this.history = new HashRouter(this) } this.init() } init() { const currentRoute = getCurrentRoute(this.options.mode) this.history.push(currentRoute) } push(route) { this.history.push(route) } replace(route) { this.history.push(route) } go(n) { this.history.go(n) } back() { this.go(-1) } forward() { this.go(1) } beforeEach(cb) { return registerHook(this.beforeHooks, cb) } afterEach(cb) { return registerHook(this.afterHooks, cb) } } const router = new Router({ mode: 'hash', // history | hash viewEle: '#content', routes: [ { path: '/page1', component: '<div>page1</div>' }, { path: '/page2', component: '<div>page2</div>' }, { path: '/page3', component: '<div>page3</div>' }, { path: '/page4', component: '<div>page4</div>' }, { path: '/', component: '<div>page1</div>' }, ] }) router.beforeEach((to, from, next) => { console.log('beforeEach', to, from) next() }) router.afterEach((to, from) => { console.log('afterEach', to, from) }) const btn1 = document.querySelector('#btn1') const btn2 = document.querySelector('#btn2') btn1.addEventListener('click', function () { router.push('/page2') }) btn2.addEventListener('click', function () { router.replace('/page3') }) </script> </body> </html>
总结: 前端路由只要把握两个点:1. 提供方法改变url而不会向服务端发起请求;2. 有方法能够监听url的变化;就已经知道前端路由具体是什么了;其它的都是结合各自的前端框架,写出符合当前框架的前端路由
参考链接: 前端进阶彻底弄懂前端路由 History API vue-router
什么是web路由?
在web开发中,“route”是指根据url分配到对应的处理程序。更通俗一点就是route就是URL到函数的映射
什么又是前端路由?
在早期的web应用中,每个url都对应一个后端的路由,这意味着,跳转到不同的页面都需要与服务端进行一次交互,也就是说一个web应用一般都是多页面的;
随着ajax的发展,慢慢有了spa,spa的出现大大提高了WEB应用的交互体验。在与用户的交互过程中,不再需要重新刷新页面,获取数据也是通过Ajax 异步获取,页面显示变的更加流畅
但由于SPA中用户的交互是通过JS改变HTML内容来实现的,页面本身的url并没有变化,这导致了两个问题
为了解决上述的两个问题,前端路由出现了
浏览器提供了可以改变url但不会请求服务端的方式,同时提供了事件用于监听浏览器url的变化,方便我们在url变化之后能够更新视图;也就说在同一个html页面内,通过浏览器提供的方式改变url,然后根据不同的url来进行不同的视图,且url的变化不会去请求服务端;
为此浏览器提供了两种改变url,但是不会去请求服务器的方式,分别是hash值的变化及history对象提供的pushState及replaceState方法
hash路由
最早的前端路由方案,可以看成是一种hack方案,因为hash值最早就是用来做锚标记的,只是后面spa流行之后才被用作前端路由
主要用到的API
通过上面的api,我们就已经可以通过改变不同的hash值,然后通过监听hashchange事件,来改变后的hash值,最后根据获取后的hash值,来更新对应的视图,达到前端路由的目的
具体例子如下所示
history路由
随着前端的spa越来越流行之后,开发者们已经不满足于通过hash的这种前端路由方式,因为url上的#无法去掉,导致url看起来很丑,会导致锚点功能失效,相同 hash 值不会触发动作将记录加入到历史栈中,为了提供更好的体验,html5拓展了history对象,提供了新的api history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目
pushState与replaceState方法的唯一区别就是,pushState是向history记录内新增一条url记录,而replaceState是用新的url替换当前旧的当前url记录
监听url的变化,通过onpopstate事件,但是需要注意的是调用history.pushState()或者history.replaceState()不会触发popstate事件. popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法),此外,a 标签的锚点也会触发该事件.
所以跟hashChange事件不同的是,我们不能直接通过onpopstate监听所有url改变的场景,所以我们需要进行拦截操作,以便url变化之后可以达到视图更新的目的
拦截的场景有:
具体例子如下所示
最后我们写一个通用一点的Router类
需要具备以下功能
整个例子只是一个思路,如果需要用于生产,需要去完善各种边界条件及支持更多的场景
总结: 前端路由只要把握两个点:1. 提供方法改变url而不会向服务端发起请求;2. 有方法能够监听url的变化;就已经知道前端路由具体是什么了;其它的都是结合各自的前端框架,写出符合当前框架的前端路由
参考链接: 前端进阶彻底弄懂前端路由 History API vue-router