Skip to content

Commit d369907

Browse files
committed
feat: don't load already loaded resource
1 parent 009c832 commit d369907

5 files changed

Lines changed: 153 additions & 4 deletions

File tree

e2e/test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,39 @@ describe('basic-loader', function () {
1414
});
1515
});
1616

17+
beforeEach(() => {
18+
document.querySelectorAll('[src="foo.js"]').forEach((tag) => tag.remove());
19+
document.querySelectorAll('[src="bar.png"]').forEach((tag) => tag.remove());
20+
document.querySelectorAll('[href="baz.css"]').forEach((tag) => tag.remove());
21+
});
22+
1723
it('should load javascript files', async () => {
1824
expect(window.foo).to.be.undefined;
1925
await load.js('foo.js');
2026
expect(window.foo).to.be.true;
2127
delete window.foo;
2228
});
2329

30+
it('should not load javascript files more than once', async () => {
31+
await load.js('foo.js');
32+
await load.js('foo.js');
33+
expect(document.querySelectorAll('script[src="foo.js"]').length).to.equal(1);
34+
delete window.foo;
35+
});
36+
37+
it('should work with preload/prefetch/... of javascript files', async () => {
38+
const link = document.createElement('link');
39+
link.rel = 'preload';
40+
link.href = 'foo.js';
41+
link.as = 'script';
42+
document.head.appendChild(link);
43+
44+
expect(window.foo).to.be.undefined;
45+
await load.js('foo.js');
46+
expect(window.foo).to.be.true;
47+
delete window.foo;
48+
});
49+
2450
it('should raise a promise error for non-existent javascript files', (done) => {
2551
load.js('foo-missing.js').catch(() => done());
2652
});
@@ -31,6 +57,24 @@ describe('basic-loader', function () {
3157
expect(document.getElementsByTagName('img')).to.have.length(1);
3258
});
3359

60+
it('should not load image files more than once', async () => {
61+
await load.img('bar.png');
62+
await load.img('bar.png');
63+
expect(document.querySelectorAll('img[src="bar.png"]').length).to.equal(1);
64+
});
65+
66+
it('should work with preload/prefetch/... of image files', async () => {
67+
const link = document.createElement('link');
68+
link.rel = 'preload';
69+
link.href = 'bar.png';
70+
link.as = 'image';
71+
document.head.appendChild(link);
72+
73+
expect(document.getElementsByTagName('img').length).to.equal(0);
74+
await load.img('bar.png');
75+
expect(document.getElementsByTagName('img')).to.have.length(1);
76+
});
77+
3478
it('should raise a promise error for non-existent image files', (done) => {
3579
load.img('bar-missing.png').catch(() => done());
3680
});
@@ -42,6 +86,25 @@ describe('basic-loader', function () {
4286
expect(getProp(target, 'color')).to.equal('rgb(255, 0, 0)');
4387
});
4488

89+
it('should not load css files more than once', async () => {
90+
await load.css('baz.css');
91+
await load.css('baz.css');
92+
expect(document.querySelectorAll('link[href="baz.css"]').length).to.equal(1);
93+
});
94+
95+
it('should work with preload/prefetch/... of css files', async () => {
96+
const link = document.createElement('link');
97+
link.rel = 'preload';
98+
link.href = 'baz.css';
99+
link.as = 'style';
100+
document.head.appendChild(link);
101+
102+
const target = document.getElementById('css-target');
103+
expect(getProp(target, 'color')).to.equal('rgb(0, 0, 0)'); // phantom is terribly picky about props
104+
await load.css('baz.css');
105+
expect(getProp(target, 'color')).to.equal('rgb(255, 0, 0)');
106+
});
107+
45108
it('should raise a promise error for non-existent css files', (done) => {
46109
load.img('baz-missing.css').catch(() => done());
47110
});

