Una guía práctica de los módulos ES6

Uno de los principales desafíos al crear una aplicación web es la rapidez con la que puede escalar y responder a las necesidades del mercado. Cuando la demanda (requisitos) aumenta, las capacidades (características) también aumentan. Por lo tanto, es importante tener una estructura arquitectónica sólida para que la aplicación crezca orgánicamente. No queremos terminar en situaciones en las que la aplicación no puede escalar porque todo en la aplicación está profundamente enredado.

Escriba un código que sea fácil de borrar, no fácil de extender.

- Tef, la programación es terrible

En este artículo, crearemos un panel simple usando módulos ES6 y luego presentaremos técnicas de optimización para mejorar la estructura de carpetas y facilitar la escritura de menos código. Veamos en profundidad por qué los módulos ES6 son importantes y cómo aplicarlos de manera efectiva.

JavaScript ha tenido módulos durante mucho tiempo. Sin embargo, se implementaron a través de bibliotecas, no integradas en el lenguaje. ES6 es la primera vez que JavaScript tiene módulos integrados (fuente).

TL; DR: si desea ver un ejemplo práctico en el que creamos un tablero utilizando módulos ES6 a partir de un diseño de diseño arquitectónico, vaya a la sección 4.

Esto es lo que abordaremos

  1. Por qué se necesitan los módulos ES6
  2. En los días en que los scripts se cargaban manualmente
  3. Cómo funcionan los módulos ES6 ( importvs export)
  4. Construyamos un tablero con módulos
  5. Técnicas de optimización para ejemplo de tablero
Si desea convertirse en un mejor desarrollador web, iniciar su propio negocio, enseñar a otros o mejorar sus habilidades de desarrollo, publicaré consejos y trucos semanales sobre los últimos lenguajes web.

1. Por qué se necesitan los módulos ES6

Veamos un par de escenarios sobre por qué los módulos son relevantes.

Escenario 1: no reinventar la rueda

Como desarrolladores, a menudo recreamos cosas que ya se han creado sin siquiera darnos cuenta, o copiamos y pegamos cosas para reducir el tiempo. Al final, se suma y nos quedamos con x número de copias idénticas repartidas por toda la aplicación. Y por cada vez que necesitemos cambiar algo, debemos hacerlo x veces dependiendo de cuántas copias tengamos.

Ejemplo

Por ejemplo, imagine una fábrica de automóviles que intenta reinventar el motor cada vez que produce un automóvil nuevo, o un arquitecto que comienza desde cero después de cada dibujo. No es imposible hacer esto, pero entonces, ¿de qué sirve el conocimiento si no puede reutilizar la experiencia que ha adquirido?

Escenario 2: barrera del conocimiento

Si el sistema está profundamente enredado y carece de documentación, es difícil para los desarrolladores nuevos o antiguos aprender cómo funciona la aplicación y cómo se conectan las cosas.

Ejemplo

Por ejemplo, un desarrollador debería poder ver cuál es el resultado de un cambio sin adivinar, de lo contrario, terminamos con muchos errores sin saber por dónde empezar. Una solución es utilizar módulos para encapsular el comportamiento, podemos reducir fácilmente el proceso de depuración e identificar rápidamente la raíz del problema.

Recientemente escribí un artículo sobre "Desarrolladores que constantemente quieren aprender cosas nuevas", con consejos sobre cómo mejorar el conocimiento.

Escenario 3: comportamiento inesperado

Al evitar la separación de preocupaciones (principio de diseño), puede provocar un comportamiento inesperado.

Ejemplo

Por ejemplo, digamos que alguien aumenta el volumen del automóvil y eso enciende los limpiaparabrisas. Ese es un ejemplo de un comportamiento inesperado y no es algo que deseamos en nuestra aplicación.

En resumen, necesitamos módulos ES6 para reutilizar, mantener, separar y encapsular de manera efectiva el comportamiento interno del comportamiento externo. No se trata de hacer que el sistema sea complejo, sino de tener la capacidad de escalar y eliminar cosas fácilmente sin romper el sistema.

2. En los días en que los scripts se cargaban manualmente

Si ha realizado desarrollo web durante un par de años, definitivamente se ha encontrado con conflictos de dependencia, como que los scripts no se cargan en el orden correcto o que JS no puede acceder a los elementos del árbol DOM.

