CommonJS Modules in JavaScript: A Comprehensive Guide 2024

CommonJS Modules in JavaScript A Comprehensive Guide 2024

Introduction

JavaScript applications have evolved from simple scripts to complex full-stack apps. This complexity led to problems like namespace pollution and lack of reusability. Modules are the solution.

Broadly, modules allow encapsulating logic and state into self-contained pieces with clear interfaces. Other code can access exported values but cannot modify internal implementation details.

However, JavaScript did not have built-in module support initially. This led to module solutions like AMD, RequireJS and CommonJS being developed by the community. Let’s understand CommonJS modules in depth.

What are CommonJS Modules?

CommonJS is a project that defines standards for JavaScript APIs outside the browser, mainly for server-side code. The CommonJS Modules specification defines a simple API for declaring modules:

  • exports object to export values
  • require() function to import from another module

For example:

// module.js
var count = 5

function increment() {
  count++
}

exports.count = count
exports.increment = increment

main.js

// main.js
var module = require('./module.js')

console.log(module.count) // 5
module.increment()
console.log(module.count) // 6

module.exports exports code, while require() imports it.

This provides isolation and organization. CommonJS modules became widely used in Node.js for server-side development.

Key Characteristics

Some key characteristics of CommonJS modules:

  • Just JavaScript – No need for special syntax. Plain JS objects export APIs.
  • Synchronous – require() imports modules synchronously and blocks further execution.
  • Caching – Modules are cached after first load. Further require() calls return the cached object.
  • Encapsulation – Modules can maintain private state that can’t be modified by other code.
  • Namespacing – Each module defines a local exports object to export its API and avoid global namespace conflicts.

Let’s explore some of these in more detail.

Synchronous

CommonJS uses synchronous loading via require(). When a module is imported, code execution is paused until the module is fully loaded:

// long-task.js
console.log('Starting long task...')
setTimeout(() => console.log('Task done!'), 3000)

module.exports = 'Result'

main.js

// main.js 
const result = require('./long-task.js')
console.log('Continuing with app logic...')

Output:

Starting long task...
Task done!
Continuing with app logic...

Loading is blocking before require() returns. This simplifies module usage, but could impact performance for very large apps.

Caching

The first time a module is loaded via require(), it is executed and cached. Further loads return the cached exports immediately:

// counter.js 

console.log('Evaluating counter module...')

module.exports = {
  count: 5  
}

main.js

// main.js
require('./counter') // evaluates
require('./counter') // cached
require('./counter') // cached

Output:

Evaluating counter module...

The module is only executed once. This improves performance for module initialization.

Caching can be reset to force re-execution by directly deleting the cache entry:

delete require.cache[require.resolve('./counter')]

Encapsulation

Modules can maintain private state that cannot be externally modified, only exposed explicitly via exports:

// module.js
let count = 0

function increment() {
  count++
}

exports.getCount = () => count

Only getCount() is exported. count and increment() are encapsulated and private.

This provides control over the public API exposed by a module.

Core API

The CommonJS Modules spec defines a simple and minimal API for declaring modules with JavaScript. Let’s explore it in detail.

module.exports

The module.exports object is where the module API is defined. Any values attached to module.exports get exported:

module.exports.count = 5
module.exports.getCount = () => count

Usually exports is assigned to module.exports for convenience:

exports.count = 5
exports.getCount = () => count 

// equivalent to:

module.exports.count = 5
module.exports.getCount = () => count

Only values on module.exports get exported, not standalone exports.

require()

require() imports another module. Pass a relative path to the module file:

const counter = require('./counter.js')

require() returns the module.exports object from that module. This gets assigned to a local variable for usage.

Any further require() calls to that module path will return the cached exports.

module

Each module automatically gets a module object with metadata:

console.log(module)

Outputs info like:

Module {
  id: '.',
  path: '/home/user',
  exports: {},
  parent: null,
  filename: '/home/user/module.js',
  loaded: false,
  children: [],
  paths: [ ... ]
}

This contains useful information about the current module which libraries can leverage.

That wraps up the main APIs that enable CommonJS modules!

