ดูวิธีใช้ Puppeteer API เพื่อเพิ่มความสามารถในการแสดงผลฝั่งเซิร์ฟเวอร์ (SSR) ลงในเว็บเซิร์ฟเวอร์ Express ที่สำคัญคือแอปของคุณต้องเปลี่ยนแปลงโค้ดเพียงเล็กน้อย แพลตฟอร์มแบบ Headless จะทํางานหนักทั้งหมด
คุณสามารถใช้ SSR กับหน้าเว็บใดก็ได้และรับมาร์กอัปสุดท้ายได้โดยใช้โค้ดเพียงไม่กี่บรรทัด
import puppeteer from 'puppeteer';
async function ssr(url) {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.goto(url, {waitUntil: 'networkidle0'});
const html = await page.content(); // serialized HTML of page DOM.
await browser.close();
return html;
}
เหตุผลที่ควรใช้ Chrome แบบ Headless
คุณอาจสนใจ Headless Chrome ในกรณีต่อไปนี้
- คุณสร้างเว็บแอปที่เครื่องมือค้นหาไม่ได้จัดทำดัชนี
- คุณหวังว่าจะได้ผลลัพธ์อย่างรวดเร็วเพื่อเพิ่มประสิทธิภาพ JavaScript และปรับปรุงการแสดงผลที่มีความหมายครั้งแรก
เฟรมเวิร์กบางรายการ เช่น Preact จะมาพร้อมกับเครื่องมือที่จัดการการแสดงผลฝั่งเซิร์ฟเวอร์ หากเฟรมเวิร์กมีโซลูชันการแสดงผลล่วงหน้า ให้ใช้โซลูชันนั้นแทนที่จะนำ Puppeteer และ Headless Chrome มาใช้ในเวิร์กโฟลว์
การ Crawl เว็บสมัยใหม่
ที่ผ่านมา Crawler ของเครื่องมือค้นหา แพลตฟอร์มการแชร์โซเชียล แม้แต่เบราว์เซอร์ล้วนใช้มาร์กอัป HTML คงที่ในการจัดทำดัชนีเว็บและแสดงเนื้อหา เว็บในปัจจุบันได้พัฒนาไปมาก แอปพลิเคชันที่ใช้ JavaScript ยังคงมีการใช้งานอยู่ ซึ่งหมายความว่าในหลายกรณี เครื่องมือการ Crawl อาจมองไม่เห็นเนื้อหาของเรา
Googlebot ซึ่งเป็น Crawler ของ Search จะประมวลผล JavaScript ในขณะเดียวกันก็คอยดูแลไม่ให้การประมวลผลนั้นทำให้ประสบการณ์การใช้งานของผู้เข้าชมเว็บไซต์แย่ลง มีข้อความแตกต่างและข้อจำกัดบางประการที่คุณต้องคำนึงถึงเมื่อออกแบบหน้าเว็บและแอปพลิเคชันให้รองรับวิธีที่ Crawler เข้าถึงและแสดงเนื้อหาของคุณ
แสดงผลหน้าเว็บล่วงหน้า
Crawler ทั้งหมดเข้าใจ HTML เราต้องใช้เครื่องมือที่มีคุณสมบัติต่อไปนี้เพื่อให้ Crawler จัดทำดัชนี JavaScript ได้
- ทราบวิธีเรียกใช้ JavaScript สมัยใหม่ทุกประเภทและสร้าง HTML แบบคงที่
- อัปเดตอยู่เสมอเมื่อเว็บเพิ่มฟีเจอร์
- ทำงานได้โดยไม่ต้องอัปเดตโค้ดแอปพลิเคชันมากนัก
ฟังดูดีใช่ไหม เครื่องมือดังกล่าวคือเบราว์เซอร์ Chrome แบบ Headless ไม่สนใจว่าคุณใช้ไลบรารี เฟรมเวิร์ก หรือเชนเครื่องมือใด
เช่น หากแอปพลิเคชันของคุณสร้างขึ้นด้วย Node.js Puppeteer เป็นวิธีที่ง่ายในการทำงานร่วมกับ Chrome แบบ Headless
เริ่มต้นด้วยหน้าเว็บแบบไดนามิกที่สร้าง HTML ด้วย JavaScript โดยทำดังนี้
public/index.html
<html>
<body>
<div id="container">
<!-- Populated by the JS below. -->
</div>
</body>
<script>
function renderPosts(posts, container) {
const html = posts.reduce((html, post) => {
return `${html}
<li class="post">
<h2>${post.title}</h2>
<div class="summary">${post.summary}</div>
<p>${post.content}</p>
</li>`;
}, '');
// CAREFUL: this assumes HTML is sanitized.
container.innerHTML = `<ul id="posts">${html}</ul>`;
}
(async() => {
const container = document.querySelector('#container');
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
})();
</script>
</html>
ฟังก์ชัน SSR
ต่อไป ให้นำฟังก์ชัน ssr()
จากก่อนหน้านี้มาปรับปรุงให้ดีขึ้นอีกนิด
ssr.mjs
import puppeteer from 'puppeteer';
// In-memory cache of rendered pages. Note: this will be cleared whenever the
// server process stops. If you need true persistence, use something like
// Google Cloud Storage (https://firebase.google.com/docs/storage/web/start).
const RENDER_CACHE = new Map();
async function ssr(url) {
if (RENDER_CACHE.has(url)) {
return {html: RENDER_CACHE.get(url), ttRenderMs: 0};
}
const start = Date.now();
const browser = await puppeteer.launch();
const page = await browser.newPage();
try {
// networkidle0 waits for the network to be idle (no requests for 500ms).
// The page's JS has likely produced markup by this point, but wait longer
// if your site lazy loads, etc.
await page.goto(url, {waitUntil: 'networkidle0'});
await page.waitForSelector('#posts'); // ensure #posts exists in the DOM.
} catch (err) {
console.error(err);
throw new Error('page.goto/waitForSelector timed out.');
}
const html = await page.content(); // serialized HTML of page DOM.
await browser.close();
const ttRenderMs = Date.now() - start;
console.info(`Headless rendered page in: ${ttRenderMs}ms`);
RENDER_CACHE.set(url, html); // cache rendered page.
return {html, ttRenderMs};
}
export {ssr as default};
การเปลี่ยนแปลงที่สำคัญมีดังนี้
- เพิ่มการแคช การแคช HTML ที่แสดงผลแล้วเป็นวิธีที่ได้ผลดีที่สุดในการเร่งเวลาในการตอบสนอง เมื่อมีการขอหน้าเว็บอีกครั้ง คุณจะไม่ใช้ Chrome แบบไม่มีส่วนหัวเลย เราจะพูดถึงการเพิ่มประสิทธิภาพอื่นๆ ในภายหลัง
- เพิ่มการจัดการข้อผิดพลาดพื้นฐานหากการโหลดหน้าเว็บหมดเวลา
- เพิ่มการเรียกใช้
page.waitForSelector('#posts')
วิธีนี้ช่วยให้มั่นใจว่าโพสต์จะอยู่ใน DOM ก่อนที่เราจะถ่ายโอนหน้าเว็บที่แปลงเป็นอนุกรม - เพิ่มวิทยาศาสตร์ บันทึกเวลาที่ Headless ใช้ในการแสดงผลหน้าเว็บและแสดงเวลาการแสดงผลพร้อมกับ HTML
- วางโค้ดในโมดูลชื่อ
ssr.mjs
ตัวอย่างเว็บเซิร์ฟเวอร์
สุดท้ายนี้ นี่คือเซิร์ฟเวอร์ Express ขนาดเล็กที่รวมทุกอย่างเข้าด้วยกัน แฮนเดิลหลักจะแสดงผล URL http://localhost/index.html
(หน้าแรก) ล่วงหน้าและแสดงผลลัพธ์เป็นการตอบกลับ ผู้ใช้จะเห็นโพสต์ทันทีที่เปิดหน้าเว็บ เนื่องจากตอนนี้มาร์กอัปแบบคงที่เป็นส่วนหนึ่งของการตอบกลับแล้ว
server.mjs
import express from 'express';
import ssr from './ssr.mjs';
const app = express();
app.get('/', async (req, res, next) => {
const {html, ttRenderMs} = await ssr(`${req.protocol}://${req.get('host')}/index.html`);
// Add Server-Timing! See https://w3c.github.io/server-timing/.
res.set('Server-Timing', `Prerender;dur=${ttRenderMs};desc="Headless render time (ms)"`);
return res.status(200).send(html); // Serve prerendered page as response.
});
app.listen(8080, () => console.log('Server started. Press Ctrl+C to quit'));
หากต้องการเรียกใช้ตัวอย่างนี้ ให้ติดตั้ง Dependency (npm i --save puppeteer express
) และเรียกใช้เซิร์ฟเวอร์โดยใช้ Node 8.5.0 ขึ้นไปและ Flag --experimental-modules
ตัวอย่างการตอบกลับที่เซิร์ฟเวอร์นี้ส่งกลับมีดังนี้
<html>
<body>
<div id="container">
<ul id="posts">
<li class="post">
<h2>Title 1</h2>
<div class="summary">Summary 1</div>
<p>post content 1</p>
</li>
<li class="post">
<h2>Title 2</h2>
<div class="summary">Summary 2</div>
<p>post content 2</p>
</li>
...
</ul>
</div>
</body>
<script>
...
</script>
</html>
Use Case ที่เหมาะกับ Server Timing API เวอร์ชันใหม่
API Server-Timing จะสื่อสารเมตริกประสิทธิภาพของเซิร์ฟเวอร์ (เช่น เวลาในการส่งคำขอและเวลาในการตอบกลับหรือการค้นหาฐานข้อมูล) กลับไปที่เบราว์เซอร์ รหัสไคลเอ็นต์สามารถใช้ข้อมูลนี้เพื่อติดตามประสิทธิภาพโดยรวมของเว็บแอป
Use Case ที่เหมาะสําหรับ Server-Timing คือรายงานเวลาที่ Chromium แบบ Headless ใช้ในการแสดงผลหน้าเว็บล่วงหน้า โดยเพียงเพิ่มส่วนหัว Server-Timing
ในการตอบกลับของเซิร์ฟเวอร์
res.set('Server-Timing', `Prerender;dur=1000;desc="Headless render time (ms)"`);
ในไคลเอ็นต์ คุณสามารถใช้ Performance API และ PerformanceObserver เพื่อเข้าถึงเมตริกต่อไปนี้
const entry = performance.getEntriesByType('navigation').find(
e => e.name === location.href);
console.log(entry.serverTiming[0].toJSON());
{
"name": "Prerender",
"duration": 3808,
"description": "Headless render time (ms)"
}
ผลลัพธ์ด้านประสิทธิภาพ
ผลลัพธ์ต่อไปนี้รวมการเพิ่มประสิทธิภาพด้านประสิทธิภาพส่วนใหญ่ที่กล่าวถึงในภายหลัง
ในแอปตัวอย่าง Chromium แบบ Headless จะใช้เวลาประมาณ 1 วินาทีในการแสดงผลหน้าเว็บบนเซิร์ฟเวอร์ เมื่อแคชหน้าเว็บแล้ว การจําลอง 3G แบบช้าของเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์จะทําให้ FCP เร็วขึ้น 8.37 วินาทีกว่าเวอร์ชันฝั่งไคลเอ็นต์
First Paint (FP) | First Contentful Paint (FCP) | |
---|---|---|
แอปฝั่งไคลเอ็นต์ | 4 วิ | 11 วิ |
เวอร์ชัน SSR | 2.3 วินาที | ~2.3 วินาที |
ผลลัพธ์เหล่านี้มีแนวโน้มที่ดี ผู้ใช้จะเห็นเนื้อหาที่มีความหมายได้เร็วขึ้นมากเนื่องจากหน้าเว็บที่แสดงผลฝั่งเซิร์ฟเวอร์ไม่ต้องใช้ JavaScript เพื่อโหลดและแสดงโพสต์อีกต่อไป
ป้องกันการให้น้ำซ้ำ
จำได้ไหมที่เราบอกว่า "เราไม่ได้เปลี่ยนแปลงโค้ดในแอปฝั่งไคลเอ็นต์" โกหก
แอป Express ของเราจะรับคําขอ ใช้ Puppeteer เพื่อโหลดหน้าเว็บในโหมด headless และแสดงผลลัพธ์เป็นการตอบกลับ แต่การตั้งค่านี้มีปัญหา
JavaScript เดียวกันที่ทำงานใน Chrome แบบไม่มีส่วนหัวบนเซิร์ฟเวอร์จะทำงานอีกครั้งเมื่อเบราว์เซอร์ของผู้ใช้โหลดหน้าเว็บในหน้าเว็บ เรามีการสร้างมาร์กอัป 2 แห่ง #doublerender
วิธีแก้ไขคือบอกให้หน้าเว็บทราบว่ามี HTML อยู่แล้ว
วิธีแก้ปัญหาอย่างหนึ่งคือให้ JavaScript ของหน้าตรวจสอบว่า <ul id="posts">
อยู่ใน DOM อยู่แล้วหรือไม่ ณ เวลาโหลด หากใช่ แสดงว่าหน้าเว็บใช้ SSR และคุณก็ไม่ต้องเพิ่มโพสต์อีกครั้ง 👍
public/index.html
<html>
<body>
<div id="container">
<!-- Populated by JS (below) or by prerendering (server). Either way,
#container gets populated with the posts markup:
<ul id="posts">...</ul>
-->
</div>
</body>
<script>
...
(async() => {
const container = document.querySelector('#container');
// Posts markup is already in DOM if we're seeing a SSR'd.
// Don't re-hydrate the posts here on the client.
const PRE_RENDERED = container.querySelector('#posts');
if (!PRE_RENDERED) {
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
}
})();
</script>
</html>
การเพิ่มประสิทธิภาพ
นอกจากการแคชผลลัพธ์ที่ผ่านการจัดการแล้ว เรายังเพิ่มประสิทธิภาพที่น่าสนใจให้กับ ssr()
ได้อีกมากมาย บางรายการอาจให้ผลลัพธ์เร็ว ในขณะที่บางรายการอาจต้องอาศัยการคาดเดามากกว่า ประโยชน์ด้านประสิทธิภาพที่คุณเห็นอาจขึ้นอยู่กับประเภทหน้าเว็บที่คุณแสดงผลล่วงหน้าและความซับซ้อนของแอป
ยกเลิกคำขอที่ไม่จำเป็น
ขณะนี้ ระบบจะโหลดทั้งหน้า (และทรัพยากรทั้งหมดที่หน้าเว็บขอ) ลงใน Chrome แบบไม่มีส่วนแสดงผลโดยไม่มีเงื่อนไข อย่างไรก็ตาม เราสนใจแค่ 2 อย่างต่อไปนี้
- มาร์กอัปที่แสดงผล
- คำขอ JS ที่สร้างมาร์กอัปนั้น
คำขอเครือข่ายที่ไม่ได้สร้าง DOM นั้นสิ้นเปลือง ทรัพยากร เช่น รูปภาพ แบบอักษร สไตล์ชีต และสื่อ จะไม่มีส่วนร่วมในการสร้าง HTML ของหน้าเว็บ องค์ประกอบเหล่านี้จะกำหนดสไตล์และเสริมโครงสร้างของหน้าเว็บ แต่ไม่ได้สร้างหน้าเว็บอย่างชัดเจน เราควรบอกเบราว์เซอร์ให้ละเว้นแหล่งข้อมูลเหล่านี้ ซึ่งจะช่วยลดภาระงานของ Chrome แบบ Headless, ประหยัดแบนด์วิดท์ และอาจเพิ่มความเร็วในการแสดงผลล่วงหน้าสำหรับหน้าเว็บขนาดใหญ่
โปรโตคอล DevTools รองรับฟีเจอร์ที่มีประสิทธิภาพที่เรียกว่าการขัดจังหวะเครือข่าย ซึ่งสามารถใช้แก้ไขคำขอก่อนที่เบราว์เซอร์จะส่ง
Puppeteer รองรับการขัดจังหวะเครือข่ายโดยเปิดpage.setRequestInterception(true)
และรอเหตุการณ์ request
ของหน้าเว็บ
ซึ่งช่วยให้เราหยุดคำขอทรัพยากรบางอย่างและปล่อยให้คำขออื่นๆ ดำเนินการต่อได้
ssr.mjs
async function ssr(url) {
...
const page = await browser.newPage();
// 1. Intercept network requests.
await page.setRequestInterception(true);
page.on('request', req => {
// 2. Ignore requests for resources that don't produce DOM
// (images, stylesheets, media).
const allowlist = ['document', 'script', 'xhr', 'fetch'];
if (!allowlist.includes(req.resourceType())) {
return req.abort();
}
// 3. Pass through all other requests.
req.continue();
});
await page.goto(url, {waitUntil: 'networkidle0'});
const html = await page.content(); // serialized HTML of page DOM.
await browser.close();
return {html};
}
ทรัพยากรสําคัญในบรรทัด
การใช้เครื่องมือสร้างแยกต่างหาก (เช่น gulp
) เพื่อประมวลผลแอปและแทรก CSS และ JS ที่สําคัญในหน้าเว็บขณะสร้างนั้นเป็นเรื่องปกติ ซึ่งอาจช่วยเร่งการแสดงผลที่มีความหมายครั้งแรกได้ เนื่องจากเบราว์เซอร์จะส่งคำขอน้อยลงในระหว่างการโหลดหน้าเว็บครั้งแรก
ใช้เบราว์เซอร์เป็นเครื่องมือสร้างแทนเครื่องมือสร้างแยกต่างหาก เราสามารถใช้ Puppeteer เพื่อจัดการ DOM ของหน้าเว็บ แทรกสไตล์ แทรก JavaScript หรือสิ่งอื่นๆ ที่คุณต้องการให้แสดงในหน้าเว็บก่อนแสดงผลล่วงหน้า
ตัวอย่างนี้แสดงวิธีขัดจังหวะการตอบกลับสําหรับสไตล์ชีตในเครื่อง และแทรกแหล่งข้อมูลเหล่านั้นในหน้าเว็บเป็นแท็ก <style>
ssr.mjs
import urlModule from 'url';
const URL = urlModule.URL;
async function ssr(url) {
...
const stylesheetContents = {};
// 1. Stash the responses of local stylesheets.
page.on('response', async resp => {
const responseUrl = resp.url();
const sameOrigin = new URL(responseUrl).origin === new URL(url).origin;
const isStylesheet = resp.request().resourceType() === 'stylesheet';
if (sameOrigin && isStylesheet) {
stylesheetContents[responseUrl] = await resp.text();
}
});
// 2. Load page as normal, waiting for network requests to be idle.
await page.goto(url, {waitUntil: 'networkidle0'});
// 3. Inline the CSS.
// Replace stylesheets in the page with their equivalent <style>.
await page.$$eval('link[rel="stylesheet"]', (links, content) => {
links.forEach(link => {
const cssText = content[link.href];
if (cssText) {
const style = document.createElement('style');
style.textContent = cssText;
link.replaceWith(style);
}
});
}, stylesheetContents);
// 4. Get updated serialized HTML of page.
const html = await page.content();
await browser.close();
return {html};
}
This code:
- Use a
page.on('response')
handler to listen for network responses. - Stashes the responses of local stylesheets.
- Finds all
<link rel="stylesheet">
in the DOM and replaces them with an equivalent<style>
. Seepage.$$eval
API docs. Thestyle.textContent
is set to the stylesheet response.
Auto-minify resources
Another trick you can do with network interception is to modify the responses returned by a request.
As an example, say you want to minify the CSS in your app but also want to
keep the convenience having it unminified when developing. Assuming you've
setup another tool to pre-minify styles.css
, one can use Request.respond()
to rewrite the response of styles.css
to be the content of styles.min.css
.
ssr.mjs
import fs from 'fs';
async function ssr(url) {
...
// 1. Intercept network requests.
await page.setRequestInterception(true);
page.on('request', req => {
// 2. If request is for styles.css, respond with the minified version.
if (req.url().endsWith('styles.css')) {
return req.respond({
status: 200,
contentType: 'text/css',
body: fs.readFileSync('./public/styles.min.css', 'utf-8')
});
}
...
req.continue();
});
...
const html = await page.content();
await browser.close();
return {html};
}
ใช้อินสแตนซ์ Chrome รายการเดียวซ้ำในการเรนเดอร์
การเปิดตัวเบราว์เซอร์ใหม่สําหรับการแสดงผลล่วงหน้าทุกครั้งจะทำให้เกิดค่าใช้จ่ายเพิ่มเติมมาก คุณอาจต้องเปิดใช้งานอินสแตนซ์เดียวแล้วนําไปใช้แสดงผลหน้าเว็บหลายหน้าซ้ำแทน
Puppeteer สามารถเชื่อมต่อกับอินสแตนซ์ Chrome ที่มีอยู่อีกครั้งได้โดยเรียกใช้ puppeteer.connect()
และส่ง URL การแก้ไขข้อบกพร่องระยะไกลของอินสแตนซ์ หากต้องการเก็บอินสแตนซ์เบราว์เซอร์ไว้นานๆ เราสามารถย้ายโค้ดที่เปิด Chrome จากฟังก์ชัน ssr()
ไปยังเซิร์ฟเวอร์ Express ได้โดยทำดังนี้
server.mjs
import express from 'express';
import puppeteer from 'puppeteer';
import ssr from './ssr.mjs';
let browserWSEndpoint = null;
const app = express();
app.get('/', async (req, res, next) => {
if (!browserWSEndpoint) {
const browser = await puppeteer.launch();
browserWSEndpoint = await browser.wsEndpoint();
}
const url = `${req.protocol}://${req.get('host')}/index.html`;
const {html} = await ssr(url, browserWSEndpoint);
return res.status(200).send(html);
});
ssr.mjs
import puppeteer from 'puppeteer';
/**
* @param {string} url URL to prerender.
* @param {string} browserWSEndpoint Optional remote debugging URL. If
* provided, Puppeteer's reconnects to the browser instance. Otherwise,
* a new browser instance is launched.
*/
async function ssr(url, browserWSEndpoint) {
...
console.info('Connecting to existing Chrome instance.');
const browser = await puppeteer.connect({browserWSEndpoint});
const page = await browser.newPage();
...
await page.close(); // Close the page we opened here (not the browser).
return {html};
}
ตัวอย่าง: งาน cron ที่จะแสดงผลล่วงหน้าเป็นระยะๆ
หากต้องการแสดงผลหน้าเว็บหลายหน้าพร้อมกัน คุณสามารถใช้อินสแตนซ์เบราว์เซอร์ที่แชร์
import puppeteer from 'puppeteer';
import * as prerender from './ssr.mjs';
import urlModule from 'url';
const URL = urlModule.URL;
app.get('/cron/update_cache', async (req, res) => {
if (!req.get('X-Appengine-Cron')) {
return res.status(403).send('Sorry, cron handler can only be run as admin.');
}
const browser = await puppeteer.launch();
const homepage = new URL(`${req.protocol}://${req.get('host')}`);
// Re-render main page and a few pages back.
prerender.clearCache();
await prerender.ssr(homepage.href, await browser.wsEndpoint());
await prerender.ssr(`${homepage}?year=2018`);
await prerender.ssr(`${homepage}?year=2017`);
await prerender.ssr(`${homepage}?year=2016`);
await browser.close();
res.status(200).send('Render cache updated!');
});
นอกจากนี้ ให้เพิ่มclearCache()
การส่งออกไปยัง ssr.js ด้วย
...
function clearCache() {
RENDER_CACHE.clear();
}
export {ssr, clearCache};
ข้อควรพิจารณาอื่นๆ
สร้างสัญญาณสําหรับหน้าเว็บ: "ระบบกําลังแสดงผลหน้าเว็บแบบ Headless"
เมื่อหน้าเว็บได้รับการแสดงผลโดย Chrome แบบ Headless ในเซิร์ฟเวอร์ ข้อมูลนี้อาจเป็นประโยชน์ต่อตรรกะฝั่งไคลเอ็นต์ของหน้าเว็บ ในแอปของฉัน ฉันใช้ฮุกนี้เพื่อ "ปิด" ส่วนต่างๆ ของหน้าเว็บที่ไม่ได้มีส่วนในการแสดงผลมาร์กอัปโพสต์ ตัวอย่างเช่น ฉันปิดใช้โค้ดที่โหลดแบบ Lazy Loading firebase-auth.js ไม่มีผู้ใช้ที่จะลงชื่อเข้าใช้
การเพิ่มพารามิเตอร์ ?headless
ลงใน URL การนําเสนอเป็นวิธีที่ง่ายในการให้หน้าเว็บมีฮุก
ssr.mjs
import urlModule from 'url';
const URL = urlModule.URL;
async function ssr(url) {
...
// Add ?headless to the URL so the page has a signal
// it's being loaded by headless Chrome.
const renderUrl = new URL(url);
renderUrl.searchParams.set('headless', '');
await page.goto(renderUrl, {waitUntil: 'networkidle0'});
...
return {html};
}
และเราค้นหาพารามิเตอร์ดังกล่าวได้ในหน้าเว็บ
public/index.html
<html>
<body>
<div id="container">
<!-- Populated by the JS below. -->
</div>
</body>
<script>
...
(async() => {
const params = new URL(location.href).searchParams;
const RENDERING_IN_HEADLESS = params.has('headless');
if (RENDERING_IN_HEADLESS) {
// Being rendered by headless Chrome on the server.
// e.g. shut off features, don't lazy load non-essential resources, etc.
}
const container = document.querySelector('#container');
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
})();
</script>
</html>
หลีกเลี่ยงการทำให้จำนวนหน้าที่มีการเปิดใน Analytics สูงเกินจริง
โปรดระมัดระวังหากคุณใช้ Analytics ในเว็บไซต์ การแสดงผลหน้าเว็บล่วงหน้าอาจทําให้จำนวนหน้าที่มีการเปิดสูงกว่าความเป็นจริง กล่าวโดยละเอียดคือ คุณจะเห็นจํานวน Hit เพิ่มขึ้น 2 เท่า โดย Hit แรกเกิดขึ้นเมื่อ Chrome แบบ Headless แสดงผลหน้าเว็บ และอีก Hit หนึ่งเกิดขึ้นเมื่อเบราว์เซอร์ของผู้ใช้แสดงผลหน้าเว็บ
แล้ววิธีแก้ไขล่ะ ใช้การขัดจังหวะเครือข่ายเพื่อยกเลิกคําขอที่พยายามโหลดคลัง Analytics
page.on('request', req => {
// Don't load Google Analytics lib requests so pageviews aren't 2x.
const blockist = ['www.google-analytics.com', '/gtag/js', 'ga.js', 'analytics.js'];
if (blocklist.find(regex => req.url().match(regex))) {
return req.abort();
}
...
req.continue();
});
ระบบจะไม่บันทึกการเข้าถึงหน้าเว็บหากโค้ดไม่โหลด จัดไป 💥
หรือจะโหลดไลบรารี Analytics ต่อไปเพื่อรับข้อมูลเชิงลึกเกี่ยวกับจํานวนการแสดงผลล่วงหน้าที่เซิร์ฟเวอร์ทําอยู่ก็ได้
บทสรุป
Puppeteer ช่วยให้คุณแสดงผลหน้าเว็บฝั่งเซิร์ฟเวอร์ได้อย่างง่ายดายด้วยการเรียกใช้ Chrome แบบ Headless ควบคู่ไปกับเว็บเซิร์ฟเวอร์ "ฟีเจอร์" ที่ฉันชอบที่สุดของแนวทางนี้คือปรับปรุงประสิทธิภาพการโหลดและความสามารถในการจัดทำดัชนีของแอปโดยไม่ต้องเปลี่ยนแปลงโค้ดมากนัก
หากอยากดูแอปที่ใช้งานได้จริงซึ่งใช้เทคนิคที่อธิบายไว้ที่นี่ โปรดดูแอป devwebfeed
ภาคผนวก
การพูดคุยเกี่ยวกับงานที่ปรากฏอยู่ก่อน
การแสดงผลฝั่งเซิร์ฟเวอร์ของแอปฝั่งไคลเอ็นต์นั้นทำได้ยาก ยากแค่ไหน เพียงดูจำนวนแพ็กเกจ npm ที่ผู้คนเขียนขึ้นสำหรับหัวข้อนั้นๆ มีรูปแบบ เครื่องมือ และบริการมากมายที่พร้อมให้ความช่วยเหลือเกี่ยวกับ SSR ของแอป JS
Isomorphic / Universal JavaScript
แนวคิดของ Universal JavaScript หมายความว่าโค้ดเดียวกันที่ทํางานบนเซิร์ฟเวอร์จะทํางานบนไคลเอ็นต์ (เบราว์เซอร์) ด้วย คุณแชร์โค้ดระหว่างเซิร์ฟเวอร์กับไคลเอ็นต์ และทุกคนก็รู้สึกผ่อนคลาย
Chrome แบบไม่มีส่วนหัวเปิดใช้ "Isomorphic JS" ระหว่างเซิร์ฟเวอร์กับไคลเอ็นต์ ซึ่งตัวเลือกนี้เป็นตัวเลือกที่ยอดเยี่ยมหากคลังของคุณไม่ทำงานบนเซิร์ฟเวอร์ (โหนด)
เครื่องมือแสดงผลล่วงหน้า
ชุมชน Node ได้สร้างเครื่องมือมากมายสำหรับจัดการกับแอป JS แบบ SSR ไม่น่าแปลกใจเลย เราพบว่าเครื่องมือเหล่านี้ได้ผลหรือไม่ได้ผลแตกต่างกันไป ดังนั้นโปรดศึกษาข้อมูลให้ละเอียดก่อนตัดสินใจใช้เครื่องมือใดเครื่องมือหนึ่ง ตัวอย่างเช่น เครื่องมือ SSR บางรายการเป็นเวอร์ชันเก่าและไม่ได้ใช้ Chrome แบบ Headless (หรือเบราว์เซอร์แบบ Headless ใดๆ ก็ตาม) แต่จะใช้ PhantomJS (หรือที่เรียกว่า Safari เวอร์ชันเก่า) แทน ซึ่งหมายความว่าหน้าเว็บจะไม่แสดงผลอย่างถูกต้องหากใช้ฟีเจอร์ที่ใหม่กว่า
ข้อยกเว้นที่โดดเด่นอย่างหนึ่งคือการแสดงผลล่วงหน้า ที่น่าสนใจคือ Prerender ใช้ Chrome แบบ Headless และมี Middleware สำหรับ Express ที่ติดตั้งใช้งานได้ทันที
const prerender = require('prerender');
const server = prerender();
server.use(prerender.removeScriptTags());
server.use(prerender.blockResources());
server.start();
โปรดทราบว่าการโหลดล่วงหน้าจะไม่แสดงรายละเอียดการดาวน์โหลดและการติดตั้ง Chrome ในแพลตฟอร์มต่างๆ บ่อยครั้ง การทำให้ถูกต้องเป็นเรื่องที่ค่อนข้างยุ่งยาก ซึ่งเป็นหนึ่งในเหตุผลที่Puppeteer ช่วยคุณทำ