Blog livestream yang ditingkatkan - Pemisahan kode

Dalam Supercharged Livestream terbaru kami, kami menerapkan pemisahan kode dan pemotongan berbasis rute. Dengan HTTP/2 dan modul ES6 native, teknik ini akan menjadi penting untuk mengaktifkan pemuatan dan penyimpanan dalam cache resource skrip secara efisien.

Tips &trik lain-lain dalam episode ini

  • asyncFunction().catch() dengan error.stack: 9.55
  • Modul dan atribut nomodule pada tag <script>: 7.30
  • promisify() di Node 8: 17:20

TL;DR

Cara melakukan pemisahan kode melalui pengelompokan berbasis rute:

  1. Dapatkan daftar titik entri Anda.
  2. Ekstrak dependensi modul dari semua titik entri ini.
  3. Temukan dependensi bersama di antara semua titik entri.
  4. Gabungkan dependensi bersama.
  5. Tulis ulang titik entri.

Pemisahan kode vs. pengelompokan berbasis rute

Pemisahan kode dan pengelompokan berbasis rute terkait erat dan sering digunakan secara bergantian. Hal ini telah menyebabkan beberapa kebingungan. Mari kita perjelas:

  • Pemisahan kode: Pemisahan kode adalah proses membagi kode menjadi beberapa paket. Jika Anda tidak mengirimkan satu paket besar dengan semua JavaScript ke klien, berarti Anda melakukan pemisahan kode. Salah satu cara spesifik untuk memisahkan kode adalah dengan menggunakan pengelompokan berbasis rute.
  • Potongan berbasis rute: Pengelompokan berbasis rute membuat bundle yang terkait dengan rute aplikasi Anda. Dengan menganalisis rute Anda dan dependensinya, kami dapat mengubah modul yang masuk ke dalam paket.

Mengapa pemisahan kode dilakukan?

Modul longgar

Dengan modul ES6 native, setiap modul JavaScript dapat mengimpor dependensinya sendiri. Saat browser menerima modul, semua pernyataan import akan memicu pengambilan tambahan untuk mendapatkan modul yang diperlukan untuk menjalankan kode. Namun, semua modul ini dapat memiliki dependensi sendiri. Bahayanya adalah browser berakhir dengan serangkaian pengambilan yang berlangsung selama beberapa perjalanan bolak-balik sebelum kode akhirnya dapat dieksekusi.

Pemaketan

Penggabungan, yang menyisipkan semua modul Anda ke dalam satu paket, akan memastikan browser memiliki semua kode yang diperlukan setelah 1 perjalanan bolak-balik dan dapat mulai menjalankan kode dengan lebih cepat. Namun, hal ini memaksa pengguna mendownload banyak kode yang tidak diperlukan, sehingga bandwidth dan waktu terbuang percuma. Selain itu, setiap perubahan pada salah satu modul asli kami akan mengakibatkan perubahan paket, sehingga versi paket yang di-cache menjadi tidak valid. Pengguna harus mendownload ulang semuanya.

Pemisahan kode

Pemisahan kode adalah jalan tengah. Kami bersedia menginvestasikan perjalanan bolak-balik tambahan untuk mendapatkan efisiensi jaringan dengan hanya mendownload yang kami butuhkan, dan efisiensi penyimpanan dalam cache yang lebih baik dengan membuat jumlah modul per paket jauh lebih kecil. Jika penggabungan dilakukan dengan benar, jumlah total perjalanan pulang-pergi akan jauh lebih rendah daripada dengan modul longgar. Terakhir, kita dapat menggunakan mekanisme pramuat seperti link[rel=preload] untuk menghemat trio kali tambahan jika diperlukan.

Langkah 1: Dapatkan daftar titik entri Anda

Ini hanyalah salah satu dari banyak pendekatan, tetapi dalam episode ini, kita mengurai sitemap.xml situs untuk mendapatkan titik entri ke situs kita. Biasanya, file JSON khusus yang mencantumkan semua titik entri digunakan.

