Modul JavaScript: Panduan Pemula

Jika Anda pendatang baru di JavaScript, jargon seperti "bundler modul vs. pemuat modul", "Webpack vs. Browserify", dan "AMD vs. CommonJS" dapat dengan cepat membuat kewalahan.

Sistem modul JavaScript mungkin menakutkan, tetapi memahaminya penting bagi pengembang web.

Dalam posting ini, saya akan membongkar kata kunci ini untuk Anda dalam bahasa Inggris sederhana (dan beberapa contoh kode). Saya harap Anda merasa terbantu!

Catatan: demi kesederhanaan, ini akan dibagi menjadi dua bagian: Bagian 1 akan mendalami penjelasan tentang apa itu modul dan mengapa kami menggunakannya. Bagian 2 (diposting minggu depan) akan menjelaskan apa artinya memaketkan modul dan berbagai cara untuk melakukannya.

Bagian 1: Adakah yang bisa menjelaskan modul apa lagi?

Penulis yang baik membagi buku mereka menjadi beberapa bab dan bagian; programmer yang baik membagi program mereka ke dalam modul.

Seperti bab buku, modul hanyalah kumpulan kata (atau kode, tergantung kasusnya).

Modul yang baik, bagaimanapun, sangat mandiri dengan fungsionalitas yang berbeda, memungkinkan mereka untuk dikocok, dihapus, atau ditambahkan seperlunya, tanpa mengganggu sistem secara keseluruhan.

Mengapa menggunakan modul?

Ada banyak keuntungan menggunakan modul demi basis kode yang luas dan saling bergantung. Yang paling penting menurut saya adalah:

1) Pemeliharaan: Menurut definisi, modul berdiri sendiri. Modul yang dirancang dengan baik bertujuan untuk mengurangi ketergantungan pada bagian-bagian basis kode sebanyak mungkin, sehingga dapat tumbuh dan berkembang secara mandiri. Memperbarui satu modul jauh lebih mudah ketika modul dipisahkan dari bagian kode lainnya.

Kembali ke contoh buku kami, jika Anda ingin memperbarui satu bab dalam buku Anda, akan menjadi mimpi buruk jika perubahan kecil pada satu bab mengharuskan Anda untuk mengubah setiap bab lainnya juga. Sebaliknya, Anda ingin menulis setiap bab sedemikian rupa sehingga perbaikan dapat dilakukan tanpa mempengaruhi bab lainnya.

2) Namespacing: Dalam JavaScript, variabel di luar lingkup fungsi tingkat atas bersifat global (artinya, semua orang dapat mengaksesnya). Karena itu, biasanya terjadi "polusi namespace", di mana kode yang sama sekali tidak terkait berbagi variabel global.

Berbagi variabel global antara kode yang tidak terkait adalah larangan besar dalam pengembangan.

Seperti yang akan kita lihat nanti di posting ini, modul memungkinkan kita untuk menghindari polusi namespace dengan membuat ruang pribadi untuk variabel kita.

3) Dapat Digunakan Kembali: Jujur saja di sini: kita semua telah menyalin kode yang sebelumnya kita tulis ke dalam proyek baru pada satu titik atau lainnya. Misalnya, bayangkan Anda menyalin beberapa metode utilitas yang Anda tulis dari proyek sebelumnya ke proyek Anda saat ini.

Itu semua baik dan bagus, tetapi jika Anda menemukan cara yang lebih baik untuk menulis beberapa bagian dari kode itu, Anda harus kembali dan ingat untuk memperbaruinya di tempat lain Anda menulisnya.

Ini jelas membuang-buang waktu. Bukankah akan jauh lebih mudah jika ada - tunggu dulu - modul yang bisa kita gunakan berulang kali?

Bagaimana Anda bisa menggabungkan modul?

Ada banyak cara untuk memasukkan modul ke dalam program Anda. Mari kita bahas beberapa di antaranya:

Pola modul

