Yazılım ile ilgili notlar

Çözdüğüm problemleri ve bu yolda öğrendiklerimi yazıyorum. - “Söz uçar, yazı kalır”

JavaScript: Modüller

31/03/2020 tarihinde eklendi

JavaScript kullanıyorsanız, ister ön-yüz ister sunucu tarafı olsun, modül kullanımına rastlamış ve kullanmışsınızdır. “import” ve “require” ifadeleri ne işe yarar, farklı kullanım şekillerinin birbirinden farkı nedir, gibi soruları merak ediyorsanız okumaya devam edin. JavaScript’e nispeten yeni bulaşmış biri olarak, bu soruların cevabını sadece bulmakla kalmayıp anlamaya da çalıştım ve bu süreci istifadenize sunmak istedim.

Cube house unsplash-logoFotoğraf Unsplash’ta Richard Ciraulo’ya aittir.

Terminoloji

Detaylara girmeden evvel terminolojiyi oturtmakta fayda var, çünkü JavaScript dünyasında kavram karmaşasına çok sık rastlanıyor.

Tarihçe

Bizi ilgilendiren iki tür modül var, kronolojik olarak:

  1. Native olmayan modüller (mesela Node.js’te kullanabildiğimiz CommonJS ve AMD gibi)
  2. Native modüller, yani JS modülleri (ES6’daki gibi).

JS modülleri nispeten yeni bir özellik. Geç gelen bir özellik olduğundan, öncesinde boşluğunu dolduracak başka modül API’leri çıkmış. Modüller, yukarıda açıkladığım gibi, Node.js’te daha önce de vardı - fakat native olarak değil.

Modüller

Her dosya bir modül diye tasavvur edebilirsiniz. Öncelikle modüllere neden ihtiyaç olduğunu düşünelim. İster bir ön-yüz (react, angular, vue veya vanilla (düz) js) ister bir sunucu uygulaması olsun (nodejs), kod parçalarını birbirinden ayırmak kodun okunabilirliğini artıran en önemli husustur. Bütün uygulamanızı 10.000 satırlık tek bir dosya olarak tutmak pek akıl kârı değil.

Genelde sunucu uygulamaları yaptığımdan örneklerimi Node.js ile veriyorum. Basit tutalım, örneğimiz;

ES Modülleri (native modüller)

Node.js ile ES modülü oluşturmak isterseniz temelde iki yönteminiz var:

İkinci yöntemi uygulayan bir örnek:

Örneği çalıştırmak için dosyaları bir klasöre indirip, node index.js komutunu kullanabilirsiniz.

Modülden iki değer “export” ediliyor, biri DEFAULT_SLEEP_MILLISECONDS diğeri de sleep fonksiyonu. Modülü kullanacak olan kod (index.js) istediği değerleri “import” ediyor ve böylece bu değerlere aynı isimlerle erişebiliyoruz. Bu örnekte “named export” kullandık, yani export’larımıza birer isim verdik.

Bir de “default” export’lar var:

Bir modülü import ederken eğer süslü parantez ({}) kullanmazsanız, o modüldeki default export’u import etmiş oluyorsunuz.

İsim çakışmalarını engellemek için, import ettiğiniz değerlere yeni isimler verebilirsiniz. Mesela:

import {DEFAULT_SLEEP_MILLISECONDS, sleep as sleepMilliseconds} from './utils.js'

Bu sayede, sleep metodunu, sleepMilliseconds ismiyle kullanabileceksiniz. İsim karmaşasının önüne geçmek için bir yöntem daha var, o da import’ları bir modül nesnesine bağlamak:

import * as utils from './utils.js'

Böylece, fonksiyona utils.sleep() şeklinde ulaşabiliyorsunuz ve import edilen isimlerin çakışma ihtimalini sıfırlamış oluyorsunuz.

Native olmayan modüller (aka. CJS, require)

Basit örneğimizi native olmayan modüllerle oluşturacak olursak:

Tıpkı native modüllerden geri kalmayacak biçimde, kafamız yeterince karışmamış gibi, CJS modüllerinde de farklı export türleri var. Bir diğer kullanım ise module.exports değerini yeni bir değerle ezmek. Bu kullanım için aşağıdaki örneğe bakalım:

exports aslında module.exports‘a işaret eden bir referans olduğundan, bu iki kullanımın etkisi aynı.

exports referansını daha iyi anlayabilmek için, aşağıdaki modülü inceleyelim. Bu modül sanki export ediyor gibi görünse de, aslında hiçbir şeyi export etmiyor.

function sleep(milliseconds) {
  return new Promise(resolve =>
    setTimeout(resolve, milliseconds)
  );
}

exports = sleep;

Bu modülü require ettiğinizde, elinize bir adet {} geliyor. Sleep metodunu export edememiş oluyorsunuz.

Bir de şöyle bir kullanıma rastlayabilirsiniz: module.exports = exports = falanfilan;. Bunun module.exports = falanfilan;‘a üstünlüğü nedir derseniz, aşağıdaki türden hatalar yapma ihtimalinizi azaltıyor:

const DEFAULT_SLEEP_MILLISECONDS = 1000;

function sleep(milliseconds) {
  return new Promise(resolve =>
    setTimeout(resolve, milliseconds || DEFAULT_SLEEP_MILLISECONDS)
  );
}

module.exports = {
  DEFAULT_SLEEP_MILLISECONDS: DEFAULT_SLEEP_MILLISECONDS
};
exports.sleep = sleep; // Geçmiş olsun, bu modülü "require" eden kod sleep'e ulaşamayacak.

Bu modülü require ettiğinizde, sleep fonksiyonunun gelmediğini göreceksiniz. Çünkü module.exports‘a yeni bir değer atadık, fakat exports hala eski değere işaret ediyor. İşte bu hatalara düşmemek ve modül dosyasında farklı noktalarda exports’a bir şeyler ekleyebilmek için, module.exports‘a yeni bir değer atarken, exports‘u da güncellemek gerekiyor. Bu da module.exports = exports = ... kullanımını doğuruyor.

Ön-yüz

Native modüllere ön-yüz örneği olarak, sleep metodumuzu kullanan basit bir html sayfası yapalım.

Burada dikkat edilecek husus, modülleri sayfaya eklerken script tag‘inin module tipinde tanımlanması. HTML dosyasını tarayıcıda doğrudan açmaya çalışırsanız, CORS‘tan ötürü JS modüllerinin yüklenmediğini göreceksiniz. Bu engeli aşmak için bir http sunucusu ayağa kaldırmanız gerekiyor, mesela python -m http.server gibi.

Çıkarılacak dersler