前端路由的实现原理解析
本文提供三种方式实现前端路由,分别是原生JS路由实现,Vue路由实现,React路由实现
什么是前端路由?
首先我们先来了解一下路由是什么?
路由是用来跟后端服务器进行交互的一种方式,根据不同的url地址展现不同的内容或页面。
在web前端单页面应用(SPA)中,路由描述的是URL与视图之间的映射关系,这种映射是单向的,即URL变化引起视图更新,且不需要刷新页面。
如何实现前端路由?
要实现前端路由,需要解决2个核心:
(1)如何检测URL变化?
(2)如何改变URL却不引起页面刷新?
解决上述两个问题,分别使用hash和history两种实现方式就可以解决:
hash实现
- hash是URL中hash(#)以及后面的部分,常用作锚点在页面内进行导航,改变URL中的hash部分不会引起页面刷新。
- 通过hashchange事件监听URL的变化,改变URL的方式只有几种:通过浏览器前进后退改变URL、通过a标签改变URL、通过window.location 改变URL,这几种情况改变URL都会触发hashchange事件。
history实现
- history 提供了pushState 和replaceState两个方法,这两个方法改变URL的path部分不会引起页面的刷新。
- history 提供类似hashchange事件的popstate事件,但popstate事件有些不同:通过浏览器前进后退改变URL时会触发popstate事件,通过pushState/replaceState或者a标签改变URL不会吃法popSate事件。好在我们可以拦截pushState/replaceState的调用和a标签的点击事件来检测URL的变化,所以监听URL变化可以实现,只是没有hashchange那么方便。
那么这里讲解一下history.pushState() 和history.replaceState()方法。
history.pushState(state,title,url)
概念:pushState()是在当前页面创建并**新的历史记录,不会造成页面刷新。
他的三个参数:
state: 是一个js对象,与要跳转到的URL对应的状态信息
title:firefox现在已经忽略这个参数,虽然它可能将来会被用到。目前最安全的使用方式是传一个空字符串,以防止将来的修改。
url:要跳转到的url地址,但是不能跨域。注意在调用pushState方法后浏览器不会加载这个URL,但也许会过一会尝试加载URL。
history.replaceState(state,title,url)
概念:replaceState()是修改当前历史记录,不会造成页面刷新。与pushState的用法一样的。
原生JS前端路由实现
下面我们分别实现hash版本和history版本的路由,示例使用原生的HTML/JS实现,不依赖任何框架。
基于hash实现
html:
<body>
<div class="js-hash">
<h1>原生JS用hash方式实现路由</h1>
<!-- 定义路由 -->
<ul>
<li><a href="#/main">main</a></li>
<li><a href="#/list">list</a></li>
</ul>
<!-- 渲染路由对应的UI -->
<div id="routerView"></div>
</div>
</body>
JS:
// 页面加载完不会触发hashchange事件,这里主动触发一次hashchange事件;DOMContentLoaded:当DOM加载完毕就会调用这个事件
window.addEventListener("DOMContentLoaded",onload);
// 监听路由的变化
window.addEventListener("hashchange",onHashChange);
// 路由视图
var routerView = null;
// 第一次加载触发onload,querySelector是返回指定css选择器的第一个子元素
function onload(){
routerView = document.querySelector("#routerView");
onHashChange();
}
// 路由变化时,根据路由渲染对应UI
function onHashChange(){
console.log(location.hash);
switch(location.hash){
case "#/main":
routerView.innerHTML = "main" ;
return;
case "#/list":
routerView.innerHTML = "list";
return
default:
return
}
}
基于history实现
html
<body>
<div class="js-history">
<!-- 这里使用history模式-->
<h1>原生JS用history方式实现路由</h1>
<ul>
<li><a href="旅游项目app/tranvel/home">home</a></li>
<li><a href="旅游项目app/tranvel/page1">page1</a> </li>
</ul>
<!-- 渲染路由对应的UI -->
<div id="routerView"></div>
</div>
</body>
JS:
// 页面加载完不会触发popState事件,这里主动触发一次popState事件
window.addEventListener("DOMContentLoaded",onloadPop);
// 监听路由的变化
window.addEventListener("popstate",onPopState);
//路由视图
var routerView = null;
function onloadPop(){
routerView = document.querySelector("#routerView");
onPopState();
// 拦截a标签的点击默认事件,点击时使用pushStatexi修改url并且更新视图,实现点击连接更新url和视图的效果
var linkList = document.querySelectorAll("a[href]");
linkList.forEach(el => el.addEventListener("click",function(e){
e.preventDefault();
console.log("el",el.getAttribute("href"));
history.pushState(null,"",el.getAttribute("href"));
onPopState();
}));
}
function onPopState(){
console.log("path",location.pathname);
switch(location.pathname){
case "/home":
routerView.innerHTML = "home page";
return;
case "/page1":
routerView.innerHTML = "page1 page";
return;
default:
return;
}
}
VUE前端路由实现
这是类似于vue-router的实现方式。
class HistoryRoute{
constructor(){
this.current = null;
}
}
class vueRouter {
constructor(options){
this.mode = options.mode || "hash";
this.routes = options.routes || [];
// 传递的路由表是数组 需要装换成{'/home':Home,'/about',About}格式
this.routesMap = this.createMap(this.routes);
// 路由中需要存放当前的路径 需要状态
this.history = new HistoryRoute;
this.init();//开始初始化操作
}
init(){
if(this.mode == 'hash'){
// 先判断用户打开时有没有hash,没有就跳转到#/
location.hash?'':location.hash = '/';
window.addEventListener('load',()=>{
this.history.current = location.hash.slice(1);
});
window.addEventListener('hashchange',()=>{
this.history.current = location.hash.slice(1);
})
}else {
location.pathname?'':location.pathname = '/';
window.addEventListener('load',()=>{
this.history.current = location.pathname;
});
window.addEventListener('popstate',()=>{
this.history.current = location.pathname;
})
}
}
createMap(routes){
return routes.reduce((memo,current)=>{
memo[current.path] = current.component
return memo
},{})
}
}
//使用vue.use就会调用install方法
vueRouter.install = function(Vue,opts) {
//每个组件都有 this.$router / this.$route 所以要mixin一下
console.log(opts)
Vue.mixin({
beforeCreate(){ //混合方法
if(this.$options && this.$options.router){//定位跟组件
this._root = this;//把当前实例挂载在_root上
this._router = this.$options.router // 把router实例挂载在_router上
//history中的current变化也会触发
Vue.util.defineReactive(this,'xxx',this._router.history);
}else {
// vue组件的渲染顺序 父 -> 子 -> 孙子
this._root = this.$parent._root;//获取唯一的路由实例
}
Object.defineProperty(this,'$router',{//Router的实例
get(){
return this._root._router;
}
});
Object.defineProperty(this,'$route',{
get(){
return {
//当前路由所在的状态
current:this._root._router.history.current
}
}
})
}
});
// 全局注册 router的两个组件
Vue.component('router-link',{
props:{
to:String,
tag:String
},
methods:{
handleClick(){
}
},
render(h){
console.log(h)
let mode = this._self._root._router.mode;
let tag = this.tag;
return <tag on-click={this.handleClick} href={mode === 'hash'?`#${this.to}`:this.to}>{this.$slots.default}</tag>
}
})
Vue.component('router-view',{//根据当前的状态 current 对应相应的路由
render(h){
//将current变成动态的 current变化应该会影响视图刷新
//vue实现双向绑定 重写Object.defineProperty
let current = this._self._root._router.history.current;
let routeMap = this._self._root._router.routesMap
return h(routeMap[current])
}
})
}
export default vueRouter;
React 前端路由实现
实现部分主要是利用react的context api来存储路由信息,子组件根据context值去渲染,代码是由hook实现
HistoryRouter
import React, { useState } from "react";
let set; // 保存setUrl,因为监听事件咱们值加入一次,所以放外面
function popstate(e) {
set(window.location.pathname);
}
// 创建context
export const RouterContext = React.createContext(window.location.pathname);
export default function({ children }) {
const [url, setUrl] = useState(window.location.pathname);
set = setUrl;
window.addEventListener("popstate", popstate);
const router = {
history: {
push: function(url, state, title) {
window.history.pushState(state, title, url);
setUrl(url);
},
replace: function(url, state, title) {
window.history.replaceState(state, title, url);
setUrl(url);
},
// 下面也需要嵌入setUrl,暂不处理
go: window.history.go,
goBack: window.history.back,
goForward: window.history.forward,
length: window.history.length
},
url: url
};
return (
<RouterContext.Provider value={router}>{children}</RouterContext.Provider>
);
}
Route
import React, { useContext } from "react";
import { RouterContext } from "./HistoryRouter";
function Route({ component, path }) {
// 获取context
const { history, url } = useContext(RouterContext);
const match = {
path,
url
};
const Component = component;
return url === path && <Component history={history} match={match} />;
}
export default Route;
Link
import React, { useContext } from "react";
import { RouterContext } from "./BrowserRouter";
import styled from "styled-components";
const A = styled.a`
text-decoration: none;
padding: 5px;
`;
function Link({ children, to }) {
const { history } = useContext(RouterContext);
const onClick = e => {
e.preventDefault();
history.push(to);
};
return (
<A href={to} onClick={onClick}>
{children}
</A>
);
}
export default Link;
上一篇: MyBatis多参数 获取不到
下一篇: 小程序搜索布局