前端路由
-
如何改变 URL 却不引起页面刷新?
-
如何检测 URL 变化了?
hash 实现
hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新
通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种:
- 通过浏览器前进后退改变 URL
- 通过<a>标签改变 URL
- 通过window.location改变URL
history 实现
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:
- 通过浏览器前进后退改变 URL 时会触发 popstate 事件
- 通过pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。
- 好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化
- 通过js 调用history的back,go,forward方法课触发该事件
js实现基础前端路由
基于 hash 实现
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<ul>
<!-- 定义路由 -->
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
</ul>
</body>
<script>
//获取渲染路由对应的UI对象
let routerView = routeView
//监听hash改变,渲染对应内容
window.addEventListener('hashchange', ()=>{
let hash = location.hash;
routerView.innerHTML = hash
})
//页面第一次加载完不会触发 hashchange,因而用load事件来监听hash值,再将视图渲染成对应的内容。
window.addEventListener('DOMContentLoaded', ()=>{
if(!location.hash){//如果不存在hash值,那么重定向到#/
location.hash="/"
}else{//如果存在hash值,那就渲染对应UI
let hash = location.hash;
routerView.innerHTML = hash
}
})
</script>
</html>
基于 history 实现
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<ul>
<li><a href='/home'>home</a></li>
<li><a href='/about'>about</a></li>
<div id="routeView"></div>
</ul>
</ul>
</body>
<script>
//获取渲染的ui对象
let routerView = routeView
window.addEventListener('DOMContentLoaded', onl oad)
window.addEventListener('popstate', ()=>{
routerView.innerHTML = location.pathname
})
//劫持a标签,通过history.pushState改变url,然后渲染对应内容
function onl oad () {
routerView.innerHTML = location.pathname
var linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => el.addEventListener('click', function (e) {
e.preventDefault()
history.pushState(null, '', el.getAttribute('href'))
routerView.innerHTML = location.pathname
}))
}
//直接通过history.pushState改变url的劫持,查看history、hash路由文章
</script>
</html>
实现VueRouter
基本框架
<template>
<div id="app">
<div id="nav">
<router-link to="/home">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
==================================================================
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
const router = new VueRouter({
mode:"history",
routes
})
export default router
==========================================
<template>
<div class="home">
<h1>这是Home组件</h1>
</div>
</template>
==========================================
<template>
<div class="about">
<h1>这是about组件</h1>
</div>
</template>
- 先 const router = new VueRouter({…}),再把router作为参数的一个属性值,new Vue({router})
- 通过Vue.use(VueRouter) 使得每个组件都可以拥有store实例
Vue.use实现
查找插件是否已安装,安装退出,否则调用插件,具体插件Vue.install文章
VueRouter.install实现
let Vue = null;
class VueRouter{
}
VueRouter.install = function (v) {
Vue = v;
};
export default VueRouter
插件中注册router-link、router-view全局组件
let Vue = null;
class VueRouter{
}
VueRouter.install = function (vm) {
Vue = vm;
//新增代码
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
export default VueRouter
给每个组件添加$route和$router
目前只有根组件有这个router值,而其他组件是还没有的,所以我们需要让其他组件也拥有这个router。
let Vue = null;
class VueRouter{
}
VueRouter.install = function (v) {
Vue = v;
// 新增代码
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router; //将VueRouter实例挂在_router上
}else { //如果是子组件
this._root= this.$parent && this.$parent._root //将根实例挂在子组件的_root上
}
}
})
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
export default VueRouter
监听每一个实例调用$router方法
let Vue = null;
class VueRouter{
}
VueRouter.install = function (v) {
Vue = v;
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
}else { //如果是子组件
this._root= this.$parent && this.$parent._root //将根实例挂在子组件的_root上
}
// 新增代码
Object.defineProperty(this,'$router',{ //劫持调用当前实例的$router,指向根实例挂载的VueRouter实例
get(){
return this._root._router
}
})
}
})
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
export default VueRouter
完善VueRouter接受的路由配置参数
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash" //判断路由模式
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes) //将数组-对象映射成对象形式
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
}
格式映射
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
值就是component
新增类管理路由属性
let Vue = null;
//新增代码
class HistoryRoute {
constructor(){
this.current = null
}
}
class VueRouter{
constructor(options) {
this.mode = options.mode || "hash"
this.routes = options.routes || [] //你传递的这个路由是一个数组表
this.routesMap = this.createMap(this.routes)
//新增代码
this.history = new HistoryRoute();
this.init()
}
createMap(routes){
return routes.reduce((pre,current)=>{
pre[current.path] = current.component
return pre;
},{})
}
//新增代码
init(){
if (this.mode === "hash"){ //判断路由模式
// 先判断用户打开时有没有hash值,没有的话跳转到#/
location.hash? '':location.hash = "/";
window.addEventListener("load",()=>{
this.history.current = location.hash.slice(1) //在HistoryRoute类中管理当前url的hash路径
})
window.addEventListener("hashchange",()=>{
this.history.current = location.hash.slice(1)//在HistoryRoute类中管理当前url的hash路径
})
} else{
location.pathname? '':location.pathname = "/";
window.addEventListener('load',()=>{
this.history.current = location.pathname //在HistoryRoute类中管理当前url的history路径
})
window.addEventListener("popstate",()=>{
this.history.current = location.pathname //在HistoryRoute类中管理当前url的history路径
})
}
}
}
通过上一步能管理路由的相关属性,实现$route的访问
VueRouter.install = function (v) {
Vue = v;
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
}else { //如果是子组件
this._root= this.$parent && this.$parent._root //将根实例挂在子组件的_root上
}
Object.defineProperty(this,'$router',{ //劫持调用当前实例的$router,指向根实例挂载的VueRouter实例
get(){
return this._root._router
}
});
//新增代码
Object.defineProperty(this,'$route',{ //劫持调用当前实例的$route,指向根实例挂载的VueRouter实例的$route
get(){
return this._root._router.history.current //返回路由当前路径相关信息
}
})
}
})
Vue.component('router-link',{
render(h){
return h('a',{},'首页')
}
})
Vue.component('router-view',{
render(h){
return h('h1',{},'首页视图')
}
})
};
完善router-view组件,根据上一步实现的管理当前路由的路径,渲染对应组件
Vue.component('router-view',{
render(h){
let current = this._self._root._router.history.current //获取路径
let routeMap = this._self._root._router.routesMap; //获取映射对象
return h(routeMap[current]) //根据路径渲染对应组件
}
})
因为router-view是一个组件,所以当路径改变时,需要重新渲染组件
组件中的this._self._root._router.history.current并不是响应式的,所以现在改变不能引起界面刷新
将_router.history进行响应式化
Vue.mixin({
beforeCreate(){
if (this.$options && this.$options.router){ // 如果是根组件
this._root = this; //把当前实例挂载到_root上
this._router = this.$options.router;
//新增代码,通过提供的defineReactive将history对象变成响应式,使得改变对象时,对象收集的dep会通知router-view的组件依赖的wacther执行update()
Vue.util.defineReactive(this,"",this._router.history)
}else { //如果是子组件
this._root= this.$parent && this.$parent._root
}
Object.defineProperty(this,'$router',{
get(){
return this._root._router
}
});
Object.defineProperty(this,'$route',{
get(){
return this._root._router.history.current
}
})
}
})
完善router-link组件,使得能够改变界面路由路径
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
================================================
Vue.component('router-link',{
props:{
to:String
},
render(h){
let mode = this._self._root._router.mode; //获取路由模式
let to = mode === "hash"?"#"+this.to:this.to //进行对应模式的路径拼接
return h('a',{attrs:{href:to}},this.$slots.default) //返回a标签
}
})
至此通过点击router-link标签就可以实现url上路径的切换以及路由的更新
至于通过history.push等动态改变方式,通过在HistoryRoute类中,添加对应的方法使用history.pushState等方式实现,只要能使得history.current的路径和界面路径相同,则就能通过响应式改变router-view路由组件渲染对应的组件视图