La razón es que el HTML en una página se carga en el orden en que aparece, lo que significa que no podemos cargar scripts antes del contenido dentro del dy> element has finished loading.

For instance, if you try to access an element within the tag using document.getElementById("id-name") and the element is not loaded yet, then you get an undefined error. To make sure that scripts are loaded properly we can use and defer async. The former will make sure that each script loads in the order it appears, while the latter loads the script whenever it becomes available.

The old fashioned way of solving such issue was to load the scripts right before the element.

But in the long run, the number of scripts adds up and we may end up with 10+ scripts while trying to maintain version and dependency conflicts.

Separation-of-concerns

In general, loading scripts as shown above is not a good idea in terms of performance, dependencies and maintainability. We don’t want the index.html file to have the responsibility of loading all the scripts — we need some sort of structure and separation of logic.

The solution is to utilize ES6’s syntax, import and export statements, an elegant and maintainable approach that allows us to keep things separated, and only available when we need it.

The import and export statements

The export keyword is used when we want to make something available somewhere, and the import is used to access what export has made available.

Original text


The thumb rule is, in order to import something, you first need to export it.

And what can we actually export?

  • A variable
  • An object literal
  • A class
  • A function
  • ++

To simplify the example as shown above, we can wrap all scripts one file.

import { jquery } from './js/jquery.js'; import { script2 } from './js/script2.js'; import { script3 } from './js/script3.js'; import { script4 } from './js/script4.js';

And then just load app.js script in our index.html. But first, in order to make it work, we need to use type="module" (source) so that we can use the import and export for working with modules.

As you can see, the index.html is now responsible for one script, which makes it easier to maintain and scale. In short, the app.js script becomes our entry point that we can use to bootstrap our application.

Note: I would not recommend having all scripts loaded in one file such as app.js, except the ones that require it.

Now that we have seen how we can use the import and export statements, let’s see how it works when working with modules in practice.

3. How ES6 modules work

What is the difference between a module and a component? A module is a collection of small independent units (components) that we can reuse in our application.

What’s the purpose?

  • Encapsulate behaviour
  • Easy to work with
  • Easy to maintain
  • Easy to scale

Yes, it makes development easier!

So what is a component really?

A component may be a variable, function, class and so forth. In other words, everything that can be exported by the export statement is a component (or you can call it a block, a unit etc).

So what is a module really?

As mentioned, a module is a collection of components. If we have multiple components that communicate, or simply must be shown together in order to form an integrated whole, then you most likely need a module.

Its a challenge to make everything reusable

A principal engineer with over 30 years of experience in electrical engineering once said, we cannot expect everything to be reused because of time, cost and not everything is meant to be reused. It is better to reuse to some extent than expecting things to be reused 100%.

In general, it means that we don’t have to make everything reusable in the app. Some things are just meant to be used once. The rule of thumb is that if you need something more than two times, then maybe it is a good idea to create a module or a component.

At first, it may sound easy to make something reusable, but remember, it requires taking the component out from its environment, and expect it to work in another one. But often times, we have to have to modify parts of it to make it fully reusable, and before you know it, you’ve created two new components.

Antoine, wrote an article describing 3 essential rules of creating reusable JS components, which is recommend to read. When he presented VueJS to his team, an experienced coworker says:

That’s great in theory, but in my experience these fancy “reusable” things are never reused.

The idea is that, not everything should be reused, such as buttons, input-fields and check boxes and so forth. The whole job of making something reusable requires resources and time, and often we end up with over-thinking scenarios that would never occur.

The CEO of Stack Overflow, Joel Spolsky says:

A 50%-good solution that people actually have solves more problems and survives longer than a 99% solution that nobody has because it’s in your lab where you’re endlessly polishing the damn thing. Shipping is a feature. A really important feature. Your product must have it.

4. Let’s build a dashboard with modules

Now that we have a basic understanding of how modules work, let’s view a practical example you’ll most likely encounter when working with JS frameworks. We’ll be creating a simple dashboard following an architectural design that consist of layouts and components.

The code for the example can be found here.

Step 1 — Design what you need

In most cases, developers would jump directly into the code. However, design is an important part of programming and it can save you a lot of time and headache. Remember, design should not be perfect, but something that leads you to the right direction.

So this is what we need based on the architectural design.

  • Components:users.js, user-profile.js and issues.js
  • Layouts: header.js and sidebar.js
  • Dashboard: dashboard.js