lib/basic-loader-amd.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ define(["exports"], function (_exports) {
1414

1515
var BODY = 'body';
1616
var HEAD = 'head';
17+
var tagToUrlAttribute = {
18+
script: 'src',
19+
link: 'href',
20+
img: 'src'
21+
};
1722

1823
var _load = function _load(tag) {
1924
// attributes example: { 'data-test': 'new-attribute-here' }
@@ -26,6 +31,7 @@ define(["exports"], function (_exports) {
2631
parent = _tagToTagDetailsFuncs.parent,
2732
tagAttributes = _tagToTagDetailsFuncs.attributes;
2833

34+
if (isAlreadyLoaded(tag, tagAttributes)) return resolve(url);
2935
var element = document.createElement(tag);
3036

3137
element.onload = function () {
@@ -46,6 +52,16 @@ define(["exports"], function (_exports) {
4652
};
4753
};
4854

55+
var isAlreadyLoaded = function isAlreadyLoaded(tag, attributes) {
56+
var urlAttribute = tagToUrlAttribute[tag];
57+
var url = attributes[urlAttribute];
58+
var rel = attributes.rel || ''; // script[src="some-url"]
59+
// link[href="some-url"][rel="stylesheet"]
60+
// img[src="some-url"]
61+
62+
return Boolean(document.querySelector("".concat(tag, "[").concat(urlAttribute, "=\"").concat(url, "\"]").concat(rel ? "[rel=\"".concat(rel, "\"]") : '')));
63+
};
64+
4965
var getScriptTagDetails = function getScriptTagDetails(url, attributes) {
5066
var hasNoAsyncOrDefer = !('async' in attributes || 'defer' in attributes);
5167
return {
@@ -82,8 +98,7 @@ define(["exports"], function (_exports) {
8298
script: getScriptTagDetails,
8399
link: getLinkTagDetails,
84100
img: getImgTagDetails
85-
}; // exporting a "default" would render the amd package to work differently
86-
101+
};
87102
var _default = {
88103
css: _load('link'),
89104
js: _load('script'),

lib/basic-loader.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
1313

1414
var BODY = 'body';
1515
var HEAD = 'head';
16+
var tagToUrlAttribute = {
17+
script: 'src',
18+
link: 'href',
19+
img: 'src'
20+
};
1621

1722
var _load = function _load(tag) {
1823
// attributes example: { 'data-test': 'new-attribute-here' }
@@ -25,6 +30,7 @@ var _load = function _load(tag) {
2530
parent = _tagToTagDetailsFuncs.parent,
2631
tagAttributes = _tagToTagDetailsFuncs.attributes;
2732

33+
if (isAlreadyLoaded(tag, tagAttributes)) return resolve(url);
2834
var element = document.createElement(tag);
2935

3036
element.onload = function () {
@@ -45,6 +51,16 @@ var _load = function _load(tag) {
4551
};
4652
};
4753

54+
var isAlreadyLoaded = function isAlreadyLoaded(tag, attributes) {
55+
var urlAttribute = tagToUrlAttribute[tag];
56+
var url = attributes[urlAttribute];
57+
var rel = attributes.rel || ''; // script[src="some-url"]
58+
// link[href="some-url"][rel="stylesheet"]
59+
// img[src="some-url"]
60+
61+
return Boolean(document.querySelector("".concat(tag, "[").concat(urlAttribute, "=\"").concat(url, "\"]").concat(rel ? "[rel=\"".concat(rel, "\"]") : '')));
62+
};
63+
4864
var getScriptTagDetails = function getScriptTagDetails(url, attributes) {
4965
var hasNoAsyncOrDefer = !('async' in attributes || 'defer' in attributes);
5066
return {
@@ -81,8 +97,7 @@ var tagToTagDetailsFuncs = {
8197
script: getScriptTagDetails,
8298
link: getLinkTagDetails,
8399
img: getImgTagDetails
84-
}; // exporting a "default" would render the amd package to work differently
85-
100+
};
86101
var _default = {
87102
css: _load('link'),
88103
js: _load('script'),

src/basic-loader.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
const BODY = 'body';
22
const HEAD = 'head';
33

4+
const tagToUrlAttribute = {
5+
script: 'src',
6+
link: 'href',
7+
img: 'src',
8+
};
9+
410
const _load = (tag) => {
511
// attributes example: { 'data-test': 'new-attribute-here' }
612
return (url, attributes = {}) => {
713
// this promise will be used by Promise.all to determine success or failure
814
return new Promise((resolve, reject) => {
915
// need to set different attributes depending on tag type
1016
const { parent, attributes: tagAttributes } = tagToTagDetailsFuncs[tag](url, attributes);
17+
if (isAlreadyLoaded(tag, tagAttributes)) return resolve(url);
18+
1119
const element = document.createElement(tag);
1220
element.onload = () => resolve(url);
1321
element.onerror = () => reject(url); // maybe we should remove the broken node, who knows
@@ -18,6 +26,17 @@ const _load = (tag) => {
1826
};
1927
};
2028

29+
const isAlreadyLoaded = (tag, attributes) => {
30+
const urlAttribute = tagToUrlAttribute[tag];
31+
const url = attributes[urlAttribute];
32+
const rel = attributes.rel || '';
33+
34+
// script[src="some-url"]
35+
// link[href="some-url"][rel="stylesheet"]
36+
// img[src="some-url"]
37+
return Boolean(document.querySelector(`${tag}[${urlAttribute}="${url}"]${rel ? `[rel="${rel}"]` : ''}`));
38+
};
39+
2140
const getScriptTagDetails = (url, attributes) => {
2241
const hasNoAsyncOrDefer = !('async' in attributes || 'defer' in attributes);
2342
return {

src/basic-loader.spec.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('basic-loader', () => {
99
body: { appendChild: sinon.fake() },
1010
head: { appendChild: sinon.fake() },
1111
createElement: sinon.fake.returns({ setAttribute: sinon.fake() }),
12+
querySelector: sinon.stub(),
1213
};
1314
});
1415

@@ -110,4 +111,40 @@ describe('basic-loader', () => {
110111
['defer', ''],
111112
]);
112113
});
114+
115+
it('should not append <link> tag when already loaded for inserting css', () => {
116+
global.document.querySelector.returns(true);
117+
load.css('foo.css');
118+
expect(document.head.appendChild).to.have.not.been.called;
119+
});
120+
121+
it('should append <script> tag with default attributes for inserting js', () => {
122+
global.document.querySelector.returns(true);
123+
load.js('bar.js');
124+
expect(document.body.appendChild).to.have.not.been.called;
125+
});
126+
127+
it('should append <img> tag with default attributes for inserting img', () => {
128+
global.document.querySelector.returns(true);
129+
load.img('baz.jpg');
130+
expect(document.body.appendChild).to.have.not.been.called;
131+
});
132+
133+
it('should consider `rel` in already loaded check for inserting css', () => {
134+
load.css('foo.css');
135+
expect(global.document.querySelector).to.have.been.calledOnce;
136+
expect(global.document.querySelector.firstCall.args).to.eql(['link[href="foo.css"][rel="stylesheet"]']);
137+
});
138+
139+
it('should not include `rel` in already loaded check for inserting js', () => {
140+
load.js('bar.js');
141+
expect(global.document.querySelector).to.have.been.calledOnce;
142+
expect(global.document.querySelector.firstCall.args).to.eql(['script[src="bar.js"]']);
143+
});
144+
145+
it('should not include `rel` in already loaded check for inserting img', () => {
146+
load.img('baz.jpg');
147+
expect(global.document.querySelector).to.have.been.calledOnce;
148+
expect(global.document.querySelector.firstCall.args).to.eql(['img[src="baz.jpg"]']);
149+
});
113150
});

0 commit comments

Comments
 (0)