1

I have an A-Frame project with a primary component street that also depends on other components and helper functions from the same repo.

Currently using the street component in an A-Frame scene requires importing 8 separate js files from the same repo each with their own <script> tag. (code / show page)

Instead, I would prefer a simpler structure to import just one file, but I'd rather not use a bundler such as webpack. I think there is an ES6 approach but I'm confused about how to go about leveraging ES6 imports while still allowing the components to access functions from other files. In other words, how to get the imported files into the same namespace.

This question helps but the comments clarify the imported functions are not added to the global namespace automatically: ES6 import equivalent of require() without exports

Maybe the structure would be something like this?

// index.html
<script src="src/street.js" ></script>
<a-scene>
 <a-entity street="dosomething"></a-entity>
</a-scene>

// helperFunctions1.js
function coolHelperFunction1a (variable) { 
  // useful code
}

// street.js
import 'helperFunctions1.js'
import 'helperFunctions2.js'

AFRAME.registerComponent('street', {
  coolHelperFunction1a (variable);
})

That probably won't work, what's the right way to do it?

Kieran F.
  • 567
  • 4
  • 16

3 Answers3

1

A-Frame and ES Modules don't play well together, and I don't think there's a solution without using a bundler like Webpack.

In short, if you want to have a single <script> element for your entire app while still organizing your code well, I'd recommend Webpack, with the generated bundle being loaded synchronously in the <head> of your HTML document. Webpack can consume ES Modules, so you're still free to use those with the build process.

More detail on why native module loading won't work:

Your code would need a few changes for native ES Module usage, but the first one presents a problem: the <script> elements loading your modules need the type="module" attribute. This has a side effect: the browser treats modules the same way it treats traditional script tags with the defer attribute, meaning the script is executed after the DOM has been parsed. This is by design, as it leads to better initial page performance. Unfortunately A-Frame expects to run before the DOM has been parsed, and further it expects that you've initialized all of your components, systems, etc. before it runs. That is why they recommend putting the a-frame script in the <head>, and you'll notice there is no defer attribute.

A-Frame is kind of going against modern web performance advice, but it has a good reason: it doesn't want the browser to render the HTML before it runs, since its using the HTML for its own purposes.

This all means using native ES modules directly in the browser isn't going to work for an A-Frame app.

Caleb Miller
  • 1,022
  • 9
  • 14
  • 1
    Thanks for this guidance! I ended up going "all in" on webpack and converted everything. https://github.com/kfarr/streetmix3d/commit/1b06ac30cd209981ecd4f16a895b5ece05ac652e Still needs some more documentation on my side but it's working as expected! – Kieran F. Nov 24 '20 at 00:15
  • 1
    Excellent! This usually feels like a chore to setup, glad to hear it's working. – Caleb Miller Nov 24 '20 at 01:09
0

The suggested structure above needs a few fixes.

First, add export to the helper functions. For example,

function helperFunction (variable) {
  code (variable);
}

changes to

export function helperFunction (variable) {
  code (variable);
}

(See How can I export all functions from a file in JS?)

In street.js we also need to make sure to reference the module object it's imported as (See MDN Creating a module object)

We also need to change the <script> tag to include type="module" to use import and export keywords.

Here is a revised structure:

// index.html
<script src="src/street.js" type="module"></script>
<a-scene>
 <a-entity street="dosomething"></a-entity>
</a-scene>

// helperFunctions1.js
export function coolHelperFunction1a (variable) { 
  // useful code
}

// street.js
import * as helperFunctions1 from 'helperFunctions1.js'
import * as helperFunctions2 from 'helperFunctions2.js'

AFRAME.registerComponent('street', {
  helperFunctions1.coolHelperFunction1a(variable);
})
Kieran F.
  • 567
  • 4
  • 16
  • 1
    Out of curiosity: I tried this approach in typescript, but had the problem that the component was not initialized as the modules where built after DOM. Meaning AFRAME.register came after the dom load, hence the attributes didn't work and the component didn't initialize. Is this due typescript? – wassx Nov 20 '20 at 08:04
  • Interesting thanks for feedback. Not sure what is the cause of your issue as I haven't tried this yet either, waiting to see if others can confirm this general architecture is valid – Kieran F. Nov 20 '20 at 20:02
  • 1
    This is the correct way to use ES Modules in the browser, but it still won't work as intended for an A-Frame app. I've detailed why in [my answer](https://stackoverflow.com/a/64960663/9310672). – Caleb Miller Nov 22 '20 at 23:20
0

Another option, don't use ES6 Modules and use webpack instead. This is a nice example of a webpack A-Frame component project: https://github.com/supermedium/superframe/blob/master/components/state/

Kieran F.
  • 567
  • 4
  • 16