Menggunakan babel untuk memproses JavaScript

Babel biasanya digunakan untuk “transpilasi”: menggunakan kode JavaScript terbaru dan mengubahnya menjadi JavaScript versi lama sehingga lebih banyak browser yang dapat mengeksekusi kode. Langkah pertama di sini adalah mengurai JavaScript baru dengan parser (Babel menggunakan babylon) yang mengubah kode menjadi apa yang disebut "Abstrak Syntax Tree" (AST). Setelah AST dibuat, serangkaian plugin akan menganalisis dan mengubah AST.

Kita akan banyak menggunakan babel untuk mendeteksi (dan kemudian memanipulasi) impor modul JavaScript. Anda mungkin tergoda untuk menggunakan ekspresi reguler, tetapi ekspresi reguler tidak cukup canggih untuk mengurai bahasa dengan benar dan sulit dikelola. Dengan mengandalkan alat yang telah teruji seperti Babel, Anda akan terhindar dari banyak masalah.

Berikut adalah contoh sederhana untuk menjalankan Babel dengan plugin kustom:

const plugin = {
  visitor: {
    ImportDeclaration(decl) {
      /* ... */
    }
  }
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});

Plugin dapat menyediakan objek visitor. Pengunjung berisi fungsi untuk semua jenis node yang ingin ditangani oleh plugin. Saat node dari jenis tersebut ditemukan saat melintasi AST, fungsi yang sesuai dalam objek visitor akan dipanggil dengan node tersebut sebagai parameter. Dalam contoh di atas, metode ImportDeclaration() akan dipanggil untuk setiap deklarasi import dalam file. Untuk lebih memahami jenis node dan AST, lihat astexplorer.net.

Langkah 2: Ekstrak dependensi modul

Untuk mem-build hierarki dependensi modul, kita akan mengurai modul tersebut dan membuat daftar semua modul yang diimpornya. Kita juga perlu mengurai dependensi tersebut, karena pada gilirannya, dependensi tersebut mungkin juga memiliki dependensi. Kasus klasik untuk rekursi.

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));
}

Langkah 3: Temukan dependensi bersama di antara semua titik entri

Karena kita memiliki kumpulan hierarki dependensi – jika perlu – hutan dependensi – kita dapat menemukan dependensi bersama dengan mencari node yang muncul di setiap pohon. Kita akan meratakan dan menghapus duplikat hutan dan memfilter untuk hanya mempertahankan elemen yang muncul di semua hierarki.

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)));
}

Langkah 4: Paketkan dependensi bersama

Untuk memaketkan kumpulan dependensi bersama, kita cukup menggabungkan semua file modul. Dua masalah muncul saat menggunakan pendekatan tersebut: Masalah pertama adalah paket akan tetap berisi pernyataan import yang akan membuat browser mencoba mengambil resource. Masalah kedua adalah dependensi dependensi belum dipaketkan. Karena telah melakukannya sebelumnya, kita akan menulis plugin babel lainnya.

Kode ini cukup mirip dengan plugin pertama kita, tetapi bukan hanya mengekstrak impor, kita juga akan menghapusnya dan menyisipkan versi file yang diimpor dalam paket:

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');
}

Langkah 5: Tulis ulang titik entri

Untuk langkah terakhir, kita akan menulis plugin Babel lainnya. Tugasnya adalah menghapus semua impor modul yang ada dalam paket bersama.

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);
}

Akhiri

Perjalanan yang menyenangkan, bukan? Ingatlah bahwa tujuan kita untuk episode ini adalah menjelaskan dan menghilangkan mitos pemisahan kode. Hasilnya berfungsi, tetapi ini khusus untuk situs demo kami dan akan gagal secara drastis dalam kasus umum. Untuk produksi, sebaiknya Anda mengandalkan alat yang sudah mapan seperti WebPack, RollUp, dll.

Anda dapat menemukan kode kami di repositori GitHub.

Sampai jumpa di lain waktu.