All components and layouts will be loaded in dashboard.js and then we will bootstrap dashboard.js in index.js.

So why do we have a layouts and components folder?

A layout is something that we need once, for instance a static template. The content inside the dashboard may change, but the sidebar and header will stay the same (and these are what is known as layouts). A layout can be either an error page, footer, status page and so forth.

The components folder is for general components we most likely will reuse more than once.

It is important to have a solid ground structure when dealing with modules. In order to effectively scale, folders must have reasonable names that make it easy to locate stuff and debug.

Later I’ll show you how to create a dynamic interface, which requires having a folder space for the components and layouts we need.

Step 2— Setup folder structure

As mentioned, we have 3 main folders: dashboard, components and layouts.

- dashboard - components - layouts index.html index.js ( entry point ) 

And in each file inside the folder, we export a class.

- dashboard dashboard.js - components issues.js user-profile.js users.js - layouts header.js sidebar.js index.html index.js ( entry point )

Step 3 — Implementation

The folder structure is all set, so the next thing to do is to create the component (a class) in each file and then export it. The code convention is the same for the rest of the files: every component is simply a class, and a method that consoles “x component is loaded” where x is the name of the component in order to indicate that the component has been loaded.

Let’s create a user class and then export it as shown below.

class Users { loadUsers() { console.log('Users component is loaded...') } } export { Users }; 

Notice, we have various options when dealing with the export statement. So the idea is that you can either export individual components, or a collection of components. For instance if we export the class, we can access the methods declared within by creating a new instance of the class.

export { name1, name2, …, nameN }; export function FunctionName(){...} export class ClassName {...} ... export * from …; export { name1, name2, …, nameN } from …; export { import1 as name1, import2 as name2, …, nameN } from …; export { default } from …; ...

Alright, so if you look at the architectural diagram in step 1, you’ll notice that the user-profile component is encapsulated by the header layout. This means that when we load the header layout, it will also load the user-profile component.

