React-router v4.2下的 history

本文翻译自[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'})
最后,还有三个相关的方法:goBackgoForwardgo
goBack方法返回到上一个页面,同时也会将locations中数组的索引减少一个。
history.goBack()
goForward方法与goBack方法相反,它会向前前进一个页面,但是这只会发生在有有可以前进的页面的时候,就是当用户在这之前点击过返回按钮。
history.goForward()
gogoBackgoForward两者有力的结合。当向这个方法中传递的参数为负值时,会依据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来预先设置初始值是非常有用的。

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

推荐阅读更多精彩内容