[深入11] 前端路由

image

导航

[深入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
[深入19] 手写Promise
[深入20] 手写函数

[react] Hooks

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

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程

前置知识

URL 和 URI

  • <font color=red>URI:统一资源标识符 ( I => Identifier:标识符Id)</font>
    (Universal Resource Identifier
  • <font color=red>URL:统一资源定位符 ( L => Locator:定位器)</font>
    Uniform Resource Locator
  • 区别:
    • URL:强调的是地址,即( <font color=red>定位</font> )这个唯一的资源
    • URI:强调的是( <font color=red>标识</font> )资源,资源具有的( <font color=red>唯一性</font> )
    • 分别标识唯一资源和标识唯一地址很麻烦,所以用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
  • <font color=red>protocol,domain,port,path,query,hash</font>

DOMContentLoaded 事件,load事件

  • window.onload window.addEventListener('load', ....)
  • DOMContentLoaded
  • 区别:
    • <font color=red>DOMContentLoaded:DOM加载完成时触发</font>
    • <font color=red>load:需要DOM,样式,脚本,图片,视频等所有资源都加载完成时才会触发,即页面加载完成才触发</font>
DOM完整解析过程:

1. 解析html
2. 解析css - 包括当html中的样式和外部引入的样式
3. 解析并运行脚本 - 报错本html中的脚本和外部引入的脚本
4. DOM构建完成 ---------------------------------------------------- DOM加载完成,触发 DOMContentLoaded
5. 加载图片,视频等其他资源
6. 页面加载完毕 --------------------------------------------------- 页面加载完成,触发 load

---
// 2021/07/25更新 - 上面的过程不完善,这里再简单记录下
// - 详见https://juejin.cn/post/6983570939342487565
1. parse-html
2. parse-styleSheet
3. evaluate-script
4. layout
5. paint
6. composite

字符串 slice 方法特例

  • slice可用于 ( 数组 ) 和 ( 字符串 )
  • 有返回值,不改变原字符串
  • <font color=red>String.prototype.slice(开始位置,结束位置) 截取字符串,不包括结束位置</font>
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:片段字符串 (哈希值,#开头到结尾)

返回值:
- window.location返回的是一个只读的Location对象,你仍然可以赋给它一个 DOMString
- 这意味着您可以在大多数情况下处理 location,就像它是一个字符串一样
- window.location = 'http://www.example.com',是 window.location.href = 'http://www.example.com'的同义词

问题:
- window.location.assign() 和 window.location.href 的区别?
- location.assign(url)是函数式的方式
- window.location.href=url 比 location.assign(url) 更快

hash路由

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

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

<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事件

  • <font color=red>如果监听了hashchange事件,hash改变,地址栏的url中的hash部分就会改变,同时hashchange也会触发</font>
  • 但是页面不会刷新,即浏览器的刷新按钮的圈圈不会转动
  • 但是可以利用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>

2020/12/24复习 - hash路由

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <a href="#/home">home页面</a>
  <a href="#/other">other页面</a>
  <div id="content">内容部分,即路由替换的部分</div>
  <div id="current-hash"></div>
  <script>
    const routes = [{
      path: '/home',
      component: 'home页面的内容'
    }, {
      path: '/other',
      component: 'other页面的内容'
    }]

    class HashRouter {
      constructor(routes) {
        this.route = {} // 路由映射
        this.createRouteMap(routes) // 创建路由映射,为 ( this.route ) 创建 ( map ) 映射;key=path;value=()=>{更新页面}
        this.init() // 初始化
      }
      createRouteMap = (routes) => {
        if (routes.length) {
          routes.forEach(({ path, component }) => {
            this.route[path] = () => {
              document.getElementById('content').innerHTML = component // 替换内容
            }
          })
        }
      }
      createRouteMap = (routes) => {
        if (routes.length) {
          routes.forEach(({ path, component }) => {
            this.route[path] = () => {
              document.getElementById('content').innerHTML = component // 替换内容
            }
          })
        }
      }
      init = () => {
        window.addEventListener('load', this.updateView, false)
        window.addEventListener('hashchange', this.updateView, false)
      }
      // 更新视图
      updateView = () => {
        // (1)
        // 这里 ( load事件 ) 和 ( hashchange事件 ) 都会触发 ( updateView方法 )
        // (2)
        // load事件: ( 页面加载完成时触发 ),包括 ( DOM,样式,图片,视频等所有资源都加载完成 )
        // DOMContentLoaded事件: 是在 ( DOM加载完成时触发 ) 
        // (3)
        // 当load事件触发时,hash并没有改变,即 window.location.hash = '' => ''.slice(1) => ''
        const hash = this.getCurrentHash() // 获取hash
        // if (Object.keys(this.route).includes(hash)) { // 还有更简单的方法
        //   this.route[hash]()
        // }
        if (this.route[hash]) this.route[hash]() // 如果this.route对象中的key对应得值存在,就执行该函数
      }
      // 获取当前地址栏的 hash
      getCurrentHash = () => {
        const hash = window.location.hash.slice(1)
        this.printHahToHtml(hash) // 该函数是用来在html中显示当前hash的
        return hash ? hash : '/home'
        // load事件触发时,hash就不存在,hash='',这种情况下即默认情况下返回 '/home' 路由
        // load事件触发时,window.location.hash => 返回 '' 空字符串
        // ''.slice(1) => 返回''

      }
      printHahToHtml = (hash) => {
        const DOM = document.getElementById('current-hash')
        DOM.innerHTML = `当前页面的hash是:=> #${hash}`
        DOM.style.setProperty('background', 'yellow')
        DOM.style.setProperty('padding', '10px')
      }
    }

    new HashRouter(routes)
  </script>
</body>
</html>

2021/07/25优化 - hash路由

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <a href="#/home">home页面</a>
    <a href="#/other">other页面</a>
    <div id="content">内容部分,即路由替换的部分</div>
    <div id="current-hash"></div>
    <script>
      const routes = [
        {
          path: "/home",
          component: "home页面的内容",
        },
        {
          path: "/other",
          component: "other页面的内容",
        },
      ];

      class HashRouter {
        constructor(routes) {
          this.route = {};
          this.createRouteMap(routes);
          this.init();
        }

        init = () => {
          window.addEventListener("load", this.updateView, false);
          window.addEventListener("hashchange", this.updateView, false);
          this.DOM_CURRENT_HASH = document.getElementById("current-hash");
          this.DOM_CONTENT = document.getElementById("content");
        };

        // createRouteMap = (routes) => {
        //   if (routes.length) {
        //     routes.forEach(({ path, component }) => {
        //       this.route[path] = () => {
        //         document.getElementById("content").innerHTML = component; // 替换内容
        //       };
        //     });
        //   }
        // };

        createRouteMap = (routes = []) => {
          routes.forEach(
            ({ path, component }) =>
              (this.route[path] = () =>
                (this.DOM_CONTENT.innerHTML = component))
          );
        };

        // updateView = () => {
        //   const hash = this.getCurrentHash(); // 获取hash
        //   if (this.route[hash]) this.route[hash](); // 如果this.route对象中的key对应得值存在,就执行该函数
        // };

        updateView = () => {
          const path = this.getCurrentHash();
          this.route?.[path]?.();
        };

        // getCurrentHash = () => {
        //   const hash = window.location.hash.slice(1);
        //   this.printHahToHtml(hash); // 该函数是用来在html中显示当前hash的
        //   return hash ? hash : "/home";
        // };

        getCurrentHash = () => {
          const path = window.location.hash.slice(1) || "/home";
          this.printHahToHtml(path); // 该函数是用来在html中显示当前hash的
          return path;
        };

        // printHahToHtml = (hash) => {
        //   const DOM = document.getElementById("current-hash");
        //   DOM.innerHTML = `当前页面的hash是:=> #${hash}`;
        //   DOM.style.setProperty("background", "yellow");
        //   DOM.style.setProperty("padding", "10px");
        // };

        printHahToHtml = (hash) => {
          const DOM = this.DOM_CURRENT_HASH;
          DOM.innerHTML = `当前页面的hash是:=> #${hash}`;
          DOM.style.setProperty("padding", "10px");
          DOM.style.setProperty("background", "yellow");
        };
      }

      new HashRouter(routes);
    </script>
  </body>
</html>

history路由

window.history 对象

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

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

  • window.history.pushState(state, title, url)
  • state:一个与添加的记录相关联的对象
  • title:新页面的标题,现在所有浏览器都忽略该参数,可以传入空字符串
  • url:新的url地址,必须与当前页面同一个域,浏览器的地址栏显示这个网址
  • window.history.pushState({}, null, url)
  • 注意:pushState不会刷新页面,只会改变History对象,地址栏url会变化
    • <font color=red>可以通过 History.state 读取状态对象</font>

popstate

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

手动实现一个 history-router

  • 原理分析
    • 第一步:给每个a标签都绑定一个click事件,click事件触发时,再去触发pushState()事件,将url的path部分改变为a的data-href自定义属性的值即路由的path,传入window.history.pushState({}, null, path),这样地址栏的url就改变了
    • 第二步:改变地址栏的url后,通过window.location.pathname获取更新的url的path部分
    • 第三步:用 path 和 route对象中的key去匹配,匹配上就去执行更新视图的函数
    • 第四步:
      • 这只是一条线:1-3步即点击a标签的情况
      • 还有一条线:浏览器的前进后退,和函数式导航go() back() forward() 则有 popstate 事件来处理,过程差不多
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>

注意:该html需要用 Live Server 启动,vscode插件,来提供server

2020/12/24复习 - history路由

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <a href="javascript:void(0)" data-href="/home">点击去home页面</a>
  <a href="javascript:void(0)" data-href="/other">点击去other页面</a>
  <div id="content">内容部分,即路由要替换的内容</div>
  <script>
    const routes = [{
      path: '/home',
      component: '<h1>home页面</h1>'
    }, {
      path: '/other',
      component: '<h1>other页面</h1>'
    }]
    class HistoryRouter {
      constructor(routes) {
        this.route = {} // 路由映射 key=path value=()=>{更新视图}
        this.createRouteMap(routes) // 创建路由映射
        this.bindEvent() // 绑定事件
        this.init() // 初始化
      }
      createRouteMap = (routes) => {
        if (routes.length) {
          routes.forEach(({ path, component }) => {
            this.route[path] = () => {
              document.getElementById('content').innerHTML = component
            }
          })
        }
      }
      bindEvent = () => {
        const a = document.getElementsByTagName('a')
        Array.prototype.forEach.call(a, aDom => {
          aDom.addEventListener('click', () => {
            const path = aDom.getAttribute('data-href')
            this.triggerPushState(path) // 触发pushState事件
          }, false)
        })
      }
      triggerPushState = (path) => {
        window.history.pushState({}, null, path)
        // pushState() 可以改变地址栏的url,但是不会触发页面更新,所以要执行下面的更新函数
        // (1) 情况1:这只是 ( 点击a标签 ) 的情况,使用的是 pushState() 函数
        // (2) 情况2:还有就是 ( 点击浏览器的前进后退按钮 ) 和 ( 函数式k导航 window.history.go() back() forward() 的情况 )
        // (3) 情况3:就是初始化时,在 ( load ) 事件触发是的情况,默认path='/'
        this.updateView()
      }
      updateView = () => {
        // 因为:在执行该方法之前,已经触发了 pushState() || popstate事件 || load事件
        // 所以:可以用window.location.pathname 获取最新的 url中的 path 部分
        const currentPath = window.location.pathname
          ? window.location.pathname
          : '/'

        if (this.route[currentPath]) this.route[currentPath]()
      }
      init = () => {
        window.addEventListener('load', this.updateView, false) // 页面加载完成时的情况
        window.addEventListener('popstate', this.updateView, false) // popstate触发的情况,浏览器前进后退和函数式导航
      }
    }

    new HistoryRouter(routes)
  </script>
</body>
</html>

注意:该html需要用 Live Server 启动,vscode插件,来提供server

2021/07/25优化 - history路由

<!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>Document</title>
  </head>
  <body>
    <a href="javascript:void(0)" data-href="/home">home</a>
    <a href="javascript:void(0)" data-href="/other">other</a>
    <div class="root">content</div>
    <script>
      // history路由
      // 原理:
      //  - 1. window.history.pushState(stateObj, title, url) 和 window.history.replaceState(stateObj, title, url) 不会触发popstate,不会刷新浏览器,但是会改变history对象,并且url地址栏会改变
      //  - 2. popstate事件,触发条件
      //       - window.history.go()/back()/forward()
      //       - 浏览器的前进,后退按钮
      // history路由实现
      // - 1. 给每个a标签添加 ( data-href自定义属性 ),并且给每个a标签 ( 绑定click事件 ),点击后获取data-href,然后触发 ( window.history.pushState({}, null, data-href))
      // - 2. 经过1后,触发了window.history.pushState()后,地址栏的url会改变,然后通过 ( window.location.pathname ) 获取改变后的 path
      // - 3. 然后通过path去匹配routes数组中的path,匹配上就去更新 ui

      const routes = [
        {
          path: "/home",
          component: "<h1>home页面</h1>",
        },
        {
          path: "/other",
          component: "<h1>other页面</h1>",
        },
      ];

      class HistoryRouter {
        constructor(routes) {
          this.route = {};
          this.init();
          this.createRouteMap(routes); // ( path ) 和 ( ui更新函数 ) 之间的 ( 映射 )
          this.bindEvent(); // a标签绑定click事件
        }

        createRouteMap = (routes = []) => {
          routes.forEach(
            ({ path, component }) =>
              (this.route[path] = () =>
                (this.DOM_CONTENT.innerHTML = component))
          );
        };

        init = () => {
          window.addEventListener("load", this.updateView, false);
          window.addEventListener("popstate", this.updateView, false);
          this.DOM_CONTENT = document.getElementsByClassName("root")[0];
        };

        bindEvent = () => {
          const aList = document.getElementsByTagName("a");
          [...aList].forEach((a) => {
            const path = a.getAttribute("data-href");
            a.addEventListener(
              "click",
              (e) => {
                window.history.pushState({}, null, path); // window.history.pushState(state, title, url),会改变地址栏url
                this.updateView(); // 更新视图
              },
              false
            );
          });
        };

        updateView = () => {
          const path = window.location.pathname; // 获取最新path
          this.route?.[path]?.();
        };
      }

      new HistoryRouter(routes);
    </script>
  </body>
</html>

注意:该html需要用 Live Server 启动,vscode插件,来提供server

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

  • 原理
    • 大体的原理和hash路由的html版本一样,微小区别
    • 都是利用 ( a标签 ) 的 ( href ) 中的 ( '#/xxx' ) 这样的hash串
    • 只要点击 a标签 => 地址栏的url的hash部分就会改变 => 同时触发 hashchange 事件 => 通过window.location.hash获取最新的hash
    • 获取到 hash 在和 routeMap 中的path匹配,匹配后就改变视图
  • 区别
    • 区别就是 Vue 自己封装了 <router-link><router-view> 组件
    • 可以通过 Vue.component() 方法注册上面两个组件
    • 通过 vm.name 可以访问到 new Vue({data: {name: xx}})中的name
手动实现一个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/6844903841519894535
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/6844903629804011533
hash history 路由 模拟实现 https://juejin.im/post/6844903625802645511
vue-router源码记录 https://juejin.im/post/6844903861455765518
VueRouter源码分析 https://juejin.im/post/6844903818203758600

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容