Chrome không có giao diện người dùng: câu trả lời cho các trang web JS kết xuất phía máy chủ

Tìm hiểu cách sử dụng API Puppeteer để thêm tính năng kết xuất phía máy chủ (SSR) vào máy chủ web Express. Điều tuyệt vời nhất là ứng dụng yêu cầu thay đổi rất nhỏ đối với mã. Không có giao diện người dùng thực hiện tất cả các công việc nặng.

Chỉ cần vài dòng mã là bạn có thể SSR trên trang bất kỳ và nhận được mã đánh dấu cuối cùng trên trang đó.

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;
}

Tại sao nên sử dụng Chrome không giao diện người dùng?

Bạn có thể quan tâm đến Chrome không có giao diện người dùng nếu:

Một số khung như Preact gửi kèm các công cụ giúp giải quyết việc kết xuất phía máy chủ. Nếu khung của bạn có giải pháp kết xuất trước, hãy sử dụng giải pháp đó thay vì đưa Puppeteer và Headless Chrome vào quy trình làm việc.

Thu thập dữ liệu web hiện đại

Trước đây, trình thu thập dữ liệu của công cụ tìm kiếm, nền tảng chia sẻ qua mạng xã hội, thậm chí các trình duyệt chỉ dựa vào mã đánh dấu HTML tĩnh để lập chỉ mục nội dung trên web và nền tảng. Web hiện đại đã phát triển thành một thứ gì đó khác biệt. Các ứng dụng dựa trên JavaScript vẫn sẽ tiếp tục hoạt động, nghĩa là trong nhiều trường hợp, các công cụ thu thập dữ liệu có thể không nhìn thấy nội dung của chúng tôi.

Googlebot, trình thu thập dữ liệu của Tìm kiếm, xử lý JavaScript mà vẫn đảm bảo không làm giảm trải nghiệm của người dùng khi truy cập trang web. Có một số điểm khác biệt và giới hạn mà bạn cần tính đến khi thiết kế các trang và ứng dụng của mình để đáp ứng cách các trình thu thập dữ liệu truy cập và hiển thị nội dung của bạn.

Trang kết xuất trước

Tất cả các trình thu thập dữ liệu đều hiểu HTML. Để đảm bảo trình thu thập dữ liệu có thể lập chỉ mục JavaScript, chúng tôi cần một công cụ:

  • Biết cách chạy tất cả các loại JavaScript hiện đại và tạo HTML tĩnh.
  • Luôn cập nhật khi web bổ sung các tính năng.
  • Chạy mà không cần cập nhật mã hoặc cập nhật mã cho ứng dụng.

Như vậy có được không? Công cụ đó chính là trình duyệt! Giao diện người dùng Chrome không quan tâm bạn sử dụng thư viện, khung hay chuỗi công cụ nào.

Ví dụ: nếu ứng dụng của bạn được xây dựng bằng Node.js, thì Puppeteer là một cách dễ dàng để làm việc với Chrome 0.headless.

Hãy bắt đầu với một trang động tạo HTML bằng 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>

Hàm SSR

Tiếp theo, chúng ta sẽ lấy hàm ssr() từ trước đó và tăng cường một chút:

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};

Những thay đổi chính:

  • Đã thêm chức năng lưu vào bộ nhớ đệm. Việc lưu vào bộ nhớ đệm HTML đã kết xuất là cách hiệu quả nhất giúp tăng tốc thời gian phản hồi. Khi trang được yêu cầu lại, bạn tránh chạy Chrome không có giao diện người dùng hoàn toàn. Tôi sẽ thảo luận về các phương án tối ưu hoá khác sau.
  • Thêm cách xử lý lỗi cơ bản nếu trang hết thời gian chờ.
  • Thêm lệnh gọi vào page.waitForSelector('#posts'). Điều này đảm bảo rằng các bài đăng tồn tại trong DOM trước khi chúng ta kết xuất trang được chuyển đổi tuần tự.
  • Thêm khoa học. Ghi lại thời gian không có giao diện người dùng để hiển thị trang và trả về thời gian kết xuất cùng với HTML.
  • Dán mã vào một mô-đun có tên ssr.mjs.

Máy chủ web mẫu

Cuối cùng, đây là máy chủ tốc độ nhỏ kết hợp tất cả lại với nhau. Trình xử lý chính kết xuất trước URL http://localhost/index.html (trang chủ) và phân phát kết quả làm phản hồi. Người dùng sẽ thấy ngay bài đăng khi họ truy cập vào trang vì mã đánh dấu tĩnh hiện đã có trong phản hồi.

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'));

