UNPKG

5.1 kBJavaScriptView Raw
1import pathToRegex from 'path-to-regexp';
2import { METHODS } from './router';
3import * as actions from './actions';
4import { extractQuery } from './pageUtils';
5
6export function matchRoute(path, routes) {
7 let route;
8
9 for (route of routes) {
10 const [url, handler, meta] = route;
11 const reg = pathToRegex(url);
12 const result = reg.exec(path);
13
14 if (result) {
15 return { handler, reg, result, meta };
16 }
17 }
18}
19
20const getRouteMeta = (routes, shouldSetPage, data) => {
21 const { method, pathName } = data;
22 const route = matchRoute(pathName, routes);
23
24 if (route && route.meta && shouldSetPage && method === METHODS.GET) {
25 return route.meta;
26 }
27 return null;
28};
29
30const findAndCallHandler = (store, routes, shouldSetPage, data) => {
31 const { method, pathName, queryParams, hashParams, bodyParams, referrer } = data;
32 const { dispatch, getState } = store;
33 const route = matchRoute(pathName, routes);
34
35 if (route) {
36 const { handler, reg, result } = route;
37 const urlParams = reg.keys.reduce((prev, cur, index) => ({
38 ...prev,
39 [cur.name]: result[index + 1],
40 }), {});
41
42 // set page only if its a HEAD or a GET. setting page data is required
43 // to make sure request rendering and redirects work correctly
44 // The only reason HEAD is included is because its the same as GET but
45 // it doesn't have a response body.
46 if (shouldSetPage && (method === METHODS.GET || method === METHODS.HEAD)) {
47 dispatch(actions.setPage(pathName, {
48 urlParams,
49 queryParams,
50 hashParams,
51 referrer,
52 }));
53 }
54
55 const h = new handler(
56 pathName,
57 urlParams,
58 queryParams,
59 hashParams,
60 bodyParams,
61 dispatch,
62 getState
63 );
64
65 let handlerMethod = method;
66 // HEAD requests are supposed to have the exact same headers and redirects
67 // as a GET request, but they must not send a response body.
68 // To support HEAD requests, we can check if the handler
69 // has a specific HEAD function, otherwise we just use its GET function
70 if (handlerMethod === METHODS.HEAD && !h[METHODS.HEAD]) {
71 handlerMethod = METHODS.GET;
72 }
73
74 if (!h[handlerMethod]) {
75 throw new Error(`No method found for ${method.toUpperCase()} ${pathName}`);
76 }
77
78 return h[handlerMethod].bind(h);
79 }
80
81 throw new Error(`No route found for ${method.toUpperCase()} ${pathName}`);
82};
83
84export default {
85 create(routes, isServer, onHandlerComplete) {
86 return store => next => action => {
87 let shouldSetPage;
88 let payload;
89 switch (action.type) {
90 case actions.NAVIGATE_TO_URL:
91 case actions.GOTO_PAGE_INDEX: {
92 const startTime = new Date().getTime();
93 next(action);
94 if (action.type === actions.NAVIGATE_TO_URL) {
95 shouldSetPage = true;
96 payload = action.payload;
97 } else {
98 shouldSetPage = false;
99 payload = { ...action.payload, method: METHODS.GET };
100 }
101 const meta = getRouteMeta(routes, shouldSetPage, payload);
102 const handler = findAndCallHandler(store, routes, shouldSetPage, payload);
103 const ret = next(handler);
104
105 // When the handler completes, we get some timing info and pass it
106 // along to onHandlerComplete
107 const timeRoute = () => {
108 const endTime = new Date().getTime();
109 const duration = endTime - startTime;
110 onHandlerComplete({ meta, startTime, endTime, duration });
111 };
112
113 ret
114 .then(timeRoute)
115 .catch(e => {
116 timeRoute();
117 throw e;
118 });
119
120 return ret;
121 }
122
123 case actions.REDIRECT: {
124 const { url } = action;
125
126 // We want to redirect the current page somewhere else.
127 // If we're on the server, this should always translate into a SET_PAGE
128 // action, because we should issue a proper 3XX status code to redirect.
129 if (isServer) {
130 return store.dispatch(actions.setPage(url));
131 }
132
133 if (url.startsWith('/')) {
134 // We have special logic for relative urls:
135 // Before we route, we want to make sure the app's router supports the
136 // path. It's easy to imagine getting a relative url that isn't in
137 // routes, but is valid, e.g. there might be a load-balancer or proxy
138 // that routes the request the appropriate frontend-application based
139 // on varying criteria such as User-Agent, authentication, path, etc
140 const path = url.split('?')[0];
141 const route = matchRoute(path, routes);
142
143 if (route) {
144 const queryParams = extractQuery(url);
145 return store.dispatch(actions.navigateToUrl(METHODS.GET, path, { queryParams }));
146 }
147 }
148
149 // base case for client -- hard redirect via window.location.
150 window.location = url;
151
152 return next(action);
153 }
154
155 default: return next(action);
156 }
157 };
158 },
159};