If you watched episode 2 of our JavaScript series, then you know that ECMAScript is the official name of the JavaScript language standard and that ECMAScript version 6 - or ES6 - introduced the idea of modules. Modules are what we've been taking advantage of this entire tutorial: exporting and requiring values from different files.

But... surprise! In ECMAScript, the require() function does not exist. Whaaaat?! The require() statement was basically invented by Node, back before ES6: Node needed a way to require files, so they invented their own way. Later, ECMAScript decided to make the idea of modules part of the standard language. But when they did, they used a different keyword than require! Yep, there are two valid syntaxes for working with modules! But... it's not a big deal: they work exactly the same, except that the official syntax has one small advantage.

Hello import & export

Let's use the official module syntax. Instead of saying const Helper = require(), say import Helper from:

215 lines assets/js/Components/RepLogApp.js
... lines 1 - 2
import Helper from './RepLogHelper';
... lines 4 - 215

It's that simple! And it doesn't change anything. In RepLogHelper, we also need to change our export to use the new syntax. Instead of module.exports = Helper, use export default Helper:

35 lines assets/js/Components/RepLogHelper.js
... lines 1 - 33
export default Helper;

We'll talk about what the default part means later. But for now, it's always export default and then what you want to export.

You can mix the two syntaxes - require and import - to a certain point, but you may run into some problems. Your best bet is to pick your favorite - mine is import and export - and use it everywhere. So let's update everything: import $ from 'jquery', import swal from 'sweetalert2' and import Routing from './Routing':

215 lines assets/js/Components/RepLogApp.js
... lines 1 - 2
import Helper from './RepLogHelper';
import $ from 'jquery';
import swal from 'sweetalert2';
import Routing from './Routing';
... lines 7 - 215

At the bottom, use export default RepLogApp:

215 lines assets/js/Components/RepLogApp.js
... lines 1 - 213
export default RepLogApp;

Cool! RepLogHelper is already ok, and in Routing.js, change this to: export default window.Routing:

6 lines assets/js/Components/Routing.js
... lines 1 - 4
export default window.Routing;

Keep going for the 3 entry files: import $ from 'jquery':

12 lines assets/js/layout.js
... lines 1 - 2
import $ from 'jquery';
... lines 4 - 12

If you don't need a return value, it's even easier: just import 'bootstrap'. Repeat that for the CSS files:

12 lines assets/js/layout.js
... lines 1 - 2
import $ from 'jquery';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.css';
import 'font-awesome/css/font-awesome.css';
import '../css/main.scss';
... lines 8 - 12

In login.js, import jQuery again, then import the CSS file:

24 lines assets/js/login.js
... lines 1 - 2
import $ from 'jquery';
import '../css/login.css';
... lines 5 - 24

And one more time in rep_log.js: import jQuery and import RepLogApp:

10 lines assets/js/rep_log.js
... lines 1 - 2
import $ from 'jquery';
import RepLogApp from './Components/RepLogApp';
... lines 5 - 10

And... assuming I didn't mess anything up, our build should still be happy! Check out the terminal: yes! No errors. Move over to your browser and check it! Looks great!

Importing Named Modules

And... yea! That's it! Just two nearly-identical syntaxes... because... more is better?! The biggest reason I want you to know about import and export is so that you know what it means when you see it in code or documentation.

But, there is one small advantage to import and export, and it relates to this default keyword:

215 lines assets/js/Components/RepLogApp.js
... lines 1 - 213
export default RepLogApp;

Usually, you'll want to export just one value from a module. In that case, you say export default and then you receive this value when using import.

But... technically... you can export multiple things from a module, as long as you give each of them a name. For example, instead of export default Helper, we could export an object with a Helper key and a foo key:

import {Helper, foo} from './RepLogHelper';

Then, the import has a slightly different syntax where you say explicitly which of those keys you want to import.

I don't usually do this in my code, but there is one case where it can be helpful. Imagine you're using a huge external library - like lodash - which is really just a collection of independent functions. If that library exports its values correctly, you could import just the functions you need, instead of importing the entire exported value:

import isEqual from 'lodash.isequal';

