[深入11] 前端路由

导航

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[react] Hooks

前置知识

URL 和 URI

  • URI:统一资源标识符 ( I => Identifier:标识符Id)
    (Universal Resource Identifier
  • URL:统一资源定位符 ( L => Locator:定位器)
    Uniform Resource Locator
  • 区别:
    • URL:强调的是地址,即( 定位 )这个唯一的资源
    • URI:强调的是( 标识 )资源,资源具有的( 唯一性 )
    • 分别标识唯一资源和标识唯一地址很麻烦,所以用URL也充当RUI的角色,即标记唯一资源还标记该资源的唯一地址

URL的组成

http://www.baidu.com:80/stu/index.html?name=xxx&age=25#teacher

  • Protocol:协议 http://https://
  • Domain:域名 www.baicu.com
  • Port:端口 :80
    • http协议的默认地址是 :80
    • https自已的默认地址是:430
  • Path:文件路径, => /开始 ?之前的部分, 本例中是:/stu/index.html
  • Query:查询字符串 => ?开头到结尾,或者?开头到#之前,本例是:?name=xxx&age=25
  • Hash:哈希值 => #开头到结尾,本例是:teacher
  • protocol,domain,port,path,query,hash

DOMContentLoaded 事件,load事件

  • window.onload window.addEventListener('load', ....)
  • DOMContentLoaded
  • 区别:
    • DOMContentLoaded:DOM加载完成时触发
    • load:需要DOM,样式,脚本,图片,视频等所有资源都加载完成时才会触发,即页面加载完成才触发
DOM完整解析过程:
1. 解析html
2. 解析css - 包括当html中的样式和外部引入的样式
3. 解析并运行脚本 - 报错本html中的脚本和外部引入的脚本
4. DOM构建完成 ---------------------------------------------------- DOM加载完成,触发 DOMContentLoaded
5. 加载图片,视频等其他资源
6. 页面加载完毕 --------------------------------------------------- 页面加载完成,触发 load

字符串 slice 方法特例

  • slice可用户数组和字符串
  • 有返回值,不改变原字符串
  • String.prototype.slice(开始位置,结束位置) 截取字符串,不包括结束位置
String.prototype.slice()
特例:
''.slice(1) ----------- 返回 '' 空字符串
案例:
window.location.hash 
// 因为:当地址栏的url中的hash不存在时,window.location.hash返回的是空字符串,这种情况如下
// 所以:window.location.hash.slice(1) => 返回空字符串

window.location 对象

window.location 对象
属性:
pathname: 返回url的path部分,/开始 ?之前 或者 /开始到结果,如果没有query和hash
origin:protocal + hostname + port 三者之和,相当于协议,域名,端口
protocal:协议 http:// https://
hostnme: 主机名
port:端口号
host:主机 (hostname + port)
search:查询字符串 (?开头到#之前,或者?开头到结尾)
hash:片段字符串 (哈希值,#开头到结尾)

hash路由

  • url中的hash以#号开头,原本用来作为锚点,从而定位到页面的特定区域
  • 当 hash 发生改变时,页面不会刷新,浏览器也不会向服务器发送请求
  • 注意:hash改变时,可以触发 hashchange 事件,在监听函数中可以请求数据,实现页面的更新操作

作为锚点,定位页面特点区域

<a href="#anchor1">锚点1</a>
<a href="#anchor2">锚点2</a>
<div id="anchor1">锚点1的位置</div>
<div id="anchor2">锚点2的位置</div>
说明:
- 点击a2,页面会跳转到div2的位置
- 并且页面的hash部分也会改变,即 url 中以 #开头的字符串会改变
- anchor:是锚的意思
- 注意:a标签的name属性已经废弃,用id代替 (因为有的教程使用name属性实现的)

hashchange事件

  • 如果监听了hashchange事件,hash改变,地址栏的url中的hash部分就会改变,同时hashchange也会触发
  • 但是页面不会刷新,即浏览器的刷新按钮的圈圈不会转动
  • 但是可以利用hashchange的回调函数更新页面的内容,注意不是页面刷新
<body>
  <a href="#anchor1">锚点1</a>
  <a href="#anchor2">锚点2</a>
  <script>
    window.addEventListener('hashchange', function() {
      console.log('111111111')
    }, false)
  </script>
</body>
说明:
- 点击a标签,url中的hash改变,hash改变,hashchange事件触发,则监听函数就会执行,输出111111

手动实现一个 hash-router

hash-router
原理:
(1) hash改变,地址栏url的hash字符串改变,触发hashchange事件
(2) 在hashchange事件的回调函数中更新视图
代码:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
<a href="#/home">home</a>
<a href="#/other">other</a>
<div id="content">内容部分</div>
<script>
  const routes = [{
    path: '/home',
    component: '<h1>home页面</h1>'
  }, {
    path: '/other',
    component: '<h1>other页面</h1>'
  }]
  class Router {
    constructor(routes) { // 构造函数
      this.route = {} // 路由映射
      this.createRouteMap(routes) // 创建路由映射
      this.init() // 初始化
    }
    createRouteMap = (routes) => {
      routes.forEach(item => {
        this.route[item.path] = () => {
          document.getElementById('content').innerHTML = item.component
          // 函数体的作用:将id是content的div中的内容换成 componet
        }
        // 循环配置
        // key是path
        // value包装成一个更新html内容的函数,在 load 和 hahschange 中调用
      })
    }
    init = () => {
      window.addEventListener('load', this.updateView, false) // 页面加载完成时触发,注意区分DOMContentLoaded
      window.addEventListener('hashchange', this.updateView, false)
    }
    updateView = () => {
      const hash = window.location.hash.slice(1) || '/home'; // 初次加载home页面
      // load事件触发时,window.location.hash => 返回 '' 空字符串
      // ''.slice(1) => 返回''
      if (this.route[hash]) this.route[hash]()
      // 存在,则执行函数
    }
  }
  new Router(routes)
</script>
</body>
</html>
注意:该html需要用 Live Server 启动,vscode插件

history路由

window.history 对象

  • window.history对象的方法:back()forward()go()pushState()replaceState()
  • pushState()
  • replaceState()
  • pushState() 和 replaceState()
    • 不会触发页面刷新,只能导致History对象发生变化,地址栏的url会变化
    • 会改变url,不会触发 popstate 事件,地址栏的url有所变化

window.history.pushState(state, title, url)

  • window.history.pushState(state, title, url)
  • state:一个与添加的记录相关联的对象
  • title:新页面的标题,现在所有浏览器都忽略该参数,可以传入空字符串
  • url:新的url地址,必须与当前页面同一个域,浏览器的地址栏显示这个网址
  • window.history.pushState({}, null, url)

popstate

  • popstate触发的条件
    • 浏览器的前进后退按钮
    • history.go(), history.back(), history.forward()
  • 注意:window.history.pushState() 和 window.history.replaceState()不会触发 popstate 事件
  • 注意:pushState()和replaceState()可以改变url,且实现不向服务器发送请求,不存在#号,比hash路由更美观,但是 History 路由需要服务器的支持,并且需将所有的路由重定向到根页面

手动实现一个 history-router

history-router
原理:
(1) 封装一个方法,在pushState()和replaceState()改变url后调用,在该方法中获取最新的window.location.path,更相信页面
(2) 通过 go() back() forward() 浏览器前进后退等触发 popstate 事件
代码:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <a href="javascript:void(0)" data-href="/home">home</a> // html中的自定义属性
  <a href="javascript:void(0)" data-href="/other">other</a>
  <div id="content">content的内容</div>
  <script>
    const routes = [{
      path: '/home',
      component: '<h1>home页面</h1>'
    }, {
      path: '/other',
      component: '<h1>other页面</h1>'
    }]
    class Router {
      constructor(routes) {
        this.route = {} // key-value键值对,key是path,value是更新视图的函数
        this.createRouteMap(routes) // 创建路由映射
        this.bindEvent() // 绑定a标签的点击事件
        this.init() // 绑定load和popstate事件
      }
      createRouteMap = (routes) => {
        routes.forEach(item => {
          this.route[item.path] = () => {
            document.getElementById('content').innerHTML = item.component
          }
        })
      }
      bindEvent = () => {
        const a = document.getElementsByTagName('a')
        Array.prototype.forEach.call(a, item => { // 第二个参数,是forEach需要传入的回调函数
          item.addEventListener('click', () => {
            const path = item.getAttribute('data-href') // 获取data-herf属性
            this.pushStateFn(path) 
            // 执行History.pushState()方法
            // 这里由于是箭头函数,this指向父级所在的上下文环境,即 bindEvent 所在的上下文环境,即Router 
          }, false)
        })
      }
      pushStateFn = (url) => {
        window.history.pushState({}, null, url) // 改变url后,调用更新视图的函数updateView
        this.updateView() // 更新视图
      }
      init = () => {
        window.addEventListener('load', this.updateView, false) // 页面加载完成时触发
        window.addEventListener('popstate', this.updateView, false) // 浏览器前进后退,History.go() back() forward()时触发
      }
      updateView = () => {
        const path = window.location.pathname || '/'; // 获取url的path部分
        if(this.route[path]) this.route[path]() // path在route中存在,就执行对象的函数,key-value键值对
      }
    }
    new Router(routes)
  </script>
</body>
</html>

手动实现一个vue-router(hash版)

手动实现一个vue-router(hash版)
vue相关前置知识
- <router-link to="#/home">home</router-link> // 点击会跳转到 '#/home' 地址
- <router-view></router-view> // 路由将显示的DOM位置
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
- 因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项
- 例如 data、computed、watch、methods 以及生命周期钩子等。
---------------
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <!-- 引入 Vue 通过CDN引入 -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <!-- 注意 router-link组件具有 to 属性 -->
    <router-link to="#/home">home</router-link>
    <router-link to="#/other">other</router-link>
    <router-view></router-view>
  </div>
  <script>
    // 创建两个vue组件
    const Home = {template: '<h1>home页面</h1>'}
    const Other = {template: '<h1>other页面</h1>'}
    // 创建vue路由数组
    const routes = [{
      path: '/home',
      component: Home
    }, {
      path: '/other',
      component: Other
    }]
    class VueRouter {
      constructor(Vue, option) {
        // 参数:
        // Vue:Vue构造函数,通过cdn引入的
        // option: 配置对象,包含routes路由数组属性
        this.$options = option
        this.routeMap = {} // 路由映射,是这样的结构 { path: component }
        this.createRouteMap(this.$options) // 创建路由映射
        this.app = new Vue({
          data: {
            currentHash: '#/'
          }
        })
        // this.app.currentHash => 可以访问到currentHash的值 '#/'
        // 举例
        // var data = {name: 'woow_wu7'}
        // var vm = new Vue({
        //  data: data
        // })
        // vm.name === data.name => true
        this.init() // 初始化监听函数
        this.initComponent(Vue) // 初始化Vue种的各种组件
      }
      createRouteMap = (option) => {
        // 注意:option 是传入VueRoute的第二个参数,即 {routes: routes}
        // 所以:options是一个对象
        option.routes.forEach(item => {
          this.routeMap[item.path] = item.component
          // this.routeMap是这样一个对象:{path: component}
        })
      }
      init = () => {
        window.addEventListener('load', this.onHashChange, false)
        // 页面加载完成触发,注意区别 DOMContentLoaded
        // load:页面加载完成时触发,包括 DOM加载完成,图片,视频等所有资源加载完成
        // DOMContentLoaded:DOM加载完成时触发
        window.addEventListener('hashchange', this.onHashChange, false)
        // 监听 hashchange 事件
        // 触发hashchange的条件:hash改变时候
      }
      onHashChange = () => {
        this.app.currentHash = window.location.hash.slice(1) || '/'
        // (1)
        // 当 hahs没有改变时,load事件触发时
        // window.location.hash = '' =>  window.location.hash.slice(1) = ''
        // 所以:此种情况:this.app.currentHash =  '/'
        // (2)
        // hash改变时,window.location.hash有值,是 '#/...' 这样的字符串
      }
      initComponent = (Vue) => {
        // router-link组件
        // props to属性
        // template 本质上会被处理成a标签,href属性是传入的 to 属性,内容是 slot 插入的内容
        Vue.component('router-link', {
          props: {
            to: {
              type: String,
              value: ''
            }
          },
          template: '<a :href="to"><slot/></a>'
        })
        Vue.component('router-view', {
          render: (h) => {
            const component = this.routeMap[this.app.currentHash] // 拿到最新hash对应的组件
            return h(component)
            // h(component) 相当于 createElement(component)
            // render: function(createElement) { return createElement(App); }
          }
        })
      }
    }
    new VueRouter(Vue, {
      routes
    })
    new Vue({
      el: '#app'
    })
  </script>
</body>
</html>

资料

URI和URL:https://www.luyuqiang.com/uri-url-urn-urc-and-data-uri
URI和URL的区别举例(很形象)https://juejin.im/post/5cd4e444e51d456e266d89fa
URL的组成(优秀)https://www.jianshu.com/p/406d19dfabd3
DOMContentLoaded和load的区别:https://www.jianshu.com/p/1a8a7e698447
window.location对象:https://wangdoc.com/javascript/bom/location.html
vue-router模拟实现 https://juejin.im/post/5b35dcb5f265da59a117344d
hash history 路由 模拟实现 https://juejin.im/post/5b330142e51d4558b10a9cc5
vue-router源码记录 https://juejin.im/post/5cf9f75ef265da1bbf690ec7
VueRouter源码分析 https://juejin.im/post/5cb2c1656fb9a0688360fb2c

https://juejin.im/post/5e339b8b51882526316a6ec7

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!
0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论