开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
路由,可以理解为提供一个可供访问的URL,浏览器请求该地址获取到正确的页面资源并呈现给用户。
路由就是就好比是图书馆记录书籍的索引,我们通过这个索引(地址)从而快速准确的获取我们想要的资源;在前后端不分离,后端通过模板引擎整合html的时期,用户每次获取页面,都需要通过访问服务器获取该索引对应的静态资源,这就意味着每次访问页面都是加载新的html,用户都要经历页面刷新过程
前端路由,顾名思义,就是对索引的维护和跳转等逻辑处理交由前端开发处理,不再需要通过访问服务器来获取索引对应资源,这种方式带来的最显而易见优势就是用户点击页面的跳转,不再需要经过页面的加载,而是无刷新感知,很好的保证操作的连贯性,带来更流畅的用户体验。
前端路由方式目前有两种,一种是hash,一种是history,这两种有什么区别呢,我们往下看:
hash
hash模式下的路由处理,依赖的底层实现基于浏览器Location对象, 先来了解下它的属性和API。
- 假如我们访问
https://juejin.cn/creator/content/article/drafts#/iscool?isright=1
location.href;
// 完整链接, 'https://juejin.cn/creator/content/article/drafts#/iscool?isright=1'
location.protocol;
// 对应http协议,最后有一个":", 'https:'
location.host;
// 域名(可能在该串最后带有一个":"并跟上 URL 的端口号), 'juejin.cn'
location.hostname;
// 域名, 'juejin.cn'
location.port;
// 返回端口号, ''
location.pathname;
// 链接中以'/'开头直到'#'前的这段路径, '/creator/content/article/drafts'
location.search;
// 链接中'?'后面的内容 注意'https://juejin.cn/creator/content/article/drafts#/iscool?isright=1' -> ''; 'https://juejin.cn/creator/content/article/drafts?isright=1' -> '?isright=1'
location.hash;
// 返回'#'后面的内容, '#/iscool?isright=1'
location.origin;
// 返回当前页面域名的标准形式
- Location.API <
https://juejin.cn/creator/content/article/drafts#/iscool?isright=1>
// 假定originUrl: 'https://juejin.cn/creator/content/article/drafts#/iscool?isright=1'
location.assign((path = ''));
// 加载给定 URL 的内容资源到当前location上(基于当前域名资源路径),如果传空字符,会刷新页面
// location.assign('notcool') -> 'https://juejin.cn/creator/content/article/notcool'
location.reload(refresh?);
// 重新加载RUL,refresh为可选参数,true -> 类似于强制刷新; false -> 允许本地缓存刷新
location.replace(url);
// 用给定的url替换掉当前资源(基于当前域名资源路径),与assign方法不同的是,replace替换的新页面不会保存在回话的History栈中,用户进入到新url无法通过后退按钮返回回去
// 满足下面这个规则:
// location.replace('notcool'|| './notcool'): originUrl -> 'https://juejin.cn/creator/content/article/notcool'
// location.replace('/notcool'): originUrl -> 'https://juejin.cn/notcool'
// location.replace('www.baidu.com'): originUrl -> 'https://juejin.cn/creator/content/article/www.baidu.com'
// location.replace('https://www.baidu.com'): originUrl -> 'https://www.baidu.com'
location.toString();
// 返回整个URL,与href效果相同,但是用它无法修改location的值
- hashchange
当 URL 的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的 URL 部分,包括#符号)
了解了Location对象以及浏览器hashchange事件,我们再看下怎么利用location处理前端路由;
- 简单的路由跳转实现
html:
<div id="apps">
<span>点击跳转路由A</span>
<span>点击跳转路由B</span>
<div id="routerView"></div>
</div>
js:
const routeList = [
{
path: '#/routerA',
name: 'routerA',
component: '这个是路由A',
},
{
path: '#/routerB',
name: 'routerB',
component: '这个是路由B',
},
]
window.addEventListener('hashchange', (hash) => {
const curHash = hash.target.location.hash
const curPage = routeList.find((item) => item.path === curHash)
document.getElementById('routerView').textContent = curPage.component
})
const spanList = document.querySelectorAll('span')
for (let i = 0; i < spanList.length; i++) {
spanList[i].addEventListener(
'click',
() => {
console.log(routeList[i])
location.replace(routeList[i].path)
},
false
)
}
你看,我们已经实现了路由的跳转,以及将对应的component渲染到了<router-view>内,routeList的结构跟vue-router是不是很类似呢。
首先,我们监听了hashchange事件,然后通过点击绑定事件,利用location.replace,将点击要跳转的路由推入地址栏,再通过监听hash的变化,更改页面显示内容(加载对应component).
history
history模式的路由处理,依赖的底层实现基于从HTML5开始,为浏览器history对象提供对history历史栈中内容操作的相关api能力的支持。
- 属性
history.length; // 返回当前加载的页面数量
history.scrollRestoration; // 许 Web 应用程序在历史导航上显式地设置默认滚动恢复行为, auto || manual
history.state; // 返回一个表示历史堆栈顶部的状态的任意(any)值(这是一种不必等待 popstate 事件而查看状态的方式)
- API
history.back()
// 异步调用, 转到浏览器会话历史的上一页, 与用户点击浏览器左上角返回行为相同,等价于history.go(-1);
history.forward()
// 异步调用, 转到浏览器会话历史的下一页, 与用户点击浏览器左上角下一页行为相同, 等价于history.go(1), 如果没有下一页,调用返回undefined, 不会报错
history.go()
// 异步调用, 通过当前页面的相对位置从浏览器历史记录(会话记录)异步加载页面, 不传参或者参数为0会重新载入当前页面
// IE浏览器指定一个字符串,而不是整数,可以转到历史记录列表中的特定 URL
history.pushState(stateObj, newpageTitle, newpageURL)
// 接收三个参数:一个状态对象(可以是能被序列化的任何东西, 序列化后640k的大小限制-原因在于 Firefox 将状态对象保存在用户的磁盘上,以便在用户重启浏览器时使用; 超过该限制会抛出异常), 一个标题 (目前被忽略), 和 (可选的) 一个 URL
// eg: history.pushState({page: 'newPage'}, "page new", "new.html"), originURL=www.jimous.com/cool.html
// 浏览器的地址栏会显示为www.jimous.com/new.html, 但是不会加载new.html(但可能会在稍后某些情况下加载这个 URL,比如在用户重新打开浏览器时),甚至也不会检查new.html是否存在(新 URL 必须与当前 URL 同源,否则 pushState() 会抛出一个异常)。
// pushState() 绝对不会触发 hashchange 事件
history.replaceState()
// 接收参数同pushState方法,与pushState()类似, 只不过是修改当前的历史记录项而不是新建一个, 使用场景在于为了响应用户操作,你想要更新状态对象 state 或者当前历史记录的 URL,比如执行了pushState之后,通过replaceState变更URL。
- onpopstate
每当活动的历史记录项发生变化时,都会触发popstate事件。如果当前活动的历史记录项是被 pushState 创建的,或者是由 replaceState 改变的,那么 popstate 事件的状态属性 state 会包含一个当前历史记录状态对象的拷贝.
window.addEventListener('popstate', (event) => {
console.log(event.state, document.location.href)
})
history.pushState({ page: 'a' }, 'title a', '?page=a')
history.pushState({ page: 'b' }, 'title b', '?page=b')
history.replaceState({ page: 'c' }, 'title c', '?page=c')
history.back() // state: {page: 'a'} location: 'file:///E:/codeEntry/github/knowledge_library/front-end/route/history.html?page=a'
history.back() // state: null location: 'file:///E:/codeEntry/github/knowledge_library/front-end/route/history.html'
history.go(2) // 点击浏览器前进键 {page: 'c'} location: 'file:///E:/codeEntry/github/knowledge_library/front-end/route/history.html?page=c'
hash模式下,前端路由的跳转浏览器不会向服务器发起请求,但是history模式不同,浏览器会把history模式下的url当做新的请求发送到服务器端,因为后台服务不存在对应路由,所以我们还需要改造一下nginx,利用nginx的location配置项的try_files(除了location下面配置的路由路径外,其他访问路径均走try_files提供的路径返回)字段,补上前端模板对应的url.
eg:
location /app {
root /Users/admin/www;
index index.html;
try_files $uri $uri/ /app/index.html;
}
vue-router关键源码解析(version: 3.6.5)
先让我们回顾下vue-router的使用:
import Vue from 'vue';
import VueRouter from 'vue-router';
const IndexPage = () => import('index.vue');
Vue.use(VueRouter);
const routes = [
{ path: '/', redirect: '/index' },
{ path: '/index', name: 'index', component: IndexPage },
];
const router = new VueRouter({
routes,
});
new Vue({
render: (h) => h(App),
router,
}).$mount('#app');
首先我们引入VueRouter对象,通过Vue.use调用它的install方法,install方法里做了三件事:
- 通过Vue.mixin,在生命周期内beforeCreate调用router的初始化,destoryed周期注册了路由;
(代码有移除一些干扰理解的代码,有兴趣可直接阅读源码)
Vue.mixin({
beforeCreate () {
...
this._router = this.$options.router
this._router.init(this)
...
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
- 向Vue原型上挂载route变量
Object.defineProperty(Vue.prototype, '$router', {get () {...})
Object.defineProperty(Vue.prototype, '$route', {get () {...}
})
- 注册RouterView,RouterLink组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
new VueRouter()
new VueRouter()内部就做了两件事:
- 调用createMatcher方法,将路由映射关系,路由动态添加的方法挂载到VueRouter类下matcher属性上。
this.matcher = createMatcher(options.routes || [], this) // vue-router\src\index.js
返回的matcher包含以下属性(如果组件包含父组件,还有parent, parent.alias等属性):
- 通过mode模式(默认为hash),添加路由监听
/** */
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
由于onpopstate事件也能监听hash的变化,所以vueRouter内部其实对于支持onpopstate事件的浏览器也默认用onpopstate事件处理路由的监听。
const eventType = supportsPushState ? 'popstate' : 'hashchange'
export const supportsPushState =
inBrowser &&
(function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && typeof window.history.pushState === 'function'
})()
最后总结一下VueRouter的流程:首先通过.install函数,为每个组件beforeCreate生命周期内注入router的init方法调用,将route挂载到Vue原型,注册<router-view>、<router-link>组件;当组件加载时,调用init方法,获取路由映射表,并注册window的路由监听方法,当用户在当前页面(组件)调用this.$router.push, 执行相应的路由加载,组件即页面。 很多相关的依赖函数都在vue-router源码,这里就不贴出来了,大家有兴趣可以自行去了解看看。 vue-router.github