1、历史
路由最早兴起于后端,路由就是:用户端发起一个HTTP请求,后台用一个函数进行处理,即 函数 => 路由。到后来前端出现ajax,页面可以进行局部刷新,出现SPA应用。
2、SPA
单页面应用仅在初始化时,加载页面相应的HTML、CSS、JavaScript。一旦页面加载完成,SPA不会因为用户的操作,而进行页面的重新加载和跳转,而是利用路由机制在实现HTML的切换,UI与用户交互,避免重复的加载。
优点:
- 用户体验好、快、内容改变不需要重新加载页面,避免了不必要的跳转和重复渲染。并不需要处理HTML文件的请求,这样节约了HTTP发送延迟。
- 基于上面一点,SPA相对服务器压力小很多。
- 前后端职责分离,架构清楚,前端进行交互逻辑,后端服务数据处理
缺点:
- 初始化时耗时比较多,为实现单页面web应用功能及显示效果,需要将JavaScript和CSS统一加载,虽然可以部分按需加载。
- SPA为单页面,但是浏览器的前进后台不能使用,所有页面的切换需要自己建立一个堆栈进行管理。
- 所有内容都在一个页面,SEO天然处于弱势。
3、前端路由
为了解决SPA应用的缺点,前端路由出现。前端路由结合SPA更新视图但是浏览器不刷新页面,只是重新渲染部门子页面,加载速度快,页面反应灵活等优点实现。
目前浏览器前端路由实现的方式有两种
- URL #hash
- H5的history interface
4、URL #hash
在最开始H5还没有流行其他的时候,SPA采用URL 的hash值作为锚点,获取锚点值,监听其改变,在进行对应的子页面渲染。window对象有一个专门监听hash变化的时候,那就是onhashchange。 监听到URL的变化,我们如何进行页面的载入了。当页面发送改变了,在执行页面载入有三种方式 查询节点内容并改变。 使用import导出js文件,js文件export模板字符串。 利用Ajax加载不同hash的HTML模块。
4.1 查询节点内容并改变
<h1 id="page"></h1>
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
<script>
window.addEventListener('hashchange', e => {
e.preventDefault();
document.querySelector('#page').innerHTML = location.hash;
})
</script>
4.2 import方法
const str = `
<div>
我是import进来的JS文件
</div>
`
export default str
<body>
<h1 id="page"></h1>
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
<script type="module">
import demo1 from './import.js'
document.querySelector('#page').innerHTML = demo1;
window.addEventListener('hashchange', e => {
e.preventDefault();
document.querySelector('#page').innerHTML = location.hash;
})
</script>
</body>
4.3 Ajax请求,例如使用jQuery封装好的ajax请求方式。
<body>
<h1 id="id"></h1>
<a href="#/id1">id1</a>
<a href="#/id2">id2</a>
<a href="#/id3">id3</a>
</body>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="module">
// import demo1 from './demo1.js'
// document.querySelector('#id').innerHTML = demo1
$.ajax({
url: './demo2.html',
success: (res) => {
document.querySelector('#id').innerHTML = res
}
});
window.addEventListener('hashchange', e => {
e.preventDefault();
document.querySelector('#id').innerHTML = location.hash;
})
</script>
4.4 通用方式
<body>
<h1 id="page">空白页</h1>
<a href="#/page1">page1</a>
<a href="#/page2">page2</a>
<a href="#/page3">page3</a>
</body>
<script type="text/javascript">
import demo1 from './import.js';
// 创建路由类
class Router {
constructor() {
// 路由对象
this.routes = {};
// 当前URL
this.curUrl = '';
}
// 根据RUL或者对应URL的回调函数
route(path, callback = () => {}) {
this.routes[path] = callback;
}
// 刷新
refresh() {
// 当前hash值
this.curUrl = window.location.hash.slice(1) || '/';
// 执行方法
this.routes[this.curUrl] && this.routes[this.curUrl]();
}
// 初始化监听函数
initPage() {
window.addEventListener('load', this.refresh.bind(this), false);
window.addEventListener('hashchange', this.refresh.bind(this), false);
}
}
// 实例化路由
window.router = new Router();
// 初始化
window.router.init();
// 获取节点
const content = document.getElementById('page');
// 直接改变
Router.route('/page1', () => {
content.innerHTML = 'page1'
});
// import改变
Router.route('/page2', () => {
content.innerHTML = demo1
});
// AJAX改变
Router.route('/page3', () => {
$.ajax({
url: './demo2.html',
success: (res) => {
content.innerHTML = res
}
})
});
</script>
5、h5 interface
5.1 向历史记录栈中添加记录:pushState(state, title, url);
- state: 一个 JS 对象(不大于640kB),主要用于在 popstate 事件中作为参数被获取。如果不需要这个对象,此处可以填 null
- title: 新页面的标题,部分浏览器(比如 Firefox )忽略此参数,因此一般为 null
- url: 新历史记录的地址,可为页面地址,也可为一个锚点值,新 url 必须与当前 url 处于同一个域,否则将抛出异常,此参数若没有特别标注,会被设为当前文档 url
// 现在是 localhost/1.html
const stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
// 浏览器地址栏将立即变成 localhost/2.html
// 但!!!
// 不会跳转到 2.html
// 不会检查 2.html 是否存在
// 不会在 popstate 事件中获取
// 不会触发页面刷新
// 这个方法仅仅是添加了一条最新记录
5.2 改变当前历史记录而不是添加新纪录:replaceState(state, title, url);
PS:
将URL设置为锚点时将不回触发hashchange。
URL设置为不同域名、端口号、协议,将会报错,这是由于浏览器的同源策略的限制。
5.3 popState(state, title, url);
当浏览器历史栈出现变化时,就会触发popState事件。
PS:
调用pushState和replaceState时不会触发popState,只有用户点击前进后退
或者使用JavaScript的go、back、forword方法时才会触发,并且对于不同文件
的切换也是不会触发的
h5 interface 手写一个路由类
<body>
<h1 id="page">空白页</h1>
<a class="route" href="#/page1">page1</a>
<a class="route" href="#/page2">page2</a>
<a class="route" href="#/page3">page3</a>
</body>
<script type="text/javascript">
import demo1 from './import.js';
// 创建路由类
class Router {
constructor() {
// 路由对象
this.routes = {};
// 当前URL
this.curUrl = '';
}
// 根据RUL或者对应URL的回调函数
route(path, callback = () => {}) {
this.routes[path] = (type) => {
if (type === 1) {
history.pushState({ path }, path, path);
}
if (type === 2) {
history.replaceState({ path }, path, path);
}
callback();
};
}
// 刷新
refresh(path, type) {
this.routes[this.curUrl] && this.routes[this.curUrl](type);
}
// 初始化监听函数
initPage() {
window.addEventListener('load', () => {
// 获取当前 URL 路径
this.curUrl = location.href.slice(location.href.indexOf('/', 8));
this.refresh(this.curUrl, 2)
}, false);
window.addEventListener('popstate', () => {
this.curUrl = history.state.path;
this.refresh(this.curUrl, 2);
}, false);
const links = document.querySelectorAll('.route')
links.forEach((item) => {
// 覆盖 a 标签的 click 事件,防止默认跳转行为
item.onclick = (e) => {
e.preventDefault();
// 获取修改之后的 URL
this.curUrl = e.target.getAttribute('href');
// 渲染
this.refresh(this.curUrl, 2);
}
});
}
}
// 实例化路由
window.router = new Router();
// 初始化
window.router.init();
// 获取节点
const content = document.getElementById('page');
// 直接改变
Router.route('/page1', () => {
content.innerHTML = 'page1'
});
// import改变
Router.route('/page2', () => {
content.innerHTML = demo1
});
// AJAX改变
Router.route('/page3', () => {
$.ajax({
url: './demo2.html',
success: (res) => {
content.innerHTML = res
}
})
});
</script>
6、Vue-router的路由实现
项目地址:github.com/vuejs/vue-r… 先看一段Vue-router源码(其他已省略)
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
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}`)
}
}
我们发现在Vue-router中,history实例和mode的类型有关。不同的类型实现不一样。 首先我们来看看HTML5History源码实现(/src/index.js)
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
go (n: number) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
我们发现其实我们在日常项目中使用的push和replace方法就是调用的this.history中的方法,也就是 HTML5History类的方法,我们找到HTML5History这个类。在这里/src/history/html5.js
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
在这个HTML5History类中定义了push和replace方法,原来如此,在内部的使用又使用了上文提到 h5 interface的pushState和replaceState,哈哈哈,其实这里的pushState和replaceState并不是h5 interface的pushState和replaceState。而是调用了import { pushState, replaceState, supportsPushState } from '../util/push-state'中的pushState和replaceState。来看看push-state源文件的实现。
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
在这里才是真正的使用了h5 interface的pushState和replaceState,真是兜兜转转一圈终于找到了呀
总结:
router 实例调用的 push 实际是 history 的方法,通过 mode 来确定匹配 history
的实现方案,从代码中我们看到,push 调用了 src/util/push-state.js 中被改写过
的 pushState 的方法,改写过的方法会根据传入的参数 replace?: boolean 来进行
判断调用 pushState 还是 replaceState ,同时做了错误捕获,如果,history 无刷
新修改访问路径失败,则调用 window.location.replace(url) ,有刷新的切换用户访
问地址 ,同理 pushState 也是这样。这里的 transitionTo 方法主要的作用是做视图
的跟新及路由跳转监测,如果 url 没有变化(访问地址切换失败的情况),在
transitionTo 方法内部还会调用一个 ensureURL 方法,来修改 url。
transitionTo 方法中应用的父方法比较多,这里不做长篇赘述。
其他两种方式hash模式abstract模式内部其实也是比较简单的,我这里就不一一为大家来解析源码。