Imagine we have a web page with client-side routing. For different urls we will show different pages. For example, using HyperApp Routing we can show "home" and "about" pages.
The web page
1 | <body> |
The app.js has the simple code from the HyperApp routing example, but
using hyperx template strings
rather than JSX.
1 | const {h, app, Router} = hyperapp |
We can serve this page using simple static server like
http-server and get the
correct routing if we load the index page first. If we try to load
the /about page, we will get a 404 error.
Why is this happening? If we do not have the web application loaded from
the index.html page listening and intercepting location change events,
then the browser has no idea that /about is part of the application. Instead
it goes directly to the server, that fails to find a page to serve.
This has another defect. Notice that the links from one client view, for
example from home to about view had to use a button click and a router
action?
1 | <button onClick=${_ => actions.router.go("/about")}>About</button> |
These links would not work if we tried using plain <a href="/about">About</a>
elements, which is a shame.
Fallback
In order to solve this problem, the server has to catch stray requests like
GET /about and return the index.html page. Then the application will pick
up the location /about and will serve the right view client-side.
There is a middleware that does that called
connect-history-api-fallback
We can use a simple Express app to serve static files with history fallback.
1 | const history = require('connect-history-api-fallback') |
Now the user can type localhost:3000/about and the right page is served:
- browser goes to the server to fetch
:3000/aboutpage - the history middleware catches the request and returns
index.html - the web application in
index.htmlinitializes itself, looks atlocationand routes to/aboutview
Not only this is a much better experience for the user, it also allows plain
href links like <a href="/about">...</a> to work correctly.
You just need to remember that your application state will NOT be preserved during such transitions, because the page is loaded again from scratch.
Parametrized views
A view does not have to be hard coded, client side routing also can allow parameters. For example, if we want to have "user" view where the user name is dynamic, we could define a view like this
1 | '/:username': (model, actions) => |
Going through the router action actions.router.go("/bahmutov") and typing
the url directly work.
But what if the path has parts? For example if we define /user/:username
view
1 | '/user/:username': (model, actions) => |
Then using actions.router.go("/user/bahmutov") works, but typing the URL
directly does not.
The history middleware does not handle relative path from the
/user/index.html page to app.js and tries to load /user/app.js.
There are two solutions to this problem: and the first one is very simple.
Since our application really lives in
index.html at the "root" of the domain, we should force loading app.js
and any other file from the root. Just change index.html and prefix the
local resources with / character.
1 | <body> |
The web application works now, even if the user opened the address
localhost:3000/user/bahmutov directly.
The second solution is to add custom a rewrite rules to the history middleware.
For example, if the client is asking for /user/app.js we should serve the
file app.js
1 | const history = require('connect-history-api-fallback') |
I prefer the first solution, since it is so much simpler.