Skip to content

Commit 4b40507

Browse files
committed
Add initial react-resolver experiments
1 parent d03075f commit 4b40507

File tree

8 files changed

+316
-32
lines changed

8 files changed

+316
-32
lines changed

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@
2424
"main": "server.js",
2525
"dependencies": {
2626
"ejs": "^2.4.1",
27+
"es6-promise": "^3.2.1",
28+
"eslint-config-jonnybuchanan": "2.0.3",
2729
"events": "1.1.0",
2830
"express": "^4.13.4",
2931
"firebase": "2.4.2",
3032
"history": "2.1.1",
3133
"isomorphic-fetch": "^2.2.1",
34+
"nwb": "0.8.1",
3235
"react": "15.0.2",
3336
"react-dom": "15.0.2",
37+
"react-resolver": "^3.0.1",
3438
"react-router": "2.4.0",
3539
"react-timeago": "3.0.0",
3640
"reactfire": "0.7.0",
3741
"scroll-behavior": "0.5.0",
3842
"setimmediate": "1.0.4",
39-
"url-parse": "^1.1.1",
40-
"eslint-config-jonnybuchanan": "2.0.3",
41-
"nwb": "0.8.1",
4243
"sw-precache": "^3.1.1",
43-
"sw-toolbox": "^3.1.1"
44+
"sw-toolbox": "^3.1.1",
45+
"url-parse": "^1.1.1"
4446
}
4547
}

public/service-worker.js

