EcmaScript Modüler Programlama

Uğur GELİŞKEN
11 min readDec 27, 2021

--

Geçmiş yıllarda JavaScript ile programlaman web yazılımları oldukça küçük bir şekilde başladı ve basit olarak kullanıcı ile etkileşimi sağlamak amacıyla kullanıldı. Bu nedenle çok fazla büyük kod yığınlarıyla çalışılma ihtiyacı da duyulmadı. Fakat zaman ilerledikçe web yazılımlarına ihtiyacın artması, JavaScript’e daha fazla yetenek kazandırılması, JavaScript’in de her platformda boy göstermeye başlamasıyla birlikte daha büyük projeler ortaya çıkmaya başladı. Hal böyle olunca da JavaScript’in desteklemediği modüler programlama mekanizması Node.js üzerinde çalışan RequireJS, CommonJS, AMD gibi modüler programalmaya olanak sağlayan framework’lerin doğmasına sebep oldu. Fakat iyi haber, artık bu tür framework’lere ihtiyacımız yok, çünkü EcmaScript 6 sürümü ile birlikte tarayıcılar artık doğal olarak modüler programlama yapısını desteklemeye başladı. Bu destekle de birlikte Angular ve React gibi güçlü framework’lerin temelleri atıldı.

Modüler programlamada mantık; kodları tek bir dosya veya birkaç dosyada parçalara ayırmak değil; kodları farklı farklı parçacıklara ayırmak, bunları da kendi aralarında modüler olarak birleştirerek programlamaktır. Aynı bir lego gibi… O an ihtiyacınız olan metotları ana kodunuza takın, gerekmeyenleri takmayın.

Bu makaledeki teknikleri öğrendikten sonra isterseniz React, isterseniz Angular gibi güçlü JavaScript framework’lerini rahatlıkla kullanabilecek kabiliyette olacaksınız. Veya dilerseniz hiç framework kullanmadan sadece modüler yapı ile de güçlü sistemler programlayabileceksiniz.

Modül Tanımlamada Temeller

Modüller, iki veya daha fazla dosya arasında import ve export deyimleri ile fonksiyon ve sınıfların aktarılabilmesini sağlayarak modüler programlamanın temellerini sağlar. Bu mantığı kavramak için bir örnek üzerinden modül kullanımını öğrenelim.

Örnekte o anki sistem zaman ve saat bilgisini metin olarak veren bir modül geliştireceğiz ve arayüzden butonlarla import edeceğimiz fonksiyonları kullanacağız.

Bir proje klasörü içerisinde index.html, index.js ve ../date/getDateTimeString.js adında dosyaları tanımlayın. Modül olarak kullanılacak getDateTimeString.js dosyasını bir klasör altına aldın. Çünkü modülleri kendi işleyiş ve yeteneklerine göre bir hiyerarşide tutmak gerekir. Bu modül zaman ile ilgili işlem yaptığı için date isimli bir klasör altında oluşturmak mantıklı olacaktır.

İlk olarak index.html’i temel olarak oluşturalım…

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="module" src="index.js"></script>
</head>
<body>
</body>
</html>

HTML5 sayfa yapımızda öncelikle Babel’i normal bir şekilde ekliyoruz. Peşine de index.js dosyamızı sayfaya dahil ediyoruz, ancak type olarak module özelliğini veriyoruz. Böylece o dosyanın artık modüler olduğunu belirtmiş oluyoruz.

Modül Tanımlama ve Dışa Aktarma

Şimdi ../date/getDateTimeString.js isimli JavaScript dosyasını oluşturup gerekli modüler olarak yazalım, sonra da inceleyelim…

../date/getDateTimeString.js

const formattedDateTime = () => {
return new Date().toJSON().slice(0,10) + " " + new Date(new Date()).toString().split(' ')[4];
}
export { formattedDateTime };

