Kamu seorang Web Developer? Pasti suka pusing melihat banyaknya module yang digunakan. Sebelumnya mari kita cari tahu pengertiannya satu persatu.
Apa itu Module?
Module adalah sebuah berkas yang berisi kode script. Module memiliki sifat khusus, yakni dapat memuat atau dimuat oleh module lainnya. Berkat sifat inilah antar module dapat saling ekspor dan impor untuk bertukar fungsi.
Masalahnya adalah saat ini sudah banyak website-website besar menggunakan banyak module didalamnya. Dengan banyaknya module yang digunakan, pasti diantaranya ada yang mempunyai fungsi yang sama.
Masalah lainnya yaitu salah urutan dalam menempatkan script, hal itu dapat membuat fungsi-fungsi yang telah dibuat, tidak berjalan sesuai rencana.
Module Bundler
Module bundler secara otomatis akan mengumpulkan semua module-module yang digunakan, mengurutkannya dengan benar dan membungkusnya menjadi satu berkas module saja.
Salah satu tools module bundler yang sering digunakan adalah Webpack. Dalam core concepts mereka menyebutkan, Webpack akan membangun sebuah dependency graph saat dijalankan. Dependency graph ini berisi pemetaan setiap module yang dibutuhkan dalam proyek dan mengeluarkan sebuah bundle module statis.
Core Concept
Untuk lebih memahami bagaimana module bundler bekerja, perlu kita pelajari dahulu bagaimana konsep dasarnya. Menurut Webpack, ada beberapa bagian dasar dari sebuah module bundler, yaitu Entry, Output, Loaders, Plugins, Mode dan Browser Compatibility [rujukan]. Namun, pada kesempatan ini kita hanya perlu menggunakan dua (2) bagian saja, yakni Entry dan Output:
- Entry
Sebuah entry point adalah titik permulaan yang digunakan oleh module bundler sebagai acuan script mana yang perlu dibaca pertama kali dan digunakan untuk permulaan pembuatan dependency graph.
- Output
Properti output memberitahukan di mana module bundler harus menyimpan hasil bundler yang telah dikumpulkan dan menentukan nama bundle-nya.
Standarisasi Module
Pertama, kita perlu membuat ulang setiap module yang telah dibaca. Struktur standar sebuah module terdiri dari beberapa properti, yaitu: id modul, lokasi berkas, script kode, dependensi, peta dependensi. Perhatikan contoh module berikut:
1
2
3
4
5
6
7
8
|
const module = {
id: ,
filePath: “.src/entry.js”,
code: `import message from ‘./message.js’;
console.log(message);`,
dependencies: [“./message.js”],
dependendyMap: { “./message.js”: module1 }
};
|
Karena banyaknya module yang digunakan, proses ini tidak mungkin kita lakukan secara manual. Untuk itu, pada berkas bundler.js, isikan kode berikut:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const fs = require(“fs”);
const { entry, output } = require(“./webpack.config”);
let ID = ;
createModule(entry);
function createModule(filePath) {
const content = fs.readFileSync(filePath, “utf8”);
console.log(content);
return {
id: ID++,
filePath
// code, Todo 1
// dependencies, Todo 2
// dependencyMap: {}, Todo 3
};
}
|
Mengisi Code
Sampai pada tahap ini kita sudah mengisikan ID dan filePath pada module baru yang dibuat. Selanjutnya untuk bagian code isikan content file yang telah dibaca. Namun, karena content tersebut masih berformat Javascript ES6, kita perlu seragamkan menjadi format CommonJS.
ES6 Module
1
2
3
4
5
|
// define dependency by import statement
import { message } from “./message”;
// expose the module by export statement
export default alertBtn;
|
CommonJS Module
1
2
3
4
5
|
// require to define dependency
const moduleName = require(“./dependency”);
// expose module by module.exports or exports
module.exports = “something”;
|
Untuk mengubah format Javascript ES6 ke CommonJS, kita dapat menggunakan tools babel transformSync seperti berikut ini:
1
|
const { code } = transformSync(content, { presets: [‘@babel/preset-env’] })
|
Sehingga, hasil akhirnya menjadi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const fs = require(“fs”);
const { entry, output } = require(“./webpack.config”);
const { transformSync } = require(“@babel/core”);
let ID = ;
createModule(entry);
function createModule(filePath) {
const content = fs.readFileSync(filePath, “utf8”);
const { code } = transformSync(content, { presets: [“@babel/preset-env”] });
return {
id: ID++,
filePath,
code
// dependencies, Todo 2
// dependencyMap: {}, Todo 3
};
}
|
Mengumpulkan Dependencies
Untuk mengumpulkan semua dependency apa saja yang digunakan oleh sebuah module, kita dapat memanfaatkan fitur Abstract Syntax Tree atau AST. Sebagai gambaran seperti apa itu AST, silakan lihat di website astexplorer.net melalui url berikut:
Dapat kita lihat di gambar bahwa AST membaca kode yang diberikan, kemudian mendapati bahwa kode tersebut memiliki dependency ke module “./message”. Untuk mendapatkan informasi AST di script module bundler, kita dapat memanfaatkan fungsi babel parseSync. Kemudian kita juga bisa memanfaatkan babel traverse untuk mendapatkan nama module dependency-nya.
Hasilnya menjadi seperti berikut:
const fs = require("fs"); const { entry, output } = require("./webpack.config"); const { traverse, transformSync, parseSync } = require("@babel/core"); let ID = 0; createModule(entry); function createModule(filePath) { const content = fs.readFileSync(filePath, "utf8"); const { code } = transformSync(content, { presets: ["@babel/preset-env"] }); const dependencies = []; const abstractSyntaxTree = parseSync(content, { sourceType: "module" }); traverse(abstractSyntaxTree, { ImportDeclaration: declare => { dependencies.push(declare.node.source.value); } }); return { id: ID++, filePath, code, dependencies // dependencyMap: {}, Todo 3 }; }
Menyelesaikan Module
Pada tahap ini kita sudah dapat membuat ulang sebuah module dengan struktur standar module yang sudah ditentukan sebelumnya. Namun karena dalam sebuah module bisa saja terdapat dependency ke module lainnya, kita perlu membuat ulang juga dependency module tersebut agar sesuai struktur standar.
Untuk itu, kita harus ulangi cara sebelumnya pada setiap dependency module yang ada. Jangan lupa perbaiki alamat berkas dependency module dahulu agar sesuai dengan alamat sebenarnya ya. Proses pembuatan ulang module pun dapat berjalan seperti sebelumnya.
Hasilnya sebagai berikut:
const fs = require("fs"); const { entry, output } = require("./webpack.config"); const { traverse, transformSync, parseSync } = require("@babel/core"); let ID = ; resolveModules(entry); function resolveModules(filePath) { const entryModule = createModule(filePath); const modules = [entryModule]; for (const module of modules) { module.dependencies.forEach(dependency => { // resolve dependency Path const dependencyPath = resolveDependencyPath(module, dependency); // create child module const childModule = createModule(dependencyPath); // Completing todo 3: Fulfill dependencyMap module.dependencyMap[dependency] = childModule.id; // add child module to module list modules.push(childModule); }); } return modules; } // resolve relativePath to fullPath, e.g. ./message.js => src/message.js function resolveDependencyPath(module, dependency) { const dirname = path.dirname(module.filePath); return path.join(dirname, dependency); } function createModule(filePath) { const content = fs.readFileSync(filePath, "utf8"); const { code } = transformSync(content, { presets: ["@babel/preset-env"] }); const dependencies = []; const abstractSyntaxTree = parseSync(content, { sourceType: "module" }); traverse(abstractSyntaxTree, { ImportDeclaration: declare => { dependencies.push(declare.node.source.value); } }); return { id: ID++, filePath, code, dependencies, dependencyMap: {} }; }
Packaging
Sampai pada tahap ini kita hanya perlu membungkus hasil module-module yang telah kita buat ulang menjadi satu berkas bundle. Untuk itu, pertama ubah dahulu module-module tersebut menjadi javascript object.
Setelah diubah menjadi javascript object, kumpulan module ini tinggal memiliki dua (2) buah properti saja. Properti pertama yaitu factory yang berfungsi sebagai pembungkus module. Properti factory sejatinya dalah sebuah function di mana di dalamnya terdapat kode utama module. Selain itu juga pada parameternya perlu ditambahkan callback function berupa require untuk mengimpor dependency module dan exports untuk keperluan mengekspor module. Hal ini sesuai dengan prinsip module wrapper yang digunakan pada NodeJS [rujukan].
Properti kedua yaitu dependencyMap. Properti ini akan berguna pada tahap selanjutnya, yaitu saat memperbaiki fungsi require().
Hasil modifikasi kodenya menjadi seperti berikut ini:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
const result = packing(entry);
// stringify module into javascript object
function stringifyModule(module) {
return `${module.id}: {
factory: function(require, module, exports) {
${module.code}
},
dependencyMap: ${JSON.stringify(module.dependencyMap)}
}`;
}
function packing(entry) {
const modules = resolveModules(entry);
const modulesString = modules.map(stringifyModule).join(“,”);
return `(function(modules){
})({${modulesString}})`;
}
|
Memperbaiki “require”
Seperti yang telah kita bahas sebelumnya, cara menambahkan dependency pada format CommonJS yaitu menggunakan perintah require(). Perintah ini tidak dapat berjalan normal pada bundle module karena alamat file yang tertulis didalamnya tidak lagi sesuai.
Kita perlu mengarahkan alamat ini agar sesuai dengan alamat pemetaan dependency (dependency map) yang telah dibuat sebelumnya. Jalan termudah untuk melakukannya ialah dengan dengan cara menimpa (override) fungsi standar require().
Fungsi require() pada dasarnya hanya menerima satu parameter, yaitu alamat berkas dependency module berada. Karena alamat berkas ini sudah tidak sesuai, maka alamat tersebut bisa kita ubah berdasarkan dependency map.
Hasilnya seperti ini:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function packing(entry) {
const modules = resolveModules(entry);
const modulesString = modules.map(stringifyModule).join(“,”);
return `(function(modules){
function require(id) {
const { factory, dependencyMap } = modules[id]
const module = { exports: {} }
function localRequire(relativePath) {
const moduleId = dependencyMap[relativePath]
return require(moduleId)
}
factory(localRequire, module, module.exports)
return module.exports
}
require()
})({${modulesString}})`;
}
|
Menyimpan bundle
Langkah terakhir adalah menyimpan hasil bundle menjadi sebuah file. Caranya adalah sebagai berikut:
1
2
3
4
5
|
const result = packing(entry);
// Write the result to ./dist/bundle.js
fs.writeFileSync(path.join(output.path, output.filename), result);
console.log(result);
|
Menjalankan Module Bundler
Untuk menjalankan module bundler yang telah kita buat, silakan gunakan perintah: npm start.
Setelah proses bundling, Anda dapat menggunakannya sebagai dependency script pada index.html seperti saat Anda menggunakan Webpack module bundler.
1
2
3
4
5
|
<body>
<div class=“app”>
</div>
<script src=“./dist/bundle.js”></script>
</body>
|