Halo semuanya!
Kali ini kita akan bahas materi seputar web development. Tulisan kali ini adalah salah satu oleh-oleh yang penulis pelajari saat mengikuti LINE Developer Day 2019 di Tokyo bulan November kemarin. Jika Anda baru memperlajari Javascript, mungkin pembahasannya akan agak sedikit lebih berat, jadi siapin kopi dulu yuk. Tulisan ini juga pas untuk kalian yang sudah mengikuti kelas Progressive Web Apps. Semoga kalian bisa mengikuti hingga akhir ya.
Apa itu module?
Hal pertama yang perlu kita pahami yaitu module. Makanan jenis apakah ini?
💻 Mulai Belajar Pemrograman
Belajar pemrograman di Dicoding Academy dan mulai perjalanan Anda sebagai developer profesional.
Daftar SekarangSayangnya, ini bukanlah jenis makanan. Sederhananya, module adalah sebuah berkas yang berisi script kode. 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.
Sebagai contoh, berikut ini adalah module sayHi.js [rujukan]
1 2 3 4 |
// sayHi.js export function sayHi(user) { alert(`Hello, ${user}!`); } |
… kemudian di sisi lain, module lainnya mengimpor module tersebut:
1 2 3 4 5 |
// main.js import {sayHi} from './sayHi.js'; alert(sayHi); // function... sayHi('John'); // Hello, John! |
The Bad Part!
Satu dua atau tiga module yang kita gunakan mungkin tidak akan bermasalah. Namun coba bayangkan website-website besar saat ini, pasti punya banyak sekali module! Dengan banyaknya module yang digunakan, sangat mungkin bahwa di antara module-module tersebut memiliki kesamaan nama fungsi.
Tidak jarang juga ditemukan module script yang bergantung pada script lainnya. Misalnya, saat menggunakan bootstrap sebagai basis template. Akan terdapat script ‘bootstrap.js’ yang tak dapat berfungsi jika sebelumnya script ‘jquery.js.’ tidak dipasang.
Berikutnya adalah hal kecil yang mungkin sering terlewatkan. Salah urutan penempatan script saja, dapat membuat fungsi-fungsi yang telah dibuat, tidak berjalan sesuai rencana.
Module Bundler
Selamat, semua hal tadi kini sudah ada solusinya. Menggunakan module bundler kita tidak perlu lagi memikirkan masalah di atas. 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. Sebagaimana yang disebutkan dalam core concepts mereka, 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. Contohnya:
1 2 3 |
module.exports = { entry: './path/to/my/entry/file.js' }; |
Output
Properti output memberitahukan di mana module bundler harus menyimpan hasil bundler yang telah dikumpulkan dan menentukan nama bundle-nya.
1 2 3 4 5 6 7 8 |
const path = require('path'); module.exports = { entry: './path/to/my/entry/file.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'my-first-webpack.bundle.js' } }; |
Yuk Praktek!
Pertama, jika Anda belum memasang NodeJS silakan pasang terlebih dahulu sesuai dengan sistem operasi yang Anda gunakan. Berkas instalasi NodeJS bisa diunduh pada tautan berikut ini.
Selanjutnya, silakan lakukan clone atau unduh contoh proyek module bundler sederhana yang telah dibuat oleh mas Sing-Ming Chen – LINE Front-end Engineer dari repository berikut ini: https://github.com/Asing1001/module-bundler
Setelah mengkloning repository di atas, jangan lupa juga untuk mengunduh semua kebutuhan module yang diperlukan selama pengembangan dengan menjalankan perintah “npm install”.
Pada berkas index.html dapat kita temukan bahwa ia memuat 3 buah module javascript yang ada di direktori src/. Module-module inilah yang akan di-bundling menjadi satu berkas.
1 2 3 4 5 6 7 |
<body> <div class="app"> </div> <script src="./src/alertBtn.js"></script> <script src="./src/entry.js"></script> <script src="./src/message.js"></script> </body> |
Sebagai latihan, kita akan menggunakan webpack terlebih dahulu. Pertama, isikan berkas konfigurasinya seperti ini:
1 2 3 4 5 6 7 8 9 10 11 12 |
// webpack.config.js const path = require("path"); module.exports = { mode: "development", devtool: "source-map", entry: "./src/entry.js", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js" } }; |
Selanjutnya, jalankan perintah “npm run webpack” untuk menjalankan proses bundling.
Setelah selesai, sesuai konfigurasi yang kita buat, hasil bundle dapat kita temukan di ‘dist/bundle.js’. Sampai pada tahap ini, kita sudah bisa mengganti module-module yang digunakan di berkas index.html menggunakan berkas hasil bundling tadi.
Sebelum
1 2 3 4 5 6 7 |
<body> <div class="app"> </div> <script src="./src/alertBtn.js"></script> <script src="./src/entry.js"></script> <script src="./src/message.js"></script> </body> |
Sesudah
1 2 3 4 5 |
<body> <div class="app"> </div> <script src="./dist/bundle.js"></script> </body> |
Membuat Module Bundler Sendiri
Sampailah kita ke bagian inti tulisan ini, yaitu membuat module bundler sendiri.
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: 0, 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 = 0; 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 = 0; 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: https://bit.ly/2sc1PBI
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
const fs = require("fs"); const { entry, output } = require("./webpack.config"); const { traverse, transformSync, parseSync } = require("@babel/core"); let ID = 0; 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(0) })({${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> |
Penutup
Untuk Anda yang masih bingung cara pembuatannya, silakan lihat langkah demi langkah solusinya pada direktori “solutions”.
Masih penasaran dengan materi lainnya dari LINE Developer Day 2019? Berikut ini tautannya ya:
- Evolusi LINE Things dan Produk yang Dirilis
- Belajar Membuat gRPC Microservice dengan LINE Armeria dan Spring WebFlux
Referensi
Ronen Amiel
Minipack
Build Your Own Webpack
Luciano Mammino
Unbundling the JavaScript module bundler
Adam Kelly
Let’s learn how module bundlers work and then write one ourselves