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/about
page - the history middleware catches the request and returns
index.html
- the web application in
index.html
initializes itself, looks atlocation
and routes to/about
view
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.