WICG / navigation-api

The new navigation API provides a new interface for navigations and session history, with a focus on single-page application navigations.
https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api
486 stars 26 forks source link

What happens when returning to entries captured by app-history #99

Open jakearchibald opened 3 years ago

jakearchibald commented 3 years ago

This came up in https://github.com/whatwg/html/issues/6207 and it might be worth 'fixing' in the new API.

  1. Page is /.
  2. User clicks link, which navigates to /article-1, which is handled within the current document by responding to the "navigate" event.
  3. User presses refresh, or navigates away and presses back (assuming no bfcache).
  4. User presses back.

Currently, the equivalent pushState browser behaviour is:

  1. /article-1 fetched and displayed.
  2. Switch url and state to / (no fetch).

I wonder how obvious it is to developers that they need to cater for this. It seems possible that the resources at:

…meaning that /article-1 is unprepared for an internal state change to /.

Other ways this could be handled:

  1. /article-1 fetched and displayed.
  2. / fetched and displayed.

This means entries that were previously using the same document are now using different documents, which is weird in its own way. Although, this is what should happen in a literal interpretation of the spec.

Or:

  1. / fetched and displayed, but with /article-1 state.
  2. Switch url and state to / (no fetch).

This means fetching something other than the URL that's in the URL bar, but it loads the same code that previously handled the transition from / to /article-1.

tbondwilkinson commented 3 years ago

This is a really hard one, because I think it would be confusing to default to loading as if we were back in '/' when the URL bar actually says '/article-1' but you're right that this is a place where developers have to add special handling (basically they have to redo in JS all the things that the page formerly did).

I don't think we should go the route of making formerly same-document history entries cross-document, that also feels bad.

To me this problem is more about how fast we can display HTML to the user on a back navigation. Right now, it's a waste because when you're on a page with a URL, but you've done a history.pushState, you know that this is the HTML you'd want to represent as the page. But when a back navigation occurs, you're going to have to re-construct this entire page, because this HTML is NOT stored in any cache, unlike a fresh page load.

So if I were to go about fixing this problem, I would think about ways that the browser could snapshot the contents of the DOM when the URL bar changes in a same-document way, as if that DOM had been requested explicitly from the network. Then, even though this isn't a full BFCache reload (since the JS context is recreated), you would at LEAST not lose the DOM content, which I assume is cheaper to cache than full JS context.

Keep in mind too that developers always have to handle this case, because at any moment the user could reload. Interestingly, reloading does transform entries into cross-document, so e.g. if you had / and /article-1 as entries and refreshed on /article-1, / is now in a different document from the one you just loaded. This has always been fairly confusing to me - and I wish that a reload had different behavior.

domenic commented 3 years ago

In general app history builds on the existing session history model, and I don't think we have any particular opportunities to change it. So I think the behavior here should be the same as it is for pushState. I don't know how we'd spec something else.

I wonder how obvious it is to developers that they need to cater for this. It seems possible that the resources at:

My understanding is that typically SPAs are done by either:

So I don't think it's very typical that / is treated specially and /article-1 is not prepared to display an app. After all, what would happen if the user copied and pasted the /article-1 URL to their friends?

tbondwilkinson commented 3 years ago

So I don't think it's very typical that / is treated specially and /article-1 is not prepared to display an app. After all, what would happen if the user copied and pasted the /article-1 URL to their friends?

Well, I do think you're over-estimating apps.

But further I think what's more frustrating than having to handle this specially is that to handle this, you have to either keep server-side rendering in sync with client-side rendering, which isn't trivial OR you have to do all client-side rendering, which has time-to-interaction implications. Vs. with multi-page applications, browsers do a really good job of making reloads and navigations cheap, with SPA, reloads and navigations can be very expensive if JS context is lost.

jakearchibald commented 3 years ago

After all, what would happen if the user copied and pasted the /article-1 URL to their friends?

That bit is fine. The server sends HTML. It's when that document also needs to handle /, that's the bit which might be problematic.

As you say, if all URLs are rewriting to the same app JS, it's fine. It's possible that's what everyone does.

jakearchibald commented 3 years ago

@tbondwilkinson

Interestingly, reloading does transform entries into cross-document, so e.g. if you had / and /article-1 as entries and refreshed on /article-1, / is now in a different document from the one you just loaded.

I don't think so, not in Chrome anyway. Where are you seeing this?

jakearchibald commented 3 years ago

Yeah, I think my mental model of how pushState was supposed to work is broken. I thought that, as long as the pushed URL gave you access to the core content addressed by the URL, then you're good.