Để chạy ví dụ này, hãy cài đặt các phần phụ thuộc (npm i --save puppeteer express) và chạy máy chủ bằng cách sử dụng Nút 8.5.0 trở lên và cờ --experimental-modules:

Dưới đây là ví dụ về phản hồi được máy chủ này gửi lại:

<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>

Trường hợp sử dụng hoàn hảo cho API Server-Timing mới

API Server-Timing thông báo các chỉ số hiệu suất của máy chủ (chẳng hạn như thời gian yêu cầu và phản hồi hoặc tra cứu cơ sở dữ liệu) cho trình duyệt. Mã ứng dụng khách có thể sử dụng thông tin này để theo dõi hiệu suất tổng thể của ứng dụng web.

Một trường hợp sử dụng hoàn hảo cho Server-Timing là để báo cáo thời gian cần để Chrome không có giao diện người dùng kết xuất trước một trang! Để làm điều đó, bạn chỉ cần thêm tiêu đề Server-Timing vào phản hồi của máy chủ:

res.set('Server-Timing', `Prerender;dur=1000;desc="Headless render time (ms)"`);

Trên ứng dụng, bạn có thể sử dụng Performance APIPerformanceObserver để truy cập vào các chỉ số sau:

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)"
}

Kết quả hiệu suất

Các kết quả sau đây kết hợp hầu hết các phương án tối ưu hoá hiệu suất được thảo luận sau đó.

Trên một trong các ứng dụng của tôi (), Chrome không có giao diện người dùng sẽ mất khoảng 1 giây để hiển thị trang trên máy chủ. Sau khi trang được lưu vào bộ nhớ đệm, tính năng Mô phỏng 3G chậm của Công cụ cho nhà phát triển sẽ đặt FCPnhanh hơn 8,37 giây so với phiên bản phía máy khách.

Hiển thị đầu tiên (FP)First Contentful Paint (FCP)
Ứng dụng phía máy khách4 giây 11 giây
Phiên bản SSR2,3 giây~2,3 giây

Những kết quả này đầy hứa hẹn. Người dùng thấy nội dung có ý nghĩa nhanh hơn nhiều vì trang kết xuất phía máy chủ không còn dựa vào JavaScript để tải và hiển thị bài đăng.

Ngăn ngừa tình trạng tái nước

Bạn có nhớ khi tôi nói "chúng tôi không thực hiện bất kỳ thay đổi nào về mã cho ứng dụng phía máy khách" không? Đó là dối trá.

Ứng dụng Express của chúng tôi nhận một yêu cầu, sử dụng Puppeteer để tải trang ở chế độ không có giao diện người dùng và phân phát kết quả dưới dạng phản hồi. Tuy nhiên, cách thiết lập này có một vấn đề.

Cùng một JS thực thi trong Chrome không có giao diện người dùng trên máy chủ sẽ chạy lại khi trình duyệt của người dùng tải trang trên giao diện người dùng. Chúng tôi có 2 nơi đang tạo mã đánh dấu. #Double kết (kết xuất kép)

Hãy khắc phục vấn đề này. Chúng ta cần cho trang biết HTML của trang đã sẵn sàng. Giải pháp tôi tìm thấy là để JS trang kiểm tra xem <ul id="posts"> đã có trong DOM tại thời điểm tải hay chưa. Nếu có, chúng tôi biết trang đó đã được SSR và có thể tránh việc thêm lại bài đăng. 👍

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>

Tối ưu hoá

Ngoài việc lưu các kết quả đã kết xuất vào bộ nhớ đệm, chúng tôi có thể thực hiện rất nhiều phương án tối ưu hoá thú vị đối với ssr(). Một số kết quả là thành công nhanh chóng trong khi một số khác có thể dễ suy đoán hơn. Lợi ích về hiệu suất mà bạn thấy cuối cùng có thể phụ thuộc vào loại trang bạn kết xuất trước cũng như độ phức tạp của ứng dụng.

Hủy bỏ các yêu cầu không cần thiết

Hiện tại, toàn bộ trang (và tất cả các tài nguyên mà trang đó yêu cầu) sẽ được tải vô điều kiện vào Chrome không có giao diện người dùng. Tuy nhiên, chúng tôi chỉ quan tâm đến hai điều:

  1. Mã đánh dấu được kết xuất.
  2. JS yêu cầu tạo ra mã đánh dấu đó.