Pola Modul digunakan untuk meniru konsep kelas (karena JavaScript tidak mendukung kelas secara asli) sehingga kita dapat menyimpan metode dan variabel publik dan pribadi di dalam satu objek - mirip dengan bagaimana kelas digunakan dalam bahasa pemrograman lain seperti Java atau Python. Itu memungkinkan kita membuat API yang dihadapi publik untuk metode yang ingin kita ekspos ke dunia, sambil tetap merangkum variabel dan metode privat dalam lingkup closure.

Ada beberapa cara untuk menyelesaikan pola modul. Dalam contoh pertama ini, saya akan menggunakan penutupan anonim. Itu akan membantu kami mencapai tujuan kami dengan meletakkan semua kode kami dalam fungsi anonim. (Ingat: di JavaScript, fungsi adalah satu-satunya cara untuk membuat cakupan baru.)

Contoh 1: Penutupan anonim

(function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); }()); // ‘You failed 2 times.’

Dengan konstruksi ini, fungsi anonim kami memiliki lingkungan evaluasinya sendiri atau "penutupan", dan kemudian kami segera mengevaluasinya. Ini memungkinkan kita menyembunyikan variabel dari namespace induk (global).

Yang menyenangkan tentang pendekatan ini adalah Anda dapat menggunakan variabel lokal di dalam fungsi ini tanpa sengaja menimpa variabel global yang ada, namun tetap mengakses variabel global, seperti:

var global = 'Hello, I am a global variable :)'; (function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); console.log(global); }()); // 'You failed 2 times.' // 'Hello, I am a global variable :)'

Perhatikan bahwa tanda kurung di sekitar fungsi anonim diperlukan, karena pernyataan yang dimulai dengan fungsi kata kunci selalu dianggap sebagai deklarasi fungsi (ingat, Anda tidak boleh memiliki deklarasi fungsi tanpa nama di JavaScript.) Akibatnya, tanda kurung di sekitarnya membuat ekspresi fungsi sebagai gantinya. Jika Anda penasaran, Anda bisa membaca selengkapnya di sini.

Contoh 2: Impor global

Pendekatan populer lainnya yang digunakan oleh pustaka seperti jQuery adalah impor global. Ini mirip dengan penutupan anonim yang baru saja kita lihat, kecuali sekarang kita meneruskan global sebagai parameter:

