前端路由 router 原理及实现
核心都是改变url,但不刷新页面,不向服务器发送请求
hash 路由
- url 中带有一个 #,# 只是客户端的状态,不会传递给服务端
http://a.com/web#order => http://a.com/web
http://a.com/#/list/detail/1 => http://a.com
复制代码
- hash 改变时,页面不会刷新
// 在调试器中
location.hash = '#/news'
location.replace('#/detail') // 替换当前记录
// https://www.baidu.com -> https://www.baidu.com/#news
复制代码
-
hash 值的更改,会在浏览器访问历史中添加一条记录,可通过浏览器的前进、返回按钮控制 hash 的切换
-
监听 hash 更改事件:
hashchange
// 案例
<div>
<a href="#/list">列表页</a>
<a href="#/detail">详情页</a>
<a href="#/other">404</a>
</div>
<div id="app" style="border: 1px solid black; min-height: 200px;"></div>
// 定义路由映射表
var routerObj = {
'#/list': '<div>列表页</div>',
'#/detail': '<div>详情页</div>',
'#/other': '<div>404</div>',
}
// hashchange 监听事件
window.addEventListener('hashchange', function () {
document.getElementById('app').innerHTML = routerObj[location.hash];
})
复制代码
- 高级写法
<div class="container">
<a href="#gray">灰色</a>
<a href="#green">绿色</a>
<a href="#">白色</a>
<button onclick="window.history.go(-1)">返回</button>
</div>
class BaseRouter {
constructor() {
this.routes = {}; // 存储path以及callback的对应关系
this.refresh = this.refresh.bind(this);
window.addEventListener('load',this.refresh);
window.addEventListener('hashchange',this.refresh);
}
route(path,callback) {
this.routes[path] = callback || function(){};
}
refresh() { // 刷新当前页面。渲染当前路径对应的操作
const path = `/${location.hash.slice(1) || ''}`;
this.routes[path]();
}
}
const body = document.querySelector('body');
function changeBgColor(color) {
body.style.backgroundColor = color;
}
const Router = new BaseRouter();
Router.route('/',function() {
changeBgColor('white');
})
Router.route('/green',function() {
changeBgColor('green');
})
Router.route('/gray',function() {
changeBgColor('gray');
})
复制代码
history 路由
- 需要服务端配合,避免刷新后导致页面404
// 对于后端来说可能是两个页面,要做一个通配符识别,将 /web* 后面的统一返回某个 html 中
http://a.com/web/order
http://a.com/web/goods
复制代码
- 用法
window.history.back(); // 后退一步 window.history.go(-1)
window.history.forward(); // 前进一步 window.history.go(1);
window.history.go(-3); // 后退 3 步
window.history.pushState(); // location.href 页面的浏览记录离会添加一个历史记录
window.history.replaceState(); // location.replace 替换掉当前的历史记录
pushState / replaceState 的参数
window.hisory.pushState(null,'new',path);
// state: 一个对象,与指定网址有关
// title:新页面名字
// url: 新页面地址
复制代码
- history 路由没有 hash 路由类似的
hashchange
事件
<div id="history-box">
<h1>history 路由</h1>
<a href="/web/list">列表页</a>
<a href="/web/detail">详情页</a>
<a href="/web/other">404</a>
</div>
<div id="app" style="border: 1px solid black; min-height: 200px;"></div>
// history 路由demo
var routerHistoryObj = {
'/web/list': '<div>history 列表页</div>',
'/web/detail': '<div>history 详情页</div>'
}
// 为每个链接添加点击事件
var length = document.querySelectorAll('#history-box a[href]').length
for (var i = 0; i < length; i++) {
document.querySelectorAll('#history-box a[href]')[i].addEventListener('click', function (event) {
event.preventDefault();
window.history.pushState({}, null, event.currentTarget.getAttribute('href'));
handleHref();
})
}
// 监听前进/后退 引起的posstate事件
window.addEventListener('popstate', handleHref);
// 根据新的路由,显示新的组件
function handleHref() {
document.getElementById('app').innerHTML = routerHistoryObj[location.pathname] || '404页面'
}
复制代码
区别
- hash 有 #, history 没有
- hash 的 # 部分内容不会传给服务器,history 的所有 url 内容服务端都可以获取到
- history 路由,应用在部署的时候,需要注意 HTML 文件的访问
- hash 通过 hashchange 监听变化, history 通过 popstate 监听变化
问题
- pushState 时,会触发 popState 事件吗?
答:不会,需要手动触发页面的重新渲染。
- popState 什么时候才会触发?
答:点击浏览器的前进、后退按钮;js中触发 back forward go 方法。
vue-router
vue2.x版本
使用方法
- 通过
cnpm i vue-router --save
下载路由 - 新建 router.js 配置路由
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// 路由关系表
const routes = [
{
path: '/',
name: 'home', // 命名路由
component: ()=> import('@/components/Home'),
meta: {
title: '首页'
}
},
{ // 404 路由
path: '*',
component: () => import('@/components/404'),
// redirect: '/' // 重定向到首页
}
]
let router = new Router({
// 默认hash 模式,设置后改为 history 模式,本地项目切换路由后页面依然没问题
// (因为vue帮我们处理了),但是线上项目需要后台配置
mode: 'history',
routes,
// 记录位置,返回到上个页面点击的位置
scrollBehavior:(to,from,savedPosition)=>{
return savedPosition;
}
// 坑:通过 router-link 并不能记住位置
})
export default router;
复制代码
- 在 main.js 中配置路由
import router from './router/router'
new Vue({
router,
...
}).$mount('#app')
复制代码
知识点
Vue.use(Router)
引入了两个组件 router-link
和 router-view
,及全局混入了$route
、$router
this.$route
带属性:params、query、matched、paththis.$router
带方法:push(location)、replace(location)…
// routr-link 和 router-view
<div>
<router-link :to="{name: 'home'}">首页</router-link>
<router-link :to="'/login'">登录页</router-link>
<router-link to="/news">新闻页</router-link>
<router-link to="/login">登录页</router-link>
// router-link 默认为 a 标签,可用 tag 改变属性
// <router-link to="/login" tag='p'>登录页</router-link>
// 用 <a> 标签跳转会刷新页面,所以尽量不要用
</div>
// 路由的出口,匹配到的路由在此渲染,transition 添加动画
<transition name='fade'>
<router-view class='router-view'></router-view>
</transition>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.75s ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.router-view {
position: absolute;
transition: all 0.75s cubic-bezier(0.55,0,0.1,1);
}
复制代码
- 命名路由
- 可直接通过名字跳转,后续如果更改了 path,则不影响 name 的跳转
- 设置了默认的子路由,则子路由的 name 会被警告,通过name跳转父路由则不会显示默认的子路由
- 子路由 和 动态匹配路由
- 默认子路由: path: ”
- 子路由中的path是否以 ‘/’ 开头的区别,加 ‘/’ 是绝对路径,不加是相对
const routes = [
{
path: '/news',
// name: 'news', // 有默认子路由时,父路由不要设置name
component: () => import('@/components/News'),
meta: {
title: '新闻',
requiredAuth: true,
},
children: [
{ // 定义默认路由为 添加新闻 页面
path: '',
component: () => import('@/components/news/NewAdd'),
meta: {
title: '新闻',
},
},
{
path: 'add', // 注意:不加 '/'
name: 'newAdd',
component: () => import('@/components/news/NewAdd'),
meta: {
title: '添加新闻'
},
},
{ // 动态匹配路由
path: 'detail/:id',
name: 'newDetail',
component: () => import('@/components/news/NewDetail'),
meta: {
title: '新闻详情'
},
},
]
},
]
// 动态匹配路由,此时页面中
<router-link :to="{name: 'newDetail',params: {id: 1}}">新闻详情-1</router-link>
<router-link :to="{name: 'newDetail',params: {id: 2}}">新闻详情-2</router-link>
// 此时对应路由页面内
// 可通过 this.$route.params.id 获取对应 id 内容。
// 但是由于在同一页面,mounted 后路由没有销毁,所以切换路由 detail/1 和 detail/2 时页面延迟导致内容串了。可通过 watch 监听
watch: {
'$route.params.id'() {
this.getNews();
}
},
// 也可用组件路由守卫
beforeRouteUpdate(to, from, next) {
console.log("新闻详情: 组件中 - beforeRouteUpdate");
this.getNews(to.params.id);
next();
},
methods: {
getNews(id = this.$route.params.id) {
this.content = `这是新闻ID为: ${id}的内容`;
},
},
复制代码
- 跳转页面的方式
// 页面中
<router-link :to="{name: 'newDetail',params: {id: 1}}">新闻详情-1</router-link>
// js 中
this.$router.push({ name: "my", query: { type: 'like' } });
// 注:query 是 ? 后面内容,params 是 & 后面内容
复制代码
路由守卫
分类
- 全局守卫:
beforeEach
、beforeResolve
、afterEach
- 路由独享守卫:
beforeEnter
- 组件守卫:
beforeRouteLeave
、beforeRouteEnter
、beforeRouteUpdate
// 全局守卫,写在 router.js 内
router.beforeEach((to,from,next)=>{})
export default router;
// 路由独享守卫,写在router.js 内 定义在路由映射表里
{
path: 'detail',
name: 'newDetail',
component: () => import('@/components/news/NewDetail'),
beforeEnter(to,from,next) {
next();
}
},
// 组件守卫,写在 组件 内
beforeRouteEnter(to, from, next) {
// 这里无法访问 this,因为没有创建实例,但是可以在next里面添加回调,唯一一个支持给next传递回调的守卫
next((vm) => {
// 通过 `vm` 访问组件实例
});
},
beforeRouteUpdate(to, from, next) {
// 当前路由改变时,该组件被复用时调用(比如:统一路由不同id的时候)
next();
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消
const answer = window.confirm("确定是否离开?");
if (answer) {
next();
} else {
next(false);
}
},
复制代码
注:
- 必须调用
next()
才可继续 afterEach(to, from)
无 next 参数,不会改变导航,因为导航已被确认
执行顺序
- 【组件】前一个组件的
beforRouteLeave
- 【全局】的
beforeEach
- 【组件】如果是路由参数变化,触发
beforeRouteUpdate
- 【配置文件】里, 下一个的
beforeEnter
- 【组件】内部声明的
beforeRouteEnter
- 【全局】的
afterEach
注意:当路由执行完,就开始页面 vue 的生命周期了(beforeCreate…)
路由元信息 meta
鉴权:鉴定是否有权限 如果需要鉴权页面过多,可以用meta属性添加是否鉴权
const routes = [{
path: '/',
meta: {
title: '首页',
requiredAuth: true,
},
},
]
// 路由全局守卫
router.beforeEach((to,from,next)=>{
console.log('路由全局守卫: router - beforeEach:',to)
// if(to.path.includes('news')) { 需要鉴权内容过多时,用 meta
if( to.matched.some(record=>record.meta.requiredAuth) ) {
if(localStorage.getItem('userId')) {
next()
} else {
next({name: 'about'})
}
} else {
next()
}
})
复制代码
异步组件
异步组件 即 实现了路由懒加载
// 未用异步组件写法,所有资源都会被打包到 app.js 里面去
import News from './components/News';
{
path: '/news',
component: News
}
// 下面写法自动实现了异步组件加载
// webpackChunkName 改变懒加载时的js名字,比如 news 页面 js 名字为 news.js
{
path: '/news',
component: () => import(/* webpackChunkName: "news" */'@/components/News'),
}
复制代码
prefetch
: 页面中可能用的js,浏览器在空闲的时候加载preload
: 页面中用到的js,优先加载,相当于提高优先级
// 默认为以下情况
<link href="/js/app.js" rel="preload" as="script"> // 具体大小
<link href="/js/login.js" rel="prefetch"> // (prefetch cache)
// Network 中按需加载的 js的 size 大小显示为:(prefetch cache)
复制代码
- 去除
prefetch
// 在 package.js 同级目录下新建 vue.config.js (可修改 webpack 配置)
module.exports = {
// 删除 HTML 相关 webpack 插件
chainWebpack: config =>{
// prefetch,当前页面可能会用到的资源,在浏览器空闲时加载
config.plugins.delete('prefetch')
}
}
// 变为:
<link href="/js/app.js" rel="preload" as="script">
<script charset="utf-8" src="/js/home.js"></script>
// Network 中按需加载的 js的 size 大小显示为具体大小
复制代码
注:
- 除了首页,都可以用懒加载路由
- 如果直接加载某页面,则该页面之前的页面也可能被加载
其他
- 直接使用 a 链接与使用 router-link 的区别?
使用 a 链接会刷新页面,使用 router-link 不会刷新页面
- Vue路由怎么跳转打开新窗口?
const obj = {
path: xxx, // 路由地址
query: {
mid: data.id // 可以带参数
}
};
const {href} = this.$router.resolve(obj);
window.open(href, '_blank');
复制代码
- 路由组件和路由为什么解耦,怎么解耦?
当路由中使用到 this.$route.params.id
时,依赖于上个页面传入的属性id,此时不能单独拿出该页面,所以需要解耦
// 在 具体页面的js中添加所用到的属性
export default {
props: ['id'],
data() {
return {
// id: this.$route.params.id
}
}
}
// 在 路由关系表里对应路由添加 props: true
{
path: '/detail/:id',
props: true,
name: 'detail',
component: ()=> import(/* webpackChunkName: 'detail' */'@/components/Detail')
},
复制代码
具体代码请到git: vue模板
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END