En son Süper Kuvvetli Canlı Yayınımızda kod bölme ve rotaya dayalı parçalara ayırma özelliklerini uyguladık. HTTP/2 ve yerel ES6 modülleri ile bu teknikler, komut dosyası kaynaklarının verimli bir şekilde yüklenmesi ve önbelleğe alınması için gerekli hale gelecektir.
Bu bölümdeki diğer ipuçları ve püf noktaları
asyncFunction().catch()
ileerror.stack
: 9:55<script>
etiketlerinde modüller venomodule
özelliği: 7:30- 8. düğümdeki
promisify()
: 17:20
Özet
Rota tabanlı parçalara ayırma yoluyla kod bölme işlemi nasıl yapılır?
- Giriş noktalarınızın listesini alın.
- Tüm bu giriş noktalarının modül bağımlılıkları ayıklanır.
- Tüm giriş noktaları arasında paylaşılan bağımlılıkları bulun.
- Paylaşılan bağımlılıkları paketleyin.
- Giriş noktalarını yeniden yazın.
Kod bölme ve rotaya dayalı parçalara ayırma
Kod bölme ve rotaya dayalı parçalara bölme birbirine çok yakındır ve genellikle birbirinin yerine kullanılır. Bu durum bazı kafa karışıklıklarına neden oldu. Bu konuyu açıklığa kavuşturmaya çalışalım:
- Kod bölme: Kod bölme, kodunuzu birden fazla pakete bölme işlemidir. JavaScript'inizin tamamını içeren büyük bir paketi müşteriye göndermiyorsanız kod bölme işlemi yapıyorsunuz demektir. Kodunuzu bölme yöntemlerinden biri, rotaya dayalı parçalara ayırma işlemidir.
- Rotaya dayalı parçalara ayırma: Rotaya dayalı parçalara ayırma, uygulamanızın rotalarıyla ilgili paketler oluşturur. Rotalarınızı ve bunların bağımlılıkları analiz ederek hangi modüllerin hangi pakete gireceğini değiştirebiliriz.
Kod bölme işlemi neden yapılır?
Gevşek modüller
Yerel ES6 modülleri sayesinde her JavaScript modülü kendi bağımlılarını içe aktarabilir. Tarayıcı bir modül aldığında tüm import
ifadeleri, kodu çalıştırmak için gerekli modülleri almak üzere ek getirme işlemlerini tetikler. Ancak bu modüllerin hepsinin kendi bağımlılıkları olabilir. Bunun tehlikesi, kodun nihayet yürütülebilmesi için tarayıcıda birden fazla gidiş dönüş süren bir getirme dizisi oluşmasıdır.
Gruplandırma
Tüm modüllerinizi tek bir pakette birleştirmek olan paketleme, tarayıcının 1 gidiş-dönüşten sonra ihtiyaç duyduğu tüm koda sahip olmasını ve kodu daha hızlı çalıştırmaya başlamasını sağlar. Ancak bu, kullanıcıyı gereksiz çok sayıda kod indirmeye zorlar. Bu nedenle bant genişliği ve zaman boşa harcanır. Ayrıca, orijinal modüllerimizden birinde yapılan her değişiklik pakette bir değişikliğe neden olur ve paketin önbelleğe alınmış tüm sürümlerini geçersiz kılar. Kullanıcıların tümünü yeniden indirmesi gerekir.
Kod bölme
Kod bölme, orta yoldur. Yalnızca ihtiyacımız olanları indirerek ağ verimliliği elde etmek ve paket başına modül sayısını çok daha küçük hale getirerek daha iyi önbelleğe alma verimliliği elde etmek için ek gidiş dönüş yatırımları yapmaya hazırız. Gruplandırma doğru şekilde yapılırsa toplam gidiş dönüş sayısı, bağımsız modüllere kıyasla çok daha düşük olur. Son olarak, gerekirse link[rel=preload]
gibi önyükleme mekanizmalarından yararlanarak üçlü tur için ek zaman kazanabiliriz.
1. adım: Giriş noktalarınızın listesini alın
Bu, birçok yaklaşımdan yalnızca biridir. Ancak bu bölümde, web sitemizin giriş noktalarını elde etmek için web sitesinin sitemap.xml
dosyasını ayrıştırdık. Genellikle tüm giriş noktalarını listeleyen özel bir JSON dosyası kullanılır.
JavaScript'i işlemek için babel kullanma
Babel genellikle "dönüştürme" için kullanılır: En son JavaScript kodunu tüketir ve daha fazla tarayıcının kodu yürütebilmesi için JavaScript'in eski bir sürümüne dönüştürür. Buradaki ilk adım, yeni JavaScript'i bir ayrıştırıcıyla (Babel, babylon'u kullanır) ayrıştırmaktır. Bu ayrıştırıcı, kodu "Soyut Söz dizimi ağacı" (AST) olarak adlandırılan bir ağaca dönüştürür. AST oluşturulduktan sonra bir dizi eklenti AST'yi analiz edip değiştirir.
JavaScript modülünün içe aktarma işlemlerini tespit etmek (ve daha sonra değiştirmek) için babel'i yoğun şekilde kullanacağız. Normal ifadelere başvurmak isteyebilirsiniz ancak normal ifadeler bir dili düzgün şekilde ayrıştıracak kadar güçlü değildir ve bakımı zordur. Babel gibi denenmiş ve test edilmiş araçlardan yararlanmak, size birçok sorundan kurtarır.
Babel'i özel bir eklentiyle çalıştırmaya dair basit bir örnek:
const plugin = {
visitor: {
ImportDeclaration(decl) {
/* ... */
}
}
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});
Bir eklenti visitor
nesnesi sağlayabilir. Ziyaretçi, eklentinin işlemek istediği tüm düğüm türleri için bir işlev içerir. AST'de gezinirken bu tür bir düğümle karşılaşıldığında visitor
nesnesinde ilgili işlev, parametre olarak bu düğümle birlikte çağrılır. Yukarıdaki örnekte, dosyadaki her import
beyanı için ImportDeclaration()
yöntemi çağrılır. Düğüm türleri ve AST hakkında daha fazla bilgi edinmek için astexplorer.net adresine göz atın.
2. adım: Modül bağımlılıkları ayıklayın
Bir modülün bağımlılık ağacını oluşturmak için söz konusu modülü ayrıştırır ve içe aktardığı tüm modüllerin listesini oluştururuz. Bu bağımlılıkların da bağımlılıkları olabileceği için bu bağımlılıkları da ayrıştırmamız gerekir. Yineleme için klasik bir örnek.
async function buildDependencyTree(file) {
let code = await readFile(file);
code = code.toString('utf-8');
// `dep` will collect all dependencies of `file`
let dep = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// Recursion: Push an array of the dependency’s dependencies onto the list
dep.push((async function() {
return await buildDependencyTree(`./app/${importedFile}`);
})());
// Push the dependency itself onto the list
dep.push(importedFile);
}
}
}
// Run the plugin
babel.transform(code, {plugins: [plugin]});
// Wait for all promises to resolve and then flatten the array
return flatten(await Promise.all(dep));
}
3. adım: Tüm giriş noktaları arasında ortak bağımlılıkları bulun
Bir dizi bağımlılık ağacımız (isterseniz bağımlılık ormanı) olduğundan, her ağaçta görünen düğümleri arayarak ortak bağımlılıkları bulabiliriz. Ormanımızı düzleştirip tekilleştirecek ve yalnızca tüm ağaçlarda görünen öğeleri tutacak şekilde filtreleyeceğiz.
function findCommonDeps(depTrees) {
const depSet = new Set();
// Flatten
depTrees.forEach(depTree => {
depTree.forEach(dep => depSet.add(dep));
});
// Filter
return Array.from(depSet)
.filter(dep => depTrees.every(depTree => depTree.includes(dep)));
}
4. Adım: Paylaşılan bağımlılıkları paketleyin
Ortak bağımlılık grubumuzu paketlemek için tüm modül dosyalarını birleştirebiliriz. Bu yaklaşım kullanıldığında iki sorun ortaya çıkar: İlk sorun, paketin tarayıcıda kaynak getirmeye çalışacak import
ifadeleri içermeye devam etmesidir. İkinci sorun, bağımlılıkların bağımlılarının paketlenmemesidir. Daha önce yaptığımız için başka bir babel eklentisi yazacağız.
Kod, ilk eklentimize oldukça benzer ancak içe aktarılanları yalnızca ayıklamak yerine kaldırıp içe aktarılan dosyanın paketlenmiş bir sürümünü ekleyeceğiz:
async function bundle(oldCode) {
// `newCode` will be filled with code fragments that eventually form the bundle.
let newCode = [];
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
newCode.push((async function() {
// Bundle the imported file and add it to the output.
return await bundle(await readFile(`./app/${importedFile}`));
})());
// Remove the import declaration from the AST.
decl.remove();
}
}
};
// Save the stringified, transformed AST. This code is the same as `oldCode`
// but without any import statements.
const {code} = babel.transform(oldCode, {plugins: [plugin]});
newCode.push(code);
// `newCode` contains all the bundled dependencies as well as the
// import-less version of the code itself. Concatenate to generate the code
// for the bundle.
return flatten(await Promise.all(newCode)).join('\n');
}
5. adım: Giriş noktalarını yeniden yazın
Son adımda, başka bir Babel eklentisi daha yazacağız. Bu işlevin amacı, paylaşılan paketteki tüm modül içe aktarma işlemlerini kaldırmaktır.
async function rewrite(section, sharedBundle) {
let oldCode = await readFile(`./app/static/${section}.js`);
oldCode = oldCode.toString('utf-8');
const plugin = {
visitor: {
ImportDeclaration(decl) {
const importedFile = decl.node.source.value;
// If this import statement imports a file that is in the shared bundle, remove it.
if(sharedBundle.includes(importedFile))
decl.remove();
}
}
};
let {code} = babel.transform(oldCode, {plugins: [plugin]});
// Prepend an import statement for the shared bundle.
code = `import '/static/_shared.js';\n${code}`;
await writeFile(`./app/static/_${section}.js`, code);
}
Sonlandır
Bu oldukça heyecan verici bir yolculuktu, değil mi? Bu bölümdeki amacımızın, kod bölme işlemini açıklamak ve bu işlemin ne kadar basit olduğunu göstermek olduğunu lütfen unutmayın. Sonuç işe yarar ancak demo sitemize özeldir ve genel durumda korkunç bir şekilde başarısız olur. Üretim için WebPack, RollUp gibi yerleşik araçlardan yararlanmanızı öneririm.
Kodumuzu GitHub deposunda bulabilirsiniz.
Tekrar görüşmek üzere.