Usage with Node.js

Node.js fully implements the CommonJS Modules spec. This allowed better organization of server-side JavaScript code.

For example, a simple Node server:

// server.js
const http = require('http')
const handler = require('./handler')

http.createServer((req, res) => {
  handler(req, res) 
}).listen(3000)

handler.js

// handler.js
module.exports = (req, res) => {
  // request handling  
}

Node.js will evaluate the handler.js module on first require() and cache it.

This modular structure keeps concerns separated. Node.js itself is also composed of many small modules.

The synchronous nature of CommonJS modules aligns with Node.js blocking I/O model. This made CommonJS a great fit as Node.js modules system.

Modules in the Browser

CommonJS worked great for server-side JavaScript in Node.js, but could not be directly used for in-browser code.

This is because require() only works for resolving local files, not network resources. Browsers also did not support synchronous blocking imports natively.

Tools like Browserify build bundles by packing modules and dependencies into a single file that can be loaded in the browser.

More recently, native JavaScript module support has been added to browsers via:

  • <script type="module"> for inline declarations
  • import and export keywords
  • fetch() based asynchronous loading

However, they still remain useful and widely used on the server side in frameworks like Express.js and hapi.js.

Summary

Some key points about CommonJS modules:

  • Provides a simple modular structure for JavaScript
  • Has require() and exports to import and export code
  • Node.js fully implements and depends on CommonJS modules
  • Loading of modules is synchronous
  • Effective for encapsulation and namespacing
  • Browser support requires build tools like Browserify
  • Foundation for many popular frameworks

While ES Modules are becoming the standard, CommonJS modules still powers much of the npm ecosystem and Node.js backend development today. Understanding them is key for any JavaScript developer.

Conclusion

CommonJS Modules fill a key gap in the JavaScript language by introducing a standard module system. This enabled better code organization and sharing – revolutionizing JavaScript development.

Modules encapsulate code into logical units with clear interfaces. The minimal CommonJS API of exports and require() made modules intuitive and approachable.

Adoption by Node.js helped cement CommonJS as the de-facto standard for modular development in JavaScript outside the browser. Concepts from CommonJS also influenced the design of native ES Modules.

Moving forward, CommonJS will continue empowering server-side JavaScript, even as ES Modules gain adoption on the frontend. Understanding CommonJS modules unlocks simpler and more scalable development of Node.js applications.

Frequently Asked Questions

Here are some common questions about CommonJS modules:

Are CommonJS modules supported in browser JavaScript code?

Not natively, but tools like Browserify can bundle CommonJS modules for client-side use. Native ES Modules are recommended for new browser code.

What module systems were used before CommonJS?

Some pre-CommonJS solutions included AMD, RequireJS, and UMD (Universal Module Definition). They provided asynchronous loading more suited for browsers.

Can different module systems be mixed?

Yes, with some interoperability tools. For example, Node.js has APIs like createRequire() to load ESM. Bundlers like webpack also handle mixed dependencies.

What module syntax is used in Node.js packages?

Most Node.js packages on npm rely on CommonJS modules. The package.json defines the main entry point module. Node uses CommonJS under the hood.

How do CommonJS modules differ from ES Modules?

Key differences are CommonJS uses require() vs import, synchronously loads modules, and exports a single object vs named exports in ES Modules.

Are there any downsides to using CommonJS modules?

Challenges can include circular dependencies, slower startup time in large apps, and complexity in dynamic module loading. ES Modules address some of these issues.

Can CommonJS modules be converted to ES Modules?

Yes, there are automated conversion tools like jscodeshift to transform CommonJS syntax to ES Modules with import/export. Some manual refactoring may be required.

What is the best way to learn CommonJS modules in Node.js?

Start by building a simple Node.js application using require() and module.exports. Refer to Node.js docs and module patterns in open source projects.

How do module bundlers like webpack work with CommonJS?

Bundlers analyze dependencies starting from entry points, pack them together into bundles, and wrap modules in closures to isolate namespaces.

Leave a Reply

Your email address will not be published. Required fields are marked *