Ideally the server should only be serving static files and to do that your express app should use some simple templating engine. This combined with a default loader in your HTML file should take care of the overall loading.
I have attached a prefetcher module below, a middleware, whose main purpose is to fetch the data required to render a route.
Important Steps
- You can have an
index.marko
or anindex.html
which will be your skeleton file. I have demonstrated it using marko.js, you can use any other templating engine.
<!-- Meta HTML/marko files -->
import meta from './commons/meta.marko';
<!doctype html>
<html>
<head>
<include(meta, input)/>
<!-- Styles will be inserted inline by build process -->
<style>{{styles.css}}</style>
<link rel="stylesheet" href="${input.assets.css.app}">
<script type="text/javascript" src="${input.assets.js.vendor}"/>
<script type="text/javascript" src="${input.assets.js.app}"/>
</head>
<body>
<!-- Your Spinner/Loader -->
<div class="app-container">
<div class="page-content">
<div id="content" class="content"></div>
<div id="shell-spinner" class="overlay">
<!--loader-->
</div>
</div>
</div>
</body>
</html>
- A Sample
server/app.js
for this process
// all imports
....
....
export default (app, ...) => {
/** all your other middlewares **/
/** all your routes **/
/** inject all your headers **/
app.get('/', (req, res, next) => {
if (req._sessionData) {
res.redirect(INDEX_URL);
} else {
// renderView is imported from the prefetcher.js, name is specified as 'login', check this function out
in renderer.js below in 4.
renderView(res, {
name: 'login',
data: {
assets: {
js: {
app: [path/to/the/js/file],
vendor: [path/to/the/vendor/js/file],
},
},
},
//dataPrefetcher (Prefetcher File in 3.) will fetch all the data before the route renders and till then your index.marko file will be shown (loader)
dataProvider: dataPrefetcher(req),
});
}
});
return app
}
- Prefetcher File
const fetchers = {
user({ ... }) {
return getSomeData().then(res => return res)
},
profile({ ... }) {
return getSomeData().then(res => return res)
},
};
const routeMap = [{
route: '/profile',
fetchers: ['user', 'profile']
}, {
route: '/home',
fetchers: ['user']
}, {
route: '/',
fetchers: ['user']
}, {
route: '/messages',
fetchers: ['user']
}];
export default req => {
let params, currentRouteConf;
for (let i = 0; i < routeMap.length; i++) {
const { route: path } = routeMap[i];
const route = new Route(path); // 'npm install route-parser'
const match = route.match(req.path);
if (match) {
params = match;
currentRouteConf = routeMap[i];
break;
}
}
if (currentRouteConf) {
const { fetchers: fetcherNames } = currentRouteConf;
const fetcherPromises = fetcherNames.map(fetcher => typeof fetcher === 'string' ? fetchers[fetcher](req, params) : fetchers[fetcher.fetcher](req, params));
return Promise.all(fetcherPromises)
.then(responses =>
fetcherNames.reduce((results, fetcher, index) => {
fetcher = typeof fetcher === 'string' ? fetcher : fetcher.datakey;
return {
...results,
[fetcher]: responses[index],
};
}, {}))
.catch(console.log);
}
return false;
};
- Renderer
....
export const renderView = (res, { name, data = {} }) => {
res.setHeader('content-type', 'text/html; charset=utf-8');
// this render is the markojs render function --> https://markojs.com/docs/rendering/#renderinput-callback
require(`/path/to/login.marko.js`).render({
...res.locals,
...data,
}, res);
};
....
- Finally your frontend will have another app.js which will take care of removing the loader from the DOM and will render the actual component in that route.
import polyfill from '@babel/polyfill';
import { h, render } from 'preact';
import Router from 'react-router';
import NotFound from './components/PageNotFound';
....
//imports
....
const onDocumentLoaded = async () => {
const shellSpinnerElmt = document.querySelector('#shell-spinner');
const contentElmt = document.querySelector('#content');
store.dispatch(..actionsToBeDispatched)
//Controlling the page spinner based on state changes
store.subscribe(() => {
const { spinner = {} } = store.getState();
shellSpinnerElmt.classList[spinner.showSpinner ? 'remove' : 'add']('hide');
});
shellSpinnerElmt.classList.add('hide');
// call the frontend React render function
render(<Router>....</Router>)
};
onDocumentLoaded();
document.addEventListener('DOMContentLoaded', onDocumentLoaded);