+12

Render React Ở Phía Server

Tổng quan

React được biết đến rất nhiều là một framework JavaScript ở phía client, nhưng bạn có biết rằng bạn có thể hay có lẽ nên render React ở phía Server?

Hãy thử tưởng tượng bạn vừa làm xong một ứng dụng Liệt kê danh sách các sự kiện mới nhất cho một khách hàng. Ứng dụng này kết nối với một thư viện API được xây dựng ở trên công cụ phía Server mà bạn thích. Một vài tuần sau khách hàng nói với bạn rằng, các trang web của họ không hề xuất hiện trên Google và trông không ổn chút nào khi được đăng lên trên Facebook. Chắc là bạn cũng đã có cách để giải quyết vấn đề này đúng không?

Bạn hiểu ra rằng để giải quyết vấn đề trên bạn cần phải render các trang React từ phía server trong lần load đầu tiên để các crawlers (trình thu thập thông tin) từ các Search Engines và các trang phương tiện truyền thông có thể đọc được các đánh dấu của bạn. Có những kinh nghiệm thực tế cho thấy Google đôi khi thực thi code JavaScript và có thể đánh chỉ mục (index) cho nội dung tổng quan của trang web, nhưng không phải lúc nào cũng vậy. Nên việc render code ở phía server luôn luôn được khuyến khích nếu bạn muốn đảm bảo chất lượng tốt cho việc tối ưu hóa các Search Engine (SEO) và việc tương thích với các dịch vụ khác như Facebook, Twitter…

Trong tutorial này, chúng ta sẽ cùng đi từng bước thông qua ví dụ về render ở server, bao gồm cả việc giải quyết các rào cản thường gặp khi xây dựng các ứng dụng React tương tác với APIs.

Những lợi ích khi render code ở phía Server

Các vấn đề SEO có thể sẽ là thứ đầu tiên khiến cho team của bạn bàn bạc về việc render ở Server, nhưng đó không phải là lợi ích tiềm năng duy nhất.

Sau đây mới là vấn đề lớn thực sự: “Render ở phía server giúp các trang web hiển thị một cách nhanh chóng hơn”. Với việc render ở Server, server sẽ trả về cho trình duyệt các đoạn HTML của trang web đã sẵn sàng để được render nên trình duyệt có thể bắt đầu render ngay mà không cần phải chờ các đoạn JavaScript được tải về và thực thi. Sẽ không còn tồn tại các trang trắng (white page) trong khi chờ trình duyệt tải và thực thi JavaScript và các assets cần để render trang web, điều mà có thể xảy ra khi bạn render code React hoàn toàn ở phía Client.

Mở đầu

Hãy cùng xem làm thế nào để thêm việc render ở Server cho một ứng dụng React cơ bản render ở Client với Babel và Webpack. Ứng dụng của chúng ta sẽ thêm phần phức tạp khi phải lấy dữ liệu từ các API của bên thứ ba. Đây là code được cung cấp trên GitHub để bạn có thể xem ví dụ đã được hoàn thiện: starter code

Đoạn code mở đầu chỉ bao gồm có một React component, hello.js, component này sẽ tạo một request bất đồng bộ lên API của ButterCMS và render danh sách các post trên blog được trả về dưới dạng JSON. ButterCMS là một blog engine thuần API miễn phí cho cá nhân sử dụng, nên sẽ rất tốt khi ta được thử với một use case thực tế. Đoạn code mở đầu được nối với một API token, nhưng nếu bạn muốn bạn có thể lấy API token của riêng bạn bằng cách đăng nhập vào ButterCMS bằng tài khoản GitHub của bạn.

import React from 'react';
import Butter from 'buttercms'

const butter = Butter('b60a008584313ed21803780bc9208557b3b49fbb');

