前端路由的实现原理解析

本文提供三种方式实现前端路由,分别是原生JS路由实现,Vue路由实现,React路由实现

什么是前端路由?

首先我们先来了解一下路由是什么?
路由是用来跟后端服务器进行交互的一种方式,根据不同的url地址展现不同的内容或页面。
在web前端单页面应用(SPA)中,路由描述的是URL与视图之间的映射关系,这种映射是单向的,即URL变化引起视图更新,且不需要刷新页面。

如何实现前端路由?

要实现前端路由,需要解决2个核心:
(1)如何检测URL变化?
(2)如何改变URL却不引起页面刷新?

解决上述两个问题,分别使用hash和history两种实现方式就可以解决:

hash实现
  1. hash是URL中hash(#)以及后面的部分,常用作锚点在页面内进行导航,改变URL中的hash部分不会引起页面刷新。
  2. 通过hashchange事件监听URL的变化,改变URL的方式只有几种:通过浏览器前进后退改变URL、通过a标签改变URL、通过window.location 改变URL,这几种情况改变URL都会触发hashchange事件。
history实现
  1. history 提供了pushState 和replaceState两个方法,这两个方法改变URL的path部分不会引起页面的刷新。
  2. 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()是在当前页面创建并激活新的历史记录,即URL地址改变,不会造成页面刷新。
他的三个参数:
state: 是一个js对象,与要跳转到的URL对应的状态信息
title:firefox现在已经忽略这个参数,虽然它可能将来会被用到。目前最安全的使用方式是传一个空字符串,以防止将来的修改。
url:要跳转到的url地址,但是不能跨域。注意在调用pushState方法后浏览器不会加载这个URL,但也许会过一会尝试加载URL。

history.replaceState(state,title,url)

概念:replaceState()是修改当前历史记录,url地址改变,不会造成页面刷新。与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);
  //路由视图
  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;

发布了23 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/diwang_718/article/details/105433594
今日推荐