Html: Expose `history.index` in addition to `history.length`

Created on 27 May 2017  Â·  37Comments  Â·  Source: whatwg/html

An integer history.index < history.length would be a big simplification for many history-based routers. Currently, many of these push/replace a "fake" first state when the app starts up only to record such an index.

There might be some privacy/security concerns, but I can't think of any negatives that are not already possible with history.length, history.back(), etc.

History state is used often for view-to-view routing in a single-page webapps. But it's also used for simpler features, such as closing popups and dialogs on back button. Doing this with the current history object is relatively hard. An approach I've run across a lot goes something like this:

  1. When the app starts up, it calls history.replaceState({index: 0})
  2. Then the app patches replaceState and pushState APIs to ensure that no one can accidentally overwrite this {index: X}.
  3. The pushState method is patched to increment {index: index + 1} for each push operation.
  4. The popstate event is is ambiguous: it could be due to link navigation or due to back/forward buttons. Thus, the app would keep {index: X} if available, or would assume a fragment navigation and would set {index: index + 1}.

This is all to be able to reconstruct index on any popstate operation. It's trivial then to do operations such as closing a popup/dialog and other history-based functions. This is best effort since a user can go back or forward several steps or iframe can push/pop state - which could easily get to the state that the app cannot track. In other words, {index: X} and the real current index could easily go out of sync.

Ultimately, what a system like this does - it emulates history.index property that I request here. Having this property would simplify things a lot and avoid patching the existing API.

additioproposal history

Most helpful comment

@Yay295 Significant benefits: the state in the history stack is very fragile - anyone can overwrite it. In fact the conflicts between app routes and other history uses are very common. History state in the stack is also very hard to use. It's much easier to make judgements about popstate event based on the previous and new values of history.index. The same not the true for history.state - going back couple of steps (e.g. go(-2)) will jump over the previously set state and it's very hard to reconcile the app's internal state because of this. Finally, local storage is not applicable for history-based operations: the same app can be open in several tabs, etc. This all could be drastically simplified with a simple history.index.

All 37 comments

@majido thoughts from Chrome's perspective?

Just wondering, what would the benefit of this be over just adding your own index to the history state? Local Storage could likely be used for this purpose as well. Knowing the actual history index doesn't seem all that useful because there could be any number of unrelated history states before and after the ones you are interested in.

@Yay295 Significant benefits: the state in the history stack is very fragile - anyone can overwrite it. In fact the conflicts between app routes and other history uses are very common. History state in the stack is also very hard to use. It's much easier to make judgements about popstate event based on the previous and new values of history.index. The same not the true for history.state - going back couple of steps (e.g. go(-2)) will jump over the previously set state and it's very hard to reconcile the app's internal state because of this. Finally, local storage is not applicable for history-based operations: the same app can be open in several tabs, etc. This all could be drastically simplified with a simple history.index.