İlk olarak normal şekilde fonksiyon tanımladık ve bir değer return ettik. Değer olarak pratik bir şekilde (genelde Ninja kod olarak da adlandırılır) zincirleme metotlarla o anki sistem tarih ve saatini bir metin olarak return ettik. Buraya kadar normal…

Sayfanın sonunda da bir export deyimi kullandık. Export, yani aktar deyimi ile süslü parantezler içinde dışa aktarılacak olan fonksiyonun ismini tanımladık. Artık bu dosya istenilen her yerde kullanılabilir, içinden de formattedDateTime fonksiyonu çekilebilir.

Farklı bir metot olarak kodların sonunda export { } içinde metodun adını yazmak yerine doğrudan metodun başında da tanımlanabilir. Mesela;

export const formattedDateTime = () => {
//..
}

İster topluca en alt satırda export edin, isterseniz de doğrudan metot veya değişken tanımlanırken başına export yazarak import edilebilmesini sağlayın.

Modül Yükleme

Sistemin zaman ve saat bilgisini verecek olan modülümüzü ve metodumuzu oluşturduk. Şimdi bunu index.js dosyası ile projeye çekelim.

Bir modül dosyasını içe çekmek için import deyimi kullanılır. Nasıl kullanıldığını aşağıdaki kod yapısı ile inceleyelim…

index.js

import { formattedDateTime } from './date/getDateTimeString.js';
console.log( formattedDateTime() );
document.querySelector('body').innerHTML = `<h1>${formattedDateTime()}</h1>`;

Script dosyasının ilk satırında import işlemi yapılır. Öncelikle süslü parantezler içinde içe çekilecek olan metot, sonrasında from deyimi ile de String olarak modül dosyasının yolu (path) tanımlanır. Eğer birden fazla metot yüklenecekse aralarına virgül işareti konularak çoklu içe çekme uygulanabilir (İlerleyen bölümlerde örnekleri göreceksiniz).

Yüklediğimiz formattedDateTime metodu ile gelen değeri konsolda yazdırdık. Ek olarak bir de HTML sayfamızda <h1> etiketleri içinde gösterdik.

Eğer modül içinde kullanılması gereken çok fazla metot varsa ve bunları tek tek süslü parantez içinde yazmak zahmetli olacaktı. Bu gibi durumlarda * as deyimlerinden faydalanılır. Aşağıda basit bir örnek yer almaktadır.

import * as getDateTimeString from './date/getDateTimeString.js';

Bu örnekte import ettiğimiz modülümüzden tüm metotları * ile tanımladık ve tüm hepsini de as (gibi anlamında) deyimi ile getDateTimeString isimli bir nesneye aktardık. Yazdığımız bu isim ile dosya isminin aynı olmasına gerek yok, ancak bu kurala uymakta fayda var. Eğer kodlarınız karmaşık bir hale gelirse, hangi nesne hangi dosyayı referans alıyor diye düşünmek zorunda kalmazsınız. Tanımlanan bu nesneden de normal Object metodu gibi modül fonksiyonu çağrılır.

getDateTimeString.formattedDateTime()

Eğer modülü kullanmadan içe aktarmak istiyorsanız aşağıdaki gibi kullanabilirsiniz. Bu kullanım metodunu kitabın ilerleyen bölümlerinde kodlarımıza birim test yazarken göreceğiz.

import './date/getDateTimeString.js';

Eğer import ile içe çektiğiniz bir modülü yine başka bir dosyada kullanmak için yine dışarı çıkarmak istetrseniz, aşağıdaki yöntemi kullanabilirsiniz.

import formattedDateTime './date/getDateTimeString.js';
export formattedDateTime;

Çoklu Modül Metotları ve Değişkenleri İçe Aktarma

Modüller içerisinde bir metot olabileceği gibi birden fazla metot veya geri döndürülen değişken değerleri olabilir. Bir önceki örnekte sadece bir metot kullandık ve import ile içe çektik. Şimdi de Array’ler ile karmaşık işler yapalım. Örneğimizde yapacağımız modülde bir Array’in elemanlarını büyükten küçüğe, küçükten büyüğe sıralayan bir modül geliştirelim. Array’i de oluşturabilmek için her çağrımızda bize 1’den 100’e kadar rastgele bir Integer değer veren bir değişken (tabi bu değişkeni de bir fonksiyon dolduracak) tanımlayalım.

