单页面前端路由简单实现

144 阅读3分钟

前沿知识:

History 接口方法:
1.back():与用户当即浏览器的Back按钮行为相同;等价于 history.go(-1)。会触发页面改变
2.forward():与用户当即浏览器的Forward按钮行为相同;等价于 history.go(1)。会触发页面改变
3.go()会触发页面改变
4.pushState() : 按指定的名称和 URL(如果提供该参数)将数据 push 进会话历史栈;只是改变会话历史栈,不会触发页面改变。URL改变,但是不会加载新页面

  • 调用 pushState() 和 window.location = "#foo"基本上一样,他们都会在当前的 document 中创建和激活一个新的历史记录;
  • 不会触发 hashchange 事件,即使新的 URL 与旧的 URL 仅哈希不同也是如此。
    5.replaceState(): 按指定的数据、名称和 URL(如果提供该参数),更新 history 栈上最新的条目。只是改变会话历史栈,不会触发页面改变。URL改变,但是不会加载新页面。

Window事件

  1. onpopState(): 调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,页面改变时才会触发。比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。
  2. onhashchange: hash值改变时触发,不会触发页面改变

hash模式

即改变url中的hash值部分,hash改变的时候会触发onhashchange事件。改变的方式有:

  1. js编程方式:window.location.hash修改
  2. a标签的href挂上hash值,通过a标签跳转
  3. 浏览器前进、后退按钮

我们要实现hash模式页面渲染,那么只要实现这三种方式都可以渲染页面即可,正好三种方式都可以触发onhashchange事件,因此我们只要在onhashchange事件中实现渲染页面,就可以实现hash模式的路由了。 接下来我们通过代码的方式将三种方式实现,其中js编程方式通过button按钮绑定点击事件来实现。

<!DOCTYPE html>
<html lang="en">
<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模式实现router</title>
    <style>
        html, body {
            height: 100%;
        }
    </style>
</head>
<body>
    <div>
        <div class="nav">
            <ul>
                <li><a href="#/">page1</a></li>
                <li><a href="#/page2">page2</a></li>
                <li><a href="#/page3">page3</a></li>
            </ul>
            <button onclick="go('#/')">page1</button>
            <button onclick="go('#/page2')">page2</button>
            <button onclick="go('#/page3')">page3</button>
        </div>       
        <div id="router-view"></div>
    </div>
    
</body>
<script type="text/javascript">
    // button是通过编程方式改变hash
    function go(path) {
        window.location.hash = path
    }

    class Router {
        constructor(routes = []) {
            this.routes = routes
            // 页面第一次加载不会触发hashchange事件,手动渲染页面
            window.addEventListener('load', this.render.bind(this), false)
            window.addEventListener('hashchange', this.render.bind(this), false)
        }

        // 获取hash值
        getHash() {
            const hash = window.location.hash
            return hash ? hash.slice(1) : '/'
        }
        // 渲染对应页面
        render() {
            let curPath = this.getHash()
            curPath = curPath.includes('/') ? curPath : '/'+curPath
            let curRoute = this.routes.find(route => route.path === curPath)
            if(!curRoute) {
                curRoute = this.routes.find(route => route.path === '/')
            }
            const view = document.getElementById('router-view')
            view.innerHTML = curRoute.component
        }
    }
    const routes = new Router([
        {
            path: '/',
            name: 'page1',
            component: `<div>page1</div>`
        },
        {
            path: '/page2',
            name: 'page2',
            component: `<div>page2</div>`
        },
        {
            path: '/page3',
            name: 'page3',
            component: `<div>page3</div>`
        }
    ])
</script>
</html>

Tips:
在Router的构造器中通过addEventListener监听load和hashchange事件。由于是绑定到window上的,但是绑定的函数都是Router类中的,故需要通过bind绑定this,防止在window上找不到render方法。

history模式

修改url的方式:

  1. js编程方式:histroy.pushState修改
  2. a标签跳转 => 拦截默认跳转,防止页面刷新,使用histroy.pushState修改url
  3. 浏览器前进、后退按钮 => 触发onpopstate事件

接下来我们通过代码将以上三种方式实现,其中js编程方式是通过button按钮绑定点击事件,a标签方式需要拦截默认跳转,因为:
histroy模式的 URL没有 # 分隔符。我们知道,改变url中的hash部分不会刷新页面,也不会向后台发送数据。但是改变url非hash值部分会自动刷新页面。故,当我们通过a标签改变URL的时候,需要拦截浏览器跳转行为。Histroy API正好提供了pushState方法,可以改变url还不刷新页面,类似于修改hash的形式。因此,histroy模式我们是借用pushState方法来完成的。
总结:histroy.pushState只可以改变路由,但是并无法刷新页面,我们利用的就是histroy.pushState改变路由却不刷新页面的方式来实现histroy模式的路由,渲染页面的操作是我们手动完成的。

因此,我们最终需要通过两种方式渲染histroy模式的路由:

  1. histroy.pushState改变路由,然后渲染页面。
  2. onpopstate事件中渲染页面
<!DOCTYPE html>
<html lang="en">
  <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>histroy模式</title>
  </head>
  <body>
    <ul>
      <li><a href="./">page1</a></li>
      <li><a href="./page2">page2</a></li>
      <li><a href="./page3">page3</a></li>
    </ul>
    <button onclick="go('page1')">page1</button>
    <button onclick="go('page2')">page2</button>
    <button onclick="go('page3')">page3</button>
    <div id="router-view"></div>
  </body>

  <script>
    class Router {
      constructor(routes) {
        this.routes = routes
        window.addEventListener('load', this.load.bind(this))
        window.addEventListener('hashchange', this.render.bind(this))
      }

      getPath() {
        const path = window.location.href.split('/')
        return path ? path.slice(-1) : '/'
      }

      go(path) {
        history.pushState(null, '', path)
        this.render()
      }

      load() {
        const aList = document.querySelectorAll('a[href]')
        aList.forEach((aNode) =>
          aNode.addEventListener('click', (e) => {
            e.preventDefault()
            // 拦截a标签点击事件,手动通过pushState的方式改变url,防止页面刷新,并手动渲染
            const href = aNode.getAttribute('href')
            history.pushState(null, '', href)
            this.render()
          })
        )
        this.render()
      }

      render() {
        const curPath = this.getPath()
        let curRoute = this.routes.find((route) => route.path === '/' + curPath)
        if (!curRoute) {
          curRoute = this.routes.find((route) => route.path === '/')
        }
        const view = document.getElementById('router-view')
        view.innerHTML = curRoute.component
      }
    }

    const routes = new Router([
      {
        path: '/',
        name: 'page1',
        component: `<div>page1</div>`,
      },
      {
        path: '/page2',
        name: 'page2',
        component: `<div>page2</div>`,
      },
      {
        path: '/page3',
        name: 'page3',
        component: `<div>page3</div>`,
      },
    ])

    // 编程方式改变路由
    function go(path) {
      routes.go(path)
    }
  </script>
</html>

Tips:
histroy.pushState方法不能在静态文件中使用,需要通过web服务启动使用,比如使用Live Server启动本地文件。