From 488ab1fa7207ecbf6271819709328b06c7ec9a85 Mon Sep 17 00:00:00 2001 From: Noeri Huisman Date: Wed, 28 Feb 2024 22:01:59 +0100 Subject: [PATCH] Defer initialization until end of aframe script or manual ready signal Co-authored-by: Chris Chua --- docs/introduction/faq.md | 33 +++++++++++++++++ src/core/a-assets.js | 12 +------ src/core/a-entity.js | 15 +------- src/core/a-mixin.js | 12 +------ src/core/a-node.js | 15 +++----- src/core/readyState.js | 35 ++++++++++++++++++ src/core/scene/a-scene.js | 12 +------ src/core/system.js | 5 ++- src/index.js | 7 ++++ tests/core/AFRAME.test.js | 10 ++++++ tests/core/readyState.test.js | 68 +++++++++++++++++++++++++++++++++++ 11 files changed, 166 insertions(+), 58 deletions(-) create mode 100644 src/core/readyState.js create mode 100644 tests/core/readyState.test.js diff --git a/docs/introduction/faq.md b/docs/introduction/faq.md index ea2d5e91a8f..0408c4fce15 100644 --- a/docs/introduction/faq.md +++ b/docs/introduction/faq.md @@ -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 `` in the document. It's still important to register any components or systems you need before this happens: + +```HTML + + + + +``` + +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 `` 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 diff --git a/src/core/a-assets.js b/src/core/a-assets.js index b66e97d4e63..514a4e9881d 100644 --- a/src/core/a-assets.js +++ b/src/core/a-assets.js @@ -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; @@ -38,7 +28,7 @@ class AAssets extends ANode { var timeout; var children; - super.connectedCallback(); + super.doConnectedCallback(); if (!this.parentNode.isScene) { throw new Error(' must be a child of a .'); diff --git a/src/core/a-entity.js b/src/core/a-entity.js index bbb9324b179..d4fb702b778 100644 --- a/src/core/a-entity.js +++ b/src/core/a-entity.js @@ -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; diff --git a/src/core/a-mixin.js b/src/core/a-mixin.js index e7865ece32b..a5f8cec4457 100644 --- a/src/core/a-mixin.js +++ b/src/core/a-mixin.js @@ -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'); diff --git a/src/core/a-node.js b/src/core/a-node.js index d08affcdb5e..a3cac8d2a8d 100644 --- a/src/core/a-node.js +++ b/src/core/a-node.js @@ -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'); @@ -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); + this.doConnectedCallback(); } doConnectedCallback () { diff --git a/src/core/readyState.js b/src/core/readyState.js new file mode 100644 index 00000000000..d435bcb4a46 --- /dev/null +++ b/src/core/readyState.js @@ -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; diff --git a/src/core/scene/a-scene.js b/src/core/scene/a-scene.js index d78a7eff24b..bb663a50c30 100644 --- a/src/core/scene/a-scene.js +++ b/src/core/scene/a-scene.js @@ -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'); @@ -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); diff --git a/src/core/system.js b/src/core/system.js index 9e36c12ce7b..4942cb88a95 100755 --- a/src/core/system.js +++ b/src/core/system.js @@ -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; @@ -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); } + } }; diff --git a/src/index.js b/src/index.js index e150147ff73..18cc6296a58 100644 --- a/src/index.js +++ b/src/index.js @@ -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'); @@ -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, @@ -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 diff --git a/tests/core/AFRAME.test.js b/tests/core/AFRAME.test.js index f0a40444710..5b23e78091b 100644 --- a/tests/core/AFRAME.test.js +++ b/tests/core/AFRAME.test.js @@ -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); + }); }); diff --git a/tests/core/readyState.test.js b/tests/core/readyState.test.js new file mode 100644 index 00000000000..4e03bb0d8d0 --- /dev/null +++ b/tests/core/readyState.test.js @@ -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(); + }); + }); + }); + }); +});