@r/platform
Version:
A set of tools to enable easy universal rendering and page navigation on a React + Redux stack
160 lines (135 loc) • 5.1 kB
JavaScript
import pathToRegex from 'path-to-regexp';
import { METHODS } from './router';
import * as actions from './actions';
import { extractQuery } from './pageUtils';
export function matchRoute(path, routes) {
let route;
for (route of routes) {
const [url, handler, meta] = route;
const reg = pathToRegex(url);
const result = reg.exec(path);
if (result) {
return { handler, reg, result, meta };
}
}
}
const getRouteMeta = (routes, shouldSetPage, data) => {
const { method, pathName } = data;
const route = matchRoute(pathName, routes);
if (route && route.meta && shouldSetPage && method === METHODS.GET) {
return route.meta;
}
return null;
};
const findAndCallHandler = (store, routes, shouldSetPage, data) => {
const { method, pathName, queryParams, hashParams, bodyParams, referrer } = data;
const { dispatch, getState } = store;
const route = matchRoute(pathName, routes);
if (route) {
const { handler, reg, result } = route;
const urlParams = reg.keys.reduce((prev, cur, index) => ({
...prev,
[cur.name]: result[index + 1],
}), {});
// set page only if its a HEAD or a GET. setting page data is required
// to make sure request rendering and redirects work correctly
// The only reason HEAD is included is because its the same as GET but
// it doesn't have a response body.
if (shouldSetPage && (method === METHODS.GET || method === METHODS.HEAD)) {
dispatch(actions.setPage(pathName, {
urlParams,
queryParams,
hashParams,
referrer,
}));
}
const h = new handler(
pathName,
urlParams,
queryParams,
hashParams,
bodyParams,
dispatch,
getState
);
let handlerMethod = method;
// HEAD requests are supposed to have the exact same headers and redirects
// as a GET request, but they must not send a response body.
// To support HEAD requests, we can check if the handler
// has a specific HEAD function, otherwise we just use its GET function
if (handlerMethod === METHODS.HEAD && !h[METHODS.HEAD]) {
handlerMethod = METHODS.GET;
}
if (!h[handlerMethod]) {
throw new Error(`No method found for ${method.toUpperCase()} ${pathName}`);
}
return h[handlerMethod].bind(h);
}
throw new Error(`No route found for ${method.toUpperCase()} ${pathName}`);
};
export default {
create(routes, isServer, onHandlerComplete) {
return store => next => action => {
let shouldSetPage;
let payload;
switch (action.type) {
case actions.NAVIGATE_TO_URL:
case actions.GOTO_PAGE_INDEX: {
const startTime = new Date().getTime();
next(action);
if (action.type === actions.NAVIGATE_TO_URL) {
shouldSetPage = true;
payload = action.payload;
} else {
shouldSetPage = false;
payload = { ...action.payload, method: METHODS.GET };
}
const meta = getRouteMeta(routes, shouldSetPage, payload);
const handler = findAndCallHandler(store, routes, shouldSetPage, payload);
const ret = next(handler);
// When the handler completes, we get some timing info and pass it
// along to onHandlerComplete
const timeRoute = () => {
const endTime = new Date().getTime();
const duration = endTime - startTime;
onHandlerComplete({ meta, startTime, endTime, duration });
};
ret
.then(timeRoute)
.catch(e => {
timeRoute();
throw e;
});
return ret;
}
case actions.REDIRECT: {
const { url } = action;
// We want to redirect the current page somewhere else.
// If we're on the server, this should always translate into a SET_PAGE
// action, because we should issue a proper 3XX status code to redirect.
if (isServer) {
return store.dispatch(actions.setPage(url));
}
if (url.startsWith('/')) {
// We have special logic for relative urls:
// Before we route, we want to make sure the app's router supports the
// path. It's easy to imagine getting a relative url that isn't in
// routes, but is valid, e.g. there might be a load-balancer or proxy
// that routes the request the appropriate frontend-application based
// on varying criteria such as User-Agent, authentication, path, etc
const path = url.split('?')[0];
const route = matchRoute(path, routes);
if (route) {
const queryParams = extractQuery(url);
return store.dispatch(actions.navigateToUrl(METHODS.GET, path, { queryParams }));
}
}
// base case for client -- hard redirect via window.location.
window.location = url;
return next(action);
}
default: return next(action);
}
};
},
};