Skip to content

Commit

Permalink
[feat] (options): Add a hrefFn option to build the URL to prefetch. (
Browse files Browse the repository at this point in the history
…#201)

* feat(options): Add a `hrefFn` option to build the URL to prefetch.

* fix conflicts on tests

* adding hrefFn usage simple SPA example

* added hrefFn demo within README

* added hrefFn demo within README

* improved hrefFn example code style

Co-authored-by: Ruben Jimenez <[email protected]>
  • Loading branch information
gilbertococchi and ruben0626 authored Nov 3, 2020
1 parent a8872b8 commit ee072d4
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 32 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ Default: None
An optional error handler that will receive any errors from prefetched requests.<br>
By default, these errors are silently ignored.
#### options.hrefFn
Type: `Function`<br>
Default: None
An optional function to generate the URL to prefetch. It receives an [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) as the argument.
### quicklink.prefetch(urls, isPriority)
Returns: `Promise`
Expand Down Expand Up @@ -339,6 +344,18 @@ quicklink.listen({
});
```
### Custom URL to prefetch via hrefFn callback
The hrefFn method allows to build the URL to prefetch (e.g. API endpoint) on the fly instead of the prefetching the `href` attribute URL.
```js
quicklink.listen({
hrefFn: function(element) {
return element.href.replace('html','json');
}
});
```
## Browser Support
The prefetching provided by `quicklink` can be viewed as a [progressive enhancement](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/). Cross-browser support is as follows:
Expand Down Expand Up @@ -374,6 +391,7 @@ After installing `quicklink` as a dependency, you can use it as follows:
* [Using Quicklink in a multi-page site](https://github.com/GoogleChromeLabs/quicklink/tree/master/demos/news)
* [Using Quicklink with Service Workers (via Workbox)](https://github.com/GoogleChromeLabs/quicklink/tree/master/demos/news-workbox)
* [Using Quicklink to prefetch API calls instead of `href` attribute](https://github.com/GoogleChromeLabs/quicklink/tree/master/demos/hrefFn)
### Research
Expand Down
18 changes: 18 additions & 0 deletions demos/hrefFn/2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Prefetch experiments</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" media="screen" href="main.css">
</head>

<body>
<h1>Page 2</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam
totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!</p>
</body>

</html>
4 changes: 4 additions & 0 deletions demos/hrefFn/2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "API Target to Prefetch Example",
"description" : "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!"
}
57 changes: 57 additions & 0 deletions demos/hrefFn/hrefFn_demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Basic demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" media="screen" href="../../test/main.css" />
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
</head>
<body>
<div class="screen">
<h1>Basic demo</h1>
<p>
<a href="2.html">Link 1</a>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
<script src="../../dist/quicklink.umd.js"></script>
<script>
quicklink.listen({
el: document.querySelector('div.screen'),
// hrefFn will prevent quicklink to prefetch the "href" attribute URL
// and the returned value will be prefetched as a result.
hrefFn: function(element) {
return element.href.replace('html','json');
}
});
</script>

<script>
// Simple SPA-like page transition for demo purposes
var container = document.querySelector('div.screen'),
link = container.querySelector('a'),
title = container.querySelector('h1'),
paragraph = container.querySelector('p');

link.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var href = e.target.href.replace('html','json');
fetch(href)
.then(response => response.json())
.then(data => {
title.innerHTML = document.title = data.title;
paragraph.innerHTML = data.description;
});
});
</script>
</body>
</html>
26 changes: 15 additions & 11 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
**/
import throttle from 'throttles';
import { priority, supported } from './prefetch.mjs';
import {priority, supported} from './prefetch.mjs';
import requestIdleCallback from './request-idle-callback.mjs';

// Cache of URLs we've prefetched
Expand Down Expand Up @@ -52,6 +52,9 @@ function isIgnored(node, filter) {
* @param {Number} [options.limit] - The total number of prefetches to allow
* @param {Function} [options.timeoutFn] - Custom timeout function
* @param {Function} [options.onError] - Error handler for failed `prefetch` requests
* @param {Function} [options.hrefFn] - Function to use to build the URL to prefetch.
* If it's not a valid function, then it will use the entry href.
* @return {Function}
*/
export function listen(options) {
if (!options) options = {};
Expand All @@ -64,6 +67,7 @@ export function listen(options) {
const ignores = options.ignores || [];

const timeoutFn = options.timeoutFn || requestIdleCallback;
const hrefFn = typeof options.hrefFn === 'function' && options.hrefFn;

const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
Expand All @@ -72,7 +76,7 @@ export function listen(options) {
// Do not prefetch if will match/exceed limit
if (toPrefetch.size < limit) {
toAdd(() => {
prefetch(entry.href, options.priority).then(isDone).catch(err => {
prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority).then(isDone).catch(err => {
isDone(); if (options.onError) options.onError(err);
});
});
Expand All @@ -92,7 +96,7 @@ export function listen(options) {
}
});
}, {
timeout: options.timeout || 2000
timeout: options.timeout || 2000,
});

return function () {
Expand Down Expand Up @@ -124,16 +128,16 @@ export function prefetch(url, isPriority, conn) {

// Dev must supply own catch()
return Promise.all(
[].concat(url).map(str => {
if (!toPrefetch.has(str)) {
[].concat(url).map(str => {
if (!toPrefetch.has(str)) {
// Add it now, regardless of its success
// ~> so that we don't repeat broken links
toPrefetch.add(str);
toPrefetch.add(str);

return (isPriority ? priority : supported)(
new URL(str, location.href).toString()
);
}
})
return (isPriority ? priority : supported)(
new URL(str, location.href).toString()
);
}
})
);
}
1 change: 1 addition & 0 deletions src/prefetch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
/**
* Checks if a feature on `link` is natively supported.
* Examples of features include `prefetch` and `preload`.
* @param {Object} link Link object.
* @return {Boolean} whether the feature is supported
*/
function hasPrefetch(link) {
Expand Down
33 changes: 13 additions & 20 deletions test/quicklink.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('quicklink tests', function () {
expect(responseURLs).to.be.an('array');
// => origins: true
expect(responseURLs).to.include(`${server}/2.html`);
expect(responseURLs).to.include('https://example.com/3.html');
expect(responseURLs).to.include('https://google.com/');
expect(responseURLs).to.include('https://example.com/1.html');
expect(responseURLs).to.include('https://github.githubassets.com/images/spinners/octocat-spinner-32.gif');
});
Expand Down Expand Up @@ -239,7 +239,7 @@ describe('quicklink tests', function () {
if (/test\/\d+\.html$/i.test(req.url())) {
await sleep(100);
URLs.push(req.url());
return req.respond({ status: 200 });
return req.respond({status: 200});
}
req.continue();
});
Expand All @@ -256,28 +256,21 @@ describe('quicklink tests', function () {
await page.waitFor(250);
expect(URLs.length).to.equal(4);
});
it('should prefetch chunks for in-viewport links', async function () {

it('should prefetch using a custom function to build the URL', async function () {
const responseURLs = [];
page.on('response', resp => {
responseURLs.push(resp.url());
});
await page.goto(`${server}/test-prefetch-chunks.html`);

await page.goto(`${server}/test-custom-href-function.html`);
await page.waitFor(1000);
expect(responseURLs).to.be.an('array');
// should prefetch chunk URLs for /, /blog and /about links
expect(responseURLs).to.include(`${host}/test/static/css/home.6d953f22.chunk.css`);
expect(responseURLs).to.include(`${host}/test/static/js/home.14835906.chunk.js`);
expect(responseURLs).to.include(`${host}/test/static/media/video.b9b6e9e1.svg`);
expect(responseURLs).to.include(`${host}/test/static/css/blog.2a8b6ab6.chunk.css`);
expect(responseURLs).to.include(`${host}/test/static/js/blog.1dcce8a6.chunk.js`);
expect(responseURLs).to.include(`${host}/test/static/css/about.00ec0d84.chunk.css`);
expect(responseURLs).to.include(`${host}/test/static/js/about.921ebc84.chunk.js`);
// should not prefetch /, /blog and /about links
expect(responseURLs).to.not.include(`${server}`);
expect(responseURLs).to.not.include(`${server}/blog`);
expect(responseURLs).to.not.include(`${server}/about`);
// should prefetch regular links
expect(responseURLs).to.include(`${server}/main.css`);

// don't care about first 4 URLs (markup)
const ours = responseURLs.slice(4);
expect(ours).to.include(`https://example.com/?url=${server}/1.html`);
expect(ours).to.include(`https://example.com/?url=${server}/2.html`);
expect(ours).to.include(`https://example.com/?url=${server}/3.html`);
expect(ours).to.include(`https://example.com/?url=${server}/4.html`);
});
});
2 changes: 1 addition & 1 deletion test/test-allow-origin-all.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<body>
<a href="https://example.com/1.html">Link 1</a>
<a href="2.html">Link 2</a>
<a href="https://example.com/3.html">Link 3</a>
<a href="https://google.com/">Link 3</a>
<a href="https://github.githubassets.com/images/spinners/octocat-spinner-32.gif">Spinner</a>
<section id="stuff">
<a href="main.css">CSS</a>
Expand Down
26 changes: 26 additions & 0 deletions test/test-custom-href-function.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Prefetch: Custom function to build the URL.</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" media="screen" href="main.css">
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
</head>

<body>
<a href="1.html">Link 1</a>
<a href="2.html">Link 2</a>
<a href="3.html">Link 3</a>
<a href="4.html">Link 4</a>
<script src="../dist/quicklink.umd.js"></script>
<script>
quicklink.listen({
hrefFn: (entry) => ( `https://example.com/?url=${entry.href}` ),
});
</script>
</body>

</html>

0 comments on commit ee072d4

Please sign in to comment.