Örneğimizi modules_arraySort klasörü altında yapacağız. Modülün olduğu dosya da ../modules/sortArray.js olarak tanımlanacak.

index.html dosya yapımız yine aynı olacak…

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="module" src="index.js"></script>
</head>
<body>
</body>
</html>

sortArray.js dosyamız üzerinde metotlarımızı geliştirelim…

// 1-100 arası rastgele sayı üret
const randomNumber = (maxNumber) => {
return Math.floor(Math.random() * maxNumber) + 1;
}
// Artan sıralama
const arrayASC = (array) => {
return array.sort( (a, b) => {
return a - b;
});
}
// Azalan sıralama
const arrayDESC = (array) => {
return array.sort( (a, b) => {
return b - a;
});
}
export { randomNumber, arrayASC, arrayDESC };

Modül içerisinde randomNumber isminde bir Integer tanımladık ve bu değişkeni tanımlaması için de bir fonksiyon kullandık. Fonksiyon, maxNumber parametre ismi ile değer karşılıyor. Bu değer, 1 ile azami olarak kaç arasında tam sayı olacağını belirlemek için kullanılacak. randomNumber metodunu çağırdığımız yerde bir tam sayıyı parametre olarak vereceğiz.

İki adet de metot oluşturduk. Bunlar arrayASC() ve arrayDESC() metotları. Bunlar da parametre olarak bir Array alacak ve ya azdan çoğa ve çoktan aza olacak şekilde sıralayıp yeni bir Array döndürecek. Parametre olarak alacakları Array’de de rastgele sayılar olacak. Bu sayıları da randomNumber ile üretip, modülü import ettiğimiz kısımda bir Array tanımlayarak push() metodu ile for döngüsü kullanarak 10 defa ekleme yapacağız.

index.js ile aşağıdaki gibi metotları modülden çekip kullanalım…

import { randomNumber, arrayASC, arrayDESC }
from './modules/arraySort.js';
const random10Numbers = [];for (let i = 0; i <= 10; i++) {
random10Numbers.push(randomNumber(100));
}
console.log("Rastgele sayılar :", random10Numbers );
console.log("Artan sayılar :", arrayASC(random10Numbers) );
console.log("Azalan sayılar :", arrayDESC(random10Numbers) );

Örneğimizde 2 adet metot, 1 adet de Array değişken dışa aktardık ve bunları import ettik. İlk olarak random10Numbers adında boş bir Array tanımladık ve for döngüsü ile 10 tekrarla rastgele değerle içini doldurduk. Rastgele değerleri de modülden çektiğimiz randomNumber() metodu ile sağladık. Bu metoda maksimum değeri de verdik, böylece “1 — maksimum değer” aralığında sonuç geri verdi. Sonrasında da arrrayASC metodu ile bu sayıları küçükten büyüğe, arrayDESC metodu ile de büyükten küçüğe sıralayarak sonuçlarını konsolda yazdırdık.

Toplu İçe Aktarma

Önceki örneğimizde import işleminde her bir export edilen metotları ve değişkenin tek tek girmiştik. Bazen bu isimler çok fazla olabilir, hepsini teker teker tanımlamaktansa, export edilenlerin hepsini import ile içe aktarmak için *operatörünü kullanabiliriz. Böylece import edilenleri komple bir Object’e aktarabiliriz.

index.js dosyamızı aşağıdaki gibi düzenleyelim.

import * as arraySort from './modules/arraySort.js';const random10Numbers = [];for (let i = 0; i <= 10; i++) {
random10Numbers.push(arraySort.randomNumber(100));
}
console.log("Rastgele sayılar:",random10Numbers );
console.log("Artan sayılar:", arraySort.arrayASC(random10Numbers));
console.log("Azalan sayılar:", arraySort.arrayDESC(random10Numbers));

İçe aktarırken * as arrayMethods yapısı ile bütün export edilen nesne ve metotları as (gibi) deyimi ile arrayMethods adlı Object’e aktardık. Obje adı ve dosya ismi aynı görülüyor, ama zorunlu bir kullanım değildir. Ancak proje karmaşıklaştıkça neyi neye aktardım diye sonraları düşünmemek için önerilir. Metot ve değişken değerleri objeye aktarıldığı için doğrudan çağırdığımızda hata alırız. Artık obje ismini de referans vererek erişim sağlayabiliriz.

Varsayılan Metod

Bazen modülü import ettiğimizde otomatik olarak bir init() veya main() gibi başlatma fonksiyonunun çalışmasını isteyebiliriz. Bahsettiğimiz bu durum, modüllerde default modül olarak anlamlandırılmaktadır. Yani modül içinde bir metodu doğrudan otomatik olarak çalıştırabiliriz. Ancak, her bir modülden sadece bir tane default, yani varsayılan metot dışa aktarılabilir.

Örnek olarak faktöriyel hesabı yapan bir modül geliştirelim… Modül olarak factorial.js dosyasını modules klasörü altında oluşturalım.

../modules/factorial.js

const sampleNumber = 5;const init = () => {
console.log("Faktöryel hesaplama modül başlatıldı");
console.log("Örnek hesaplama: calculateFactorial (5) gibidir");
}
const calculateFactorial = number => {
if (number === 0 || number === 1)
return 1;
for (let i = number - 1; i >= 1; i--) {
number *= i;
}
return number;
}
const factorialExample = calculateFactorial(sampleNumber);export { calculateFactorial, factorialExample, sampleNumber };
export default init;

Öncelikle sampleNumber adında basit bir değişken tanımladık ve 5 değerini tuttuk. Bu değeri modül içinde tanımladığımız createFactorial() metodunun nasıl çalıştığını örneklemek için parametre değeri olarak vereceğiz. Örnekte 5! hesabı yaptırıp bunu init() metodu içinde konsolda yazdırdık. Modül yüklendiğinde örnek bir hesap factorialExample değişkeni ile konsolda gösterilecek. Bu değişkene de calculateFactorial() metodundan sonuç gelecek.

Son olarak export deyimi ile varsayılan olarak kullanılmayacak metot ve değişkenleri dışa çıkardık. Örnek olması amacıyla sampleNumber isimli değişkeni de çıkardık, ancak bu değişkeni import etmeyeceğiz. Yani bir modülde sadece import edilecek olanları export etmeniz beklenmez, zaten bunu bilemeyiz de. Şimdilik sadece modül dışından bu değişkeni görmek istersek diye export ediyoruz.

Varsayılan modülü de export default deyimini kullanarak dışa çıkardık.

Daha önce de belirtiğimiz gibi export edilecek olan metot veya değişkenleri son satırda belirtmek yerine doğrudan tanımlandıkları yerde gösterebiliriz.

Mesela init() metodu için…

export default init = () => { ... }

Yalnız dikkat edin, export veya export default kullanılırken const, let veya var gibi tanımlamalar kullanılmaz.

Modülü kullanacağımız index.js dosyasını yazalım…

import init, { calculateFactorial, factorialExample } from './modules/factorial.js';

init();

console.log("Örnek hesap (5!): ", factorialExample);
console.log("10! çözümü: ", calculateFactorial(10));

Yaptığımız işlemler neticesinde aşağıdaki gibi konsolda çıktı verecektir.

İçe Aktarılan Metotları Yeniden Adlandırma

Bazen projenizi geliştirirken başkalarının modüllerinden faydalanmanız gerekir, genelde de böyle olur. Ancak başkasının modülünde yazdığı metot ve değişken isimleri sizin projenize uygun olmayabilir. Gerek dil farklılığı, gerek yazım formatı bakımından uyumsuzluk olduğunu düşündüğünüzde bu metot isimlerini değiştirmek isteyebilirsiniz. Modül içinden bu düzenlemeyi yapmak hem zahmetli hem de çeşitli hatalara yol açabilecek kritik bir işlemdir.

Bir modülden metot veya değişken yüklediğinizde, bunlara yeni isimler verebilirsiniz. Daha önceki örneklerimizde * as deyimi ile tüm metot ve değişkenleri bir Object’e aktarıp yeni bir referans isimle kullanmıştık. Aynı mantıkla her bir import edilen metot ve değişkeni yeniden isimlendirebilir, sonra da bu isimle çağırabiliriz.

Önceki konumuzda yaptığımız örneği, bahsettiğimiz bu yeni isimlendirme tekniğiyle yeniden kullanalım.

Sadece index.js’de aşağıdaki gibi düzenle yapmak yeterlidir.

import init, { 
calculateFactorial as faktoryelHesapla,
factorialExample as faktoryelOrnegi
} from './modules/factorial.js';

init();

console.log("Örnek hesap (5!): ", faktoryelOrnegi);
console.log("10! çözümü: ", faktoryelHesapla(10));

Yazdığımız koda baktığımızda init() metodunu yeniden adlandırmadığımızı görürüz, çünkü varsayılan metot yeniden adlandırılamaz. Diğer iki metot ise Türkçe isimlendirme faktoryelHesapla ve faktoryelOrnegi adlarıyla ile yeniden adlandırıldı ve kullanıldı.

Eğer bu tür yeniden adlandırma tekniği kullanacaksanız, her bir import edileni ayrı ayrı satırlarda örnekteki gibi tanımlayın. Böylece daha iyi bir kod okunabilirliği sağlanmış olur.

Dinamik Modül Yükleme (layz-load)

Son olarak EcmaScript 11 ile gelmiş olan dinamik modül yükleme tekniğini inceleyeceğiz. Öncesinde neden dinamik yükleme özelliğine ihtiyaç duyulduğuna değinelim…

Klasik import yöntemi statik bir yöntemdir ve her zaman içe aktarılan modüldeki tüm kodları yükler ve proje başladığında da tüm kodların işlenmesine neden olur. Bu durum projeye yük getirecek ve performans kayıplarına neden olacaktır. Hatta bazen projenin bozulmasına bile sebep olabilir. Bir modülü koşullu veya isteğe bağlı olarak yüklemek isteyebiliriz. Buna da dinamik yükleme denir.

Statik yüklemenin diğer bir dezavantajı da projeyi yavaşlatmasıdır. Her bir kod belli bir yükleme süresi ister. Günümüzde her ne kadar internet hızı artmış olsa da çok fazla kaynakla çalıştığımız zaman çok daha dikkatli olmamız ve nereden ne kazanırız hesabı yapmamız gerekir. Örneğin bir web projesi veya mobil uygulama düşünün. Her bir sayfa için ayrı ayrı modül kullandığınızı varsayalım. Uygulama açıldığında tüm modülleri yüklemek anlamsız olur, bunun yerine hangi sayfa açıksa onun modülü yüklense yeterlidir. Veya iletişim formu için kullanacağınız bir mail gönderme modülü olduğunu düşünün. Eğer kullanıcı bir form doldurup mail göndermek istemedikçe o modülün yüklenmesi gerekmez. Sadece gönder butonuna tıkladığında veya iletişim sayfasına gelindiğinde o modülün yüklenmesi gerekir. Böylece hem bellekte fazladan yer kaplamaz, hem de uygulamayı yavaşlatmaz, risk oluşturmaz, yan etki yapmaz.

Bazen de modüller çok fazla sayıda, ancak belli bir isim hiyerarşisinde olabilir. Mesela moduleA, moduelB… gibi belli bir hiyerarşide gidiyorsa, bu hiyerarşiyi kullanarak dinamik isimlendirme ile modül yüklemeniz gerekebilir. Yani ../modules/module<A|B> gibi… İşte bu durumda da tüm modülleri projeye yığmak yerine, dinamik isimlendirme ile import etmek zorunlu olacaktır.