Các yêu cầu mạng không tạo DOM là lãng phí. Các tài nguyên như hình ảnh, phông chữ, biểu định kiểu và nội dung nghe nhìn không tham gia vào quá trình tạo HTML của trang. Các mô-đun này định kiểu và bổ sung cho cấu trúc của trang, nhưng không tạo ra cấu trúc rõ ràng. Chúng ta nên yêu cầu trình duyệt bỏ qua các tài nguyên này. Điều này làm giảm khối lượng công việc cho Chrome không có giao diện người dùng, tiết kiệm băng thông và có thể tăng tốc thời gian kết xuất trước cho các trang lớn hơn.

DevTools Protocol hỗ trợ một tính năng mạnh mẽ có tên là Chặn mạng. Tính năng này có thể dùng để sửa đổi các yêu cầu trước khi trình duyệt đưa ra. Puppeteer hỗ trợ tính năng chặn mạng bằng cách bật page.setRequestInterception(true) và theo dõi sự kiện request của trang. Điều đó cho phép chúng tôi huỷ yêu cầu đối với một số tài nguyên nhất định và cho phép những tài nguyên khác tiếp tục thực hiện.

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};
}

Tài nguyên quan trọng cùng dòng

Thường thì bạn nên sử dụng các công cụ xây dựng riêng biệt (chẳng hạn như gulp) để xử lý ứng dụng và đưa CSS và JS quan trọng vào nội tuyến tại thời điểm xây dựng. Điều này có thể tăng tốc độ hiển thị có ý nghĩa đầu tiên vì trình duyệt đưa ra ít yêu cầu hơn trong quá trình tải trang ban đầu.

Thay vì sử dụng một công cụ xây dựng riêng, hãy sử dụng trình duyệt làm công cụ xây dựng! Chúng tôi có thể sử dụng Puppeteer để thao tác với DOM của trang, kiểu cùng dòng, JavaScript hoặc bất kỳ nội dung nào khác mà bạn muốn lưu lại trên trang trước khi kết xuất trước.

Ví dụ này cho thấy cách chặn phản hồi đối với biểu định kiểu cục bộ và nội tuyến các tài nguyên đó vào trang dưới dạng thẻ <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:

  1. Use a page.on('response') handler to listen for network responses.
  2. Stashes the responses of local stylesheets.
  3. Finds all <link rel="stylesheet"> in the DOM and replaces them with an equivalent <style>. See page.$$eval API docs. The style.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};
}

Sử dụng lại một phiên bản Chrome duy nhất trong các lượt kết xuất

Việc khởi chạy một trình duyệt mới cho mỗi lượt kết xuất trước sẽ tạo ra nhiều hao tổn. Thay vào đó, bạn nên chạy một phiên bản duy nhất và sử dụng lại phiên bản đó để kết xuất nhiều trang.

Puppeteer có thể kết nối lại với một thực thể Chrome hiện có bằng cách gọi puppeteer.connect() và truyền vào đó URL gỡ lỗi từ xa của thực thể đó. Để duy trì một phiên bản trình duyệt hoạt động trong thời gian dài, chúng ta có thể di chuyển mã khởi chạy Chrome từ hàm ssr() và vào máy chủ 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};
}

Ví dụ: công việc cron để kết xuất trước định kỳ

Trong ứng dụng trang tổng quan App Engine, tôi đã thiết lập một trình xử lý cron để kết xuất lại định kỳ các trang hàng đầu trên trang web. Điều này giúp khách truy cập luôn thấy nội dung nhanh, mới mẻ, đồng thời tránh và giúp họ không phải thấy "chi phí khởi động" của một lượt kết xuất trước mới. Việc tạo ra một số phiên bản Chrome sẽ gây lãng phí trong trường hợp này. Thay vào đó, tôi sẽ dùng một phiên bản trình duyệt dùng chung để kết xuất nhiều trang cùng lúc:

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!');
});

Tôi cũng đã thêm tệp xuất clearCache() vào ssr.js:

...
function clearCache() {
  RENDER_CACHE.clear();
}

export {ssr, clearCache};

Lưu ý khác

Tạo tín hiệu cho trang: "Bạn đang hiển thị ở chế độ không có giao diện người dùng"

Khi trang của bạn hiển thị qua Chrome không có giao diện người dùng trên máy chủ, logic phía máy khách của trang có thể biết được điều đó. Trong ứng dụng của mình, tôi đã dùng hook này để "tắt" các phần không đóng vai trò trong việc hiển thị mã đánh dấu bài đăng. Ví dụ: tôi đã vô hiệu hoá mã tải từng phần firebase-auth.js. Không có người dùng nào để đăng nhập!

