[深刻11] 前端路由

导航

[深刻01] 执行上下文
[深刻02] 原型链
[深刻03] 继承
[深刻04] 事件循环
[深刻05] 柯里化 偏函数 函数记忆
[深刻06] 隐式转换 和 运算符
[深刻07] 浏览器缓存机制(http缓存机制)
[深刻08] 前端安全
[深刻09] 深浅拷贝
[深刻10] Debounce Throttle
[深刻11] 前端路由
[深刻12] 前端模块化
[深刻13] 观察者模式 发布订阅模式 双向数据绑定
[深刻14] canvas
[深刻15] webSocket
[深刻16] webpack
[深刻17] http 和 https
[深刻18] CSS-interview
[react] Hooksjavascript

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CIcss

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程html

前置知识

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自已的默认地址是:443
  • 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)
  • 注意:pushState不会刷新页面,只会改变History对象,地址栏url会变化
    • 能够经过 History.state 读取状态对象

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:www.luyuqiang.com/uri-url-urn…
URI和URL的区别举例(很形象)juejin.im/post/5cd4e4…
URL的组成(优秀)www.jianshu.com/p/406d19dfa…
DOMContentLoaded和load的区别:www.jianshu.com/p/1a8a7e698…
window.location对象:wangdoc.com/javascript/…
vue-router模拟实现 juejin.im/post/5b35dc…
hash history 路由 模拟实现 juejin.im/post/5b3301…
vue-router源码记录 juejin.im/post/5cf9f7…
VueRouter源码分析 juejin.im/post/5cb2c1…vue

相关文章
相关标签/搜索