What is a module and why do we need them?
Modules are self-contained blocks of code that, when combined, build applications. They contain constants and functions and perform one task or one set of related tests (send requests, perform calculations, et cetera). They are often called scripts.
Modules are not supported by the browser. For years, web developers employed a simple workaround: script tag after script tag after script tag. While this is fine when you only need to load a few files, there are some serious drawbacks.
Every time the browser encounters a script tag, it has to stop what it's doing and make another HTTP request. Again, this is fine if there are only a few script tags but it increases load time.
Scripts add variables and functions to the global namespace or window. The more scripts you need to load the more polluted the global namespace becomes. It's easy to slip up and override window property. A bug such as this is very difficult to locate.
Since the scripts are dependent on each other, you have to load them in the correct order. If one script references a global property, the script that adds that property to the window must be loaded first.
Modules are great because they allow developers to write well-organized code that can be reused from project to project. If someone writes a really great module, they can distribute it to all developers. But what happens when our web app grows too big and script tags are no longer feasible?
What do module bundlers do and how do they work?
The bundling process is really interesting. It consists of two phases: dependency resolution and packing.
During dependency resolution, the bundler starts at the entry point to the app. It recursively scans every file that is used in the application looking out for the require function. If a file is not used in the application, it is not included in the final script. As the bundler scans the app, it constructs a directed graph modeling which scripts are dependent on other scripts.
If app.js requires dependency1.js and dependency2.js and dependency1.js requires dependency3.js, the graph would look like this:
During packing, the bundler traverses the resulting graph and creates an object containing scripts. Each key in the object maps to a script wrapped in a function. The function ensures each scripts namespace remains untouched. A new bundled script is created. The modules object will look something like this:
When you start the application, the interpreter begins execution at the entry point specified by the developer. When it reaches a require function, it grabs the function from the script object. It invokes the function. An exports object is created, properties are added to it, and it is returned. It is saved in a constant in the calling script. The calling file now has access to the exported values.
Most applications import the same script into multiple files. Bundlers use memoization to ensure the same script isn't run every time it it required. It stores the return object in cache after running for the first time. With every subsequent run, it simply grabs the object from the cache.
This implementation is specific to Webpack. I opted for this bundler because it's the one I've encountered most in my short web development journey. There are a ton of other options available. Some other options include Parcel and Rollup.
Really Great Sources