Lines changed: 249 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,249 @@
1-
// This file is intentionally without code.
2-
// It's present so that service worker registration will work when serving from the 'public' directory.
1+
/**
2+
* Copyright 2015 Google Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// This generated service worker JavaScript will precache your site's resources.
18+
// The code needs to be saved in a .js file at the top-level of your site, and registered
19+
// from your pages in order to be used. See
20+
// https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js
21+
// for an example of how you can register this script and handle various service worker events.
22+
23+
/* eslint-env worker, serviceworker */
24+
/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren */
25+
'use strict';
26+
27+
28+
29+
30+
importScripts("sw-toolbox.js","runtime-caching.js");
31+
32+
33+
/* eslint-disable quotes, comma-spacing */
34+
var PrecacheConfig = [["build/app.js","313d71d0a78527fb7beef3c3ff0138eb"],["build/app.js.map","44e5c1202bd9dc38796ebd8cb1c14040"],["build/vendor.js","f7535c3346e07c644907b4c1fec47a1b"],["css/style.css","ebd9bc483b1656c536c56c3035432bdd"],["img/android-chrome-144x144.png","31f44c8f8845e41196b389f2fdae392d"],["img/android-chrome-192x192.png","7c3470aa18f85e4454a35ef3ff8b8f6e"],["img/android-chrome-36x36.png","fc5a14316848badbd501e198f8607088"],["img/android-chrome-48x48.png","f8576bca4be18a4367e4847a09fc6945"],["img/android-chrome-72x72.png","6b26a8a135b07174d298489cc010083a"],["img/android-chrome-96x96.png","04cda150a70eb58221af3fdd4f7d4a6f"],["img/apple-touch-icon-114x114.png","e18affb685f0457672283a88c04084c9"],["img/apple-touch-icon-120x120.png","cd14469c7457cfc6d3aaf15d34faeddf"],["img/apple-touch-icon-144x144.png","95a8cb7d006c59252dd68ba73d31632a"],["img/apple-touch-icon-152x152.png","15dd03590ff7289c09cf10027597e699"],["img/apple-touch-icon-180x180.png","0b101591e8e263c6bff9133c7772194a"],["img/apple-touch-icon-57x57.png","628a477075d84a8d0996392aa6dec37c"],["img/apple-touch-icon-60x60.png","6b9fe001bc9e35320f9bb4eb28b1e6f1"],["img/apple-touch-icon-72x72.png","5830f2a4f9249b3bc3998481cc00825d"],["img/apple-touch-icon-76x76.png","812e9eb119b6bdd8f465a2d1118465b9"],["img/apple-touch-icon-precomposed.png","e45a9a06a4a9b850e3089c4e6e3ebc8d"],["img/apple-touch-icon.png","0b101591e8e263c6bff9133c7772194a"],["img/browserconfig.xml","f337354b6f80663075e7b32058c65149"],["img/favicon-16x16.png","9d784dc3f4da5477156423f5f106c1c6"],["img/favicon-32x32.png","21ea2cf9cd43cdc1f808cca76a1f6fa4"],["img/favicon-96x96.png","11e36fff4c95b572ffaeef9a848da568"],["img/favicon.ico","eaa33e22fc5dab05262d316b59160a45"],["img/logo.png","930a492dadf1ccb881bd91d424c8bf9e"],["img/mstile-144x144.png","3e9a3c273f9ac3b7a158132445534860"],["img/mstile-150x150.png","b0af3ec429e6828dc0606d8bb8e1421f"],["img/mstile-310x150.png","499b08d0d170e6ed89491d7e9691a8e8"],["img/mstile-310x310.png","625111493ee72a39db1420c9c235dfb3"],["img/mstile-70x70.png","4cdf64d2b55d8116c4ce8dd361a95772"],["img/safari-pinned-tab.svg","9bfe87bb482c5d6facab0d0084ce1e80"],["img/splashscreen-icon-384x384.png","e3080842f30a9137e1464f01ffb97e71"],["index-static.html","6b98cb5ee877097cadbd77807342c96c"],["manifest.json","3c0f2d46b398124d8358a69be42ee0c2"],["runtime-caching.js","87003e567d298b1b58cf2f57b4fb0ee2"],["service-worker.js","432e0ae0b3a06471d0c62c6844b88d07"],["sw-toolbox.js","42dd9073ba0a0c8e0ae2230432870678"]];
35+
/* eslint-enable quotes, comma-spacing */
36+
var CacheNamePrefix = 'sw-precache-v1-sw-precache-' + (self.registration ? self.registration.scope : '') + '-';
37+
38+
39+
var IgnoreUrlParametersMatching = [/^utm_/];
40+
41+
42+
43+
var addDirectoryIndex = function (originalUrl, index) {
44+
var url = new URL(originalUrl);
45+
if (url.pathname.slice(-1) === '/') {
46+
url.pathname += index;
47+
}
48+
return url.toString();
49+
};
50+
51+
var getCacheBustedUrl = function (url, now) {
52+
now = now || Date.now();
53+
54+
var urlWithCacheBusting = new URL(url);
55+
urlWithCacheBusting.search += (urlWithCacheBusting.search ? '&' : '') +
56+
'sw-precache=' + now;
57+
58+
return urlWithCacheBusting.toString();
59+
};
60+
61+
var isPathWhitelisted = function (whitelist, absoluteUrlString) {
62+
// If the whitelist is empty, then consider all URLs to be whitelisted.
63+
if (whitelist.length === 0) {
64+
return true;
65+
}
66+
67+
// Otherwise compare each path regex to the path of the URL passed in.
68+
var path = (new URL(absoluteUrlString)).pathname;
69+
return whitelist.some(function(whitelistedPathRegex) {
70+
return path.match(whitelistedPathRegex);
71+
});
72+
};
73+
74+
var populateCurrentCacheNames = function (precacheConfig,
75+
cacheNamePrefix, baseUrl) {
76+
var absoluteUrlToCacheName = {};
77+
var currentCacheNamesToAbsoluteUrl = {};
78+
79+
precacheConfig.forEach(function(cacheOption) {
80+
var absoluteUrl = new URL(cacheOption[0], baseUrl).toString();
81+
var cacheName = cacheNamePrefix + absoluteUrl + '-' + cacheOption[1];
82+
currentCacheNamesToAbsoluteUrl[cacheName] = absoluteUrl;
83+
absoluteUrlToCacheName[absoluteUrl] = cacheName;
84+
});
85+
86+
return {
87+
absoluteUrlToCacheName: absoluteUrlToCacheName,
88+
currentCacheNamesToAbsoluteUrl: currentCacheNamesToAbsoluteUrl
89+
};
90+
};
91+
92+
var stripIgnoredUrlParameters = function (originalUrl,
93+
ignoreUrlParametersMatching) {
94+
var url = new URL(originalUrl);
95+
96+
url.search = url.search.slice(1) // Exclude initial '?'
97+
.split('&') // Split into an array of 'key=value' strings
98+
.map(function(kv) {
99+
return kv.split('='); // Split each 'key=value' string into a [key, value] array
100+
})
101+
.filter(function(kv) {
102+
return ignoreUrlParametersMatching.every(function(ignoredRegex) {
103+
return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.
104+
});
105+
})
106+
.map(function(kv) {
107+
return kv.join('='); // Join each [key, value] array into a 'key=value' string
108+
})
109+
.join('&'); // Join the array of 'key=value' strings into a string with '&' in between each
110+
111+
return url.toString();
112+
};
113+
114+
115+
var mappings = populateCurrentCacheNames(PrecacheConfig, CacheNamePrefix, self.location);
116+
var AbsoluteUrlToCacheName = mappings.absoluteUrlToCacheName;
117+
var CurrentCacheNamesToAbsoluteUrl = mappings.currentCacheNamesToAbsoluteUrl;
118+
119+
function deleteAllCaches() {
120+
return caches.keys().then(function(cacheNames) {
121+
return Promise.all(
122+
cacheNames.map(function(cacheName) {
123+
return caches.delete(cacheName);
124+
})
125+
);
126+
});
127+
}
128+
129+
self.addEventListener('install', function(event) {
130+
var now = Date.now();
131+
132+
event.waitUntil(
133+
caches.keys().then(function(allCacheNames) {
134+
return Promise.all(
135+
Object.keys(CurrentCacheNamesToAbsoluteUrl).filter(function(cacheName) {
136+
return allCacheNames.indexOf(cacheName) === -1;
137+
}).map(function(cacheName) {
138+
var urlWithCacheBusting = getCacheBustedUrl(CurrentCacheNamesToAbsoluteUrl[cacheName],
139+
now);
140+
141+
return caches.open(cacheName).then(function(cache) {
142+
var request = new Request(urlWithCacheBusting, {credentials: 'same-origin'});
143+
return fetch(request).then(function(response) {
144+
if (response.ok) {
145+
return cache.put(CurrentCacheNamesToAbsoluteUrl[cacheName], response);
146+
}
147+
148+
console.error('Request for %s returned a response with status %d, so not attempting to cache it.',
149+
urlWithCacheBusting, response.status);
150+
// Get rid of the empty cache if we can't add a successful response to it.
151+
return caches.delete(cacheName);
152+
});
153+
});
154+
})
155+
).then(function() {
156+
return Promise.all(
157+
allCacheNames.filter(function(cacheName) {
158+
return cacheName.indexOf(CacheNamePrefix) === 0 &&
159+
!(cacheName in CurrentCacheNamesToAbsoluteUrl);
160+
}).map(function(cacheName) {
161+
return caches.delete(cacheName);
162+
})
163+
);
164+
});
165+
}).then(function() {
166+
if (typeof self.skipWaiting === 'function') {
167+
// Force the SW to transition from installing -> active state
168+
self.skipWaiting();
169+
}
170+
})
171+
);
172+
});
173+
174+
if (self.clients && (typeof self.clients.claim === 'function')) {
175+
self.addEventListener('activate', function(event) {
176+
event.waitUntil(self.clients.claim());
177+
});
178+
}
179+
180+
self.addEventListener('message', function(event) {
181+
if (event.data.command === 'delete_all') {
182+
console.log('About to delete all caches...');
183+
deleteAllCaches().then(function() {
184+
console.log('Caches deleted.');
185+
event.ports[0].postMessage({
186+
error: null
187+
});
188+
}).catch(function(error) {
189+
console.log('Caches not deleted:', error);
190+
event.ports[0].postMessage({
191+
error: error
192+
});
193+
});
194+
}
195+
});
196+
197+
198+
self.addEventListener('fetch', function(event) {
199+
if (event.request.method === 'GET') {
200+
var urlWithoutIgnoredParameters = stripIgnoredUrlParameters(event.request.url,
201+
IgnoreUrlParametersMatching);
202+
203+
var cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
204+
var directoryIndex = 'index.html';
205+
if (!cacheName && directoryIndex) {
206+
urlWithoutIgnoredParameters = addDirectoryIndex(urlWithoutIgnoredParameters, directoryIndex);
207+
cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
208+
}
209+
210+
var navigateFallback = '';
211+
// Ideally, this would check for event.request.mode === 'navigate', but that is not widely
212+
// supported yet:
213+
// https://code.google.com/p/chromium/issues/detail?id=540967
214+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1209081
215+
if (!cacheName && navigateFallback && event.request.headers.has('accept') &&
216+
event.request.headers.get('accept').includes('text/html') &&
217+
/* eslint-disable quotes, comma-spacing */
218+
isPathWhitelisted([], event.request.url)) {
219+
/* eslint-enable quotes, comma-spacing */
220+
var navigateFallbackUrl = new URL(navigateFallback, self.location);
221+
cacheName = AbsoluteUrlToCacheName[navigateFallbackUrl.toString()];
222+
}
223+
224+
if (cacheName) {
225+
event.respondWith(
226+
// Rely on the fact that each cache we manage should only have one entry, and return that.
227+
caches.open(cacheName).then(function(cache) {
228+
return cache.keys().then(function(keys) {
229+
return cache.match(keys[0]).then(function(response) {
230+
if (response) {
231+
return response;
232+
}
233+
// If for some reason the response was deleted from the cache,
234+
// raise and exception and fall back to the fetch() triggered in the catch().
235+
throw Error('The cache ' + cacheName + ' is empty.');
236+
});
237+
});
238+
}).catch(function(e) {
239+
console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
240+
return fetch(event.request);
241+
})
242+
);
243+
}
244+
}
245+
});
246+
247+
248+
249+