Interestingly, if I Google for 'pushState demo', the first result with a working demo is https://css-tricks.com/using-the-html5-history-api/, and the demo makes the same incorrect assumption.

I have a vague worry that the current API encourages this thinking. Eg:

// The amazing lightbox library!
appHistory.addEventListener("navigate", event => {
  if (!event.canRespond || event.hashChange) return;
  const url = new URL(event.destination.url);
  if (!/\.(jpe?g|gif|png|avif|webp)$/.test(url.pathname)) return;

  displayImageInAModal(url);
});

This captures navigations to images and displays them in-page. The link is sharable (it'll just point to the image). The back button will appear to work. It'll even sometimes work across documents thanks to bfcache.

I guess we just have to document that the above is not okay.

tbondwilkinson commented 3 years ago

I don't think so, not in Chrome anyway. Where are you seeing this?

For instance starting on google.com, call pushState(undefined, undefined, '/maps') Refresh the page, you'll end up at the google maps frontend, not google search homepage with a '/maps' path.

Or am I misunderstanding what you mean?

tbondwilkinson commented 3 years ago

The image example is interesting - I assume open in new tab/window would still work as expected. But perhaps we'd only allow making cross-document navigations same-document if the other document is of the same "type", e.g a webpage and not a resource.

jakearchibald commented 3 years ago

Interestingly, reloading does transform entries into cross-document, so e.g. if you had / and /article-1 as entries and refreshed on /article-1, / is now in a different document from the one you just loaded.

Maybe I'm reading this wrong. Here's a test:

  1. Go to https://example.com.
  2. In the console, run history.pushState({}, '', '/article-1').
  3. Press refresh.
  4. Press back.

When you said "/ is now in a different document from the one you just loaded", I thought you meant that 4 would result in a change of document.

3 results in a change of document, but it updates both history entries, so 4 traverses to the same document.

tbondwilkinson commented 3 years ago

Your console should show a 404 error since that path doesn’t exist at example.com. But the browser does attempt the load I believe and makes the network request.

But I believe 4 would result in a change of document, had the network request in 3 not been a 404, unless I'm wrong in my terminology and document doesn't mean what I think it means.

jakearchibald commented 3 years ago

I'm not seeing that behaviour in Chrome, Firefox, or Safari https://static-misc-2.glitch.me/push-state-test/

tbondwilkinson commented 3 years ago

Ah, now I see what you mean. You're right! :) Another fun quirk.

jakearchibald commented 3 years ago

It's consistent with navigating away then pressing back (unless bfcache), and hash-change navigations, so that's one thing at least!

bathos commented 3 years ago

Although it doesn’t seem like this is something that can be solved (where it actually needs solving, I mean?) by AppHistory alone, perhaps if AppHistory ends up being friendly to pairing with the proposed URLPattern and/or “Declarative routing” APIs, this would in turn make it more straightforward and attractive to share a single source of truth for route definitions between the client, service worker, and backend. If that pattern were easier to realize, it might help reduce the odds of folks accidentally introducing disagreements between server and client routing.

jakearchibald commented 3 years ago

@domenic I'm happy for this to close unless you're interested in solving the image use-case in https://github.com/WICG/app-history/issues/99#issuecomment-824629248, perhaps in some 'opt-in' way that forces the new history entry to use its own document state. Fwiw, my PR will probably support that.

Otherwise, feels like we should stick with the current behaviour.

domenic commented 3 years ago

I think we should probably stick with the current behavior, but I'm happy to keep this open to track efforts about documentation, or thinking if there's some way to steer people away from such behavior, or the ideas that @bathos mentions.

domenic commented 3 years ago

I'm trying to refresh myself on this to see if we can add documentation but I unfortunately lost track of the problem. Given the lightbox image code in https://github.com/WICG/app-history/issues/99#issuecomment-824629248 what is the problem scenario? Is the problem that the navigate handler isn't properly handling navigations to other URLs and closing the modal in that case, so e.g. the back button never closes the modal?

jakearchibald commented 3 years ago
  1. User is on /cool-app
  2. User navigates to /cool-app/photo.webp
  3. This navigation is captured, and a lightbox is shown (we now have two history entries pointing to the same document)
  4. User presses refresh, and the document in both history entries is updated to the image at /cool-app/photo.webp (not in a lightbox)
  5. User presses back, URL changes to /cool-app, but nothing else happens, they're still looking at the image.
jakearchibald commented 3 years ago

One solution might be an option that, in step 3, creates a new history entry with the same document, but a different document state.

This means the reload in step 4 would only replace the document in the second history entry.