一、如何实现前端路由
1、什么是前端路由?
URL 变化引起 UI 更新(无需刷新页面)
所以要思考两个问题:
- 改变URL,却不会引起页面的刷新
- 如何检测URL变化
2、实现方案
hash实现
hash是URL中#后面那部分,同时修改hash值不会引起页面的刷新,也不会向服务器重新发送请求。通过hashchange事件可以监听它的变化。改变hash值有以下三种方式:
- 浏览器前进后退改变URL
- 通过a标签改变URL
- 通过window.location.hash改变URL
备注:以上三种方式均可以触发hashchang事件
history实现
history是HTML5新增的,提供了两个方法用来修改浏览器的历史记录,且都不会引起页面的刷新
- pushState,向浏览器中新增一条历史记录,同时修改地址栏
- replaceState,直接替换浏览器中当前的历史记录,同时修改地址栏
history提供了popstate监听事件,但是只有以下两种情况会触发该事件
- 点击浏览器前进后退的按钮
- 显示调用history的back、go、forward方法
备注:pushState与replaceState均不会触发popstate事件
二、原生方式实现
hash实现方式
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<ul>
<!-- 定义路由 -->
<li><a href="">home</a></li>
<li><a href="">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
</ul>
<script>
let routerView = document.querySelector('#routeView')
window.addEventListener('DOMContentLoaded', ()=>{
if(!location.hash){//如果不存在hash值,那么重定向到#/
location.hash="/"
}else{//如果存在hash值,那就渲染对应UI
let hash = location.hash;
routerView.innerHTML = hash
}
})
window.addEventListener('hashchange', ()=>{
let hash = location.hash;
routerView.innerHTML = hash
console.log('hashChange')
})
</script>
</body>
</html>
复制代码
代码解析:
- 页面第一次加载完毕,并不会触发hashchange事件,所以需要手动的给routeView(UI)赋值
- 点击A链接触发hashchange事件需要做两件事儿
- 修改location中的hash值
- 给routeView(UI)赋值
history实现
<!DOCTYPE html>
<html lang="en">
<body>
<div>
<ul>
<!-- 定义路由 -->
<li><a href="https://juejin.cn/home">home</a></li>
<li><a href="https://juejin.cn/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
</div>
<script>
let routerView = document.querySelector('#routeView')
window.addEventListener('popstate', ()=>{
let pathName = location.pathname;
routerView.innerHTML = pathName
console.log('popstate');
})
window.addEventListener('DOMContentLoaded', load, false)
function load (e) {
!location.pathname && (location.pathname="/") //如果不存在hash值,那么重定向到/
let ul = document.querySelector('ul')
ul.addEventListener('click', function (e) {
e.preventDefault()
if (e.target.nodeName === 'A') {
let src = e.target.getAttribute('href')
history.pushState(src, null, src) // 修改URL中的地址
routerView.innerHTML = src // 更新UI
}
}, false)
}
</script>
</body>
</html>
复制代码
代码解析:
- 页面加载完毕后,给A链接绑定事件(这里我通过事件代理的方式来实现),同时要阻止A标签的默认事件
- 点击A标签并不会触发popstate事件,所有需要手动的去修改URL地址,然后更新routeView(UI)
- 当我们点击浏览器的前进后退安按钮会触发popstate事件,同样事件触发后,修改URL地址,然后更新routeView(UI)
总结:
- 页面第一次加载,需要以下两件事儿:
- 需要判断URL中hash|pathname是否有值,为空的话,需要为他们赋值(/)
- 由于第一次加载并不会触发hashchange|popstate事件,所以需要手动更新UI
- 事件触发后,需要做两件事儿
- 修改URL中的值(为location.hash、history.pushState赋值)
- 更新UI
三、Vue-router实现
这里只实现了hash模式部分,history部分也大相径庭,可以自行补充。实现Vue-router之前先看看Vue中的插件机制是什么样的,它是如何注册的?
1、Vue.use() 全局注册插件
先贴上Vue.use代码
Vue.use = function(plugin) {
// 全局维护一个插件列表,防止多次注册相同的插件
const installedPlugins = this._installedPlugins || (this._intalledPlugins = [])
if (installedPlugin.indexOf(plugin) > -1) return this
const args = toArray(argumens, 1) // 将类数组转换成数组,并从1开始截取
args.unshift(this) // 将vue的构造函数放置args的第一位
if (typeof plugin.install === 'function') {
plugin.install.applay(plugin, args)
} else if(typeof plugin === 'function') {
plugin.apply(null, args)
}
}
/**
* 将类数组转化成数组
*/
function toArray (list, start) {
start = start || 0
let i = list.length - start
const ret = new Array(i)
while (i--) {
console.log('i=', i)
ret[i] = list[i + start]
}
return ret
}
复制代码
代码解析:
- 首先接收一个插件构造函数plugin
- 判断传入的插件在Vue的全局插件列表(_intalledPlugins)是否存在,不存就加入,存在则返回
- 拼接参数列表args,截取use函数第一个后面是插件的参数,然后将Vue构造函数unshit到数组中的第一位
- 执行install或可执行函数,并传入args参数列表
小结:
- Vue会全局维护一个installedPlugins插件列表,防止插件被多次注册
- Vue的插件必须是一个带有install方法的对象,或者可执行函数,它会被被当作install方法来执行。同时install方法或者可执行函数,可接收两个参数,分别是:
- 第一个参数用来接受Vue的构造函数
- 第二个参数是可选的选项对象
2、VueRouter.install方法
// eslint-disable-next-line no-unused-vars
let _Vue
export function install (Vue) {
_Vue = Vue
Vue.mixin({
beforeCreate () {
// 1、将router挂在根组件上
// 2、使每个vue实例上都有一个_routerRoot指向根组件实例
if (this.$options && this.$options.router) { // 只有根组件才有router
this._routerRoot = this
this._router = this.$options.router
this._router.init(this) // 调用router实例的init方法
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else { // 子组件(这里是一级一级传入的)
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
}
})
// 3、this.$router -> this._routerRoot.router this.$route -> this._routerRoot.router._route
Object.defineProperty(Vue.prototype, '$router', {
get () {
return this._routerRoot._router
}
})
Object.defineProperty(Vue.prototype, '$route', {
get () {
return this._routerRoot._route
}
})
// 4、组册router-link 与 router-view两个组建
Vue.component('router-link', {
props: {
to: String
},
render (h) {
var mode = this._routerRoot._router.mode
let to = mode === 'hash' ? '#' + this.to : this.to
return h('a', { attrs: { href: to } }, this.$slots.default)
}
})
Vue.component('router-view', {
render (h) {
var component = this._routerRoot._route.component
return h(component)
}
})
}
复制代码
代码解析:
- 将Vue作为参数传入在插件内部使用(你总不想打包的时候将Vue打包进去吧!)
- 使用Vue.mixin主要做以下几件事儿:
- 将router挂在根组件上
- 使每个vue实例上都有一个_routerRoot指向根组件实例
- 通过Vue.util.defineReactive()定义了响应式的_route属性,通过修改Vue实例上的_route,会自动调用Vue实例的render()方法,RouteView组件内容会更新
- 调用router实例的init方法,并将vue实例作为参数传入
- 将Vue实例的$router、$route分别代理到router、router._route上
(也就是我们平时调用this.route(当前路由对象)) - 组册router-link 与 router-view两个组件
3、VueRouter类实现
上文中,我们已经实现了VueRouter.install方法,且方法里面调用了VueRouter实例上的init方法,现在我们一起实现一个VueRouter类
constructor构造函数
构造函数主要做以下几件事儿:
- 接收options参数,保存mode、路由列表(options.routes)在实例上
- 将路由列表映射成key-value形式,方便后面使用
- 根据mode类别( HashHistory | HTML5History | AbstractHistory )来实例化一个history(我这里就写一个hash模式的)
init方法实现
实现思路:
- 保存Vue实例app在router实例上
- 获取当前路径location(#后面那边分)
- 通过location获取当前route
- 保存当前路由(history.current = route)
- 执行history中的cb回调,并传入参数route(这里就是修改Vue实例上的_route),由于Vue实例上的_route被修改,Vue根组件上的render被执行,RouteView内容被更新
- 设置hashchange路由监听事件,当路由变化时,重新执行第2步–第5步
执行流程
- 保存Vue实例
- 调用history.transitionTo(以上的第2步–第5步)
- 执行history.listen(cb)(Ps:cb是一个回调函数,用来修改Vue实例的_route值)
// init方法中
history.listen(route => {
this.app._route = route
})
.....
// History类中的listen
listen (cb) {
this.cb = cb
}
复制代码
具体实现如下:
import { install } from './install.js'
import History from './history'
class VueRouter {
constructor (options) {
this.app = null
this.mode = options.mode || 'hash'
this.routes = options.routes
this.history = new History(options.routes)
}
init (app) {
this.app = app // 保存vue的实例
var history = this.history
// history 暂时不考虑 -- 没法测试
// !location.pathname && (location.pathname = '/')
// window.addEventListener('popstate', e => {
// let path = location.pathname
// this.history.transitionTo(
// this.history.getCurrentRoute(path),
// route => this.history.updateRoute(route)
// )
// })
history.transitionTo(// 这里主要做两件事儿 1)初始化的时候更新路由,待用vue实例render 2)给hash做事件监听
history.getCurrentLocation(),
(route) => {
history.setupListener(route)
}
)
history.listen(route => {
this.app._route = route
})
}
push (location) {
this.history.push(location)
}
replace (location) {
this.history.replace(location)
}
}
VueRouter.install = install
export default VueRouter
复制代码
4、History类实现
class History {
constructor (routes) {
this.current = null
this.cb = null
this.routerMap = this.createRouterMap(routes)
this.ensureHash() // 判断URL中是否带有#/,没有的话就给URL重置一下
}
ensureHash () {
!location.hash && (location.hash = '/')
}
transitionTo (location, onComplete) {
let route = this.routerMap[location]
this.updateRoute(route)
onComplete && onComplete(route)
}
updateRoute (route) {
this.current = route
this.cb && this.cb(route)
}
getCurrentRoute (location) {
return this.routerMap[location]
}
createRouterMap (routes = []) {
return routes.reduce((module, route) => {
module[route.path] = route
return module
}, {})
}
setupListener (route) {
window.addEventListener('hashchange', e => {
this.transitionTo(
this.getCurrentLocation()
)
})
}
getCurrentLocation () {
var href = window.location.href
var index = href.indexOf('#')
return index > -1 ? href.slice(index + 1) : '/'
}
push (location) {
this.transitionTo(
location,
() => {
// 修改window中的hash
pushHash(location)
}
)
}
replace (location) {
this.transitionTo(
location,
() => {
replaceHash(location)
}
)
}
listen (cb) {
this.cb = cb
}
}
export default History
// 直接替换hash(可以理解为重定向)
function pushHash (hash) {
location.hash = hash
}
// 替换url后面那部分的hash
function replaceHash (hash) {
var href = window.location.href
var index = href.indexOf('#')
var base = index > -1 ? href.slice(0, index) : href
window.location.replace(base + '#' + hash)
}
复制代码
四、参考
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END