UNPKG

8.69 kBJavaScriptView Raw
1import {
2 Tapable,
3 AsyncSeriesHook,
4 AsyncParallelBailHook,
5 SyncHook,
6} from 'tapable';
7import _ from 'lodash';
8import React from 'react';
9import { renderRoutes } from 'react-router-config';
10import { Router } from 'react-router';
11import { HashRouter } from 'react-router-dom';
12import { createBrowserHistory } from 'history';
13import { render, hydrate } from 'react-dom';
14import RouteHandler from '../router/handler';
15import ErrorBoundary from '../components/ErrorBoundary';
16import { generateMeta } from '../utils/seo';
17import possibleStandardNames from '../utils/reactPossibleStandardNames';
18import PreloadDataManager from '../utils/preloadDataManager';
19
20const possibleHtmlNames = _.invert(possibleStandardNames);
21const getPossibleHtmlName = key => possibleHtmlNames[key] || key;
22
23export default class ClientHandler extends Tapable {
24 historyUnlistener = null;
25
26 routeHandler = null;
27
28 constructor(options) {
29 super();
30 this.addPlugin = this.addPlugin.bind(this);
31 this.manageHistoryChange = this.manageHistoryChange.bind(this);
32 this.preloadManager = new PreloadDataManager();
33
34 window.PAW_HISTORY = window.PAW_HISTORY || createBrowserHistory({
35 basename: options.env.appRootUrl,
36 });
37 this.history = window.PAW_HISTORY;
38 this.historyUnlistener = this.history.listen(this.manageHistoryChange);
39
40 this.hooks = {
41 locationChange: new AsyncParallelBailHook(['location', 'action']),
42 beforeLoadData: new AsyncSeriesHook(['setParams', 'getParams']),
43 beforeRender: new AsyncSeriesHook(['Application']),
44 renderRoutes: new AsyncSeriesHook(['AppRoutes']),
45 renderComplete: new SyncHook(),
46 };
47 this.options = options;
48 this.manageServiceWorker();
49 }
50
51 manageHistoryChange(location, action) {
52 this.hooks.locationChange.callAsync(location, action, () => null);
53 if (this.routeHandler) {
54 this.updatePageMeta(location).catch((e) => {
55 // eslint-disable-next-line
56 console.log(e);
57 });
58 }
59 }
60
61 async updatePageMeta(location) {
62 const routes = this.routeHandler.getRoutes();
63 const currentRoutes = RouteHandler.matchRoutes(routes, location.pathname.replace(this.options.env.appRootUrl, ''));
64 const promises = [];
65
66 let seoData = {};
67 const pwaSchema = this.routeHandler.getPwaSchema();
68 const seoSchema = this.routeHandler.getDefaultSeoSchema();
69
70 currentRoutes.forEach((r) => {
71 if (r.route && r.route.component && r.route.component.preload) {
72 promises.push(r.route.component.preload(undefined, {
73 route: r.route,
74 match: r.match,
75 ...this.preloadManager.getParams(),
76 }));
77 }
78 });
79
80 await Promise.all(promises).then(() => {
81 currentRoutes.forEach((r) => {
82 let routeSeo = {};
83 if (r.route.getRouteSeo) {
84 routeSeo = r.route.getRouteSeo();
85 }
86 seoData = _.assignIn(seoData, r.route.seo, routeSeo);
87 });
88
89 const metaTags = generateMeta(seoData, {
90 baseUrl: window.location.origin,
91 url: window.location.href,
92 seoSchema,
93 pwaSchema,
94 });
95
96 metaTags.forEach((meta) => {
97 let metaSearchStr = 'meta';
98 let firstMetaSearchStr = '';
99 const htmlMeta = {};
100
101 if (meta.name === 'title') {
102 document.title = meta.content;
103 }
104
105 Object.keys(meta).forEach((key) => {
106 htmlMeta[getPossibleHtmlName(key)] = meta[key];
107 if (!firstMetaSearchStr) {
108 firstMetaSearchStr = `meta[${getPossibleHtmlName(key)}=${JSON.stringify(meta[key])}]`;
109 }
110 metaSearchStr += `[${getPossibleHtmlName(key)}=${JSON.stringify(meta[key])}]`;
111 });
112
113 const alreadyExists = document.querySelector(metaSearchStr);
114 if (!alreadyExists) {
115 const previousExists = document.querySelector(firstMetaSearchStr);
116 if (previousExists) {
117 previousExists.remove();
118 }
119
120 const metaElement = document.createElement('meta');
121 Object.keys(htmlMeta).forEach((htmlMetaKey) => {
122 metaElement.setAttribute(htmlMetaKey, htmlMeta[htmlMetaKey]);
123 });
124 document.getElementsByTagName('head')[0].appendChild(metaElement);
125 }
126 });
127 });
128 }
129
130 manageServiceWorker() {
131 if (this.options.env.serviceWorker) {
132 this.hooks.renderComplete.tap('AddServiceWorker', (err) => {
133 if (err) return;
134 if ('serviceWorker' in navigator) {
135 navigator.serviceWorker.register(`${this.options.env.appRootUrl}/sw.js`).catch(() => null);
136 }
137 });
138 } else {
139 // remove previously registered service worker
140 this.hooks.renderComplete.tap('RemoveServiceWorker', (err) => {
141 if (err) return;
142 if ('serviceWorker' in navigator) {
143 navigator.serviceWorker.getRegistrations().then((registrations) => {
144 registrations.forEach(registration => registration.unregister());
145 });
146 }
147 });
148 }
149 }
150
151 addPlugin(plugin) {
152 try {
153 if (plugin.hooks && Object.keys(plugin.hooks).length) {
154 _.each(plugin.hooks, (hookValue, hookName) => {
155 this.hooks[hookName] = hookValue;
156 });
157 }
158 } catch (ex) {
159 // eslint-disable-next-line
160 console.log(ex);
161 }
162 if (plugin.apply) {
163 plugin.apply(this);
164 }
165 }
166
167 async run({ routeHandler }) {
168 this.routeHandler = routeHandler;
169 const { env } = this.options;
170 const root = _.get(env, 'clientRootElementId', 'app');
171
172 if (!document.getElementById(root)) {
173 // eslint-disable-next-line
174 console.warn(`#${root} element not found in html. thus cannot proceed further`);
175 return false;
176 }
177 const domRootReference = document.getElementById(root);
178 const renderer = env.serverSideRender && !env.singlePageApplication ? hydrate : render;
179
180 const routes = routeHandler.getRoutes();
181
182 const currentPageRoutes = RouteHandler.matchRoutes(routes, window.location.pathname.replace(this.options.env.appRootUrl, ''));
183
184 const promises = [];
185
186 if (window.PAW_PRELOADED_DATA) {
187 const preloadedData = JSON.parse(atob(window.PAW_PRELOADED_DATA));
188
189 // Wait for preload data manager to get executed
190 await new Promise(r => this
191 .hooks
192 .beforeLoadData
193 .callAsync(this.preloadManager.setParams, this.preloadManager.getParams, r));
194
195 currentPageRoutes.forEach((r, i) => {
196 if (
197 (typeof preloadedData[i] !== 'undefined')
198 && r.route && r.route.component && r.route.component.preload
199 ) {
200 promises.push(r.route.component.preload(preloadedData[i], {
201 route: r.route,
202 match: r.match,
203 ...this.preloadManager.getParams(),
204 }));
205 }
206 });
207 }
208
209 const AppRouter = (this.options.env.singlePageApplication && this.options.env.hashedRoutes)
210 ? HashRouter : Router;
211
212 let RouterParams = {
213 history: this.history,
214 };
215 if (this.options.env.singlePageApplication) {
216 RouterParams = {};
217 }
218
219 const AppRoutes = {
220 renderedRoutes: renderRoutes(routes),
221 setRenderedRoutes: (r) => {
222 AppRoutes.renderedRoutes = r;
223 },
224 getRenderedRoutes: () => AppRoutes.renderedRoutes,
225 };
226
227 await Promise.all(promises).catch();
228
229 await (new Promise(r => this.hooks.renderRoutes.callAsync({
230 setRenderedRoutes: AppRoutes.setRenderedRoutes,
231 getRenderedRoutes: AppRoutes.getRenderedRoutes,
232 }, r)));
233
234 // await this.hooks.renderRoutes.callAsync({
235 // setRenderedRoutes: AppRoutes.setRenderedRoutes,
236 // getRenderedRoutes: AppRoutes.getRenderedRoutes,
237 // }, () => null);
238 const children = (
239 <AppRouter basename={env.appRootUrl} {...RouterParams}>
240 {AppRoutes.renderedRoutes}
241 </AppRouter>
242 );
243 const Application = {
244 children,
245 currentRoutes: currentPageRoutes.slice(0),
246 routes: routes.slice(0),
247 };
248
249 return new Promise((resolve) => {
250 this.hooks.beforeRender.callAsync(Application, async () => {
251 // Render according to routes!
252 renderer(
253 <ErrorBoundary ErrorComponent={routeHandler.getErrorComponent()}>
254 {Application.children}
255 </ErrorBoundary>,
256 domRootReference,
257 // div,
258 () => {
259 window.PAW_PRELOADED_DATA = null;
260 delete window.PAW_PRELOADED_DATA;
261 if (document.getElementById('__pawjs_preloaded')) {
262 document.getElementById('__pawjs_preloaded').remove();
263 }
264 this.hooks.renderComplete.call();
265 resolve();
266 },
267 );
268 });
269 });
270 }
271}