Then, at least in theory, thanks to a feature called "tree shaking", Webpack would realize that you're only using a few parts of that library, and only include those in the final, compiled file. In reality, this still seems a bit buggy: the unused code doesn't always get removed. But, the point is this: import and export have a subtle advantage and are the ECMAScript standard. So, use them!

Ok, it's time to find out how we can create a crazy-fast production build!

Leave a comment!

  • 2018-04-23 weaverryan

    Hey Chuck Norris!

    Hmm, so I think you're getting confused because there are a few different ideas floating around at the same time. Let's see if we can clear then up!

    1) You shouldn't need to do *any* special setup in Encore (or even Webpack) to get this lazy-chunk loading to work. That may not make sense yet, but keep reading :).

    2) require.ensure and using import() as a function (like you did above) are actually different syntaxes for doing the *exact* same thing. For simplicity, let's use require.ensure, because using import() like this requires one extra bit of setup.

    So, big picture: you normally run around using either require or import to get import a module you need. And, as you know, when you do this, Webpack/Encore will find *all* the pieces of JavaScript you need, and eventually put them into the final, built file - e.g. app.js.

    But, suddenly, you realize that there is one module that you only need *sometimes*, and so you don't want it to be packaged in app.js: it makes that file unnecessarily big. So instead, you want to load this file *only* when you need it. That's when you use require.ensure or import() as a function. It should look similar to your code above:


    // app.js
    if (something) {
    require.ensure(['./my-component'], (require) => {
    // the last .default is needed if you're using the "export default" format in my-component
    const myComponent = require('./my-component').default;
    // use myComponent
    });
    }

    As soon as you have code like this (yea, the syntax is a bit odd), Webpack will NOT package my-component into the built app.js anymore. Instead, it will generate a separate JavaScript file that contains only the code for my-component. It will probably call this 0.js, but actually, you don't care :). Why? Because, at runtime, when your code gets inside the if statement, Webpack will automatically make an AJAX call to 0.js and then execute the callback (2nd argument to require.ensure) once that file has finished downloading. So, *you* never need to know or care what this filename is called: you just say "load this lazily", and Webpack takes care of the rest.

    Does that makes sense? I was confused by how you seemed to try to lazily load this file in app.js and then also again in your "Symfony project" via a different method. Btw, when you mean "in your Symfony project", did you mean from inside a Twig template?

    Let me know if this helps!

    Cheers!

  • 2018-04-19 Chuck Norris

    Hi Ryan,

    thanks for your answer.
    In my previous full front project, I used require.ensure, like this :

    in webpack :


    module.exports = {
    output : {
    chunkFilename: '[name].bundle.js',
    }

    And in my app.js


    if (something) {
    require.ensure([], () => {
    require('./my-component');
    }, 'my-component-name');
    }

    Now, in my symfony project, I'm using import like that :


    if (something) {
    import(/* webpackChunkName: "my-component-name" */ './my-component').then(...).catch(...);
    }

    But this doesn't work.
    I think its because there is no chunkFilename option inmy webpack.config.js
    I don't knwo how to set it with Encore.
    I looked inside Encore options and the closest I found is configureFilenames.

    So I tried setting in webpack :


    .configureFilenames({
    js: '[name].js',
    })

    That doesn't work either.

    Thanks for any help.

  • 2018-04-18 weaverryan

    Hey Chuck Norris!

    Cool question :). The answer is that you do this the exact same way as in Webpack :), because the filename is determined at the time when you do the code splitting. The exact code depends on which method your using for splitting (require vs import). But, for example: https://webpack.js.org/guid...

    Specifically, the do return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {

    That should be all you need: there is nothing in your webpack config that needs to change for this :). If you see something different, let me know. But also... usually... those 0.js and 1.js names are fine - you shouldn't normally need to refer to these.

    Cheers!

  • 2018-04-18 Chuck Norris

    Hi,

    Again, great tutorials :).

    I have one question regarding Encore. I'm using code splitting for importing js file only when needed.
    It works fine, but the imported files are name only with numeric value (like 0.js, 1.js, etc ...).

    I jnow we can change this when we use webpack directly, but I can't find any option in Encore.

    Thanks for any help.