var Hello = React.createClass({
  getInitialState: function() {
    return {loaded: false};
  },
  componentWillMount: function() {
    butter.post.list().then((resp) => {
      this.setState({
        loaded: true,
        resp: resp.data
      })
    });
  },
  render: function() {
    if (this.state.loaded) {
      return (
        <div>
          {this.state.resp.data.map((post) => {
            return (
              <div key={post.slug}>{post.title}</div>
            )
          })}
        </div>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
});

export default Hello;

Dưới đây là những gì còn lại được bao gồm trong gói code mở đầu:

  • package.json - cho các Dependencies
  • Các thiết lập cho Webpack và Babel
  • index.html - trang HTML cho ứng dụng
  • index.js - load React và render component Hello

Để có thể chạy ứng dụng, đầu tiên hãy clone repository về:

git clone ...
cd ..

Cài các Dependencies:

npm install

Sau đó bắt đầu chạy development Server:

npm run start

Truy cập đến http://localhost:3000 để xem ứng dụng:

Nếu bạn xem source code của trang web được render, bạn sẽ thấy rằng con dấu được gửi cho trình duyệt chỉ là cái link kết nối đến file JavaScript. Điều này nghĩa là nội dung của trang web không đảm bảo có thể được thu thập bởi các Search Engine hay các nền tảng phương tiện truyền thông:

Bổ sung xử lý render ở phía Server

Tiếp theo, chúng ta sẽ thực hiện render ở Server để toàn bộ đoạn HTML đã được tạo ra có thể được gửi xuống dưới trình duyệt. Nếu bạn muốn xem tất cả thay đổi một lúc, hãy xem phần thay đổi ở trên GitHub.

Để bắt đầu, hãy cài đặt Express, một framework ứng dụng ở phía server của Node.js:

npm install express --save

Chúng ta muốn tạo ra một server có thể render các React components:

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Hello from './Hello.js';

function handleRender(req, res) {
  // Renders Hello component thành đoạn String HTML
  const html = ReactDOMServer.renderToString(<Hello />);

  // Load nội dung của trang index.html
  fs.readFile('./index.html', 'utf8', function (err, data) {
    if (err) throw err;

    // Chèn đoạn React HTML đã được render vào div chính
    const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${html}</div>`);

    // Gửi dữ liệu trả về cho phía client
    res.send(document);
  });
}

const app = express();

// Phục vụ các file đã được tạo với middleware quản lý file tĩnh
app.use('/build', express.static(path.join(__dirname, 'build')));

// Phục vụ các request bằng hàm handleRender
app.get('*', handleRender);

// Chạy server
app.listen(3000);

Hãy cùng phân tích những gì đang diễn ra:

Hàm handleRender sẽ xử lý tất cả các requests. Class ReactDOMServer được import ở đầu file sẽ cung cấp phương thức renderToString() để render ra một phần tử React cùng với đoạn code HTML đầu tiên của nó.

ReactDOMServer.renderToString(<Hello />);

Đoạn code trên sẽ trả về HTML cho component Hello, và sau đó ta thêm vào trong đoạn code HTML của file index.html để tạo ra đoạn HTML đầy đủ cho trang web ở phía server.

const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${html}</div>`);

Để chạy server, hãy cập nhật đoạn code đầu trong file package.json và sau đó chạy lệnh npm run start:

"scripts": {
  "start": "webpack && babel-node server.js"
},

Truy cập đến http://localhost:3000 để xem ứng dụng. Như bạn thấy, trang web giờ đây đã được render từ phía Server. Nhưng có một vấn đề, nếu xem Source của trang web thông qua browser, bạn sẽ để ý thấy các post của blog vẫn chưa được trả về. Vậy chuyện gì đang xảy ra? Nếu mở tab network của Chrome, chúng ta sẽ thấy các request để lấy API đang diễn ra ở dưới Client.

Mặc dù chúng ta đang render component React trên Server, các API đang được request bất đồng bộ thông qua hàm componentWillMount và component thì được render trước khi request hoàn thành. Nên ngay cả khi chúng ta render trên Server, ta cũng chỉ làm nó theo từng phần. Hóa ra đây lại là vấn đề chung của React đã được rất nhiều người bàn luận và có nhiều cách giải quyết khác nhau. Xem React Issue.

Lấy dữ liệu về trước khi render

Để giải quyết vấn đề này, chúng ta cần phải chắc chắn rằng request API hoàn thiện trước khi component Hello được render xong. Điều này có nghĩa là phải tạo một request lấy API ngoài chu kỳ render component của React và lấy dữ liệu trước khi render component. Hãy cùng đi từng bước để tiếp cận vấn đề này nhưng bạn cũng có thể xem thay đổi trên GitHub.

Để đẩy việc lấy dữ liệu trước khi render, ta sẽ cài đặt react-transmit:

npm install react-transmit --save

React Transmit cung cấp cho ta các component bọc ngoài (thường được gọi là “high-order components”) hoạt động trên cả Server lẫn Client dùng để lấy dữ liệu. Và đây là component của chúng ta khi nó được sử dụng cùng với React Transmit:

import React from 'react';
import Butter from 'buttercms'
import Transmit from 'react-transmit';

const butter = Butter('b60a008584313ed21803780bc9208557b3b49fbb');

var Hello = React.createClass({
  render: function() {
    if (this.props.posts) {
      return (
        <div>
          {this.props.posts.data.map((post) => {
            return (
              <div key={post.slug}>{post.title}</div>
            )
          })}
        </div>
      );
    } else {
      return <div>Loading...</div>;
    }
  }
});

export default Transmit.createContainer(Hello, {
  // Đây là những thiết lập cần có nếu không sẽ không thể render
  initialVariables: {},
  // Mỗi fragment sẽ được đưa vào một prop
  fragments: {
    posts() {
      return butter.post.list().then((resp) => resp.data);
    }
  }
});

Chúng ta đã bọc component trong một higher-order component hỗ trợ việc lấy dữ liệu bằng cách sử dụng Transmit.createContainer. Ta cũng đã loại bỏ các phương thức trong vòng đời khỏi các component React vì sẽ không cần thiết phải lấy dữ liệu đến hai lần. Và phương thức render cũng được đổi để dùng tham chiếu của props thay cho state, vì React Transmit sẽ chuyền dữ liệu cho component qua props.

Để đảm bảo rằng Server lấy dữ liệu trước khi render, ta sẽ import Transmit và sử dụng phương thức Transmit.renderToString thay cho ReactDOM.renderToString.

import express from 'express';
import fs from 'fs';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Hello from './Hello.js';
import Transmit from 'react-transmit';

function handleRender(req, res) {
  Transmit.renderToString(Hello).then(({reactString, reactData}) => {
    fs.readFile('./index.html', 'utf8', function (err, data) {
      if (err) throw err;

      const document = data.replace(/<div id="app"><\/div>/, `<div id="app">${reactString}</div>`);
      const output = Transmit.injectIntoMarkup(document, reactData, ['/build/client.js']);

      res.send(document);
    });
  });
}

const app = express();

// Phục vụ các file đã được tạo với middleware quản lý file tĩnh
app.use('/build', express.static(path.join(__dirname, 'build')));

// Phục vụ các request bằng hàm handleRender
app.get('*', handleRender);

// Chạy server
app.listen(3000);

Khởi động lại server và truy cập đến http://localhost:3000. Xem source của trang web và bạn sẽ thấy trang web giờ đây đã hoàn toàn được render ở phía server.

Kết luận

Sử dụng React ở phía server có thể rất khó khăn, đặc biệt là khi phải lấy dữ liệu từ các APIs. Thật may mắn khi cộng đồng React đang cố gắng và tạo ra rất nhiều công cụ hữu dụng. Nếu bạn cần các framework cho việc xây dựng một ứng dụng React lớn có thể render ở client và server, hãy thử Electrode của Walmart Labs hay Next.js. Hoặc nếu là một lập trình viên Ruby on Rails bạn có thể thử AirBnB’s Hypernova


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí