Reactjs - Bài 13 - server-side renderring

Goal

Trong các bài trước chúng ta chỉ sử dụng Reactjs ở phía Client, chúng ta có thể render HTML hoàn toàn ở phía Client. Trong bài này tôi sẽ giới thiệu cách ứng dụng Reactjs trong việc render HTML ở phía Server.

Client-side rendering

Trong các ví dụ trước, chúng ta render View hoàn toàn ở phía client nhờ vào method React.render(). Hãy xem hình minh họa bên dưới

Client-side-rendering

Để giúp các bạn hiểu rõ quá trình chuyển việc render từ phía client sang phía server cho nên tôi sẽ liệt kê các bộ phận cũng như từng bước của việc render ở phía Client như sau:

  1. File index.html: layout của view, chứa DOM để gắn ReactElement vào (trong các bài trước, tôi gắn React Element vào thẻ ).
  2. Include các file react.js, JSXTransformer.js,(ngoài ra còn có các file khác như superagent.js hay React-addon…)
  3. React.createClass() định nghĩa Virtual DOM
  4. React.createElement() tạo Virtual DOM
  5. React.render() render Virtual DOM vào thẻ
  6. Có một được một trang index.html hoàn chỉnh.

Source code mẫu cho Client-side rendering:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic Example Props</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/superagent/0.15.7/superagent.min.js"></script>
  <script type="text/jsx">

    var Avatar = React.createClass({
      propTypes: {
        id: React.PropTypes.string.isRequired,
        name: React.PropTypes.string.isRequired,
        width: React.PropTypes.number.isRequired,
        height: React.PropTypes.number.isRequired,
        initialLike: React.PropTypes.bool.isRequired,
        // Thêm Interface onDelete()
        onDelete: React.PropTypes.func.isRequired,
      },
      getInitialState() {
        return {
          liked: this.props.initialLike
        };
      },
      onClick() {
        this.setState({liked: !this.state.liked});
      },
      // Ủy quyền cho Component cha xử lý.
      _onDelete() {
        this.props.onDelete(this.props.id);
      },
      render() {
        var textLike = this.state.liked ? 'like' : 'haven\'t liked';
        return (
          <li key={this.props.id}>
            <span>{this.props.id}</span>
            <img  src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
            <span>{this.props.name}</span>
            <button onClick={this.onClick}>{textLike}</button>
            <button onClick={this._onDelete}>Delete</button>
          </li>
        );
    }
    });
    var request = window.superagent;
    var Avatars = React.createClass({

      getInitialState() {
        return {
          avatars: [
            {id: 1, name: "Avatar 1", height: 100, width: 100, like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
            {id: 2, name: "Avatar 2", height: 100, width: 100, like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
            {id: 3, name: "Avatar 3", height: 100, width: 100, like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
          ]
        }
      },
      // Thêm method deleteItem() set lại State (chứa các Component con) cho Component cha này
      deleteItem(id) {
        this.setState({
          avatars: this.state.avatars.filter(function(avatar){
            return avatar.id !== id;
          })
        });
      },
      componentDidMount: function() {
        var self = this;
        request.get('http://localhost:3000/api/employees', function(res) {
        console.log(res);
          self.setState({avatars: res.body});
        });
      },

      render() {  
        var avatars = this.state.avatars.map(function(avatar){
        // use below solution
        // map(function(){},this) 
        // or map(function(){}.bind(this)) 
        // or var that = this; onDelete = {that.deleteUser}
        // to pass this value to map function.
        // bind onDelete (event) to deleteUser.
          return <Avatar onDelete={this.deleteItem} id={avatar.id} name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src} initialLike={avatar.like} />;
        }, this);
        return (
          <ul>
            {avatars}
          </ul>
        );
      }
    });

    var AvatarsComponent = React.render(<Avatars />, document.body);
  </script>
</body>
</html>

Server-side rendering

Server-side rendering thực chất là việc tạo ra và trả về browser 1 trang index.html hoàn chỉnh.

Server-side-rendering

Để thực hiện điều đó chúng ta cũng cần các bước trên, nhưng sẽ có một số biến đổi tương đượng như sau:

  1. File index.html
    Chúng ta sẽ dùng ngôn ngữ template để gắn ReactElement vào vị trí DOM cần thiết (vì ở server thì chưa có Real DOM để chúng ta get và gắn vào). Ở đây tôi dùng ngôn ngữ template là jade, cho nên chúng ta cũng sẽ có file index.jade. File này cũng chứa layout của View

  2. Include các file react.js, JSXTransformer.js
    Tôi dùng nodejs, nên tôi sẽ dùng method require() để include các package tương ứng.

  3. React.createClass() không thay đổi

  4. React.createElement() không thay đổi
  5. React.render() ở phía client có 2 ý nghĩa là render Virtual DOM thành HTML (hay real DOM) và gắn vào thẻ . Điều đó, phía server sẽ thực hiện một cách tương tự như sau:

    • Dùng React.renderToString. (Vui lòng tham khảo method này để hiểu thêm) để trả về một HTML string (chứa nội dung HTML mà ta muốn render).
    • Truyền HTML string đó vào thông qua biến !{markup}. Jade engine sẽ render ra index.html hoàn chỉnh
  6. Trả về cho Browser trang index.html vừa tạo ra ở trên.

Server-side-rendering-static

Source code:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_rendering_static/server-rendering-static

Lưu ý: bạn hãy tự cài những module còn thiếu nhé, vì có những module tôi dùng như là global module nên khi viết source này tôi không có cài lại.

Sau khi bạn clone về, bạn start cái server bằng lệnh:

node server

và vào link:

http://localhost:5000

Boom! bạn đã có được một View giống như khi render ở phía client.
Tuy nhiên khi bạn thử click và button như like hay là delete thì chẳng có điều gì xảy ra. Tại sao vậy?

Tại vì thực ra server chỉ trả về cho Browser 1 file HTML tĩnh có “gắn” phần HTML vừa được React tạo ra vào thẻ <body> thôi chứ không có phần source code JS đi kèm theo. Điều này khác với việc render ở Client là bản thân souce code JS đã include sẵn. Cho nên bây giờ ta tiếp tục “trả” về cho client source code JS đó.

Để thực hiện điều này, tôi dùng browserify để “write code that uses require in the same way that you would use it in Node”. Cho nên chúng ta sẽ tận dụng cách viết ở phía Node Server cho phía Client. Browserify sẽ bundle up (tập hợp) tất cả các module cần thiết định nghĩa trong file mà chúng ta tạm gọi là browser.js thành file bundle.js và gửi cho Client.

Hình mình họa:

server-side rendering dynamic

Bây giờ, chúng ta sẽ tạo một file là browser.js để thông qua nó Browserify tạo file bundle.js:

'use strict';
var React   = require('react'),
    Avatar = require('./avatar');
React.render(<Avatar />, document.body);

Vì ở đây chúng ta dùng JSX syntax cho nên chúng ta cũng cần reactify để giúp Browserify transform JSX.

Bạn có thể thử command như bên dưới để thử bundle up file bundle.js từ file browser.js ở trên:

browserify -t reactify browser.js > bundle.js

Khi bạn mở file bundle.js, bạn sẽ thấy file bundle.js này đã gộp Reactjs và file avatar.js lại thành một.

OK, chúng ta đã có bundle.js giờ chỉ cần gửi file này về cho browser bằng cách thêm đoạn code sau vào bên dưới body trong file index.jade

    |     
    script(src='./bundle.js')

Và thêm dòng code sau app.use(Express.static(__dirname + '/')); vào bên dưới dòng app.set('view engine', 'Jade'); ở file server.js để báo cho express.static biết là nơi chứa các file static.

Bây giờ bạn reload lại page, thử click các button thử xem, It’s work!

Source code:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_rendering_dynamic/server-rendering

Lưu ý: bạn hãy tự cài những module còn thiếu nhé, vì có những module tôi dùng như là global module nên khi viết source này tôi không có install vào.

Việc sử dụng Javascript ở cả server và client người ta gọi là Isomorphic JavaScript (Javascript đồng hình)

!= Nodejs

  1. react-php-v8js
    https://github.com/reactjs/react-php-v8js

  2. react-rails
    https://github.com/reactjs/react-rails

  3. React.NET
    https://github.com/reactjs/React.NET

  4. react-python
    https://github.com/reactjs/react-python