前端路由简介以及vue-router实现原理
yuyutoo 2025-05-30 18:37 7 浏览 0 评论
作者:muwoo
来源:
https://zhuanlan.zhihu.com/p/37730038
后端路由简介
路由这个概念最先是后端出现的。在以前用模板引擎开发页面时,经常会看到这样
http://www.xxx.com/login
大致流程可以看成这样:
- 浏览器发出请求
- 服务器监听到80 端口(或443)有请求过来,并解析 url 路径
- 根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等)
- 浏览器根据数据包的 Content-Type 来决定如何解析数据
简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。
前端路由
- hash 模式
随着 ajax 的流行,异步数据请求交互运行在不刷新浏览器的情况下进行。而异步交互体验的更高级版本就是 SPA —— 单页应用。单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的,为了实现单页应用,所以就有了前端路由。
类似于服务端路由,前端路由实现起来其实也很简单,就是匹配不同的 url 路径,进行解析,然后动态的渲染出区域 html 内容。但是这样存在一个问题,就是 url 每次变化的时候,都会造成页面的刷新。那解决问题的思路便是在改变 url 的情况下,保证页面的不刷新。在 2014 年之前,大家是通过 hash 来实现路由,url hash 就是类似于:
http://www.xxx.com/#/login
这种# 。后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变化,还会触发 hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听 hashchange来实现更新页面部分内容的操作:
unction matchAndUpdate () {
// todo 匹配 hash 做 dom 更新操作
}
window.addEventListener('hashchange', matchAndUpdate)
Vue router 实现
我们来看一下vue-router 是如何定义的:
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [...]
})
new Vue({
router
...
})
可以看出来 vue-router 是通过Vue.use 的方法被注入进 Vue 实例中,在使用的时候我们需要全局用到 vue-router 的router-view 和 router-link 组件,以及 this.$router/$route 这样的实例对象。那么是如何实现这些操作的呢?下面我会分几个章节详细的带你进入 vue-router的世界。
造轮子 -- 动手实现一个数据驱动的 router
经过上面的阐述,相信您已经对前端路由以及vue-router 有了一些大致的了解。那么这里我们为了贯彻无解肥,我们来手把手撸一个下面这样的数据驱动的 router:
new Router({
id: 'router-view', // 容器视图
mode: 'hash', // 模式
routes: [
{
path: '/',
name: 'home',
component: '<div>Home</div>',
beforeEnter: (next) => {
console.log('before enter home')
next()
},
afterEnter: (next) => {
console.log('enter home')
next()
},
beforeLeave: (next) => {
console.log('start leave home')
next()
}
},
{
path: '/bar',
name: 'bar',
component: '<div>Bar</div>',
beforeEnter: (next) => {
console.log('before enter bar')
next()
},
afterEnter: (next) => {
console.log('enter bar')
next()
},
beforeLeave: (next) => {
console.log('start leave bar')
next()
}
},
{
path: '/foo',
name: 'foo',
component: '<div>Foo</div>'
}
]
})
思路整理
首先是数据驱动,所以我们可以通过一个 route 对象来表述当前路由状态,比如:
current = {
path: '/', // 路径
query: {}, // query
params: {}, // params
name: '', // 路由名
fullPath: '/', // 完整路径
route: {} // 记录当前路由属性
}
current.route 内存放当前路由的配置信息,所以我们只需要监听 current.route 的变化来动态 render 页面便可。
接着我么需要监听不同的路由变化,做相应的处理。以及实现 hash 和history 模式。
据驱动
这里我们延用 vue 数据驱动模型,实现一个简单的数据劫持,并更新视图。首先定义我们的 observer
class Observer {
constructor (value) {
this.walk(value)
}
walk (obj) {
Object.keys(obj).forEach((key) => {
// 如果是对象,则递归调用walk,保证每个属性都可以被defineReactive
if (typeof obj[key] === 'object') {
this.walk(obj[key])
}
defineReactive(obj, key, obj[key])
})
}
}
function defineReactive(obj, key, value) {
let dep = new Dep()
Object.defineProperty(obj, key, {
get: () => {
if (Dep.target) {
// 依赖收集
dep.add()
}
return value
},
set: (newValue) => {
value = newValue
// 通知更新,对应的更新视图
dep.notify()
}
})
}
export function observer(value) {
return new Observer(value)
}
再接着,我们需要定义 Dep 和 Watcher:
export class Dep {
constructor () {
this.deppend = []
}
add () {
// 收集watcher
this.deppend.push(Dep.target)
}
notify () {
this.deppend.forEach((target) => {
// 调用watcher的更新函数
target.update()
})
}
}
Dep.target = null
export function setTarget (target) {
Dep.target = target
}
export function cleanTarget() {
Dep.target = null
}
// Watcher
export class Watcher {
constructor (vm, expression, callback) {
this.vm = vm
this.callbacks = []
this.expression = expression
this.callbacks.push(callback)
this.value = this.getVal()
}
getVal () {
setTarget(this)
// 触发 get 方法,完成对 watcher 的收集
let val = this.vm
this.expression.split('.').forEach((key) => {
val = val[key]
})
cleanTarget()
return val
}
// 更新动作
update () {
this.callbacks.forEach((cb) => {
cb()
})
}
}
到这里我们实现了一个简单的订阅-发布器,所以我们需要对 current.route做数据劫持。一旦current.route更新,我们可以及时的更新当前页面:
// 响应式数据劫持
observer(this.current)
// 对 current.route 对象进行依赖收集,变化时通过 render 来更新
new Watcher(this.current, 'route', this.render.bind(this))
恩....到这里,我们似乎已经完成了一个简单的响应式数据更新。其实 render 也就是动态的为页面指定区域渲染对应内容,这里只做一个简化版的 render:
render() {
let i
if ((i = this.history.current) && (i = i.route) && (i = i.component)) {
document.getElementById(this.container).innerHTML = i
}
}
hash 和 history
接下来是 hash 和history模式的实现,这里我们可以沿用 vue-router的思想,建立不同的处理模型便可。来看一下我实现的核心代码:
this.history = this.mode === 'history' ?
new HTML5History(this) :
new HashHistory(this)
当页面变化时,我们只需要监听 hashchange 和 popstate 事件,做路由转换 transitionTo:
/**
* 路由转换
* @param target 目标路径
* @param cb 成功后的回调
*/
transitionTo(target, cb) {
// 通过对比传入的 routes 获取匹配到的 targetRoute 对象
const targetRoute = match(target, this.router.routes)
this.confirmTransition(targetRoute, () => {
// 这里会触发视图更新
this.current.route = targetRoute
this.current.name = targetRoute.name
this.current.path = targetRoute.path
this.current.query = targetRoute.query || getQuery()
this.current.fullPath = getFullPath(this.current)
cb && cb()
})
}
/**
* 确认跳转
* @param route
* @param cb
*/
confirmTransition (route, cb) {
// 钩子函数执行队列
let queue = [].concat(
this.router.beforeEach,
this.current.route.beforeLeave,
route.beforeEnter,
route.afterEnter
)
// 通过 step 调度执行
let i = -1
const step = () => {
i ++
if (i > queue.length) {
cb()
} else if (queue[i]) {
queue[i](step)
} else {
step()
}
}
step(i)
}
}
这样我们一方面通过 this.current.route = targetRoute 达到了对之前劫持数据的更新,来达到视图更新。另一方面我们又通过任务队列的调度,实现了基本的钩子函数beforeEach、beforeLeave、beforeEnter、afterEnter。
到这里其实也就差不多了,接下来我们顺带着实现几个API吧:
/**
* 跳转,添加历史记录
* @param location
* @example this.push({name: 'home'})
* @example this.push('/')
*/
push (location) {
const targetRoute = match(location, this.router.routes)
this.transitionTo(targetRoute, () => {
changeUrl(this.router.base, this.current.fullPath)
})
}
/**
* 跳转,添加历史记录
* @param location
* @example this.replaceState({name: 'home'})
* @example this.replaceState('/')
*/
replaceState(location) {
const targetRoute = match(location, this.router.routes)
this.transitionTo(targetRoute, () => {
changeUrl(this.router.base, this.current.fullPath, true)
})
}
go (n) {
window.history.go(n)
}
function changeUrl(path, replace) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
if (replace) {
window.history.replaceState({}, '', `${base}#/${path}`)
} else {
window.history.pushState({}, '', `${base}#/${path}`)
}
}
源码:https://link.zhihu.com/?target=
https%3A//github.com/muwoo/blogs/tree/master/src/router
今天给到大家福利是《Vue.js源码全方位深入解析 (含Vue3.0源码分析)》,领取方式,转发+点赞,私信我 "源码", 即可免费获取。
相关推荐
- 实现分页的最简单的方式(附带源码下载)
-
分页是每个网站并不可少的,但是处理往往非常麻烦,虽然有很多的插件可以使用,但复杂度依然不减,今天我们来手敲一个最简单的实现方式,看够之后分页再也不是难题,而且以后用插件也懂原理了。手动擒来!文章概略:...
- 什么是XSS攻击,我们又该如何防范?
-
什么是XSS?XSS,全称为CrossSiteScripting,意思是跨站脚本攻击,为了与层叠样式表(CSS:CascadingStyleSheets...
- 网站wn万能命令wn.run怎么用?
-
wn万能命令是一个在线工具快捷跳转平台,将各种高质量的在线工具按应用站点聚合起来,让你以方便快捷好记的方式寻找和使用网页型在线工具。...
- 游客在三清山景区坐椅子拍照被强制收费 官方通报:已责令摄影点立即整改
-
大皖新闻讯7月31日,大皖新闻报道了游客称三清山景区坐下椅子拍照就被强制收费一事,引发关注。31日下午,三清山景区管理局发布情况通报称,经调查核实,视频反映情况属实,已责令该摄影点立即整改。官方发布情...
- 使用JavaScript如何获取网站网址
-
在做网站开发时,我们有时候会获取当前页面的完整路径。在网页前端如何实现呢?请在网页脚本代码段中粘贴如下代码。functiongetRootPath(){//获取当前网址,...
- 重磅发布!时间终于确定
-
]|\[sS])*"|'(?:[^\']|\[sS])*'|[^)}]+)s*)/g,l=window.testenv_reshost||window.__moon_host||"res.wx.qq...
- 收到一个神奇的任务,聊天工具运行不正常,原来是浏览器的问题
-
如何神奇呢?先来简单介绍一下背景,接下来再说一下这个现象如何神奇。出问题的是一个即时聊天工具插件,被嵌入到客户的第三方程序中。工程师说,我们自己的例子程序是没问题的,而且也不是所有的客户端都有问题。他...
- 微服务架构实战:商城的用户登录与账户切换设计、订单查询设计
-
商城的用户登录与账号切换设计在移动商城的设计中,除商品和分类查询是完全开放权限的页面外,其他涉及个人隐私的个人信息、订单查询和购物车等都必须进行权限管理。有关用户权限管理的功能,在这里根据移动设备的特...
- 前端进阶-每日一练(4)
-
本期问题:document.write和innerHTML、innerText有什么区别?jQuery中如何中断each遍历?如何查找数组中的最小值和最大值?如何做到优雅降级,渐进增强?$....
- js中的正则表达式入门
-
什么是正则表达式呢?...
- 前端路由简介以及vue-router实现原理
-
作者:muwoo来源:https://zhuanlan.zhihu.com/p/37730038...
- 详解三类的友情链接不能交换
-
“假PR页面导出链接太多专门的友情链接页无效链接内页的链接关联性网页快照更新的速度收录的数量nofollow生成静态伪装图片链接链接工厂下拉列表或滚动js脚本转向fram...
- 福建推进河(湖)长制工作:当好“施工队长”建设八闽幸福河湖
-
]|\[sS])*"|'(?:[^\']|\[sS])*'|[^)}]+)s*)/g,l=window.testenv_reshost||window.__moon_host||"res.wx.qq...
- 如何愉快地使用Via浏览器
-
...
- 将 Safari 打造成 iOS 里的快速启动中心:Bookmarklet
-
Safari作为iOS设备上我们使用最为频繁的App之一,它除了能让我们翱翔于无边的网际之外,其实利用它本身自带的书签功能我们还可以将其打造成一个快速启动中心。在桌面环境中大多数浏览器都允许...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
推荐7个模板代码和其他游戏源码下载的网址
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)