Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defer initialization until end of aframe script or manual ready signal #5481

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/introduction/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,39 @@ Phones with Adreno 300 series GPUs are notoriously problematic. Set [renderer pr

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)

## Can I load A-Frame as an ES module?

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:

```HTML
<head>
<script type="importmap">
{
"imports": {
"aframe": "https://aframe.io/releases/1.5.0/aframe.min.js",
}
}
</script>
<script type="module">
import 'aframe';
AFRAME.registerComponent('my-component', {
...
});
</script>
</head>
```

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:

```JS
window.AFRAME_ASYNC = true;
await import('aframe');

// Asynchronously register components/systems

window.AFRAME.ready();
```

## What order does A-Frame render objects in?

[sortTransparentObjects]: ../components/renderer.md#sorttransparentobjects
Expand Down
12 changes: 1 addition & 11 deletions src/core/a-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ class AAssets extends ANode {
this.timeout = null;
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

this.doConnectedCallback();
}

doConnectedCallback () {
var self = this;
var i;
Expand All @@ -38,7 +28,7 @@ class AAssets extends ANode {
var timeout;
var children;

super.connectedCallback();
super.doConnectedCallback();

if (!this.parentNode.isScene) {
throw new Error('<a-assets> must be a child of a <a-scene>.');
Expand Down
15 changes: 1 addition & 14 deletions src/core/a-entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,26 +57,13 @@ class AEntity extends ANode {
this.setEntityAttribute(attr, oldVal, newVal);
}

/**
* Add to parent, load, play.
*/
connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

AEntity.prototype.doConnectedCallback.call(this);
}

doConnectedCallback () {
var self = this; // Component.
var assetsEl; // Asset management system element.
var sceneEl;

// ANode method.
super.connectedCallback();
super.doConnectedCallback();

sceneEl = this.sceneEl;

Expand Down
12 changes: 1 addition & 11 deletions src/core/a-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,8 @@ class AMixin extends ANode {
this.isMixin = true;
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

this.doConnectedCallback();
}

doConnectedCallback () {
super.connectedCallback();
super.doConnectedCallback();

this.sceneEl = this.closestScene();
this.id = this.getAttribute('id');
Expand Down
15 changes: 5 additions & 10 deletions src/core/a-node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* global customElements, CustomEvent, HTMLElement, MutationObserver */
var utils = require('../utils/');
var readyState = require('./readyState');

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

Expand Down Expand Up @@ -32,19 +33,13 @@ class ANode extends HTMLElement {
this.mixinEls = [];
}

onReadyStateChange () {
if (document.readyState === 'complete') {
this.doConnectedCallback();
}
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
// Defer if not ready to initialize.
if (!readyState.canInitializeElements) {
document.addEventListener('aframeready', this.connectedCallback.bind(this));
return;
}
ANode.prototype.doConnectedCallback.call(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this call has changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the inheritance was a bit wonky. The doConnectedCallback function was essentially setup as a "virtual function" with subclasses overriding it and calling the base implementation (super.doConnectedCallback()), yet connectedCallback explicitly called the ANode implementation, forcing all subclasses to also override connectedCallback and replicating the same logic. From an OOP standpoint this didn't really make sense. Having one shared connectedCallback implementation is the cleanest solution IMO.

this.doConnectedCallback();
}

doConnectedCallback () {
Expand Down
35 changes: 35 additions & 0 deletions src/core/readyState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* global CustomEvent */

/**
* Flag indicating if A-Frame can initialize the scene or should wait.
*/
module.exports.canInitializeElements = false;

/**
* Waits for the document to be ready.
*/
function waitForDocumentReadyState () {
if (document.readyState === 'complete') {
emitReady();
return;
}

document.addEventListener('readystatechange', function onReadyStateChange () {
if (document.readyState !== 'complete') { return; }
document.removeEventListener('readystatechange', onReadyStateChange);
emitReady();
});
}
module.exports.waitForDocumentReadyState = waitForDocumentReadyState;

/**
* Signals A-Frame that everything is ready to initialize.
*/
function emitReady () {
if (module.exports.canInitializeElements) { return; }
module.exports.canInitializeElements = true;
setTimeout(function () {
document.dispatchEvent(new CustomEvent('aframeready'));
});
}
module.exports.emitReady = emitReady;
12 changes: 1 addition & 11 deletions src/core/scene/a-scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,6 @@ class AScene extends AEntity {
document.documentElement.classList.remove('a-fullscreen');
}

connectedCallback () {
// Defer if DOM is not ready.
if (document.readyState !== 'complete') {
document.addEventListener('readystatechange', this.onReadyStateChange.bind(this));
return;
}

this.doConnectedCallback();
}

doConnectedCallback () {
var self = this;
var embedded = this.hasAttribute('embedded');
Expand All @@ -90,7 +80,7 @@ class AScene extends AEntity {
this.setAttribute('screenshot', '');
this.setAttribute('xr-mode-ui', '');
this.setAttribute('device-orientation-permission-ui', '');
super.connectedCallback();
super.doConnectedCallback();

// Renderer initialization
setupCanvas(this);
Expand Down
5 changes: 4 additions & 1 deletion src/core/system.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var components = require('./component');
var schema = require('./schema');
var utils = require('../utils/');
var ready = require('./readyState');

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

// Initialize systems for existing scenes
for (i = 0; i < scenes.length; i++) { scenes[i].initSystem(name); }
if (ready.canInitializeElements) {
for (i = 0; i < scenes.length; i++) { scenes[i].initSystem(name); }
}
};
7 changes: 7 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ var shaders = require('./core/shader').shaders;
var systems = require('./core/system').systems;
// Exports THREE to window so three.js can be used without alteration.
var THREE = window.THREE = require('./lib/three');
var readyState = require('./core/readyState');

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

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

// Wait for ready state, unless user asynchronously initializes A-Frame.
if (!window.AFRAME_ASYNC) {
readyState.waitForDocumentReadyState();
}

module.exports = window.AFRAME = {
AComponent: require('./core/component').Component,
AEntity: AEntity,
Expand All @@ -104,6 +110,7 @@ module.exports = window.AFRAME = {
schema: require('./core/schema'),
shaders: shaders,
systems: systems,
emitReady: readyState.emitReady,
THREE: THREE,
utils: utils,
version: pkg.version
Expand Down
10 changes: 10 additions & 0 deletions tests/core/AFRAME.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,14 @@ suite('AFRAME', function () {
test('exposes component prototype', function () {
assert.ok(AFRAME.AComponent);
});

test('exposes THREE.js as global and on AFRAME', function () {
assert.ok(window.THREE);
assert.ok(AFRAME.THREE);
assert.strictEqual(AFRAME.THREE, window.THREE);
});

test('exposes emitReady function', function () {
assert.ok(AFRAME.emitReady);
});
});
68 changes: 68 additions & 0 deletions tests/core/readyState.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* global AFRAME, assert, suite, test, setup */
var readyState = require('core/readyState');

suite('readyState', function () {
setup(function (done) {
// Test setup initializes AFRAME when document is already ready.
// This timeout ensures the readyState is reset before running the tests here.
setTimeout(function () {
readyState.canInitializeElements = false;
done();
});
});

suite('waitForDocumentReadyState', function () {
test('emits aframeready when document is ready', function (done) {
var listenerSpy = this.sinon.spy();
document.addEventListener('aframeready', listenerSpy);

assert.equal(document.readyState, 'complete');
readyState.waitForDocumentReadyState();

setTimeout(function () {
assert.ok(listenerSpy.calledOnce);
done();
});
});
});

suite('emitReady', function () {
test('emits aframeready', function (done) {
var listenerSpy = this.sinon.spy();
document.addEventListener('aframeready', listenerSpy);

assert.ok(listenerSpy.notCalled);
readyState.emitReady();

setTimeout(function () {
assert.ok(listenerSpy.calledOnce);
assert.ok(readyState.canInitializeElements);
done();
});
});

test('emits aframeready event only once', function (done) {
var listenerSpy = this.sinon.spy();
document.addEventListener('aframeready', listenerSpy);

assert.ok(listenerSpy.notCalled);
// Calling emitReady multiple times should result in only one event being emitted.
readyState.emitReady();
readyState.emitReady();

setTimeout(function () {
assert.ok(listenerSpy.calledOnce);
assert.ok(readyState.canInitializeElements);

// Calling again after the event fired should not emit.
readyState.emitReady();
setTimeout(function () {
// Assert total count is still only once.
assert.ok(listenerSpy.calledOnce);
assert.ok(readyState.canInitializeElements);
done();
});
});
});
});
});
Loading