前端路由应该理解的一些前置知识

URI和URL

与URI相比,我们更熟悉URL。URL正是使用web浏览器等访问web页面时需要输入的网页地址。比如我们经常输入的 https://www.baidu.com/ 就是 URL

统一资源标识符

URI是 Uniform Resource Identifier 的缩写。RFC2396分别对这三个单词进行了以下定义:

  • Uniform

    • 规定统一的格式可以方便处理多种不同类型的资源,而不用根据上下文环境来识别资源指定的访问方式。
  • Resopurce

    • 可标识的任何资源。不仅是文档文件,图像或者服务等能够区别于其他类型的,全部可作为资源,它可以是多个对象的集合体。
  • Identifier

    • 标识可标识的对象,也可以称为标识符。

综上所述,URI就是由某个协议方案表示的资源的定位标识符。洗衣方案是指访问资源时所使用的协议类型名称。HTTP就是协议的一种方案,除此之外,还有ftp、file、TELNET等30多种标准URI协议方案。

协议方案由互联网号码分配局管理颁布。URI用字符标识某一互联网资源,而URL表示资源的地点,可见URL是URI的子集。

URI的通用语法

URI的通用语法由 5 个组件组成:

URI = scheme:[//authority]path[?query][#fragment]

其中,authority 组件可以由以下3个组件组成:

authority = [userinfo@]host[:post]
  • 在 authority 中,userinfo作为登陆欣欣,通常形式为指定用户名和密码,当从服务器端获取资源时作为身份人中凭证使用,userinfo为可选项。
  • 服务器地址 host 在使用绝对路径URI时需指定访问的服务器地址,地址可用为被 DNS 解析的域名,或者是 IPv4 地址以及 IPv6 地址。
  • post 为服务器连接的网络端口号,作为可选项,如果不指定,则自动使用默认的端口号。
  • path 为带层次的文件路径,指定服务器上的文件路径,以访问特定的资源。
  • query 为查询字符串,针对指定路径的文件资源,可使用查询字符串传入任意查询参数。

统一资源标识符 URI 通用语法中列举了几种 URI 例子,如下所示:

ftp://ftp.is.co.za//rfc/rfc1808.txt
http://www.itef.org/rfc/rfc2396.txt
ldap://[2001.db8::7]/c=GB?objectClass?one
mailto:John.Doe@example.com
news:comp.infosystems.www.servers.unix
tel:+1-826-555-1212
telnet://192.0.2.16a:18/
urn:oasis:names:specification:docbook:dxt:xml:4.1.2

URL的通用语法

统一资源定位器URL作为URI的一种,如同网络的门牌,表示了一个互联网资源的 地址,如 https://www.baidu.con 表示通过 HTTP 协议从主机名为 www.baidu.com 的主机上获取首页资源。

URL的语法定义与URI一致:

URL = scheme:[//authority]path[?query][#fragment]

以 https://www.baidu.com:80/foo/baz?title=moment 为例,其中 https 表示加密的超文本传输协议,www.baidu.com 为服务器的地址,80 为服务器上的都安口号,foo/baz 为资源路径,?title=moment为路径的查询,以 "?" 开头,各个参数是以 "&" 为分割,以等号 "=" 分开参数名称与数据。

URI和URL总结

URL是一种具体的URI,它是URI的一个子集,它不仅唯一标识资源,而且还提供了定位该资源的信息。URI是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位,是绝对的。

总的来说,人的身份证号就是URI,通过身份证号可以让我们能找到具体的一个人。而通过具体的地址去查找某个人,例如广东省广州市天河区某某大学一栋101宿舍的云梦泽,通过具体的地址也可以找到某个人,也起到了URI的作用,所以URL是URI的子集。URL是描述某一资源的具体位置。

浏览器记录

浏览器记录是浏览器中各个页面用户的导航记录。在现代浏览器中,浏览器记录并没有直接的 API 可获取,其可通过 window.history.length 获取当前记录栈的长度信息。浏览器记由浏览器统一管理,并不属某个具体的页面,与页面形式及其内存均无关系。

window.history 对象上存在着诸多属性,通过 lib.dom.d.ts 文件中查看History有以下定义:

interface History {
    readonly length: number;
    scrollRestoration:  "auto" | "manual";
    readonly state: any;
    back(): void;
    forward(): void;
    go(delta?: number): void;
    pushState(data: any, unused: string, url?: string | URL | null): void;
    replaceState(data: any, unused: string, url?: string | URL | null): void;
}

history.pushState

在 HTML 文档中,history.pushState() 方法向当前浏览器会话的历史堆栈中添加一个状态(state)。

history.pushState 方法作为 HTML5 特性的一部分,目前被广泛使用。history.pushState 用于无刷w新增历史栈记录,调用 history.pushState 方法可改变浏览器路径。

pushState() 需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和一个可选的URL.详情可看 MDN文档。其基本语法如下所示:

pushState(state:Object, title:string,[url:string]):undefined

当设置第三个参数URL时,可改变浏览器URL,且不会刷新浏览器。

history.pushState(null, null, "/foo/bar");
console.log(location.pathname); // /foo/bar

如果URL中包含 Unicode 字符,则浏览器也会将字符按 UTF-8 编码。

history.pushState(null, null, "/中文");
console.log(location.pathname); // /%E4%B8%AD%E6%96%87

通过以下代码示例通过设置state,title和url创建一条新的历史记录:

<!-- index.html文件 -->
<body>
<script>
    const state = { nickname: "moment", age: 7 };
    const title = "";
    const url = "title.html";

    history.pushState(state, title, url);
</script>
</body>

<!-- title.html文件 -->
<body>
<h1>这个是title页面</h1>
<div class="age"></div>
<div class="nickname"></div>
<script>
    const nickname = document.querySelector(".nickname");
    const age = document.querySelector(".age");

    nickname.innerHTML = window.history.state.nickname;
    age.innerHTML = window.history.state.age;
</script>
</body>

上例代码通过 pushState 改变浏览器URL,并传进了 state 参数,页面跳转到 title.html 的页面,在 title.html 文件中通过 history.state 获取传进来的数据并通过 innerHTNL 展示出来

因为历史栈由浏览器统一管理,不属于某个具体页面,并不存在于页面的内存中,所以历史栈在刷新页面后不会丢失,栈中记录的各 state 对象也为持久化存储,在导航过程中也不会丢失。

histiry.pushState 使用结构化拷贝算法进行序列化存储,会将拷贝后的结果记录在历史栈记录中。结构化拷贝算法除了能拷贝基本类型,还能考悲剧更多的对象类型。相比 JSON 的序列化,这样的序列化更为安全,如循环引用的对象,结构化序列的手段将会序列化成功,而 JSON 的序列化将会报错,原因在于结构化蓄力的手段保存了每一个访问过的对象的记录,遇到复制过的对象会进行跳过。

结构化拷贝算法要注意特殊的场景,如果 history.pushState 的 state 对象中有 dom节点、error对象、function函数等,调用 history.pushState方法会抛出异常,且对某些对象的特定属性,如 regExp的lastIndex、Object对象的 setter 和 getter 等,结构化拷贝的过程都会丢失。

历史栈变化

history.pushState 的调用会引起历史栈的变化,浏览器通常会维护一个用户访问过的历史栈,以便用户进行导航。用户通常可以通过浏览器的前进和后退按钮或者调用 window.history.go 等方法在历史栈中进行移动,可理解为下图所示的栈指针,不改变历史栈的内容,栈内的记录数量不会发生改变:

当调用 history.pushState 方法时,历史栈的内容会被修改,行为表现为添加历史栈的栈记录,url和state会被推到栈顶。历史栈会有一个指针指向栈的其中一个内容,栈指针所指向的内容正是浏览器当前所运行的url所对应的页面。

当调用 history.pushState({a:3},null,'/c') 方法后,则栈记录加1,栈指针也会指向最新的栈记录位置。通过 history.length 能查看当前历史栈的长度。

浏览器提供了两个 API 接口用以调用浏览器本身自带的返回和向前,它们分别是 back() 方法和 forward() 方法。其中 back() 方法会在会话历史记录中向后移动一页。如果没有上一页,则此方法调用不执行任何操作。而在会话历史中向前移动一页。它与使用delta参数为 1 时调用 history.go(delta)的效果相同。 还有一个 go 方法从会话历史记录中加载特定页面,你可以使用它在历史记录中前后移动,具体取决于 delta 参数的值,具体语法如下:

window.history.go(delta);

delta 参数可选,相当于当前页面你要去往历史页面的位置。负值表示向后移动,正值表示向前移动。因此,例如:history.go(2) 向前移动两页,history.go(-2) 则向后移动两页。如果未向该函数传参或delta相等于0,则该函数与调用 location,reload() 具有相同的效果。具体有以下示例:

// 向后移动一页 back()
window.history.go(-1);

// 向前移动一页 forward()
window.history.go(1);

// 向后移动两页
window.history.go(-2);

// 向前移动两页
window.history.go(2);

history.replaceState

history.replaceState 的用法与 history.pushState 非常相似,区别在于 history.replaceState 将修改当前的历史记录想而不是新建一个,其语法为:

history.replaceState(stateObj, title[, url]);

history.replaceState 不会改变历史栈中记录的数量,它只会更新当前栈的信息,历史栈的长度不会发生变化。

通过相对路径太黏和修改浏览器记录

history.replaceState 与 history.pushState,除支持绝对路径导航外,还支持相对路径导航:如:

window.history.pushState(null, null, "../one");
window.history.replaceState(null, null, "./one.two");
  • 它们的规则和在 Node,js 中的 url.resolve(),具体可参考 Node.js官方文档 ,两者相对路径的解决规则一致。对于相对路径导航,其遵循以下规则:
    • 如果路径以 "/" 开头,则会替换掉整个路径;
    • 如果路径不以 "/" 开头,则会得到相对当前URI地址的路径(在浏览器无base元素的存在的情况下),跟你混路径解决规则会替换 URL 地址中的最后一级目录,即最后一个 "/" 分隔符后缪按的路径部分,如:
// 当前路径为 /one/two/three
console.log(location.pathname); // /one/two/three
window.history.pushState(null, null, "four");
console.log(location.pathname); // /one/two/four

如果当前路径的最后一个字符为 "/",则可以认为 "/" 后紧接空字符串,执行相对路径导航会替换空字符串部分。

如果路径中含有 "." "..",则表示当前路径及上一级路径。

// 当前路径为 /one/two/three
console.log(location.pathname); // /one/two/three
window.history.pushState(null, null, "./four");
console.log(location.pathname); // /one/two/four
// 当前级路径改为 five 下一级路径改为 six
window.history.pushState(null, null, "./five/././six");
console.log(location.pathname); // /one/two/five/six

对于 ".." 操作符,其表明回到上一句路径。

// 当前路径为 /one/two/three
console.log(location.pathname); // /one/two/three
window.history.pushState(null, null, "../four");
console.log(location.pathname); // /one/four

// 当前路径为 /one
console.log(location.pathname); // /one
window.history.pushState(null, null, "../five/six");
console.log(location.pathname); // /five/six

Location

Location 接口表示其链接到的对象的位置(URL)。所做的修改反映在与之相关的对象上。 Document 和 Window 接口都有这样一个链接的 Location,分别通过 Document.location和Window.location 访问。其中 Document.location和Window.location指向同一个对象:

console.log(window.location === document.location); // true

通过 lib.dom.d.ts 文件查看 Location 的类型定义,主要有以下属性和方法:

interface Location {
    hash: string;
    host: string;
    hostname: string;
    href: string;
    toString(): string;
    readonly origin: string;
    pathname: string;
    port: string;
    protocol: string;
    search: string;
    assign(url: string | URL): void;
    reload(): void;
    replace(url: string | URL): void;
}

hash

Location 接口的 hash 属性返回一个 USVString,其中会包含 URL 标识中的 '#' 和后面 URL 片段标识符。这里 fragment 不会经过百分比编码(URL 编码)。如果 URL 中没有 fragment,该属性会包含一个空字符串,""。

<a id="myAnchor" href="/moment.com#elegance">Examples</a>
<script>
  const link = document.querySelector("a");
  console.log(link.hash);
</script

如果设置的 location.hash 值与浏览器的 URL 地址的 hash值相同,就不会触发任何事件,也不会添加任何历史记录。或者如果前后两次对 location.hash 设置了相同的值,则第一次 locationn.hash 设置生效,第二次相同的设置不会产生任何事件和历史记录。

host

Location 接口的 host 属性是包含了主机的一段 USVString,其中包含:主机名,如果 URL 的端口号是非空的,还会跟上一个 ':' ,最后是 URL 的端口号。

console.log(location.host);
// 该输出结果在 create-react-app创建的项目中输出的是 localhost:3000
// 在 live Server 打开的文件打开的html文件输出的 127.0.0.1:5500
// 而在普通方式打开的html文件中输出的空字符串

而 hostname 属性则不携带端口号。

href

Location 接口的 href 属性是一个字符串化转换器 (stringifier), 返回一个包含了完整 URL 的 USVString 值,且允许 href 的更新。

<a id="myAnchor" href="/moment.com#elegance">Examples</a>
<script>
  const link = document.querySelector("a");
  console.log(link.href); // http://127.0.0.1:5500/moment.com#elegance
</script>

replace

与 history.replaceState 类似,window.location.replace会替换当前的栈记录,但在设置绝对路径中,这意味着用户将不能用 "后退" 按钮再次回到旧页面。

location.href = "https://www.foo.com";
location.href = "https://www.bar.com";
location.replace("https://www.baz.com");

在上面的例子中,由于在 www.bar.com 中调用了 location.href 方法,www.bar.com 的页面记录将替换为 https://www.baz.com 的页面记录。当用户在 https://www.baz.com 页面单机浏览器 "后退" 按钮是,将回到 www.foo.com 页面。

浏览器相关事件

popstate事件

每当激活同一文档中不同的历史记录条目时,popstate 事件就会在对应的 window 对象上触发。如果当前处于激活状态的历史记录条目是由 history.pushState() 方法创建的或者是由 history.replaceState() 方法修改的,则 popstate 事件的 state 属性包含了这个历史记录条目的 state 对象的一个拷贝。

调用 history.replaceState() 和 history.pushState() 不会触发 popstate 事件。当移动栈指针或单机浏览器的 "前进" 或 "后退" 按钮时,将触发 popstate 事件,可通过 window.addEventListener 监听该事件。

我们通过以下代码来监听 popstate 事件的变化:

window.addEventListener("popstate", function (event) {
  console.log(event);
});

history.state 同步的是记录栈中的值,每次导航都会获得新的 state 对象。栈记录中的 state 对象是深拷贝存储在浏览器中的,无论在浏览器进行导航,还是刷新当前页面或是关闭浏览器页签再恢复,历史栈的内容都存在且不会被销毁。

当前后两次设置相同的 location.hash 值时,不会触发两次 popstate 事件。若通过 location.href 设置 hash 值,则无论前后设置的值是否相同,都会触发 popstate 事件。当前后两次设置的值相同时,只添加一个历史栈。

hashchange 事件

hashchange 事件用于监听浏览器值的 hash值变化,其监听方式为:

window.addEventListener("hashchange", function (event) {
  console.log(event);
});

hashchange 事件可以通过设置 location.hash、在地址栏手动修改 hash、调用 window.history.go、在浏览器中单击 "前进" 或 "后退" 按钮等方式触发。且每次事件都会得到当前的 url 和旧的 url。

值得注意的是,pushState 不会触发 hashChange 事件,即使前后当行的 URL 仅 hash 部分发生改变,也是如此。

贡献者: mankueng