欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

前端路由的实现原理解析

程序员文章站 2022-03-25 13:49:06
...

本文提供三种方式实现前端路由,分别是原生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()是在当前页面创建并**新的历史记录,不会造成页面刷新。
他的三个参数:
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;

相关标签: JS