En son Supercharged Canlı Yayınımızda kod bölme ve rotaya dayalı öbekleme uyguladık. HTTP/2 ve yerel ES6 modülleriyle bu teknikler, komut dosyası kaynaklarının verimli bir şekilde yüklenmesini ve önbelleğe alınmasını sağlamak için gerekli hale gelecek.
Bu bölümdeki çeşitli ipuçları ve püf noktaları
error.stack
ileasyncFunction().catch()
: 9:55<script>
etiketlerinde modüller venomodule
özelliği: 7:30- 8. Düğümdeki
promisify()
: 17:20
Özet
Rotaya dayalı öbekleme aracılığıyla kod bölme 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ını çıkarın.
- Tüm giriş noktaları arasındaki paylaşılan bağımlılıkları bulun.
- Paylaşılan bağımlılıkları gruplandırın.
- Giriş noktalarını yeniden yazın.
Kod bölme ve rotaya dayalı öbekleme
Kod bölme ve rotaya dayalı öbekleme, birbiriyle yakından ilişkilidir ve genellikle birbirinin yerine kullanılır. Bu durum bazı karışıklıklara yol açtı. Sorunu netleştirmeye çalışalım:
- Kod bölme: Kod bölme, kodunuzu birden fazla gruba bölme işlemidir. Müşteriye tüm JavaScript'lerinizi içeren büyük bir paket göndermiyorsanız kod bölme işlemi yaparsınız. Kodunuzu bölmenin belirli bir yolu, rota tabanlı öbekleme kullanmaktır.
- Rotaya dayalı yığın oluşturma: Rotaya dayalı yığın oluşturma, uygulamanızın rotalarıyla ilgili paketler oluşturur. Rotalarınızı ve bağımlılıklarını analiz ederek hangi modüllerin hangi pakete dahil olduğunu değiştirebiliriz.
Kod bölme neden yapılır?
Serbest modüller
Yerel ES6 modülleriyle her JavaScript modülü kendi bağımlılıklarını içe aktarabilir. Tarayıcı bir modül aldığında, tüm import
ifadeleri, kodu çalıştırmak için gereken modülleri elde etmek için ek getirmeleri tetikler. Ancak tüm bu modüllerin kendi bağımlılıkları
olabilir. Tehlike ise tarayıcının, kod nihai olarak yürütülene kadar birden çok gidiş-dönüş süren birçok getirme işlemi içermesidir.
Paket haline getirme
Tüm modüllerinizi tek bir paket halinde satır içine almak, tarayıcının 1 gidiş-dönüşten sonra ihtiyaç duyduğu tüm koda sahip olmasını ve kodu daha hızlı çalmaya başlayabilmesini sağlar. Ancak bu durum kullanıcıyı gerekli olmayan çok miktarda kodu indirmeye zorlayarak bant genişliği ve zaman kaybına neden olur. Ayrıca, orijinal modüllerimizden birinde yapılacak her değişiklik pakette 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 içeriğin tamamını tekrar indirmesi gerekir.
Kod bölme
Kod bölme çözümü bu noktada önemli bir çözümdür. 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 önbelleğe alma verimliliğini artırmak için ek gidiş gelişler yatırımı yapmaya hazırız. Paketleme doğru yapılırsa toplam gidiş dönüş sayısı gevşek modüllere kıyasla çok daha az olur. Son olarak, gerektiğinde üç tur daha kazanabilmek için link[rel=preload]
gibi önceden yükleme mekanizmalarından yararlanabiliriz.
1. Adım: Giriş noktalarınızın bir listesini alın
Bu, birçok yaklaşımdan yalnızca biri. Ancak bölümde, web sitemize giriş noktalarını almak için web sitesinin sitemap.xml
kodunu 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'i kullanma
Babel yaygın olarak "transkript" için kullanılır: Yeni bir JavaScript kodu tüketmek ve daha fazla tarayıcının kodu yürütebilmesi için kodu eski bir JavaScript sürümüne dönüştürmek. Buradaki ilk adım, yeni JavaScript'i, kodu "Soyut Söz Dizimi Ağacı" (AST) adlı bir ayrıştırıcıyla (Babel babylon kullanır) ayrıştırmaktır. AST oluşturulduktan sonra bir dizi eklenti AST'yi analiz edip bozar.
Bir JavaScript modülünün içe aktarımlarını tespit etmek (ve daha sonra değiştirmek) için babel'ı yoğun bir şekilde kullanacağız. Normal ifadelere başvurmak cazip gelebilir, ancak normal ifadeler bir dili düzgün şekilde ayrıştıracak kadar güçlü değildir ve kullanımı zordur. Babel gibi denenmiş ve test edilmiş araçlara güvenmeniz, birçok baş ağrısından tasarruf etmenizi sağlar.
Aşağıda, Babel'i özel bir eklentiyle çalıştırmaya ilişkin basit bir örnek verilmiştir:
const plugin = {
visitor: {
ImportDeclaration(decl) {
/* ... */
}
}
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});
Eklentiler bir visitor
nesnesi sağlayabilir. Ziyaretçi, eklentinin işlemek istediği her düğüm türü için bir işlev içerir. AST'den geçiş yapılırken bu türden bir düğümle karşılaşılırsa parametre olarak bu düğümle birlikte visitor
nesnesindeki karşılık gelen işlev çağrılır. Yukarıdaki örnekte, dosyadaki her import
bildirimi 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 sitesine göz atın.
2. Adım: Modül bağımlılıklarını çıkarın
Bir modülün bağımlılık ağacını oluşturmak için bu modülü ayrıştıracağız ve içe aktardığı tüm modüllerin listesini oluşturacağız. Bu bağımlılıkları da ayrıştırmamız gerekir, çünkü aynı zamanda bağımlılıkları da içerebilir. Yinelemenin klasik örneği!
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ındaki paylaşılan bağımlılıkları bulun
Bağımlılık ormanımız (varsa, bir bağımlılık ormanı) olduğu için her ağaçta görünen düğümleri arayarak paylaşılan bağımlılıkları bulabiliriz. Yalnızca tüm ağaçlarda görülen öğeleri tutacak şekilde ormanımızı ve filtrelerimizi düzleştirip tekilleştireceğ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ı gruplandırın
Paylaşılan bağımlılıklar kümemizi bir araya getirmek için tüm modül dosyalarını birbirine bağlamamız yeterliydi. Bu yaklaşım kullanılırken iki sorun ortaya çıkar: İlk sorun, pakette hâlâ import
ifadelerinin bulunması ve böylece tarayıcının kaynakları getirmeye çalışmasıdır. İkinci sorun, bağımlılıkların
bağımlılıklarının paketlenmemiş olmasıdır. Bunu daha önce de yaptığımız için başka bir babel eklentisi yazacağız.
Kod, ilk eklentimize oldukça benziyor, ancak sadece içe aktarmaları almak yerine, bu öğeleri kaldırıp içe aktarılan dosyanın paketlenmiş bir sürümünü de ekleriz:
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ım olarak başka bir Babel eklentisi yazacağız. Görevi, paylaşılan paketteki tüm modül içe aktarmalarını 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);
}
Bitiş
Oldukça etkileyici bir yolculuk, değil mi? Bu bölümde amacımızın, kod bölme işlemini açıklığa kavuşturmak ve açıklığa kavuşturmak olduğunu lütfen unutmayın. Sonuç işe yarıyor. Ancak demo sitemize özgüdür ve genel senaryoda oldukça 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.