Modules

Yoseob Shin·2022년 4월 19일
0

javascript

목록 보기
16/24
post-custom-banner

Modules are reusable pieces of code in a file that can be exported and then imported for use in another file.

A modular program is one whose components can be separated, used individually, and recombined to create a complex system.

출처: shinyo627

이런 모듈러 방식 프로그래밍 전략은 각각 다른 로직을 갖고 있는 함수나 데이타를 여러 파일에 나눠 다른파일에 불러들여 재사용 및 변수 이름 콜리젼도 피할수 있으며 읽기도 쉬워 디버깅에 용이하다.

This modular strategy is sometimes called separation of concernsand is useful for several reasons.

Benefits of having separation of concerns

By isolating code into separate files, called modules, you can:

  • find, fix, and debug code more easily.
  • reuse and recycle defined logic in different parts of your application.
  • keep information private and protected from other modules.
  • prevent pollution of the global namespace and potential naming collisions, by cautiously selecting variables and behavior we load into a program.

Implementations of Modules in JavaScript: Node.js vs ES6

There are multiple ways of implementing modules depending on the runtime environment in which your code is executed. In JavaScript, there are two runtime environments and each has a preferred module implementation:

  1. The Node runtime environment and the module.exports and require() syntax.
  2. The browser’s runtime environment and the ES6 import/export syntax.

Implementing Modules in Node

Every JavaScript file that runs in a Node environment is treated as a distinct module. The functions and data defined within each module can be used by any other module, as long as those resources are properly exported and imported.

module.exports

To create a module, you simply have to create a new file where the functions can be declared. Then, to make these functions available to other files, add them as properties to the built-in module.exports object:

/* converters.js */
function celsiusToFahrenheit(celsius) {
  return celsius * (9/5) + 32;
}
 
module.exports.celsiusToFahrenheit = celsiusToFahrenheit;
 
module.exports.fahrenheitToCelsius = function(fahrenheit) {
  return (fahrenheit - 32) * (5/9);
};
  • At the top of the new file, converters.js, the function celsiusToFahrenheit() is declared.
  • On the next line of code, the first approach for exporting a function from a module is shown. In this case, the already-defined function celsiusToFahrenheit() is assigned to module.exports.celsiusToFahrenheit.
  • Below, an alternative approach for exporting a function from a module is shown. In this second case, a new function expression is declared and assigned to module.exports.fahrenheitToCelsius. This new method is designed to convert Fahrenheit values back to Celsius.

module.exports is an object that is built-in to the Node.js runtime environment. Other files can now import this object, and make use of these two functions, with another feature that is built-in to the Node.js runtime environment: the require() function.

require()

The require() function accepts a string as an argument. That string provides the file path to the module you would like to import.

/* water-limits.js */
const converters = require('./converters.js');
 
const freezingPointC = 0;
const boilingPointC = 100;
 
const freezingPointF = converters.celsiusToFahrenheit(freezingPointC);
const boilingPointF = converters.celsiusToFahrenheit(boilingPointC);
 
console.log(`The freezing point of water in Fahrenheit is ${freezingPointF}`);
console.log(`The boiling point of water in Fahrenheit is ${boilingPointF}`);

When you use require(), the entire module.exports object is returned and stored in the variable converters. This means that both the .celsiusToFahrenheit() and .fahrenheitToCelsius() methods can be used in this program.

We could also use destrucring to be more selective with require()

/* celsius-to-fahrenheit.js */
const { celsiusToFahrenheit } = require('./converters.js');
 
const celsiusInput = process.argv[2]; 
const fahrenheitValue = celsiusToFahrenheit(celsiusInput);
 
console.log(`${celsiusInput} degrees Celsius = ${fahrenheitValue} degrees Fahrenheit`);

With this approach, the remainder of the program works the same way but the program avoids importing a function it does not need. === Better performance.

Avoid importing all datas/functions from module/ libraries

Implementing Modules in the Browser

Named exports

/* dom-functions.js */
const toggleHiddenElement = (domElement) => {
    if (domElement.style.display === 'none') {
      domElement.style.display = 'block';
    } else {
      domElement.style.display = 'none';
    }
}
 
const changeToFunkyColor = (domElement) => {
  const r = Math.random() * 255;
  const g = Math.random() * 255;
  const b = Math.random() * 255;
 
  domElement.style.background = `rgb(${r}, ${g}, ${b})`;
}
 
export { toggleHiddenElement, changeToFunkyColor };
/* dom-functions.js */
export const toggleHiddenElement = (domElement) => {
  // logic omitted...
}
 
export const changeToFunkyColor = (domElement) => {
  // logic omitted...
}

ES6 Import Syntax

The ES6 syntax for importing named resources from modules is similar to the export syntax:

/* 예시
import { exportedResourceA, exportedResourceB } from '/path/to/module.js';
*/

/* secret-messages.js */
import { toggleHiddenElement, changeToFunkyColor } from '../modules/dom-functions.js';
 
const buttonElement = document.getElementById('secret-button');
const pElement = document.getElementById('secret-p');
 
buttonElement.addEventListener('click', () => {
  toggleHiddenElement(pElement);
  changeToFunkyColor(buttonElement);
});
<!-- secret-messages.html --> 
<html>
  <head>
    <title>Secret Messages</title>
  </head>
  <body>
    <button id="secret-button"> Press me... if you dare </button>
    <p id="secret-p" style="display: none"> Modules are fancy! </p>
    <script type="module" src="./secret-messages.js"> </script>
  </body>
</html>

the addition of the attribute type='module' to the < script > element. Failure to do so can cause some browsers to throw an error. For example, in Chrome you might see this error:
Uncaught SyntaxError: Cannot use import statement outside a module.

