ดูวิธีใช้ Puppeteer API เพื่อเพิ่มความสามารถในการแสดงผลฝั่งเซิร์ฟเวอร์ (SSR) ไปยังเว็บเซิร์ฟเวอร์ Express และส่วนที่ดีที่สุดคือแอป ต้องมีการเปลี่ยนแปลงโค้ดเพียงเล็กน้อย แบบ Headless ทำงานยก หนักสุด
ในโค้ด 2-3 บรรทัด คุณสามารถ 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
คุณอาจสนใจ Chrome แบบ Headless ในกรณีต่อไปนี้
- คุณได้สร้างเว็บแอปที่เครื่องมือค้นหาไม่ได้จัดทำดัชนี
- คุณคาดหวังว่าจะได้รับผลลัพธ์อย่างรวดเร็วในการเพิ่มประสิทธิภาพประสิทธิภาพ JavaScript และปรับปรุง First Media Paint
บางเฟรมเวิร์ก เช่น เตรียมการให้บริการด้วยเครื่องมือที่จัดการการแสดงผลฝั่งเซิร์ฟเวอร์ หากเฟรมเวิร์กของคุณมีโซลูชันการแสดงผลล่วงหน้า ให้ใช้โซลูชันดังกล่าวแทนการนำ Puppeteer และ Headless Chrome ไปใช้ในเวิร์กโฟลว์
การรวบรวมข้อมูลเว็บสมัยใหม่
ที่ผ่านมา โปรแกรมรวบรวมข้อมูลของเครื่องมือค้นหา แพลตฟอร์มการแชร์ผ่านโซเชียล และเบราว์เซอร์ต่างๆ เคยพึ่งพามาร์กอัป HTML แบบคงที่เพียงอย่างเดียวในการจัดทำดัชนีเว็บและแสดงเนื้อหา เว็บสมัยใหม่ได้พัฒนาไปอย่างมาก แอปพลิเคชันที่ใช้ JavaScript จะยังคงอยู่ ซึ่งหมายความว่าในหลายๆ กรณี เครื่องมือรวบรวมข้อมูลอาจมองไม่เห็นเนื้อหาของเรา
Googlebot ซึ่งเป็น Crawler ของ Search จะประมวลผล JavaScript ในขณะเดียวกันก็จะไม่ทำให้ประสบการณ์การใช้งานของผู้ใช้ที่เข้าชมเว็บไซต์แย่ลง มีความแตกต่างและข้อจำกัดบางอย่างที่คุณต้องคำนึงถึงเมื่อออกแบบหน้าเว็บและแอปพลิเคชันให้รองรับวิธีที่โปรแกรมรวบรวมข้อมูลเข้าถึงและแสดงเนื้อหา
หน้าที่แสดงผลล่วงหน้า
โปรแกรมรวบรวมข้อมูลทั้งหมดเข้าใจ HTML เพื่อให้แน่ใจว่าโปรแกรมรวบรวมข้อมูลจัดทำดัชนี JavaScript ได้ เราจำเป็นต้องใช้เครื่องมือที่
- รู้วิธีเรียกใช้ JavaScript สมัยใหม่ทุกประเภทและสร้าง HTML แบบคงที่
- ไม่พลาดทุกข่าวสารขณะที่เว็บเพิ่มฟีเจอร์ต่างๆ
- ทำงานได้โดยมีการอัปเดตโค้ดเพียงเล็กน้อยหรือไม่ต้องติดตั้งเลย
น่าสนใจใช่ไหม เครื่องมือนั้นก็คือเบราว์เซอร์ Chrome แบบ Headless จะไม่สนใจไลบรารี เฟรมเวิร์ก หรือเครือเครื่องมือที่คุณใช้
ตัวอย่างเช่น ถ้าแอปพลิเคชันของคุณสร้างด้วย Node.js Puppeteer ก็เป็นวิธีที่ง่ายในการทำงานกับ Chrome 0.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 ก่อนที่เราจะดัมพ์หน้าที่เรียงอันดับ - เพิ่มวิทยาศาสตร์ บันทึกระยะเวลาการใช้ส่วนหัวแบบไม่มีส่วนหัวในการแสดงผลหน้าเว็บและแสดงผลเวลาแสดงผลพร้อม HTML
- วางโค้ดไว้ในโมดูลที่ชื่อ
ssr.mjs
ตัวอย่างเว็บเซิร์ฟเวอร์
สุดท้าย นี่คือเซิร์ฟเวอร์แบบเร่งด่วนขนาดเล็กที่รวบรวมทุกอย่างไว้ด้วยกัน เครื่องจัดการหลักจะแสดงผลล่วงหน้าสำหรับ 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'));
หากต้องการเรียกใช้ตัวอย่างนี้ ให้ติดตั้งการอ้างอิง (npm i --save puppeteer express
) และเรียกใช้เซิร์ฟเวอร์โดยใช้โหนด 8.5.0+ และแฟล็ก --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>
กรณีการใช้งานที่สมบูรณ์แบบสำหรับ Server-Timing API ใหม่
API Server-Timing คือการสื่อสารเมตริกประสิทธิภาพของเซิร์ฟเวอร์ (เช่น เวลาส่งคำขอและเวลาในการตอบกลับ หรือการค้นหาฐานข้อมูล) กลับไปยังเบราว์เซอร์ รหัสไคลเอ็นต์สามารถใช้ข้อมูลนี้เพื่อติดตาม ประสิทธิภาพโดยรวมของเว็บแอป
กรณีการใช้งานที่ดีที่สุดสำหรับ Server-Timing คือการรายงานระยะเวลาที่ Chrome แบบไม่มีส่วนหัวในการแสดงผลหน้าเว็บล่วงหน้า โดยเพิ่มส่วนหัว 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)"
}
ผลลัพธ์ด้านประสิทธิภาพ
ผลลัพธ์ต่อไปนี้จะรวมการเพิ่มประสิทธิภาพประสิทธิภาพส่วนใหญ่ซึ่งเราจะกล่าวถึงในภายหลัง
ในแอปหนึ่งของฉัน (โค้ด) Chrome แบบไม่มีส่วนหัวจะใช้เวลาประมาณ 1 วินาทีในการแสดงผลหน้าเว็บบนเซิร์ฟเวอร์ เมื่อแคชหน้าเว็บแล้ว การจำลอง 3G Slow ในเครื่องมือสำหรับนักพัฒนาเว็บจะทำให้ FCP อยู่ที่ 8.37 วินาทีกว่าเวอร์ชันฝั่งไคลเอ็นต์
การแสดงผลครั้งแรก (FP) | First Contentful Paint (FCP) | |
---|---|---|
แอปฝั่งไคลเอ็นต์ | 4 วิ | 11 วิ |
เวอร์ชัน SSR | 2.3 วิ | ประมาณ 2.3 วินาที |
ผลลัพธ์เหล่านี้มีแนวโน้มที่ดี ผู้ใช้จะเห็นเนื้อหาที่มีความหมายได้เร็วขึ้นมากเนื่องจากหน้าที่แสดงผลฝั่งเซิร์ฟเวอร์ไม่ต้องอาศัย JavaScript ในการโหลด + แสดงโพสต์อีกต่อไป
ป้องกันการดื่มน้ำในร่างกาย
จำได้ไหมว่าเมื่อผมพูดว่า "เราไม่ได้ทำการเปลี่ยนแปลงโค้ดใดๆ ในแอปฝั่งไคลเอ็นต์" เมื่อกี้คุณโกหก
แอป Express ของเราจะรับคำขอ ใช้ Puppeteer เพื่อโหลดหน้าเว็บแบบ Headless และแสดงผลเป็นการตอบกลับ แต่การตั้งค่านี้มีปัญหา
JS เดียวกันที่เรียกใช้ใน Chrome แบบไม่มีส่วนหัวบนเซิร์ฟเวอร์จะทำงานอีกครั้งเมื่อเบราว์เซอร์ของผู้ใช้โหลดหน้าเว็บในฟรอนท์เอนด์ เรามีสถานที่ 2 แห่งที่สร้างมาร์กอัป #doubleRender
มาแก้ไขกัน เราต้องบอกหน้าเว็บว่า HTML พร้อมใช้งานแล้ว
วิธีแก้ไขที่เจอคือให้ตรวจสอบว่า <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 แบบไม่มีส่วนหัว ประหยัดแบนด์วิดท์ และอาจทำให้การแสดงผลล่วงหน้าเร็วขึ้นสำหรับหน้าขนาดใหญ่
โปรโตคอล 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 เพื่อแสดงผลล่วงหน้าเป็นระยะๆ
ในแอปแดชบอร์ด App Engine ฉันได้ตั้งค่าตัวแฮนเดิล cron เพื่อแสดงหน้ายอดนิยมในเว็บไซต์เป็นระยะๆ วิธีนี้ช่วยให้ผู้เข้าชมเห็นเนื้อหาที่เร็วและสดใหม่อยู่เสมอ รวมทั้งหลีกเลี่ยงและป้องกันไม่ให้เห็น "ค่าใช้จ่ายในการเริ่มต้น" การแสดงผลล่วงหน้าครั้งใหม่ การระบุ Chrome หลายอินสแตนซ์ ถือเป็นการเสียเปล่าสำหรับกรณีนี้ แต่ฉันใช้อินสแตนซ์ของเบราว์เซอร์ที่แชร์เพื่อแสดงผล หน้าเว็บหลายหน้าพร้อมกัน
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};
ข้อควรพิจารณาอื่นๆ
สร้างสัญญาณสําหรับหน้า: "คุณกำลังแสดงผลแบบไม่มีส่วนหัว"
เมื่อ Chrome แบบไม่มีส่วนหัวบนเซิร์ฟเวอร์แสดงผลหน้าเว็บ ข้อมูลนี้อาจเป็นประโยชน์สำหรับตรรกะฝั่งไคลเอ็นต์ของหน้าเว็บ ในแอป ผมใช้ฮุกนี้เพื่อ "ปิด" ส่วนต่างๆ ของหน้าที่ไม่ได้มีส่วนในการแสดงผลมาร์กอัปของโพสต์ เช่น ฉันปิดใช้โค้ดที่โหลดแบบ 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 เท่า โดย 1 ครั้งเมื่อ Chrome แบบไม่มีส่วนหัวแสดงผลหน้าเว็บ และอีก 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();
});
จะไม่มีการบันทึก Page Hit หากโค้ดไม่โหลด บูม 🎥
หรือให้โหลดไลบรารี Analytics ต่อไปเพื่อดูข้อมูลเชิงลึกเกี่ยวกับจำนวนการแสดงผลล่วงหน้าของเซิร์ฟเวอร์
บทสรุป
Puppeteer ช่วยให้การแสดงผลหน้าฝั่งเซิร์ฟเวอร์ทำได้ง่ายๆ โดยเรียกใช้ Chrome แบบไม่มีส่วนหัวในเว็บเซิร์ฟเวอร์ของคุณ "ฟีเจอร์" ที่ฉันชื่นชอบคือคุณปรับปรุงประสิทธิภาพการโหลดและความสามารถในการจัดทำดัชนีของแอปโดยไม่ต้องเขียนโค้ด
ถ้าอยากจะดูแอปที่ใช้งานได้และใช้เทคนิคที่อธิบายไว้ที่นี่ โปรดดูแอป devwebfeed
ภาคผนวก
การสนทนาเกี่ยวกับศิลปะก่อนหน้า
แอปการแสดงผลฝั่งเซิร์ฟเวอร์ทำได้ยาก ยากแค่ไหน ลองดูว่ามีคนเขียนแพ็กเกจ npm ซึ่งเขียนมาเพื่อหัวข้อนั้นๆ โดยเฉพาะกี่รายการ เรามี รูปแบบ tools และบริการมากมายนับไม่ถ้วนที่พร้อมให้ความช่วยเหลือกับแอป SSRing JS
Isomorphic / Universal JavaScript
แนวคิดของ Universal JavaScript หมายถึงโค้ดเดียวกับที่ทำงานบนเซิร์ฟเวอร์จะทำงานบนไคลเอ็นต์ (เบราว์เซอร์) ด้วย คุณแชร์โค้ดระหว่างเซิร์ฟเวอร์กับไคลเอ็นต์ และทุกคนก็รู้สึกสงบ
Chrome แบบ Headless จะเปิดใช้ "isomorphic JS" ระหว่างเซิร์ฟเวอร์และไคลเอ็นต์ ซึ่งเป็นตัวเลือกที่ดีหากไลบรารีของคุณไม่ทำงานบนเซิร์ฟเวอร์ (โหนด)
เครื่องมือแสดงผลล่วงหน้า
ชุมชนโหนดได้สร้างเครื่องมือมากมายสำหรับการจัดการแอป SSR JS ไม่แปลกใจเลย โดยส่วนตัวแล้ว เราพบว่า YMMV ด้วยเครื่องมือบางอย่าง ดังนั้นคุณควรทำการบ้านก่อนที่จะตัดสินใจ เช่น เครื่องมือ SSR บางอย่างเป็นเวอร์ชันเก่าและไม่ได้ใช้ Chrome แบบไม่มีส่วนหัว (หรือเบราว์เซอร์ที่ไม่มีส่วนหัวเพื่อการดำเนินการดังกล่าว) แต่จะใช้ PhantomJS (หรือ Safari ตัวเดิม) แทน ซึ่งหมายความว่าหน้าของคุณจะแสดงผลอย่างไม่ถูกต้องหากใช้ฟีเจอร์ใหม่อยู่
ข้อยกเว้นที่สําคัญอย่างหนึ่งคือแสดงผลล่วงหน้า การแสดงผลล่วงหน้ามีความน่าสนใจตรงที่ใช้ Chrome แบบไม่มีส่วนหัวและมิดเดิลแวร์ดรอปอินสำหรับ Express ดังนี้
const prerender = require('prerender');
const server = prerender();
server.use(prerender.removeScriptTags());
server.use(prerender.blockResources());
server.start();
โปรดทราบว่า Preserve ไม่ได้ให้รายละเอียดเกี่ยวกับการดาวน์โหลดและติดตั้ง Chrome ในแพลตฟอร์มต่างๆ ในหลายๆ ครั้ง เรื่องนี้ค่อนข้างยาก จึงจะทำให้ถูกต้อง ซึ่งเป็นเหตุผลหนึ่งที่ Puppeteer ทำสิ่งนั้นให้คุณ ฉันพบปัญหาเกี่ยวกับบริการออนไลน์ที่แสดงผลบางแอปของฉันด้วย เช่น