import { UserProfile } from '../components/users-profile.js'; class Header { loadHeader() { // Creata a new instance const userProfile = new UserProfile(); // Invoke the method (component) userProfile.loadUserProfile(); // Output loading status console.log('Header component is loaded...') } } export { Header };

Now that each component and layout has an exported class, we then import it in our dashboard file like this:

// From component folder import { Users } from '../components/users.js'; import { Issues } from '../components/issues.js'; // From layout folder import { Header } from '../layouts/header.js'; import { Sidebar } from '../layouts/sidebar.js'; class Dashboard { loadDashboard(){ // Create new instances const users = new Users(); const issues = new Issues(); const header = new Header(); const sidebar = new Sidebar(); console.log('Dashboard component is loaded'); } } export { Dashboard } 

In order to understand what is really going on in the dashboard file, we need to revisit the drawing in step 1. In short, since each component is a class, we must create a new instance and then assign it to an object. Then we use the object to execute the methods as shown in method loadDashboard().

Currently, the app doesn’t output anything because we haven’t executed the method loadDashboard(). In order to make it work we need to import the dashboard module in file index.js like this:

import { Dashboard } from './dashboard/dashboard.js'; const dashboard = new Dashboard(); dashboard.loadDashboard(); 

And then the console outputs:

As shown, everything works and the components load successfully. We can also go ahead and create two instances and then do something like this:

import { Dashboard } from './dashboard/dashboard.js'; const dashboard_1 = new Dashboard(); const dashboard_2 = new Dashboard(); dashboard_1.loadDashboard(); dashboard_2.loadDashboard();

Which outputs the same as shown above, but since we have to new instances, we get the results twice.

In general, this allows us to easily maintain and reuse the module in the files needed without interfering with other modules. We just create a new instance which encapsulates the components.

However, as previously mentioned, the purpose was to cover the dynamic of how we can work with modules and components using the import and export statements.

In most cases when working with JS frameworks, we usually have a route that can change the content of the dashboard. Right now, everything along such as layouts is loaded every time we invoke the method loadDashboard() which is not an ideal approach.

5. Optimization techniques for dashboard example

Now that we have a basic understanding of how modules work, the approach is not really scalable or intuitive when we deal with large applications that consist of a lots of components.

We need something that is known as a dynamic interface. It allows us to create a collection of the components we need, and easily access it. If you are using Visual Studio Code, the IntelliSense shows you what components are available, and which one you’ve already used. It means you don’t have to open the folder/file manually to see what components has been exported.

So if we have a module with twenty components, we don’t want to import each component one line after the other. We simply want to get what we need, and that’s it. If you’ve worked with namespaces in languages such as C#, PHP, C++ or Java, you’ll notice that this concept is similar in nature.

Here’s what we want to achieve:

// FILE: dashboard.js // From component folder import { users, issues } from '../components'; // From layout folder import { header, sidebar } from '../layouts'; class Dashboard { loadDashboard(){ // Invoke methods users.loadUsers(); issues.loadIssues(); header.loadHeader(); sidebar.loadSidebar(); console.log('Dashboard component is loaded'); } } export let dashboard = new Dashboard(); 

As shown, we have less lines of code, and we made it declarative without losing the context. Let’s see what changes we’ve made.

Create a dynamic interface (also known as a barrels)

A dynamic interface allows us to create a collection of things we need. It’s like creating a toolbox with our favorite tools. One thing that is important to mention is that a dynamic interface should not be added in every single folder, but to folders that consist of many components.

They greatly simplify the imports and make them look clearer. We just don’t want to have too many barrel files since that is counter productive and usually leads to circular dependency issues which sometimes can be quite tricky to resolve.

- Adrian Fâciu

In order to create a dynamic interface, we create a file named index.js which is located in the root of each folder to re-export a subset of files or components we need. The same concept works in TypeScript, you just change the type from .js to .ts like index.ts.

The index.js is the first file that loads when we access the root folder space — it’s the same concept as index.html that boots our HTML content. This means we don’t have to explicitly write import { component } from './components/index.js' , but instead import { component } from './components.

Here’s how a dynamic interface looks.

// Root space -> components folder // Dynamic interface export { users } from './users'; export { issues } from './issues'; export { userProfile } from './user-profile';

By using a dynamic interface, we end up with one less root level to access, and also less code.

// Before import { Users } from '../components/users.js'; import { Issues } from '../components/issues.js'; import { Header } from '../layouts/header.js'; import { Sidebar } from '../layouts/sidebar.js'; // After (with dynamic interface) import { users, issues } from '../components'; import { header, sidebar } from '../layouts'; 

Create a new instance at runtime

We removed the four instances in our dashboard.js, and instead created an instance at runtime when every component is exported. If you want to decide the name of the object, you can do export default new Dashboard(), and then import dashView without the curly braces.

// Before export class { dashboard }; const dashboard = new Dashboard(); dashboard.loadDashboard(); // After export const dashboard = new Dashboard(); dashboard.loadDashboard()

As shown, we can directly invoke the method without needing to create a new instance, and also write less code. However, this is a personal preference and you can freely decide what is a practical use case for your app and requirements.

And finally, we load all components and layouts with one method.

import { dashboard } from './dashboard/dashboard'; dashboard.loadDashboard();

Conclusion

I started with the intention of just showing a short example of how you can import and export a component, but then felt the need to share everything I know (almost). I hope this article provides you some insight into how to deal with ES6 modules effectively when building apps, and the things that are important in terms of separation-of-concerns (design principle).

The takeaways:

  • With ES6 modules we can easily reuse, maintain, separate and encapsulate components from being changed by external behavior
  • A module is a collection of components
  • A component is an individual block
  • Don’t try to make every everything reusable as it requires time and resources, and most often we don’t reuse it
  • Create an architectural diagram before diving into the code
  • In order to make components available in other files, we must first export and then import
  • By using index.js (same concept for TypeScript index.ts) we can create dynamic interfaces (barrels) to quickly access the things we need with less code and fewer hierarchical paths
  • You can export a new instance at runtime by using export let objectName = new ClassName()

The good news is that things have changed and we are moving towards a component-based and reusable paradigm. The question is how can we reuse not only plain JS code, but HTML elements too in a practical and intuitive way. It looks like that ES6 modules combined with web components may just give us what we need to build performant and scalable apps.

Here are a few articles I’ve written about the web-ecosystem along with personal programming tips and tricks.

  • A comparison between Angular and React
  • A chaotic mind leads to chaotic code
  • Developers that constantly want to learn new things
  • Learn these core Web Concepts
  • Boost your skills with these important JavaScript methods
  • Program faster by creating custom bash commands

You can find me on Medium where I publish on a weekly basis. Or you can follow me on Twitter, where I post relevant web development tips and tricks along with personal dev stories.