Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose history.index in addition to history.length #2710

Closed
dvoytenko opened this issue May 26, 2017 · 39 comments
Closed

Expose history.index in addition to history.length #2710

dvoytenko opened this issue May 26, 2017 · 39 comments
Labels
addition/proposal New features or enhancements topic: history

Comments

@dvoytenko
Copy link

dvoytenko commented May 26, 2017

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.

@domenic
Copy link
Member

domenic commented May 26, 2017

@majido thoughts from Chrome's perspective?

@Yay295
Copy link
Contributor

Yay295 commented May 27, 2017

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.

@dvoytenko
Copy link
Author

@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.

@natechapin
Copy link

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.

@dvoytenko
Copy link
Author

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

@zcorpan zcorpan added addition/proposal New features or enhancements topic: history labels Jun 2, 2017
@annevk
Copy link
Member

annevk commented Jun 28, 2017

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?

@dvoytenko
Copy link
Author

@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.

@spanicker
Copy link

@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?

@domenic
Copy link
Member

domenic commented Jul 26, 2017

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?

@RByers
Copy link

RByers commented Jul 26, 2017

@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.

@tbondwilkinson
Copy link

tbondwilkinson commented Aug 11, 2017

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.

@Yay295
Copy link
Contributor

Yay295 commented Aug 11, 2017

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.

@tbondwilkinson
Copy link

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.

@cdumez
Copy link

cdumez commented Aug 11, 2017

@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.

@dvoytenko
Copy link
Author

@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.

@dvoytenko
Copy link
Author

@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.

@geoffreygaren
Copy link

No objection.

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

@domenic
Copy link
Member

domenic commented Aug 14, 2017

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

@cdumez
Copy link

cdumez commented Aug 14, 2017

Sure.

@domenic
Copy link
Member

domenic commented Aug 14, 2017

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?

domenic added a commit that referenced this issue Aug 18, 2017
@domenic
Copy link
Member

domenic commented Aug 18, 2017

PR posted: #2944. Review appreciated!

@freesamael
Copy link

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.

@annevk
Copy link
Member

annevk commented Aug 30, 2017

Reopening to make sure the above comment gets considered.

cc @smaug----

@annevk annevk reopened this Aug 30, 2017
@smaug----
Copy link

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.

@freesamael
Copy link

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.

@smaug----
Copy link

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

@smaug----
Copy link

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

@dvoytenko
Copy link
Author

@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?

@tbondwilkinson
Copy link

tbondwilkinson commented Aug 31, 2017 via email

@dvoytenko
Copy link
Author

@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.

@dvoytenko
Copy link
Author

@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.

@freesamael
Copy link

@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?

@dvoytenko
Copy link
Author

@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.

@smaug----
Copy link

One issue with the History API in the spec is that it doesn't map at all to what browsers implement.
See for example #1454

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

@freesamael
Copy link

@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 :/

@domenic
Copy link
Member

domenic commented Feb 7, 2018

I've posted #3460 for removing history.index from the spec for now, due to lack of implementation movement and Gecko's objections in #2985.

alice pushed a commit to alice/html that referenced this issue Jan 8, 2019
@dvoytenko
Copy link
Author

I'm going to close this issue for now.

@iahu
Copy link

iahu commented Dec 7, 2021

It seems that history.index is not be adopted. So any idea to check if a history is the last one for now?
what if a property like history.last ?

@domenic
Copy link
Member

domenic commented Dec 7, 2021

Check out https://github.com/WICG/app-history/!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements topic: history
Development

No branches or pull requests