(function (globalVariable) { // Keep this variables private inside this closure scope var privateFunction = function() { console.log('Shhhh, this is private!'); } // Expose the below methods via the globalVariable interface while // hiding the implementation of the method within the // function() block globalVariable.each = function(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }; globalVariable.filter = function(collection, test) { var filtered = []; globalVariable.each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered; }; globalVariable.map = function(collection, iterator) { var mapped = []; globalUtils.each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped; }; globalVariable.reduce = function(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; globalVariable.each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator; }; }(globalVariable)); 

Dalam contoh ini, globalVariable adalah satu-satunya variabel yang bersifat global. Manfaat dari pendekatan ini dibandingkan penutupan anonim adalah Anda mendeklarasikan variabel global di awal, membuatnya sangat jelas bagi orang yang membaca kode Anda.

Contoh 3: Antarmuka objek

Namun pendekatan lain adalah membuat modul menggunakan antarmuka objek mandiri, seperti:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; // Expose these functions via an interface while hiding // the implementation of the module within the function() block return { average: function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }, failing: function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; } } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

Seperti yang Anda lihat, pendekatan ini memungkinkan kita memutuskan variabel / metode apa yang ingin kita jadikan pribadi (misalnya myGrades ) dan variabel / metode apa yang ingin kita tampilkan dengan meletakkannya di pernyataan pengembalian (misalnya rata-rata & gagal ).

Contoh 4: Mengungkap pola modul

Ini sangat mirip dengan pendekatan di atas, kecuali bahwa ini memastikan semua metode dan variabel dirahasiakan sampai diekspos secara eksplisit:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; }; // Explicitly reveal public pointers to the private functions // that we want to reveal publicly return { average: average, failing: failing } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

Itu mungkin tampak seperti banyak yang harus diperhatikan, tetapi itu hanya puncak gunung es ketika datang ke pola modul. Berikut adalah beberapa sumber yang menurut saya berguna dalam eksplorasi saya sendiri:

  • Mempelajari Pola Desain JavaScript oleh Addy Osmani: harta karun detail dalam pembacaan singkat yang mengesankan
  • Cukup Baik oleh Ben Cherry: ikhtisar berguna dengan contoh penggunaan lanjutan dari pola modul
  • Blog Carl Danley: gambaran umum pola modul dan sumber daya untuk pola JavaScript lainnya.

CommonJS dan AMD

Semua pendekatan di atas memiliki satu kesamaan: penggunaan variabel global tunggal untuk membungkus kodenya dalam suatu fungsi, sehingga membuat namespace privat untuk dirinya sendiri menggunakan cakupan closure.

Meskipun setiap pendekatan efektif dengan caranya sendiri, mereka memiliki kelemahan.

Pertama, sebagai pengembang, Anda perlu mengetahui urutan dependensi yang tepat untuk memuat file Anda. Misalnya, Anda menggunakan Backbone dalam proyek Anda, jadi Anda menyertakan tag skrip untuk kode sumber Backbone di file Anda.

Namun, karena Backbone memiliki ketergantungan yang kuat pada Underscore.js, tag skrip untuk file Backbone tidak dapat ditempatkan sebelum file Underscore.js.

Sebagai pengembang, mengelola dependensi dan melakukan hal-hal ini dengan benar terkadang bisa memusingkan.

Kelemahan lainnya adalah bahwa mereka masih dapat menyebabkan tabrakan namespace. Misalnya, bagaimana jika dua modul Anda memiliki nama yang sama? Atau bagaimana jika Anda memiliki dua versi modul, dan Anda membutuhkan keduanya?

Jadi Anda mungkin bertanya-tanya: dapatkah kita merancang cara untuk meminta antarmuka modul tanpa melalui cakupan global?

Fortunately, the answer is yes.

There are two popular and well-implemented approaches: CommonJS and AMD.

CommonJS

CommonJS is a volunteer working group that designs and implements JavaScript APIs for declaring modules.

A CommonJS module is essentially a reusable piece of JavaScript which exports specific objects, making them available for other modules to require in their programs. If you’ve programmed in Node.js, you’ll be very familiar with this format.

With CommonJS, each JavaScript file stores modules in its own unique module context (just like wrapping it in a closure). In this scope, we use the module.exports object to expose modules, and require to import them.

When you’re defining a CommonJS module, it might look something like this:

function myModule() { this.hello = function() { return 'hello!'; } this.goodbye = function() { return 'goodbye!'; } } module.exports = myModule;

We use the special object module and place a reference of our function into module.exports. This lets the CommonJS module system know what we want to expose so that other files can consume it.

Then when someone wants to use myModule, they can require it in their file, like so:

var myModule = require('myModule'); var myModuleInstance = new myModule(); myModuleInstance.hello(); // 'hello!' myModuleInstance.goodbye(); // 'goodbye!'

There are two obvious benefits to this approach over the module patterns we discussed before:

1. Avoiding global namespace pollution

2. Making our dependencies explicit

Moreover, the syntax is very compact, which I personally love.

Another thing to note is that CommonJS takes a server-first approach and synchronously loads modules. This matters because if we have three other modules we need to require, it’ll load them one by one.

Now, that works great on the server but, unfortunately, makes it harder to use when writing JavaScript for the browser. Suffice it to say that reading a module from the web takes a lot longer than reading from disk. For as long as the script to load a module is running, it blocks the browser from running anything else until it finishes loading. It behaves this way because the JavaScript thread stops until the code has been loaded. (I’ll cover how we can work around this issue in Part 2 when we discuss module bundling. For now, that’s all we need to know).

AMD

CommonJS is all well and good, but what if we want to load modules asynchronously? The answer is called Asynchronous Module Definition, or AMD for short.

Loading modules using AMD looks something like this:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) { console.log(myModule.hello()); });

What’s happening here is that the define function takes as its first argument an array of each of the module’s dependencies. These dependencies are loaded in the background (in a non-blocking manner), and once loaded define calls the callback function it was given.

Next, the callback function takes, as arguments, the dependencies that were loaded — in our case, myModule and myOtherModule — allowing the function to use these dependencies. Finally, the dependencies themselves must also be defined using the define keyword.

