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()
denganerror.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:
- Dapatkan daftar titik entri Anda.
- Ekstrak dependensi modul dari semua titik entri ini.
- Temukan dependensi bersama di antara semua titik entri.
- Gabungkan dependensi bersama.
- 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.