Vue-Router

109 阅读3分钟

目标

  • 针对 react / vue ,能够根据业务需求⼝喷 router 的关键配置,包括但不限于:路由的匹配规则、

路由守卫、路由分层等。

  • 能够描述清楚 history 的主要模式,知道 history 和 router 的边界;

知识要点

什么是 Router,以及 Router 发展的历史

在 SPA(即只有⼀个 html ) 的出现后,前端可以⾃由控制组件的渲染,来模拟⻚⾯的跳转。

⻚⾯是怎么发⽣跳转,向服务端请求的呢?-- 浏览器劫持。

在讲这部分内容前,我们先来说⼀下,hash 路由和 history 路由的区别

SPA的⽅法,需要拦截请求;

  • hash 路由,当我的hash
  • history 的 go / forward / back 的时候,我的浏览器的地址,是发⽣了改变的,

总结:

后端路由是根据 url 访问相关的 controller 进⾏数据资源和模板引擎的拼接,返回前端;

前端路由是通过 js 根据 url 返回对应的组件加载。

所以,前端的路由包含两个部分:

  • url 的处理
  • 组件加载

分类

history 路由

  • hash 路由
  • memory 路由 *

window.location.hash = "xxx"

history./(go|back|repalce|push|forward)/

路由守卫

触发流程

  1. 【组件】- 前⼀个组件 beforeRouteLeave

  2. 【全局】- router.beforeEach

  3. 【组件】-如果是路由的参数变化,触发 beforeRouteUpdate ;

  4. 【配置⽂件】⾥,下⼀个 beforeEnter

  5. 【组件】内部声明的 beforeRouteEnter

  6. 【全局】调⽤ beforeResolve

  7. 【全局】的 router.afterEach

简单路由的实现

history:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>H5 路由</title>
</head>
<body>
    <div id="container">
        <a href="./" >首页</a>
        <a href="./about">关于我们</a>
        <a href="./user">用户列表</a>
    </div>
    <div id="context"></div>
    <script>
        class BaseRouter {
            constructor() {
                this.routes = {};
                this._bindPopstate();
                this.init();
            }

            init(path) {
                window.history.replaceState({path}, null, path);
                const cb = this.routes[path];
                if(cb) {
                    cb();
                }
            }

            route(path, callback) {
                this.routes[path] = callback || function() {}
            }

            go(path) {
                window.history.pushState({path}, null, path);
                const cb = this.routes[path];
                if(cb) {
                    cb();
                }
            }

            _bindPopstate() {
                window.addEventListener('popstate', e => {
                    const path = e.state && e.state.path;
                    this.routes[path] && this.routes[path]();
                })
            }
        }

        const Route = new BaseRouter();

    Route.route('./about', () => changeText("关于我们页面"));
    Route.route('./user', () => changeText("用户列表页"));
    Route.route('./', () => changeText("首页"));

    function changeText(arg) {
        document.getElementById('context').innerHTML = arg;
    }

    container.addEventListener('click' , e => {
        if(e.target.tagName === 'A') {
            e.preventDefault();
            Route.go(e.target.getAttribute('href'))
        }
    })
    </script>
</body>
</html>

hash:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>hash 路由</title>
</head>
<body>
    <div id="container" >
        <button onclick="window.location.hash = '#'">首页</button>
        <button onclick="window.location.hash = '#about'">关于我们</button>
        <button onclick="window.location.hash = '#user'">用户列表</button>
    </div>

    <div id="context"></div>
    
</body>
<script>

    class BaseRouter {
        constructor() {
            this.routes = {};
            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 = `/${window.location.hash.slice(1) || ''}`;
            this.routes[path]();
        }      
    }

    const Route = new BaseRouter();

    Route.route('/about', () => changeText("关于我们页面"));
    Route.route('/user', () => changeText("用户列表页"));
    Route.route('/', () => changeText("首页"));

    function changeText(arg) {
        document.getElementById('context').innerHTML = arg;
    }

</script>
</html>

实现路由核心思想:

history定义:

  • 含当前的路径的状态
  • 次路径下的状态
  • 要实现路由监听,如果路径变化,需要通知⽤户
  1. createWebHistory()创建⼀个历史导航,创建⼀个对象,包含路径、状态、以及push/replace 切换的⽅法
  2. 跳转时,没有状态,所以我们需要传⼀些⾃⼰的状态forward, back, current, scroll.
  3. 实现⼀个 changeLocation 的⽅法,currentLocation 和 historyState ,相当于是路由中的 location 和 history
  4. 实现push,replace方法
  5. 实现历史监听historyListenner, 实际调用popstate, 设置监听callback
const history = createWebHistory()
webHistory.listen((to, from, {isBack}) => {
  console.log(to, from, {isBack})
})
//1 创建⼀个历史导航
function createWebHistory() {
 // 创建⼀个对象,包含路径、状态、以及push/replace 切换的⽅法
  const historyNavigation = useHistoryStateNavigation();
  // 构建⼀个监听函数,监听浏览器的前进和后退
  const {location, state} = historyNavigation;
  const historyListeners = useHistoryListeners(state, location);
  const routerHistory = Object.assign(
   {},
    historyNavigation,
    historyListeners
 )
  Object.defineProperty(routerHistory, 'location', {
    get: () => historyNavigation.location.value
 })
  Object.defineProperty(routerHistory, 'state', {
    get: () => historyNavigation.state.value
 })
  return routerHistory
}

function createCurrentLocation() {
  const {pathname, search, hash} = window.location;
  return pathname + search + hash; 
}

function useHistoryStateNavigation() {
 // const currentLocation = '/' // 更改的时候,尽量是⼀个引⽤类型。
  const currentLocation = {
    // 如何获取路径呢?⽤ window.location 的结果去拼接,我们再封装⼀个⽅法
 value: createCurrentLocation();
 }
   // 除了路径以外,还要拿浏览器的状态
  const historyState = {
 value: window.history.state
 }
 
  //2
  if(!historyState.value) {
    // 但是你这样改,是不会更新你的 historyState 的,没有同步到路由系统中。3
changeLocation(//3
   currentLocation.value,
      buildState(null, currentLocation.value, null, true),//2
      true
   )
    // 打印⼀下。
 }
  //3
  function changeLocation(to, state, replace) {
 window.history[replace?'replace':'pushState'](state, null, to);
    historyState.value = state;
 }
  //4
  function push(to, data) {
  // 去哪,带的新的状态是谁?
  
  // 跳转前:从哪⼉,去哪⼉
  // why ? 为了做路由守卫。
  const currentState = Object.assign(
   {},
    historyState.value,
    // 只需要改去哪⼉,和当前的滚动条的位置。
   {forward: to, scroll: {left: window.pageXOffset, top:
window.pageYOffset}}
 )
  // 本质是没有跳转的,只是,更新了状态,后续在 Vue 中,可以监控到详细的状态变化。
  // 所以这⾥是 replace 模式
  changeLocation(currentState.current, currentState, true);
  
  // 跳转后:从这⼉到哪⼉
  const state = Object.assign(
   {},
    buildState(currentLocation.value, to, null),
   {position: currentState.position+1},
    data,
 )
  
  // 这⾥要真正的跳转,所以是 push 模式
  changeLocation(to, state, false);
  currentLocation.value = to;
}
  //4
  function replace(to, data){
  // 创建⼀个状态,并进⾏合并
  // 只要替换掉 current 即可。
 const state = Object.assign(
   {},
    buildState(historyState.value.back, to, historyState.value.forward,
true),
    data
 )
   return {
 location: currentLocation,
    state: historyState,
    push,
    replace
 }
}

  
  //5
  function useHistoryListeners(historyState, currentLocation) {
  const popStateHandler = ({state}) => {
    const to = createCurrentLocation(); // 去哪
    const from = currentLocation.value; // 从哪⼉来
    const prevState = historyState.value;
    // 开始修改啦
    currentLocation.value = to;
    historyState.value = state;
    let isBack = state.position - prevState.position < 0;
    // ⽤户扩展的地⽅,就在这⾥,也就是连接 vue 组件和 history 之间的核⼼。
    // !!!!!!!!!!!!!!!!   !!!!!!!!!!!!!!!!!
     listeners.forEach(listener => {
       listener(to, from, {isBack})
     })
 }
  window.addEventListener('popstate', popStateHandler) 
    function listen(cb) {
     listeners.push(cb);
   }
   return {
     listen
   }
 }