For example, myModule might look like this:

define([], function() { return { hello: function() { console.log('hello'); }, goodbye: function() { console.log('goodbye'); } }; });

So again, unlike CommonJS, AMD takes a browser-first approach alongside asynchronous behavior to get the job done. (Note, there are a lot of people who strongly believe that dynamically loading files piecemeal as you start to run code isn’t favorable, which we’ll explore more when in the next section on module-building).

Aside from asynchronicity, another benefit of AMD is that your modules can be objects, functions, constructors, strings, JSON and many other types, while CommonJS only supports objects as modules.

That being said, AMD isn’t compatible with io, filesystem, and other server-oriented features available via CommonJS, and the function wrapping syntax is a bit more verbose compared to a simple require statement.

UMD

For projects that require you to support both AMD and CommonJS features, there’s yet another format: Universal Module Definition (UMD).

UMD essentially creates a way to use either of the two, while also supporting the global variable definition. As a result, UMD modules are capable of working on both client and server.

Here’s a quick taste of how UMD goes about its business:

(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['myModule', 'myOtherModule'], factory); } else if (typeof exports === 'object') { // CommonJS module.exports = factory(require('myModule'), require('myOtherModule')); } else { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); } }(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it's returned (see below) function goodbye(){}; // A public method because it's returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye } }));

For more examples of UMD formats, check out this enlightening repo on GitHub.

Native JS

Phew! Are you still around? I haven’t lost you in the woods here? Good! Because we have *one more* type of module to define before we’re done.

As you probably noticed, none of the modules above were native to JavaScript. Instead, we’ve created ways to emulate a modules system by using either the module pattern, CommonJS or AMD.

Fortunately, the smart folks at TC39 (the standards body that defines the syntax and semantics of ECMAScript) have introduced built-in modules with ECMAScript 6 (ES6).

ES6 offers up a variety of possibilities for importing and exporting modules which others have done a great job explaining — here are a few of those resources:

  • jsmodules.io
  • exploringjs.com

What’s great about ES6 modules relative to CommonJS or AMD is how it manages to offer the best of both worlds: compact and declarative syntax and asynchronous loading, plus added benefits like better support for cyclic dependencies.

Probably my favorite feature of ES6 modules is that imports are live read-only views of the exports. (Compare this to CommonJS, where imports are copies of exports and consequently not alive).

Here’s an example of how that works:

// lib/counter.js var counter = 1; function increment() { counter++; } function decrement() { counter--; } module.exports = { counter: counter, increment: increment, decrement: decrement }; // src/main.js var counter = require('../../lib/counter'); counter.increment(); console.log(counter.counter); // 1

In this example, we basically make two copies of the module: one when we export it, and one when we require it.

Moreover, the copy in main.js is now disconnected from the original module. That’s why even when we increment our counter it still returns 1 — because the counter variable that we imported is a disconnected copy of the counter variable from the module.

So, incrementing the counter will increment it in the module, but won’t increment your copied version. The only way to modify the copied version of the counter variable is to do so manually:

counter.counter++; console.log(counter.counter); // 2

On the other hand, ES6 creates a live read-only view of the modules we import:

// lib/counter.js export let counter = 1; export function increment() { counter++; } export function decrement() { counter--; } // src/main.js import * as counter from '../../counter'; console.log(counter.counter); // 1 counter.increment(); console.log(counter.counter); // 2

Cool stuff, huh? What I find really compelling about live read-only views is how they allow you to split your modules into smaller pieces without losing functionality.

Then you can turn around and merge them again, no problem. It just “works.”

Looking forward: bundling modules

Wow! Where does the time go? That was a wild ride, but I sincerely hope it gave you a better understanding of modules in JavaScript.

In the next section I’ll walk through module bundling, covering core topics including:

  • Why we bundle modules
  • Different approaches to bundling
  • ECMAScript’s module loader API
  • …and more. :)

NOTE: To keep things simple, I skipped over some of the nitty-gritty details (think: cyclic dependencies) in this post. If I left out anything important and/or fascinating, please let me know in the comments!