Güçlendirilmiş canlı yayın blogu - Kod bölme

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 ile asyncFunction().catch(): 9:55
  • <script> etiketlerinde modüller ve nomodule ö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?

  1. Giriş noktalarınızın listesini alın.
  2. Tüm bu giriş noktalarının modül bağımlılıklarını çıkarın.
  3. Tüm giriş noktaları arasındaki paylaşılan bağımlılıkları bulun.
  4. Paylaşılan bağımlılıkları gruplandırın.
  5. 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.