vue-router路由的原理实现及路由守卫

516 阅读4分钟

背景

前后端开发未分离的时候,路由基本上都是由服务端控制。
前端/客户端 发起HTTP请求 -> 服务端 -> url 路由去匹配不同的路由 / 返回不同的数据(Restful接口)

优点: ssr, 直接返回了一个html, 渲染了页面结构, seo效果好,首屏渲染耗时短。
缺点: 前端代码和服务端代码融合在一起,开发协同很乱,服务端压力大,构建html端过程放在服务器上。

单页面应用

SPA单页面应用。
页->html.
单页 -> 只有一个html文件
特点:
1、页面中的交互式不刷新页面的, 
2、加载过的公共资源,无重复加载。除了首页之外的其他页面,都是通过异步组件的方式注册。

前端路由router 原理及其表现。

vue -> hash, history
react -> hash, history
    1、页面间的交互不会刷新页面。
    2、不同 url/路径/路由 会渲染不同的内容。
 Hash 和 History的区别?
     1、hash#, history没有
     2、hash#部分内容不会传给服务端, history的所有url内容服务端都可以获取到。
     3、history路由, 应用在部署的时候,需要注意html文件的访问。
     4、hash 通过 hashchange监听变化, history通过popstate监听变化。
     

hash特性

    1、url中带有一个#,#只是浏览器/客户端端状态,不会传递给服务端
    2、hash值的改变不会导致页面刷新
    3、hash值的更改,会在浏览器访问历史中添加一条记录。可以通过浏览器的返回、前进按钮来控制hash的切换。
    4、hash值的改变,触发hashchange事件
    window.addEventListener('hashchange', ()=>{});
    5、如何更改hash
        1、location.hash = '#aaa';
        2、<a href="#user">点击跳转到user</a>
        

History 特性

   hash有个#符合,不美观,服务端无法接受到hash部分。
   window.history.back();
   window.history.forward();
   window.history.go(-3);
   window.history.pushState();
   window.history.replaceState();

pushState / replaceState 的参数

   window.history.pushState(null, 'new', path);
   1、state, 是一个对象,是一个与指定网址相关的对象
   2、title, 新页面的地址
   3、url, 页面新的地址
   

面试题

   1、pushState会触发popState事件吗
   pushState/replaceState 都不会,需要收到触发页面的重新渲染。
   2、什么情况下会触发popState事件
       1、点击浏览器的后退按钮
       2、点击浏览器的前端按钮 
       3、js back
       4、js forward
       5、js go

nginx配置

   1、index.html存在服务器本地
   www.sb.com/a/
   www.sb.com/b/
   ```nginx
       try_files $url $rul/ /home/dist/index.html;
   ```
   2、index.html 存在于远程地址。oss/cdn
   nginx 配置在a服务器,Index.html被传到了cdn上。
   www.sb.com/main/a/
   www.sb-cdn.com/file/index.html
   ```nginx
       location /main/{
           rewrite^ /file/index.html break;
           proxy_pass https://www.sb-cdn.com;
       }
   ```

hash路由实现

  // index.html
  <body>
    <div id="container">
        <a href="#white">白色</a>
        <a href="#green">绿色</a>
        <a href="#gray">灰色</a>
        <button onclick="window.history.go(-1)">按钮</button>
        <script src="./index.js" ></script>
    </div>
</body>
   // index.js
   class HashRouter{
    /** 只是做了一层存储 */
    route(path, cb){
        this.routes[path] = cb || function() {};
    }
    /**
     * 渲染当前路径对应的操作
     */
    refresh(){
        const path = `/${location.hash.slice(1) || ''}`
        this.routes[path]();
    }

    constructor(){
        this.routes = {};
        this.refresh = this.refresh.bind(this);
        window.addEventListener('load', this.refresh);
        window.addEventListener('hashchange', this.refresh);
    }
}

const body = document.querySelector("body");
function changeBgColor(color){
    body.style.backgroundColor = color;
}

const Router = new HashRouter();
Router.route('/white', function(){
    changeBgColor('white');
})
Router.route('/green', function(){
    changeBgColor('green');
})
Router.route('/gray', function(){
    changeBgColor('gray');
})

history路由实现

 // index.html
 <body>
    <div class="container">
        <a href="/green">绿色</a>
        <a href="/gray">灰色</a>
        <a href="/">白色</a>
        <button onclick="window.history.go(-1)">按钮</button>
        <script src="./index.js" ></script>
    </div>
</body>
// index.js
class HistoryRouter{
    constructor(){
        this.routes = {};
        this.bindPopState();
        this.init(location.pathname);
    }
    init(path){
        window.history.replaceState({path}, null, path);
        this.invockPathCallback(path);
    }

    invockPathCallback(path){
        const cb = this.routes[path];
        cb && cb();
    }
    route(path, callback){
        this.routes[path] = callback || function(){};
        this.invockPathCallback(path);
    }

    go(path){ console.log(path);
        window.history.pushState({  path }, null, path);
        this.invockPathCallback(path);
    }

    bindPopState(){
        window.addEventListener('popstate', function(e){
            const path = e.target && e.target.path;
            this.invockPathCallback(path);
        })
    }
}

const body = document.querySelector("body");
function changeBgColor(color){
    body.style.backgroundColor = color;
}

const Router = new HistoryRouter();
Router.route('/', function(){
    changeBgColor('white');
})
Router.route('/green', function(){
    changeBgColor('green');
})
Router.route('/gray', function(){
    changeBgColor('gray');
})
const container = document.querySelector('.container');
container.addEventListener('click', function(e){
    if(e.target.tagName === 'A'){
        e.preventDefault();
        Router.go(e.target.getAttribute('href'));
    }
})

全局导航守卫

1、router.beforeEach(to, from, next)
2、router.beforeResolve()(会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用)
3、router.afterEach(to,from)

路由独享守

router.beforeEnter(只在进入路由时触发,不会在 paramsquery 或 hash 改变时触发)

组件内守卫

export default { 
    beforeRouteEnter(to, from) { 
        // 在渲染该组件的对应路由被验证前调用 
        // 不能获取组件实例 `this` !
        // 因为当守卫执行时,组件实例还没被创建! 
    },
    beforeRouteUpdate(to, from) {
        // 在当前路由改变,但是该组件被复用时调用
        // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
        // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 
        // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this` 
    }, beforeRouteLeave(to, from) { 
        // 在导航离开渲染该组件的对应路由时调用 
        // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this` 
    }, 
}

导航守卫被触发的过程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。