本文翻译自[https://medium.com/@pshrmn/a-little-bit-of-history-f245306f48dd ]
本文为本人在学习过程中的翻译,如有错误希望指出以及谅解。
如果你想要理解React router,你首先要学习history,更明确的说就是这个为React router提供了核心功能的history包。这使得客户端的项目可以很容易的增加基于位置的导航,这对于单页面应用来说是必不可少的。
npm install --save history
history有三种类型:browser,hash,memory。这个包分别提供了用于创建各个类型的历史信息的方法。
import {createBrowserHistory, createHashHistory, createMemoryHistory} from 'history'
如果你使用了React router,它会自动为你创建history对象,这样你就不需要真正的去跟history打交道。但是,去理解各种类型的history还是很重要的,这样你就能够决定哪一种才是真正适合你的项目的。
什么是history
不管你创建了什么类型的history,你最终得到的对象都拥有着基本相同的属性和方法。
Location
history对象中最重要的属性就是'location'。location对象表名了你的应用当前位于什么位置。它包含了一些从URL中获取到的属性:pathname,search,hash。
除此之外,每个location对象中都一个与之相关的唯一的'key'属性。这个'key'可以用于鉴别跟存储特定location的相关数据。
最后,每个location对象都有一个与之相关的state,这提供了一种在location上添加一些信息,但是这些信息不会展示在URL上的方法。
{
pathname: '/here',
search: '?key=value',
hash: '#extra-information',
state: {model: true},
key: 'abc123'
}
一个history对象被创建的时候需要一个初始化的location值。不同类型的history的location的初始值的确定方式是不一样的。比如:browser类型的history会先解析当前URL值。
One location to rule them all?
尽管我们可以访问的当前路径只有一个,但是history对象保持着对一些路径的跟踪。正是这种能够在location数组中添加,移除位置的能力让history对象具有了记录历史的功能(让其称作‘history’),如果history对象只知道当前路径的相关信息,那它就更应当叫做‘present’(当前)。
除了在对象中保存着一个路径数组,history对象也维护着一个索引值,这个索引表示着当前所在路径在这个路径数组中的位置。
对于memory 类型的history,这些都是被明确声明的。对于browser跟hash类型的history,这个路径信息的数组以及索引值都是由浏览器控制的并且不能被用户直接访问。
导航
一个只有location属性的对象并不能让我们产生很大兴趣。一个history对象真正让人产生兴趣的地方在于它有一些导航的方法。
Push
Push
方法可以让你导航到一个新的地址。这个操作会在当前的location数组之后添加一个新的地址。在这个方法操作之后,location数组中任何位于这个地址之后的地址(因为用户通过后退按钮回到上一个页面会在当前地址之后产生一个地址)都会被清除。
默认情况下,当你点击通过React router生成的<Link>
的时候,调用的是history.push
方法。
history.push({pathname: '/new-place'})
Replace
Replace
方法跟push
方法类似,但是不同的是Replace
是在当前索引的位置替换地址而不是在当前索引的位置之后添加一个新的地址。位于当前索引位置之后的地址不会被清楚。
对于重定向来说使用replace
这个方法是很好的,这也正是React router中的<Redirect>
组件所采取的方法。
例如:假设你处在第一个页面并且点击了一个导航到第二个页面的链接,但是这第二个页面又会将你重定向到第三个页面。如果这里的重定向使用的是push
方法,那么当你在第三个页面点击返回按钮的时候会将你带回第二个页面(此时可能会再次将你又重定向会第三页),相反,如果你使用replace
方法,在第三页执行返回操作会将你带回第一页。
history.replace({pathname: '/go-here-instead'})
最后,还有三个相关的方法:goBack
,goForward
,go
。
goBack
方法返回到上一个页面,同时也会将locations中数组的索引减少一个。
history.goBack()
goForward
方法与goBack
方法相反,它会向前前进一个页面,但是这只会发生在有有可以前进的页面的时候,就是当用户在这之前点击过返回按钮。
history.goForward()
go
是goBack
,goForward
两者有力的结合。当向这个方法中传递的参数为负值时,会依据location数组的位置信息后退到相应的页面,当传递的参数为正值时,会前进相应的页面。
history.go(-3)
Listen!
History使用观察者模式来在地址发生改变的时候来通知外部的代码。
每个history对象都有一个listen
方法,这个方法接收一个函数作为它的参数。这个函数会被添加到history中的用于保存监听函数的数组中。任何时候当地址发生改变时(无论是通过在代码中调用history对象的方法还是通过点击浏览器的按钮),history对象会调用它所有的监听函数。这使得你能够编写一些代码用于监听当地址发生改变时,来执行相关的操作。
const youAreHere = document.getElementById('youAreHere')
history.listen(function (location) {
youAreHere.textContent = location.pathname
})
React Router的router组件会订阅它的history对象,这样它才能够在地址发生改变时重新渲染加载。
Linking things together
每个history也都有一个createHref
方法,这个方法接收一个location对象作为参数,然后输出一个URL。
在内部history可以使用location对象来进行导航。但是,对于那些不是来自于history包中的元素,比如:<a>
,它是不知道location对象是什么的。所以为了能够生成那些不知道location对象但是依旧能够正确的进行导航的HTML的标签,我们就必须能够生成真正的URLs。
consot location = {
pathname: '/one-fish',
search: '?two=fish',
hash: '#red-fish-blue-fish'
}
const url = history.createHref(location)
const link = document.createElement('a')
a.href = url
// <a href='/one-fish?two=fish#red-fish-blue-fish'></a>
以上已经包含了history的基本的API,虽然history还有好多别的属性跟方法,但是上述的一些属性跟方法已经足够我们来理解以及使用history对象了。
With our powers combined
你在确定出最适合你的项目的history类型的时候需要清楚这些history类型之间的区别。下面我们会一次阐述这三种类型的history的实例。
In the browser
browser跟hash类型的history都是在浏览器端进行使用的。它们是在跟web API中的history跟location进行交互的,所以当前的location跟浏览地址栏中显示的是一样的。
const browserHistory = createBrowserHistory()
const hashHistory = createHashHistory()
这两者之间的区别在于他们根据URL中的信息来创建location对象方法的不同。browser类型的history使用了完整的URL地址信息,但是hash类型只使用了URL地址中位于第一个'#'标志之后的部分。
// Given the following URL
url = 'http://www.example.com/this/is/the/path?key=value#hash'
// a browser history creates the location object:
{
pathname: '/this/is/the/path',
search: '?key=value',
hash: '#hash'
}
// a hash history creates the location object:
{
pathname: 'hash',
search: '',
hash: ''
}
Hashing things out
你问什么要用hash类型的history呢?因为理论上当你访问一个地址的时候在你的服务器上通常都会有一个与之相对应的文件。
但是对于动态服务器来说,被请求的内容并非都要真正存在于服务器上。取而代之的是,服务器根据请求的地址来动态决定并生成相应的HTML信息。
但是,对于静态服务器来说它只能返回真正存在于服务器上的文件。它所能做的最具有动态化的就是当URL地址只指定了目录信息的时候返回index.html
。
考虑到静态服务器的一些限制,最简单的解决方案就是从你的服务器获取HTML的‘真实’location只有一个:就是你的应用只有一个URL,但是这违背了使用history的初衷。为了避免这个限制,hash类型的history使用了URL中的hash部分来设置以及访问位置信息。
// If example.com uses a static file server, these URLs would
// both fetch html from /my-site/index.html
http://www.example.com/my-site#/one
http://www.example.com/my-site#/two
// However, with a hash history, an application's location will be
// different for each URL because the location is derived from
// the hash section of the URL
{ pathname: '/one' }
{ pathname: '/two' }
虽然hash模式的history工作的很好,但是这会有被黑客入侵的风险,因为这个模式严重依赖于将所有的路径信息存储在URL的hash中。所以,只有在你的网站没有动态服务器可以用来提供HTML的时候采取采用hash模式的history。
Memory: The Catch-all History
关于memory location的最好的地方在于只要可以运行JavaScript你就可以使用它。
一个最简单的例子就是你可以在Node环境下运行的单元测试中使用它。这样你就可以测试那些依赖于history对象的代码而不需要真正在浏览器中测试。
更重要的是你还可以在移动App里面使用memory类型的history。你可以在react-native
类型的App中通过react-router-native
来使用memory类型的history来允许使用基于location的导航。
如果你真的想用,你甚至可以在浏览器中使用memory history(这样你就用不到浏览器的地址栏了)
memory history跟browse historyr以及hash history最大的区别在于:memory history在内存中保存着自己的location数组。在创建memory history的时候你可以传入一些信息用于设置初始状态。这个状态包括:保存在数组中的位置信息以及当前位置在这个数组中的索引。这区别于browser
history以及hash history的依靠浏览器来存储相应的数组。
const history = createMemoryHistory({
initialEntries: ['/', '/next', '/last'],
initialIndex: 0
})
无论你最终选择了什么类型的history,你最终都可以很容易并且高效的实现导航以及以及位置的加载渲染。
Notes
[1].search
属性的值是字符串形式的而不是解析过的对象。这是因为在有些情况下这些字符串只有一点点不同,然而这就需要需要用不同的字符串的解析包来解析这些字符串。正因如此,history让你自己来决定如何解析这些字符串而不是给你一个解析字符串的方法。如果你正在寻找解析字符串的方法,这里有一些流行的选择:'qs','query-string','querystring'以及原生的'URLSearchParams'。
[2].限制就是缺少安全性。浏览器的历史的location数组中不仅仅包含了我们已经访问过的地址的信息。允许了对这个数组的访问就会泄露用户的浏览历史的信息,这不是一个网站应该被允许做的。
[3].默认情况下,browser history里面会创建一个location对象,这个对象里面的pathname
属性保存着URL的完整路径。但是你可以自己为history指定一个生成的名称,这样完整路径中的一部分就会被完全忽略掉。
const history = createBrowserHistory({ basename: '/path' })
// given the url: [http://www.example.com/path/here](http://www.example.com/path/here)
// the history object will create the location
{ pathname: '/here', ... }
[4].理论上来说,在你的应用中你可以针对每个有效的URL都返回同一个HTML文件。如果你的URL都是静态的,那这可以运行,但是这样会创建许多多余的文件。如果你在URL中使用参数来匹配大量的可能值,这种方案是不可行的。
[5].如果你在使用memory history的时候没有给它提供初始的location以及index值,那它会自己生成默认值:
entries = [{ pathname: '/' }]
index = 0
这对大多数应用来说足够了,但是对于session自动恢复来说为history来预先设置初始值是非常有用的。