1 |
|
2 |
|
3 | import pathToRegexp from 'path-to-regexp';
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | const cache = new Map();
|
15 |
|
16 | function decodeParam(val) {
|
17 | try {
|
18 | return decodeURIComponent(val);
|
19 | } catch (err) {
|
20 | return val;
|
21 | }
|
22 | }
|
23 |
|
24 | function matchPath(routePath, urlPath, end, parentParams) {
|
25 | const key = `${routePath}|${end}`;
|
26 | let regexp = cache.get(key);
|
27 |
|
28 | if (!regexp) {
|
29 | const keys = [];
|
30 | regexp = { pattern: pathToRegexp(routePath, keys, { end }), keys };
|
31 | cache.set(key, regexp);
|
32 | }
|
33 |
|
34 | const m = regexp.pattern.exec(urlPath);
|
35 | if (!m) {
|
36 | return null;
|
37 | }
|
38 |
|
39 | const path = m[0];
|
40 | const params = Object.create(null);
|
41 |
|
42 | if (parentParams) {
|
43 | Object.assign(params, parentParams);
|
44 | }
|
45 |
|
46 | for (let i = 1; i < m.length; i += 1) {
|
47 | params[regexp.keys[i - 1].name] = m[i] && decodeParam(m[i]);
|
48 | }
|
49 |
|
50 | return { path: path === '' ? '/' : path, keys: regexp.keys.slice(), params };
|
51 | }
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | function matchRoute(route, baseUrl, path, parentParams) {
|
63 | let match;
|
64 | let childMatches;
|
65 | let childIndex = 0;
|
66 |
|
67 | return {
|
68 | next() {
|
69 | if (!match) {
|
70 | match = matchPath(route.path, path, !route.children, parentParams);
|
71 |
|
72 | if (match) {
|
73 | return {
|
74 | done: false,
|
75 | value: {
|
76 | route,
|
77 | baseUrl,
|
78 | path: match.path,
|
79 | keys: match.keys,
|
80 | params: match.params
|
81 | }
|
82 | };
|
83 | }
|
84 | }
|
85 |
|
86 | if (match && route.children) {
|
87 | while (childIndex < route.children.length) {
|
88 | if (!childMatches) {
|
89 | const newPath = path.substr(match.path.length);
|
90 | const childRoute = route.children[childIndex];
|
91 | childRoute.parent = route;
|
92 |
|
93 | childMatches = matchRoute(childRoute, baseUrl + (match.path === '/' ? '' : match.path), newPath.charAt(0) === '/' ? newPath : `/${newPath}`, match.params);
|
94 | }
|
95 |
|
96 | const childMatch = childMatches.next();
|
97 | if (!childMatch.done) {
|
98 | return {
|
99 | done: false,
|
100 | value: childMatch.value
|
101 | };
|
102 | }
|
103 |
|
104 | childMatches = null;
|
105 | childIndex += 1;
|
106 | }
|
107 | }
|
108 |
|
109 | return { done: true };
|
110 | }
|
111 | };
|
112 | }
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 | function resolveRoute(context, params) {
|
124 | if (typeof context.route.action === 'function') {
|
125 | return context.route.action(context, params);
|
126 | }
|
127 |
|
128 | return null;
|
129 | }
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | function isChildRoute(parentRoute, childRoute) {
|
141 | let route = childRoute;
|
142 | while (route) {
|
143 | route = route.parent;
|
144 | if (route === parentRoute) {
|
145 | return true;
|
146 | }
|
147 | }
|
148 | return false;
|
149 | }
|
150 |
|
151 | class Router {
|
152 | constructor(routes, options = {}) {
|
153 | if (Object(routes) !== routes) {
|
154 | throw new TypeError('Invalid routes');
|
155 | }
|
156 |
|
157 | this.baseUrl = options.baseUrl || '';
|
158 | this.resolveRoute = options.resolveRoute || resolveRoute;
|
159 | this.context = Object.assign({ router: this }, options.context);
|
160 | this.root = Array.isArray(routes) ? { path: '/', children: routes, parent: null } : routes;
|
161 | this.root.parent = null;
|
162 | }
|
163 |
|
164 | resolve(pathOrContext) {
|
165 | const context = Object.assign({}, this.context, typeof pathOrContext === 'string' ? { path: pathOrContext } : pathOrContext);
|
166 | const match = matchRoute(this.root, this.baseUrl, context.path.substr(this.baseUrl.length));
|
167 | const resolve = this.resolveRoute;
|
168 | let matches = null;
|
169 | let nextMatches = null;
|
170 |
|
171 | function next(resume, parent = matches.value.route) {
|
172 | matches = nextMatches || match.next();
|
173 | nextMatches = null;
|
174 |
|
175 | if (!resume) {
|
176 | if (matches.done || !isChildRoute(parent, matches.value.route)) {
|
177 | nextMatches = matches;
|
178 | return Promise.resolve(null);
|
179 | }
|
180 | }
|
181 |
|
182 | if (matches.done) {
|
183 | return Promise.reject(Object.assign(new Error('Page not found'), { context, status: 404, statusCode: 404 }));
|
184 | }
|
185 |
|
186 | return Promise.resolve(resolve(Object.assign({}, context, matches.value), matches.value.params)).then(result => {
|
187 | if (result !== null && result !== undefined) {
|
188 | return result;
|
189 | }
|
190 |
|
191 | return next(resume, parent);
|
192 | });
|
193 | }
|
194 |
|
195 | context.url = context.path;
|
196 | context.next = next;
|
197 |
|
198 | return next(true, this.root);
|
199 | }
|
200 | }
|
201 |
|
202 | Router.pathToRegexp = pathToRegexp;
|
203 | Router.matchPath = matchPath;
|
204 | Router.matchRoute = matchRoute;
|
205 | Router.resolveRoute = resolveRoute;
|
206 |
|
207 | export default Router;
|
208 |
|