renderToPipeableStream
renderToPipeableStream
me-render pohon (tree) React menjadi Node.js Stream yang pipeable.
const { pipe, abort } = renderToPipeableStream(reactNode, options?)
- Referensi
- Penggunaan
- Me-render pohon React sebagai HTML menjadi Node.js Stream
- Streaming lebih banyak konten saat dimuat
- Menentukan apa yang masuk ke dalam shell
- Mencatat laporan kerusakan di server
- Memulihkan dari error di dalam shell
- Memulihkan dari error di luar shell
- Mengatur kode status
- Menangani error yang berbeda dengan cara yang berbeda
- Menunggu semua konten dimuat untuk crawler dan static generation
- Membatalkan server rendering
Referensi
renderToPipeableStream(reactNode, options?)
Panggil renderToPipeableStream
untuk me-render pohon React Anda sebagai HTML menjadi Node.js Stream.
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
Di sisi klien, panggil hydrateRoot
untuk membuat HTML yang dihasilkan server menjadi interaktif.
Parameter
-
reactNode
: Node React yang ingin anda render menjadi HTML. Contohnya, sebuah elemen JSX seperti<App />
. Ini diharapkan mewakili keseluruhan dokumen. Jadi, komponenApp
harus me-render tag<html>
. -
options
(opsional): Objek berisi opsi streaming.bootstrapScriptContent
(opsional): Jika ditentukan, string ini akan diletakkan di dalam tag<script>
inline.bootstrapScripts
(opsional): Senarai URL string untuk tag<script>
yang akan di-emit (dipancarkan) di halaman. Gunakan ini untuk menyertakan<script>
yang memanggilhydrateRoot
. Abaikan jika Anda sama sekali tidak ingin menjalankan React di sisi klien.bootstrapModules
(opsional): Sama sepertibootstrapScripts
, tetapi meng-emit<script type="module">
sebagai gantinya.identifierPrefix
(opsional): Awalan string yang digunakan React untuk ID yang dihasilkan olehuseId
. Berguna untuk menghindari konflik saat menggunakan banyak root di halaman yang sama. Harus sama dengan awalan yang dioper kehydrateRoot
.namespaceURI
(opsional): String dengan URI namespace root untuk streaming. Default ke HTML biasa. Berikan'http://www.w3.org/2000/svg'
untuk SVG atau'http://www.w3.org/1998/Math/MathML'
untuk MathML.nonce
(opsional): Stringnonce
untuk mengizinkan skrip untukscript-src
Content-Security-Policy.onAllReady
(opsional): Callback yang diaktifkan saat semua proses rendering selesai, termasuk shell dan semua konten tambahan. Anda dapat menggunakan ini sebagai gantionShellReady
untuk crawler dan static generation. Jika Anda memulai streaming di sini, Anda tidak akan mendapatkan progressive loading apa pun. Stream akan berisi HTML final.onError
(opsional): Callback yang diaktifkan setiap kali ada kesalahan server, baik yang dapat dipulihkan atau tidak. Secara default, ini hanya memanggilconsole.error
. Jika Anda menggantinya untuk mencatat laporan kerusakan, pastikan anda masih memanggilconsole.error
. Anda juga dapat menggunakannya untuk menyesuaikan kode status sebelum shell di-emit.onShellReady
(opsional): Callback yang diaktifkan tepat setelah shell awal di-render. Anda dapat mengatur kode status dan memanggilpipe
di sini untuk memulai streaming. React akan mengalirkan konten tambahan setelah shell bersama tag<script>
inline yang menggantikan fallback pemuatan HTML dengan konten.onShellError
(opsional): Callback yang diaktifkan jika ada error saat me-render shell awal. Ini menerima error sebagai argumen. Belum ada bita yang di-emit dari stream.onShellReady
maupunonAllReady
tidak akan dipanggil. Dengan demikian, Anda dapat menampilkan shell HTML fallback.progressiveChunkSize
(opsional): Jumlah bita dalam sebuah chunk. Baca lebih lanjut tentang default heuristic.
Kembalian
renderToPipeableStream
mengembalikan objek dengan dua metode:
pipe
menghasilkan HTML dalam bentuk Writable Node.js Stream yang disediakan. Panggilpipe
dalamonShellReady
jika Anda ingin mengaktifkan streaming, atau panggil dalamonAllReady
untuk crawler dan static generation.abort
memungkinkan Anda membatalkan server rendering dan me-render sisanya di sisi klien.
Penggunaan
Me-render pohon React sebagai HTML menjadi Node.js Stream
Panggil renderToPipeableStream
untuk me-render pohon React Anda sebagai HTML menjadi Node.js Stream:
import { renderToPipeableStream } from 'react-dom/server';
// Sintaks route handler tergantung framework backend yang Anda gunakan
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
Bersamaan dengan komponen root, Anda perlu menyediakan daftar path <script>
bootstrap. Komponen root Anda harus mengembalikan seluruh dokumen termasuk tag root <html>
.
Misalnya, mungkin terlihat seperti ini:
export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>Aplikasi Saya</title>
</head>
<body>
<Router />
</body>
</html>
);
}
React akan menyisipkan doctype dan tag bootstrap <script>
Anda ke dalam stream HTML yang dihasilkan:
<!DOCTYPE html>
<html>
<!-- ... HTML dari komponen Anda ... -->
</html>
<script src="/main.js" async=""></script>
Di sisi klien, skrip bootstrap Anda harus menghidrasi seluruh document
dengan memanggil hydrateRoot
:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
Ini akan melampirkan event listener untuk HTML yang dibuat server dan menjadikannya interaktif.
Deep Dive
URL aset yang bersifat final (seperti file JavaScript and CSS) sering kali di-hash setelah proses build (pembangunan). Misalnya, alih-alih styles.css
Anda mungkin mendapatkan styles.123456.css
. Hashing nama file aset statis menjamin sebuah aset pada setiap build memiliki nama file yang berbeda. Ini berguna karena memungkinkan anda mengaktifkan caching jangka panjang dengan aman untuk aset statis: file dengan nama tertentu tidak akan mengubah konten.
Namun, jika Anda tidak mengetahui URL aset hingga selesainya proses build, tidak ada cara bagi Anda untuk memasukkan aset tersebut ke dalam source code. Misalnya, hardcoding "/styles.css"
dalam JSX seperti sebelumnya tidak akan berfungsi. Untuk menjauhkan aset tersebut dari source code Anda, komponen Root dapat membaca nama file asli dari map yang dioper sebagai prop:
export default function App({ assetMap }) {
return (
<html>
<head>
...
<link rel="stylesheet" href={assetMap['styles.css']}></link>
...
</head>
...
</html>
);
}
Di sisi server, render <App assetMap={assetMap} />
dan berikan assetMap
URL aset:
// Anda perlu mendapatkan JSON ini dari build tooling yang Anda gunakan, misalnya dari keluaran build.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
Karena server Anda sekarang me-render <App assetMap={assetMap} />
, Anda juga perlu me-render assetMap
di sisi klien untuk menghindari error hidrasi. Anda dapat melakukan serialisasi dan mengoper assetMap
kepada klien seperti ini:
// Anda perlu mendapatkan JSON ini dari build tooling yang Anda gunakan.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
// Hati-hati: di sini aman menggunakan stringify() karena data ini tidak dibuat oleh pengguna.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
Pada contoh di atas, opsi bootstrapScriptContent
menambahkan tag <script>
inline ekstra yang mengatur variabel global window.assetMap
klien. ini memungkinkan kode klien membaca assetMap
yang sama:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
Baik klien maupun server me-render App
dengan prop assetMap
yang sama, sehingga tidak ada error hidrasi.
Streaming lebih banyak konten saat dimuat
Streaming memungkinkan pengguna untuk mulai melihat konten bahkan sebelum semua data dimuat di server. Misalnya, perhatikan halaman profil yang menampilkan sampul, sidebar dengan daftar teman dan foto, dan daftar postingan:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}
Bayangkan bahwa memuat data <Posts />
membutuhkan waktu. Idealnya, Anda ingin menampilkan konten lain dari halaman profil kepada pengguna tanpa perlu menunggu daftar postingan. Untuk melakukannya, letakkan Posts
dalam <Suspense>
boundary:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Ini memberitahu React untuk memulai streaming HTML sebelum Posts
memuat datanya. React akan mengirimkan HTML fallback pemuatan (PostsGlimmer
) terlebih dahulu, kemudian ketika Posts
selesai memuat datanya, React akan mengirimkan HTML yang tersisa bersama dengan tag <script>
inline yang menggantikan fallback pemuatan dengan HTML itu. Dari perspektif pengguna, halaman pertama kali akan tampil dengan PostsGlimmer
, kemudian digantikan dengan Posts
.
Anda dapat membuat <Suspense>
boundary bersarang sehingga urutan pemuatan lebih terperinci:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
Pada contoh di atas, React dapat memulai streaming halaman lebih awal. Hanya ProfileLayout
dan ProfileCover
yang harus menyelesaikan rendering terlebih dahulu karena tidak berada dalam <Suspense>
boundary. Apabila Sidebar
, Friends
, atau Photos
perlu memuat data, React akan mengirimkan HTML fallback BigSpinner
terlebih dahulu. Kemudian, ketika lebih banyak data tersedia, lebih banyak konten yang ditampilkan hingga semuanya terlihat.
Streaming tidak perlu menunggu React sendiri dimuat di peramban maupun menunggu aplikasi Anda menjadi interaktif. Konten HTML dari server akan ditampilkan secara bertahap sebelum tag <script>
mana pun dimuat.
Baca selengkapnya tentang cara kerja streaming HTML.
Menentukan apa yang masuk ke dalam shell
Bagian aplikasi Anda di luar <Suspense>
disebut shell (cangkang):
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
Ini menentukan status pemuatan paling awal yang dapat dilihat pengguna:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>
Jika Anda memasukkan seluruh cakupan aplikasi di level root ke dalam <Suspense>
, shell hanya akan berisi spinner tersebut. Ini bukan pengalaman pengguna yang menyenangkan karena melihat spinner besar di layar dapat terasa lebih lambat dan lebih menyebalkan daripada menunggu sedikit lebih lama dan melihat tata letak yang sebenarnya. Inilah alasan mengapa biasanya Anda ingin menempatkan batas <Suspense>
sehingga shell terasa minimal tetapi lengkap—seperti kerangka dari keseluruhan tata letak halaman.
Callback onShellReady
diaktifkan ketika seluruh shell telah di-render. Biasanya, Anda akan mulai melakukan streaming:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
Pada saat onShellReady
diaktifkan, komponen di dalam <Suspense>
bersarang mungkin masih memuat data.
Mencatat laporan kerusakan di server
Secara default, semua error di server dicatat di konsol. Anda dapat mengganti perilaku ini untuk mencatat laporan kerusakan:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
Jika Anda menyediakan implementasi onError
khusus, jangan lupa juga untuk mencatat kesalahan di konsol seperti di atas.
Memulihkan dari error di dalam shell
Pada contoh berikut, shell berisi ProfileLayout
, ProfileCover
, dan PostsGlimmer
:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Jika terjadi error saat me-render komponen tersebut, React tidak akan memiliki HTML yang berarti untuk dikirim ke klien. Ganti onShellError
untuk mengirim HTML fallback yang tidak bergantung pada server rendering sebagai upaya terakhir:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
Jika ada error saat membuat shell, onError
dan onShellError
akan diaktifkan. Gunakan onError
untuk pelaporan kesalahan dan gunakan onShellError
untuk mengirim dokumen HTML cadangan. HTML fallback Anda tidak harus berupa halaman error. Sebagai gantinya, Anda dapat menyertakan shell alternatif yang me-render aplikasi Anda hanya pada klien.
Memulihkan dari error di luar shell
Pada contoh berikut, komponen <Posts />
berada di dalam <Suspense>
sehingga bukan merupakan bagian dari shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Jika error terjadi pada komponen Posts
atau suatu tempat di dalamnya, React akan mencoba memulihkannya:
- Mengirimkan fallback pemuatan dari
<Suspense>
boundary terdekat (PostsGlimmer
) ke HTML halaman. - “Menyerah” untuk mencoba me-render konten
Posts
di server lagi. - Saat kode JavaScript dimuat di klien, React akan mencoba lagi me-render
Posts
dari sisi klien.
Jika percobaan me-render ulang Posts
dari klien juga gagal, React akan melempar kesalahan di klien. Seperti halnya semua error yang terjadi selama rendering, error boundary induk terdekat menentukan bagaimana error ditampilkan kepada pengguna. Dalam praktiknya, ini berarti bahwa pengguna akan melihat indikator pemuatan hingga dipastikan bahwa error tidak dapat dipulihkan.
Jika percobaan me-render ulang Posts
dari klien berhasil, fallback pemuatan dari server akan diganti dengan keluaran rendering klien. Pengguna tidak akan tahu bahwa ada kesalahan server. Namun, callback onError
server dan callback onRecoverableError
klien akan aktif sehingga Anda bisa mendapatkan pemberitahuan tentang error tersebut.
Mengatur kode status
Streaming menimbulkan tradeoff (pengorbanan). Anda ingin memulai streaming halaman sedini mungkin agar pengguna dapat melihat konten lebih cepat. Namun, begitu Anda memulai streaming, Anda tidak dapat lagi menyetel kode status respons.
Dengan membagi aplikasi Anda ke dalam shell (di atas semua batas <Suspense>
) dan konten lainnya, Anda telah menyelesaikan sebagian dari masalah ini . Jika shell terjadi error, Anda akan mendapatkan callback onShellError
yang memungkinkan Anda menyetel kode status error. Jika tidak, Anda tahu bahwa aplikasi dapat pulih pada klien, sehingga Anda dapat mengirimkan “OK”.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Ada yang salah</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
Jika sebuah komponen di luar shell (yaitu di dalam <Suspense>
boundary) melontarkan error, React tidak akan berhenti me-render. Ini berarti callback onError
akan diaktifkan, tetapi Anda masih akan mendapatkan onShellReady
alih-alih onShellError
. Ini karena React akan mencoba memulihkan dari error itu dari sisi klien, seperti yang dijelaskan di atas.
Namun, jika mau, Anda dapat menggunakan fakta bahwa ada kesalahan untuk menyetel kode status:
let didError = false;
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Ada yang salah</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
Ini hanya akan menangkap error di luar shell yang terjadi saat membuat konten shell awal, jadi tidak lengkap. Jika mengetahui apakah terjadi error untuk beberapa konten sangat penting, Anda dapat memindahkannya ke dalam shell.
Menangani error yang berbeda dengan cara yang berbeda
Anda dapat membuat subclass Error
sendiri dan menggunakan operator instanceof
untuk memeriksa kesalahan mana yang dilontarkan. Misalnya, Anda dapat menentukan NotFoundError
khusus dan melontarkannya dari komponen Anda. Kemudian callback onError
, onShellReady
, dan onShellError
dapat melakukan sesuatu yang berbeda bergantung pada jenis kesalahan:
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
response.send('<h1>Ada yang salah</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
Perlu diingat bahwa setelah Anda meng-emit shell dan memulai streaming, Anda tidak dapat mengubah kode status.
Menunggu semua konten dimuat untuk crawler dan static generation
Streaming menawarkan pengalaman pengguna yang lebih baik karena pengguna dapat melihat konten saat tersedia.
Namun, saat crawler mengunjungi halaman Anda, atau jika Anda membuat halaman pada saat proses pembangunan, Anda mungkin ingin membiarkan semua konten dimuat terlebih dahulu, lalu menghasilkan keluaran akhir HTML alih-alih menampilkannya secara bertahap.
Anda dapat menunggu semua konten dimuat menggunakan callback onAllReady
:
let didError = false;
let isCrawler = // ... depends on your bot detection strategy ...
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
if (!isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onAllReady() {
if (isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
Pengunjung reguler akan mendapatkan stream konten yang dimuat secara progresif. Crawler akan menerima keluaran akhir HTML setelah semua data dimuat. Namun, ini juga berarti crawler harus menunggu semua data, beberapa di antaranya mungkin lambat dimuat atau error. Bergantung pada aplikasi Anda, Anda juga dapat memilih untuk mengirim shell ke crawler.
Membatalkan server rendering
Anda dapat memaksa server rendering untuk “menyerah” setelah timeout:
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);
React akan menghapus fallback pemuatan yang tersisa sebagai HTML, dan akan mencoba me-render sisanya dari sisi klien.