Renaming Imports to Avoid Naming Collisions

Inevitably, you will run into a situation where the resources you wish to import share a name with some other value that already exists in your program (or from another imported module).

For example, suppose you had access to two modules, greeterEspanol.js and greeterFrancais.js. Each exports a function called greet():

/* inside greeterEspanol.js */
const greet = () => {
  console.log('hola');
}
export { greet };
 
/* inside greeterFrancais.js */
const greet = () => {
  console.log('bonjour');
}
export { greet };

Now, let’s say you wanted to use each of these functions in a program, called main.js, that simulates a conversation between a spanish-speaker and a french-speaker.

import { greet } from 'greeterEspanol.js';
import { greet } from 'greeterFrancais.js';

The code above will throw an error on line 2 due to the fact that the identifier greet has already been defined on line 1. Thankfully, ES6 includes syntax for renaming imported resources by adding in the keyword as. It looks like this:

import { exportedResource as newlyNamedResource } from '/path/to/module'
/* main.js */
import { greet as greetEspanol } from 'greeterEspanol.js';
import { greet as greetFrancais } from 'greeterFrancais.js';
 
greetEspanol(); // Prints: hola
greetFrancais(); // Prints: bonjour

Now, both of the imported functions can be called within main.js using their new identifiers, greetEspanol() and greetFrancais().

Default Exports and Imports

So far, the examples shown have used the named export syntax, in which a module’s resources are exported individually by name.

Every module also has the option to export a single value to represent the entire module called the default export. Often, though not always, the default export value is an object containing the entire set of functions and/or data values of a module.

The syntax for exporting a default object looks like this:

const resources = { 
  valueA, 
  valueB 
}
export { resources as default };
const resources = { 
  valueA, 
  valueB 
}
export { resources as default };

At first glance, it looks like the resources object is being exported as a named export. However, the clause as default renames the exported object to default, a reserved identifier that can only be given to a single exported value.

shorthand syntax using export default

const resources = {
  valueA,
  valueB
}
export default resources;

Importing default values.

import importedResources from 'module.js';

Notice that the curly braces are gone from the import statement.

This syntax is actually shorthand for import { default as importedResources } from 'module.js and the imported default value may be given any name the programmer chooses.

It should be noted that if the default export is an object, the values inside cannot be extracted until after the object is imported, like so:

// This will work...
import resources from 'module.js'
const { valueA, valueB } = resources;
 
// This will not work...
import { valueA, valueB } from 'module.js'

예시:

/* dom-functions.js */
const toggleHiddenElement = (domElement) => {
    if (domElement.style.display === 'none') {
      domElement.style.display = 'block';
    } else {
      domElement.style.display = 'none';
    }
}
 
const changeToFunkyColor = (domElement) => {
  const r = Math.random() * 255;
  const g = Math.random() * 255;
  const b = Math.random() * 255;
 
  domElement.style.background = `rgb(${r}, ${g}, ${b})`;
}
 
const resources = { 
  toggleHiddenElement, 
  changeToFunkyColor
}
export default resources;

This default exports object can now be used within secret-messages.js like so:

import domFunctions from '../modules/dom-functions.js';

/*  ***Above is same as but above line is shorthand version ***
import { default as domFunctions } from '../modules/dom-functions.js';
*/
 
const { toggleHiddenElement, changeToFunkyColor } = domFunctions;
 
const buttonElement = document.getElementById('secret-button');
const pElement = document.getElementById('secret-p');
 
buttonElement.addEventListener('click', () => {
  toggleHiddenElement(pElement);
  changeToFunkyColor(buttonElement);
});

Dynamic Imports

The standard import syntax is static and will always result in all code in the imported module being evaluated at load time. In situations where you wish to load a module conditionally or on demand, you can use a dynamic import instead. The following are some reasons why you might need to use dynamic import:

When importing statically significantly slows the loading of your code and there is a low likelihood that you will need the code you are importing, or you will not need it until a later time.
When importing statically significantly increases your program's memory usage and there is a low likelihood that you will need the code you are importing.
When the module you are importing does not exist at load time
When the import specifier string needs to be constructed dynamically. (Static import only supports static specifiers.)
When the module being imported has side effects, and you do not want those side effects unless some condition is true. (It is recommended not to have any side effects in a module, but you sometimes cannot control this in your module dependencies.)

Use dynamic import only when necessary. The static form is preferable for loading initial dependencies, and can benefit more readily from static analysis tools and tree shaking.

To dynamically import a module, the import keyword may be called as a function. When used this way, it returns a promise.

import('/modules/my-module.js')
  .then((module) => {
    // Do something with the module.
  });

//This form also supports the await keyword.

let module = await import('/modules/my-module.js');

// **** 예시 ****

const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
  link.addEventListener("click", e => {
    e.preventDefault();

    import('/modules/my-module.js')
      .then(module => {
        module.loadPageInto(main);
      })
      .catch(err => {
        main.textContent = err.message;
      });
  });

Review

  • The benefits of implementing modular programs.
  • How to use the Node.js module.exports object to export code from a file - meaning its functions and/or data can be used by other files/modules.
  • How to use the Node.js require() function to import the functions and/or data from another module.
  • How to use object destructuring to only import the desired components of a module.
  • How to use the ES6 export statement to export code from a file - meaning its functions and/or data can be used by other files/modules.
  • How to use the ES6 import statement to import functions and/or data from another module.
  • How to rename imported resources using the as keyword.
  • How to export and import a default value.
profile
coder for web development + noodler at programming synthesizers for sound design as hobbyist.
post-custom-banner

0개의 댓글