Việc thêm tham số ?headless vào URL hiển thị là một cách đơn giản để tạo hấp dẫn cho trang:

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};
}

Và trong trang này, chúng ta có thể tìm thông số đó:

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>

Tránh làm tăng số lượt xem trang Analytics

Hãy cẩn thận nếu bạn đang dùng Analytics trên trang web của mình. Trang kết xuất trước có thể dẫn đến số lượt xem trang tăng cao. Cụ thể, bạn sẽ thấy số lượt truy cập nhiều gấp đôi, một lượt truy cập khi Chrome không có giao diện người dùng hiển thị trang và một lượt truy cập khác khi trình duyệt của người dùng hiển thị trang.

Vậy cách khắc phục là gì? Sử dụng tính năng chặn mạng để huỷ mọi yêu cầu cố gắng tải thư viện 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();
});

Lượt truy cập trang không bao giờ được ghi lại nếu mã không bao giờ tải. Chính xác EN!

Ngoài ra, hãy tiếp tục tải thư viện Analytics để nhận thông tin chi tiết về số lượng lượt kết xuất trước mà máy chủ của bạn đang thực hiện.

Kết luận

Puppeteer giúp bạn dễ dàng hiển thị các trang hiển thị phía máy chủ bằng cách chạy Chrome không có giao diện người dùng trên máy chủ web của bạn. "Tính năng" yêu thích của tôi ở phương pháp này là bạn cải thiện hiệu suất tảikhả năng lập chỉ mục của ứng dụng mà không cần thay đổi đáng kể về mã!

Nếu bạn muốn xem một ứng dụng hoạt động được có dùng các kỹ thuật được mô tả ở đây, hãy xem ứng dụng devwebfeed.

Phụ lục

Nội dung về tác phẩm nghệ thuật trước đây

Rất khó để hiển thị ứng dụng phía máy khách ở phía máy chủ. Khó đến mức nào? Hãy xem có bao nhiêu gói npm mà mọi người đã viết dành riêng cho chủ đề này. Hiện có vô số mẫu, toolsdịch vụ để hỗ trợ các ứng dụng SSRing JS.

JavaScript đẳng cấp / Universal JavaScript

Khái niệm Universal JavaScript có nghĩa là: cùng một mã chạy trên máy chủ cũng chạy trên ứng dụng (trình duyệt). Bạn chia sẻ mã giữa máy chủ và ứng dụng khách và mọi người đều cảm thấy một khoảnh khắc của zen.

Chrome không có giao diện người dùng bật "JS đẳng cấp" giữa máy chủ và ứng dụng. Đây là một lựa chọn hữu ích nếu thư viện của bạn không hoạt động trên máy chủ (Nút).

Công cụ kết xuất trước

Cộng đồng Nút đã tạo rất nhiều công cụ để xử lý các ứng dụng SSR JS. Không có gì bất ngờ! Cá nhân tôi cũng nhận thấy rằng YMMV với một số công cụ trong số này, vì vậy, bạn nên làm bài tập về nhà trước khi quyết tâm thực hiện một công cụ nào đó. Ví dụ: một số công cụ SSR đã cũ và không sử dụng Chrome không có giao diện người dùng (hoặc bất kỳ trình duyệt không có giao diện người dùng nào cho vấn đề đó). Thay vào đó, các trang sử dụng PhantomJS (còn gọi là Safari cũ). Điều này có nghĩa là các trang của bạn sẽ không hiển thị đúng cách nếu đang sử dụng các tính năng mới.

Một trong những trường hợp ngoại lệ đáng chú ý là Trình kết xuất trước. Quá trình kết xuất trước thú vị ở chỗ sử dụng Chrome không có giao diện người dùng và đi kèm với phần mềm trung gian cho Express:

const prerender = require('prerender');
const server = prerender();
server.use(prerender.removeScriptTags());
server.use(prerender.blockResources());
server.start();

Lưu ý rằng tính năng Kết xuất trước sẽ bỏ qua thông tin chi tiết về việc tải xuống và cài đặt Chrome trên các nền tảng khác nhau. Thông thường, điều đó khá phức tạp, đó là một trong những lý do khiến Puppeteer làm cho bạn. Tôi cũng gặp vấn đề với dịch vụ trực tuyến hiển thị một số ứng dụng của mình:

chromestatus hiển thị trong trình duyệt
Trang web hiển thị trong trình duyệt
chromestatus được hiển thị bằng cách kết xuất trước
Cùng trang web được kết xuất bằng Render.io