server.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ var express = require('express')
22
var React = require('react')
33
var renderToString = require('react-dom/server').renderToString
44
var ReactRouter = require('react-router')
5+
var Resolver = require('react-resolver').Resolver
6+
var RouterContext = React.createFactory(ReactRouter.RouterContext)
57

68
require('babel/register')
79
var routes = require('./src/routes')
@@ -16,18 +18,27 @@ app.get('*', function(req, res) {
1618
ReactRouter.match({
1719
routes: routes,
1820
location: req.url
19-
}, function(err, redirectLocation, props) {
21+
}, function(err, redirectLocation, renderProps) {
2022
if (err) {
2123
res.status(500).send(err.message)
2224
}
2325
else if (redirectLocation) {
2426
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
2527
}
26-
else if (props) {
27-
var markup = renderToString(
28-
React.createElement(ReactRouter.RouterContext, props, null)
29-
)
30-
res.render('index', { markup: markup })
28+
else if (renderProps) {
29+
// https://github.com/allenkim67/isomorphic-demo/blob/2be59306196e84f66041dc7a03bbd0c805d371aa/server.js
30+
Resolver
31+
.resolve(function() {return RouterContext(renderProps)})
32+
.then(function(resolverRes) {
33+
console.log(resolverRes.data)
34+
var markup = renderToString(
35+
React.createElement(resolverRes.Resolved, renderProps, null)
36+
)
37+
res.render('index', {
38+
markup: markup,
39+
scriptTag: '<script>window.__REACT_RESOLVER_PAYLOAD__ = ' + JSON.stringify(resolverRes.data) + '</script>'
40+
})
41+
})
3142
}
3243
else {
3344
res.sendStatus(404)

src/Comment.js

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ var React = require('react')
22
var ReactFireMixin = require('reactfire')
33

44
var CommentThreadStore = require('./stores/CommentThreadStore')
5-
var HNService = require('./services/HNService')
5+
// var HNService = require('./services/HNService')
66
var HNServiceRest = require('./services/HNServiceRest')
77
var SettingsStore = require('./stores/SettingsStore')
88

99
var CommentMixin = require('./mixins/CommentMixin')
1010

1111
var cx = require('./utils/buildClassName')
12+
var resolve = require('react-resolver').resolve
1213

1314
/**
1415
* A comment in a thread.
@@ -86,21 +87,22 @@ var Comment = React.createClass({
8687
}
8788
},
8889

89-
bindFirebaseRef() {
90-
if (SettingsStore.offlineMode) {
91-
HNServiceRest.itemRef(this.props.id).then(function(res) {
92-
return res.json()
93-
}).then(function(snapshot) {
94-
this.replaceState({ comment: snapshot })
95-
}.bind(this))
96-
}
97-
else {
98-
this.bindAsObject(HNService.itemRef(this.props.id), 'comment', this.handleFirebaseRefCancelled)
99-
}
100-
101-
if (this.timeout) {
102-
this.timeout = null
103-
}
90+
bindFirebaseRef(props) {
91+
console.log('bindFirebaseRef', props)
92+
// if (SettingsStore.offlineMode) {
93+
// HNServiceRest.itemRef(props.id).then(function(res) {
94+
// return res.json()
95+
// }).then(function(snapshot) {
96+
// this.replaceState({ comment: snapshot })
97+
// }.bind(this))
98+
// }
99+
// else {
100+
// this.bindAsObject(HNService.itemRef(props.id), 'comment', this.handleFirebaseRefCancelled)
101+
// }
102+
103+
// if (this.timeout) {
104+
// this.timeout = null
105+
// }
104106
},
105107

106108
/**
@@ -186,4 +188,18 @@ var Comment = React.createClass({
186188
}
187189
})
188190

189-
module.exports = Comment
191+
/*
192+
What I'm attempting to do here is resolve comment so that we
193+
go through react-resolver anytime we need that data instead of
194+
directly through bindFirebaseRef per the resolver examples I have
195+
seen. Data seems to get returned client-side, but nothing correctly
196+
when doing the server-side render.
197+
*/
198+
module.exports = resolve('comment', function(props) {
199+
return HNServiceRest.itemRef(props.id).then(function(res) {
200+
return res.json()
201+
}).then(function(snapshot) {
202+
console.log('Comment snapshot:', snapshot)
203+
return snapshot
204+
})
205+
})(Comment)

0 commit comments

Comments
 (0)