Blog livestream yang ditingkatkan - Pemisahan kode

Di Livestream Supercharged terbaru, kami menerapkan pemisahan kode dan pemotongan berbasis rute. Dengan modul HTTP/2 dan ES6 native, teknik ini akan menjadi penting untuk memungkinkan pemuatan dan cache yang efisien untuk resource skrip.

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 (Ringkasan)

Cara melakukan pemisahan kode melalui pemotongan berbasis rute:

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

Pemisahan kode vs. pemotongan berbasis rute

Pemisahan kode dan pemotongan berbasis rute terkait erat dan sering digunakan secara bergantian. Hal ini telah menyebabkan kebingungan. Mari kita coba menjelaskannya:

  • Pemisahan kode: Pemisahan kode adalah proses membagi kode Anda 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 membagi kode Anda adalah dengan menggunakan pemotongan berbasis rute.
  • Pemotongan berbasis rute: Pemotongan berbasis rute membuat paket yang terkait dengan rute aplikasi Anda. Dengan menganalisis rute dan dependensinya, kami dapat mengubah modul yang digunakan dalam paket.

Mengapa pemisahan kode?

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, browser berakhir dengan serangkaian pengambilan yang berlangsung selama beberapa perjalanan bolak-balik sebelum kode akhirnya dapat dieksekusi.

Pemaketan

Pemaketan, yang menyejajarkan semua modul menjadi satu paket tunggal akan memastikan browser memiliki semua kode yang diperlukan setelah 1 kali pulang pergi dan dapat mulai menjalankan kode dengan lebih cepat. Namun, hal ini memaksa pengguna mendownload banyak kode yang tidak diperlukan, sehingga bandwidth dan waktu menjadi terbuang percuma. Selain itu, setiap perubahan pada salah satu modul asli kami akan mengakibatkan perubahan dalam paket, sehingga membatalkan versi paket yang di-cache. 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 apa yang kami butuhkan, dan meng-cache efisiensi yang lebih baik dengan membuat jumlah modul per paket jauh lebih kecil. Jika pemaketan dilakukan dengan benar, jumlah total perjalanan pulang pergi akan jauh lebih rendah daripada modul lepas. Terakhir, kita dapat menggunakan mekanisme pramuat seperti link[rel=preload] untuk menghemat waktu round trio tambahan jika diperlukan.

Langkah 1: Dapatkan daftar titik entri

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

Menggunakan babel untuk memproses JavaScript

Babel biasanya digunakan untuk “transpilasi”: menggunakan kode JavaScript yang paling baru dan mengubahnya menjadi JavaScript versi lama sehingga lebih banyak browser yang dapat mengeksekusi kode tersebut. Langkah pertama di sini adalah mengurai JavaScript baru dengan parser (Babel menggunakan babylon) yang mengubah kode menjadi “Pohon Sintaksis Abstrak” (AST). Setelah AST dihasilkan, serangkaian plugin menganalisis dan mengurai 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 ampuh untuk mengurai bahasa dengan benar dan sulit dipelihara. Mengandalkan alat yang sudah teruji seperti Babel akan mencegah banyak masalah kepala.

Berikut ini 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 jenis node apa pun yang ingin ditangani oleh plugin. Jika node dengan jenis tersebut ditemukan saat melintasi AST, fungsi yang sesuai pada objek visitor akan dipanggil dengan node tersebut sebagai parameter. Pada 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 membuat hierarki dependensi modul, kita akan mengurai modul tersebut dan membuat daftar semua modul yang diimpornya. Kita juga perlu mengurai dependensi tersebut karena 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 sekumpulan hierarki dependensi – forest dependensi jika ada – kita dapat menemukan dependensi bersama dengan mencari node yang muncul di setiap hierarki. Kita akan meratakan dan menghapus duplikat hutan dan memfilter untuk mempertahankan elemen yang muncul di semua pohon.

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 menyambungkan semua file modul. Dua masalah akan 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 kita sudah pernah melakukannya sebelumnya, kita akan menulis plugin babel yang lain.

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

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 {i>plugin<i} 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 ini benar-benar menyenangkan, bukan? Perlu diingat bahwa tujuan kita untuk episode ini adalah menjelaskan dan mengungkap pemisahan kode. Hasilnya berhasil, tetapi ini khusus untuk situs demo kami dan akan gagal secara mengerikan dalam kasus umum. Untuk produksi, sebaiknya andalkan alat yang sudah ada seperti WebPack, RollUp, dll.

Anda dapat menemukan kode kami di repositori GitHub.

Sampai jumpa di lain waktu.