I can't immediately see problems with this from Chrome's perspective (we already propagate the history index to each renderer process, it's just not exposed to the web platform). In terms of privacy/security, history.index seems to me to be analogous to trying to read location.href cross-origin: you might be able to use it to tell that cross-origin state exists, but you can't actually read the state.

I added some more details in the description to highlight the current difficulties with history API.

history.index seems to me to be analogous to trying to read location.href cross-origin

Well, you cannot read the latter, but the former is updated in a top-level document when a cross-origin child document navigates, as history is shared across documents, right?

@annevk history.length is updated likewise and can be read by the top-level context when a child document navigates. A lot of times history.length and history.index will be in sync. And in all cases both top and child contexts will receive popstate event. Often, an app router would immediately replaceState with its own "inferred" index to continue to be able to properly track the top-level history state.

@natechapin and I are ready to make this change in Chrome.
Should we send a Blink Intent and proceed or start a WICG thread first?

Please see https://whatwg.org/working-mode#additions. I don't understand why WICG would be involved here. What this really needs is interest from more than one implementer. Maybe @smaug---- has thoughts?

@cdumez Any thoughts from WebKit?

If it's true that this doesn't have any new privacy implications above exposing history.length then it seems to me like an easy win if it reduces JS size / complexity of frameworks like AMP.

I agree that this would be a huge benefit to web devs but I think it doesn't go far enough. I think there should be a second unique id in addition to the index. This is because I think devs should be able to differentiate between replace'd states from their original push.

Also -- would this index max out at 50 like the history.length? Would the same page then have a different index if it were stored?

For instance:

  1. push 50
  2. get index -> returns 50 (or 0?)
  3. push 1
  4. get index -> returns 50
  5. back
  6. get index -> returns 49 :(

In that case they'd only be indexes into a history "slice" but not really absolute indexes, which makes them about as useful as history.length (which, in my opinion, isn't useful at all).

The only new information such a limited index would expose is direction in popstate events. Which don't get me wrong, would be a bit more information than we have today.

The spec doesn't limit history.length to 40 entries as far as I can tell, and a quick test in Chrome shows they don't either.

I misremembered - it's actually 50.

for (let i = 0; i < 100; i++) {
history.pushState('', '', '');
console.log(history.length)
}

Will log the first 50 and then repeat at 50.

@RByers I don't see anything particular bad with supporting history.index in WebKit given that we already expose history.length. It also seems like it would be a added with minimal effort.

cc @geoffreygaren in case he has a different opinion.

@tbondwilkinson @Yay295 "Huh" on history.length <= 50. But I suppose, history.index should simply follow whatever history.length does. The polyfills pretty much do just that.

@tbondwilkinson And I agree, more things could probably be done about history. This is just a low-effort idea that could simplify many use cases.

No objection.

If we do discover a privacy or security problem, we can consider mitigations like per-domain history lists.

@cdumez @geoffreygaren thanks for taking a look! Should we take this as "implementer interest" from WebKit, and start working on the spec/tests?

Sure.

Awesome! It looks like the spec will be pretty short; something like "return the index of the current entry in the session history within the top-level browsing context's joint session history." Plus, throwing if not fully active, like all other history.* properties.

I can do that spec PR easily. But writing the web platform tests will be the real work, especially given all the iframe cases, nested iframes, multiple iframes contributing to form a single joint session history, non-fully-active Documents, and such. I won't have time for that for a few weeks, but maybe @spanicker and @natechapin can work on that as part of their change to Blink?

PR posted: https://github.com/whatwg/html/pull/2944. Review appreciated!

It's worth noting that history.length can decrease in Firefox. For example, open the following link in Firefox:

http://freesamael.github.io/gecko/shistory/length-change/page1.html

Wait for Page 1 to load iframe 1/2/3/4/5, and then follow the links to Page 2/3/4/5. If you don't have any addon causing bfcache being disabled (such as lastpass), you'll notice that history.length decreases when navigating from Page 4 to Page 5.

This is because we're binding the session history entries of dynamically added subframes to the lifetime of the frame element, so when bfcache drops the document, all the associated dynamic subframe entries are removed as well. In addition, gecko would merge duplicated root entries in this case, so history.length would change. This same behavior will apply to history.index if we implement it.

For a single page application, this indicates the start index can change in some cases. I'm not sure web developers are able to deal with it, if they start relying on history.index.

Reopening to make sure the above comment gets considered.

cc @smaug----

Last time I checked, also IE/Edge shrink session history when iframes are removed.
And probably because webkit/blink don't do that, they end up occasionally loading pages from session history to iframes which never had such pages loaded.
(for example http://mozilla.pettay.fi/moztests/history2/Start.html)

But I'm not sure whether removing entries changes much here. The current session history entry has still an index in the session history transaction list.

I'm not quite sure what the use case would be, but if a single page application stores a startIndex at the first load, or a lastIndex each time popState / pushState occurs, both values can be incorrect and out-of-sync of the real history indices they meant to be. Then comparing startIndex or lastIndex to current history.index wouldn't be meaningful.

That is very true. Should we expose some kind of start/end indexes. I mean, startIndex would be the initial page load and endIndex the last pushState/fragmentNavigation

But yeah, I think .index as such shouldn't be added to the spec.

@freesamael

For a single page application, this indicates the start index can change in some cases. I'm not sure web developers are able to deal with it, if they start relying on history.index.

What's the advise for web developers to deal with this now?

I've already posted above but just reiterating that index and length are
both useless for anything but the simplest checks and that's something that
should definitely be fixed.

I spent a long time creating workarounds for Google search's interface with
the history API and search isn't the only google product that has a wrapper
to provide extra utility on top of history that could be implemented by the
browser.

On Thu, Aug 31, 2017 at 6:03 PM Dima Voytenko notifications@github.com
wrote:

@freesamael https://github.com/freesamael

For a single page application, this indicates the start index can change
in some cases. I'm not sure web developers are able to deal with it, if
they start relying on history.index.

What's the advise for web developers to deal with this now?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/whatwg/html/issues/2710#issuecomment-326343608, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ACRDfmNoIN241Zkkai-ZO1JNLSvGyVVRks5sdtlPgaJpZM4NoHAX
.

@tbondwilkinson To be fair, the suggested history.index is a very basic and minimal-effort improvement that is aimed to make things like closing popup menu UIs possible without corrupting an app's history state. This goal is probably still achievable even given Firefox's behavior with iframes described above.

I'll be happy to see a more universal and mature Web History API. But I think this should be developed outside of this bug'd scope. I don't know if there's an effort to create a better API at this time. If you know of any, please let me know.

@tbondwilkinson, et al, I took a stub and created a bigger-scope #2992. I'd still like to get a definitive on on the history.index proposal separately, since it's much smaller scope.

@dvoytenko

What's the advise for web developers to deal with this now?

What if we add something similar to history.index & history.length but in a per-root-document manner instead? What I mean is that when a single page application first loaded, it always index = 0 & length = 1, then these values increase on pushState & iframe navigation, etc. Much like what @smaug---- suggested above.

In this way things are more controllable to web developers, since the startIndex is always 0 and length only counts session history entries inside the single page application. Would that be helpful?

@freesamael Not sure. I'm a bit skeptical of moderate changes to this API. I feel like it either needs to be a major review (e.g. #2992) or some minor addons to make current API slightly easier.

Specifically with your idea of startIndex = 0:

  1. What would happen on reloads or hard nav? E.g. if I have a single-page app, I'm on View 1 (index = 0) and soft-navigate to View 2 (index = 1). Then user refreshes the page, so it's View 2 and index = 0? That seems wrong. It implies that the app cannot navigate back, where in fact it can. Most of routers today will implement soft back navigation with ease in such a case.
  2. Does this really help with Firefox case and child iframes? A child iframe can push history as much as it wants (it can even call history.back() and navigate the parent context). From what I understood, Firefox simply clears out all history entries created by an iframe when it's removed, which could reset the index and length. As I see this, this is a tangent to the startIndex solution - it'd jump just the same.

One issue with the History API in the spec is that it doesn't map at all to what browsers implement.
See for example https://github.com/whatwg/html/issues/1454

So modifying the API in the spec may end up revealing that mismatch with implementations.

@dvoytenko I image reloading wouldn't be a problem (a normal reload wouldn't clear length/index; force reload would both clear the length/index and bring user to the initial view) but yes the iframe issue remains. I don't have better suggestions for now :/

I've posted https://github.com/whatwg/html/pull/3460 for removing history.index from the spec for now, due to lack of implementation movement and Gecko's objections in #2985.

I'm going to close this issue for now.

Was this page helpful?
0 / 5 - 0 ratings