Skip to content

Commit 4a89bb6

Browse files
mrxzchrisirhc
andauthored
Defer initialization until end of aframe script or manual ready signal (#5481)
Co-authored-by: Noeri Huisman <[email protected]> Co-authored-by: Chris Chua <[email protected]>
1 parent af4523e commit 4a89bb6

File tree

11 files changed

+166
-58
lines changed

11 files changed

+166
-58
lines changed

docs/introduction/faq.md

+33
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,39 @@ Phones with Adreno 300 series GPUs are notoriously problematic. Set [renderer pr
344344

345345
Using A-Frame online sometimes is not possible or inconvenient, like for instance when traveling or during public events with poor Internet connectivity. A-Frame is mostly self-contained so including the build (aframe.min.js) in your project will be sufficient in many cases. Some specific parts are lazy loaded and only fetched when used. This is for example the case of the fonts for the text component and the 3D models for controllers. In order to make an A-Frame build work either offline or without relying on A-Frame hosting infrastructure (typically cdn.aframe.io), you can monitor network requests on your browser console. This will show precisely what assets are being loaded and thus as required for your specific experience. Fonts can be found via FONT_BASE_URL in the whereas controllers via MODEL_URLS. Both can be modified in the source and included in your own [custom build](https://github.com/aframevr/aframe#generating-builds)
346346

347+
## Can I load A-Frame as an ES module?
348+
349+
You can load A-Frame as an ES module using a [side effect import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only). A-Frame will then initialize any `<a-scene>` in the document. It's still important to register any components or systems you need before this happens:
350+
351+
```HTML
352+
<head>
353+
<script type="importmap">
354+
{
355+
"imports": {
356+
"aframe": "https://aframe.io/releases/1.5.0/aframe.min.js",
357+
}
358+
}
359+
</script>
360+
<script type="module">
361+
import 'aframe';
362+
AFRAME.registerComponent('my-component', {
363+
...
364+
});
365+
</script>
366+
</head>
367+
```
368+
369+
If it's not possible to register everything synchronously to importing A-Frame, you can set the `window.AFRAME_ASYNC` flag. This prevents A-Frame from initializing `<a-scene>` tags, until you give a ready signal by calling `window.AFRAME.emitReady()`. Note that this flag must be set before importing A-Frame, as shown in the following example:
370+
371+
```JS
372+
window.AFRAME_ASYNC = true;
373+
await import('aframe');
374+
375+
// Asynchronously register components/systems
376+
377+
window.AFRAME.ready();
378+
```
379+
347380
## What order does A-Frame render objects in?
348381

349382
[sortTransparentObjects]: ../components/renderer.md#sorttransparentobjects

src/core/a-assets.js

+1-11
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,6 @@ class AAssets extends ANode {
1717
this.timeout = null;
1818
}
1919

20-
connectedCallback () {
21-
// Defer if DOM is not ready.
22-
if (document.readyState !== 'complete') {
23-
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
24-
return;
25-
}
26-
27-
this.doConnectedCallback();
28-
}
29-
3020
doConnectedCallback () {
3121
var self = this;
3222
var i;
@@ -38,7 +28,7 @@ class AAssets extends ANode {
3828
var timeout;
3929
var children;
4030

41-
super.connectedCallback();
31+
super.doConnectedCallback();
4232

4333
if (!this.parentNode.isScene) {
4434
throw new Error('<a-assets> must be a child of a <a-scene>.');

src/core/a-entity.js

+1-14
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,13 @@ class AEntity extends ANode {
5757
this.setEntityAttribute(attr, oldVal, newVal);
5858
}
5959

60-
/**
61-
* Add to parent, load, play.
62-
*/
63-
connectedCallback () {
64-
// Defer if DOM is not ready.
65-
if (document.readyState !== 'complete') {
66-
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
67-
return;
68-
}
69-
70-
AEntity.prototype.doConnectedCallback.call(this);
71-
}
72-
7360
doConnectedCallback () {
7461
var self = this; // Component.
7562
var assetsEl; // Asset management system element.
7663
var sceneEl;
7764

7865
// ANode method.
79-
super.connectedCallback();
66+
super.doConnectedCallback();
8067

8168
sceneEl = this.sceneEl;
8269

src/core/a-mixin.js

+1-11
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,8 @@ class AMixin extends ANode {
1818
this.isMixin = true;
1919
}
2020

21-
connectedCallback () {
22-
// Defer if DOM is not ready.
23-
if (document.readyState !== 'complete') {
24-
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
25-
return;
26-
}
27-
28-
this.doConnectedCallback();
29-
}
30-
3121
doConnectedCallback () {
32-
super.connectedCallback();
22+
super.doConnectedCallback();
3323

3424
this.sceneEl = this.closestScene();
3525
this.id = this.getAttribute('id');

src/core/a-node.js

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* global customElements, CustomEvent, HTMLElement, MutationObserver */
22
var utils = require('../utils/');
3+
var readyState = require('./readyState');
34

45
var warn = utils.debug('core:a-node:warn');
56

@@ -32,19 +33,13 @@ class ANode extends HTMLElement {
3233
this.mixinEls = [];
3334
}
3435

35-
onReadyStateChange () {
36-
if (document.readyState === 'complete') {
37-
this.doConnectedCallback();
38-
}
39-
}
40-
4136
connectedCallback () {
42-
// Defer if DOM is not ready.
43-
if (document.readyState !== 'complete') {
44-
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
37+
// Defer if not ready to initialize.
38+
if (!readyState.canInitializeElements) {
39+
document.addEventListener('aframeready', this.connectedCallback.bind(this));
4540
return;
4641
}
47-
ANode.prototype.doConnectedCallback.call(this);
42+
this.doConnectedCallback();
4843
}
4944

5045
doConnectedCallback () {

src/core/readyState.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* global CustomEvent */
2+
3+
/**
4+
* Flag indicating if A-Frame can initialize the scene or should wait.
5+
*/
6+
module.exports.canInitializeElements = false;
7+
8+
/**
9+
* Waits for the document to be ready.
10+
*/
11+
function waitForDocumentReadyState () {
12+
if (document.readyState === 'complete') {
13+
emitReady();
14+
return;
15+
}
16+
17+
document.addEventListener('readystatechange', function onReadyStateChange () {
18+
if (document.readyState !== 'complete') { return; }
19+
document.removeEventListener('readystatechange', onReadyStateChange);
20+
emitReady();
21+
});
22+
}
23+
module.exports.waitForDocumentReadyState = waitForDocumentReadyState;
24+
25+
/**
26+
* Signals A-Frame that everything is ready to initialize.
27+
*/
28+
function emitReady () {
29+
if (module.exports.canInitializeElements) { return; }
30+
module.exports.canInitializeElements = true;
31+
setTimeout(function () {
32+
document.dispatchEvent(new CustomEvent('aframeready'));
33+
});
34+
}
35+
module.exports.emitReady = emitReady;

src/core/scene/a-scene.js

+1-11
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,6 @@ class AScene extends AEntity {
7070
document.documentElement.classList.remove('a-fullscreen');
7171
}
7272

73-
connectedCallback () {
74-
// Defer if DOM is not ready.
75-
if (document.readyState !== 'complete') {
76-
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
77-
return;
78-
}
79-
80-
this.doConnectedCallback();
81-
}
82-
8373
doConnectedCallback () {
8474
var self = this;
8575
var embedded = this.hasAttribute('embedded');
@@ -90,7 +80,7 @@ class AScene extends AEntity {
9080
this.setAttribute('screenshot', '');
9181
this.setAttribute('xr-mode-ui', '');
9282
this.setAttribute('device-orientation-permission-ui', '');
93-
super.connectedCallback();
83+
super.doConnectedCallback();
9484

9585
// Renderer initialization
9686
setupCanvas(this);

src/core/system.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
var components = require('./component');
22
var schema = require('./schema');
33
var utils = require('../utils/');
4+
var ready = require('./readyState');
45

56
var parseProperties = schema.parseProperties;
67
var parseProperty = schema.parseProperty;
@@ -152,5 +153,7 @@ module.exports.registerSystem = function (name, definition) {
152153
systems[name] = NewSystem;
153154

154155
// Initialize systems for existing scenes
155-
for (i = 0; i < scenes.length; i++) { scenes[i].initSystem(name); }
156+
if (ready.canInitializeElements) {
157+
for (i = 0; i < scenes.length; i++) { scenes[i].initSystem(name); }
158+
}
156159
};

src/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ var shaders = require('./core/shader').shaders;
5959
var systems = require('./core/system').systems;
6060
// Exports THREE to window so three.js can be used without alteration.
6161
var THREE = window.THREE = require('./lib/three');
62+
var readyState = require('./core/readyState');
6263

6364
var pkg = require('../package');
6465

@@ -82,6 +83,11 @@ console.log('THREE Version (https://github.com/supermedium/three.js):',
8283
pkg.dependencies['super-three']);
8384
console.log('WebVR Polyfill Version:', pkg.dependencies['webvr-polyfill']);
8485

86+
// Wait for ready state, unless user asynchronously initializes A-Frame.
87+
if (!window.AFRAME_ASYNC) {
88+
readyState.waitForDocumentReadyState();
89+
}
90+
8591
module.exports = window.AFRAME = {
8692
AComponent: require('./core/component').Component,
8793
AEntity: AEntity,
@@ -104,6 +110,7 @@ module.exports = window.AFRAME = {
104110
schema: require('./core/schema'),
105111
shaders: shaders,
106112
systems: systems,
113+
emitReady: readyState.emitReady,
107114
THREE: THREE,
108115
utils: utils,
109116
version: pkg.version

tests/core/AFRAME.test.js

+10
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,14 @@ suite('AFRAME', function () {
44
test('exposes component prototype', function () {
55
assert.ok(AFRAME.AComponent);
66
});
7+
8+
test('exposes THREE.js as global and on AFRAME', function () {
9+
assert.ok(window.THREE);
10+
assert.ok(AFRAME.THREE);
11+
assert.strictEqual(AFRAME.THREE, window.THREE);
12+
});
13+
14+
test('exposes emitReady function', function () {
15+
assert.ok(AFRAME.emitReady);
16+
});
717
});

tests/core/readyState.test.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* global AFRAME, assert, suite, test, setup */
2+
var readyState = require('core/readyState');
3+
4+
suite('readyState', function () {
5+
setup(function (done) {
6+
// Test setup initializes AFRAME when document is already ready.
7+
// This timeout ensures the readyState is reset before running the tests here.
8+
setTimeout(function () {
9+
readyState.canInitializeElements = false;
10+
done();
11+
});
12+
});
13+
14+
suite('waitForDocumentReadyState', function () {
15+
test('emits aframeready when document is ready', function (done) {
16+
var listenerSpy = this.sinon.spy();
17+
document.addEventListener('aframeready', listenerSpy);
18+
19+
assert.equal(document.readyState, 'complete');
20+
readyState.waitForDocumentReadyState();
21+
22+
setTimeout(function () {
23+
assert.ok(listenerSpy.calledOnce);
24+
done();
25+
});
26+
});
27+
});
28+
29+
suite('emitReady', function () {
30+
test('emits aframeready', function (done) {
31+
var listenerSpy = this.sinon.spy();
32+
document.addEventListener('aframeready', listenerSpy);
33+
34+
assert.ok(listenerSpy.notCalled);
35+
readyState.emitReady();
36+
37+
setTimeout(function () {
38+
assert.ok(listenerSpy.calledOnce);
39+
assert.ok(readyState.canInitializeElements);
40+
done();
41+
});
42+
});
43+
44+
test('emits aframeready event only once', function (done) {
45+
var listenerSpy = this.sinon.spy();
46+
document.addEventListener('aframeready', listenerSpy);
47+
48+
assert.ok(listenerSpy.notCalled);
49+
// Calling emitReady multiple times should result in only one event being emitted.
50+
readyState.emitReady();
51+
readyState.emitReady();
52+
53+
setTimeout(function () {
54+
assert.ok(listenerSpy.calledOnce);
55+
assert.ok(readyState.canInitializeElements);
56+
57+
// Calling again after the event fired should not emit.
58+
readyState.emitReady();
59+
setTimeout(function () {
60+
// Assert total count is still only once.
61+
assert.ok(listenerSpy.calledOnce);
62+
assert.ok(readyState.canInitializeElements);
63+
done();
64+
});
65+
});
66+
});
67+
});
68+
});

0 commit comments

Comments
 (0)