Her ne kadar dinamik yükleme istemi avantajlı gibi görünse de sadece gerektiği hallerde bu yönteme başvurun. Çünkü çeşitli test araçları dinamik yüklemeyi şimdilik desteklemez ve kodun geçerliliği ispatlanamaz. Test araçları statik yüklenen modülleri daha rahat analiz eder. Ayrıca dinamik yükleme sistemini yönetmek, büyük projelerde kontrol kaybına sebep olabilir.

Bu açıklamalardan sonra artık modülleri hangi hallerde nasıl dinamik olarak yükleyebileceğimize örneklerle bakalım…

Dinamik Modül Yükleme (then(), await)

Dinamik olarak yüklenen modüllerde import deyimi bir fonksiyon çağırabilir. Daha önceki örneklerimizde faktöriyel hesabı yapan modülü dinamik olarak yükleyip kullanalım.

const factorial_module = './modules/factorial.js';

import(factorial_module)
.then((module) => {

console.log("Modül yüklendi...");

module.default();

console.log(module.factorialExample);

console.log("10! çözümü: ", module.calculateFactorial(10));

}).catch(error => {
console.log("Hata: ", error);
});

Öncelikle hangi modül yüklenecekse onun yolu (path) factorial_module isimli bir değişkende tutuldu. Sonrasında import() metoduna parametre olarak verildi. Bu metoda da zincirleme metotlarla .then() ve .catch() metotları eklendi. Eğer bir hata olursa (dosya yolu hatalı olabilir, işlem yaparken yanlış yazılabilir vs.) hatayı konsolda yazdırıyoruz. Modül yüklendiğinde .then() bloğuna girilecek… Yüklenen modül de module nesnesine aktarılacak. Artık bu isim referans alınarak modül içindeki elemanlara erişilebilir. Blok içinde ilk olarak modülün yüklendiğini konsolda gösterdik. Ardından modülün varsayılan metodunu çağırdık. Buraya dikkat edin, export default ile dışa çıkardığımız varsayılan metodun adı init() olmasına rağmen, burada .default() ile çağırdık. Sonrasında da normal bir şekilde referans verdiğimiz modül ismini kullanarak metot ve değişkenlere eriştik. Sonuç yine aynı olacaktır.

Eğer varsayılan modülü module.init() olarak çağırsaydık, aşağıdaki gibi “Böyle bir fonksiyon yok” hatası alacaktık.

Eğer modülün dosya yolunu da yanlış yazsaydık, aşağıdaki gibi fetch hatası alacaktık.

Dinamik olarak modül yüklerken isterseniz await ile asenkron da yükleyebilirsiniz. Aşağıdaki kod yapısını inceleyin.

const module = await import("./modules/factorial.js");

HTML Sayfalarında Gömülü Olarak Modül Yükleme

Modüller konusunu incelerken hep index.js içinden modül yükledik, ancak istersek HTML sayfasından da doğrudan yükleyebiliriz. Çoklu sayfalarla çalışırken bazen doğrudan sayfaya gömülü olarak yazmak daha pratik olabilmektedir.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script>
const factorial_module = './modules/factorial.js';
import(factorial_module)
.then((module) => {
document.write("<p>Modül yüklendi...</p>");
module.default();
document.write(`<p>${module.factorialExample}`);
document.write(`<p>10! çözümü: ${module.calculateFactorial(10)}</p>`);
}).catch(error => {
console.log("Hata: ", error);
});
</script>
</head>
<body>
</body>
</html>

Örneğimizde yine faktöriyel işleminden ilerledik. Bazı işlemleri konsolda yazdırırken, bazılarını HTML’de gösterdik.

--

--

Uğur GELİŞKEN

Full-stack Developer [ UI / UX | JAM Stack | ME(A,R,V)N | LAMP ], Author, Pro Gamer