Hướng dẫn xây dựng Universal Javascript App với Next.js (Fullstack React)

Meteor.js là Fullstack Javascript Framework tất cả trong một thì Next.js lại khá gọn nhẹ nhưng chứa đựng những thứ rất cần thiết cho việc xây dựng Universal Javascript App. Bài viết này sẽ hướng dẫn xây dựng Universal Javascript App với Next.js và phân tích một số điểm mạnh yếu của framework này.

Nội dung chính

Universal Javascript App là gì

Universal Javascript App là ứng dụng web được viết bằng javascript (trước đây còn được gọi là isomorphic), chạy được cả server và phía browser chung một mã nguồn. Tất nhiên có một số chỉ cần chạy phía browser như các hiệu ứng chuyển trang, chuyển động, hiệu ứng của các phần tử, hoặc có sử dụng local storage của browser, còn một số mã nguồn liên quan đến lớp xử lý (business logic) hoặc các hoạt động đặc thù của server như phiên làm việc (session), xác thực (authentication),…

Điểm đặc biệt của Universal Javascript App ở phía server thường là cấu trúc tách biệt hoàn toàn:

  • Với dữ liệu, ta sẽ dùng API chứ không kết nối trực tiếp đến database
  • Với phiên làm việc và xác thực cũng sẽ dùng server riêng chẳng hạn với giao thức OAuth hoặc JWT

Với kiến trúc này, mọi công việc sẽ được chia nhỏ và không bị ảnh hưởng nhiều khi một thành phần bị thay đổi (Microservices).

Next.js là gì

Next.js là một framework nhỏ gọn được xây dựng từ React, Babel và Wepack được tạo ra để giúp lập trình viên tạo ứng dụng web React có tính năng SSR (Server Side Render). Như các bạn đã biết với React.js, thì chúng ta đang xây dựng ứng dụng front-end với UI được quản lý bởi React, điểm yếu là chỉ client-side-render, nên việc SEO sẽ gặp khó khăn (có thể khắc phục bằng hệ thống ở server có khả năng xử lý javascript như Prerender.io), Next.js giúp chúng ta có tính năng SSR rất dễ dàng, ngoài ra còn có các tính năng đặc trưng (thừa kế Babel và Webpack) như:

  • Hot Module Replacement (HMR) – Webpack
  • Đóng gói bundle (có code split) và chuyển mã nguồn (Babel ví dụ từ ES6 -> ES5 cho trình duyệt web hiểu)
  • Tạo chỉ mục (indexing) cho các tập tin trong thư mục pages
  • Phục vụ static files, với Next.js thì không cần webserver khác (Có thể dùng Nginx làm reversed proxy)

Xây dựng ứng dụng web Universal Javascript App với Next.js

Mục tiêu

Đây là kết quả của chúng ta sẽ làm, liệt kê thẻ bài có pagination, sau lần tải đầu tiên do server render thì tất cả từ giờ các link đều chạy bằng ajax:

Thẻ bài pokemon

Cài đặt

Trên trang chủ của Next.js hướng dẫn sử dụng bằng npm, tuy nhiên như bài viết YARN sẽ thay NPM  quản lý thư viện Javascript đã nói thì YARN nhanh hơn npm nhiều, nên chúng ta sẽ sử dụng yarn để thiết lập dự án.

mkdir demo-nextjs
cd demo-nextjs
yarn init #điền các thông tin tùy bạn
yarn add next #thêm next.js vào dự án

Sau khi yarn tải và cài đặt các gói thư viện cần thiết, chúng ta cần thêm đoạn sau vào file package.json:

{
  //... # các dòng đã có thì giữ nguyên
  "scripts": {
    "dev": "next"
  }
}

Tập tin package.json sẽ như hình sau:

nội dung package.json

Tiếp theo sẽ tạo thư mục có tên là pages

mkdir pages

Tiếp tục tạo tập tin index.js trong thư mục pages, với nội dung như sau:

import React from 'react'
export default () => <div> Hello World!</div>

Một component rất đơn giản cho sự khởi đầu hoàn hảo:

yarn run dev

Bây giờ mở http://localhost:3000 bạn sẽ thấy “Hello world”

Điểm khác biệt với khi bạn sử dụng React.js thường với Next.js là SSR là mã nguồn được render từ server, do vậy, trong HTML trả về khi view-source sẽ có như sau:

...<body><div id="__next"><div data-reactroot="" data-reactid="1" data-react-checksum="1574179767"> Hello World!</div></div>...

Đây là điểm đặc biệt sẽ giúp cho SEO của bạn tốt hơn, mặc dù Google đã có thể chạy javascript trên trang web của bạn để index, nhưng các các hệ thống tìm kiếm khác như Bing, Yahoo thì không.

Trước khi tiếp tục, chúng ta sẽ tìm hiểu sơ qua cấu trúc thư mục bên trong dự án sử dụng Next.js

  • pages/index.js tương đương route là /
  • pages/about.js tương đương route /about
  • pages/account.js tương đương route /account

Cứ như vậy đơn giản là tạo file là bạn có thể tạo route, với cấp con thì sẽ tạo thư mục, ví dụ:

  • pages/account/manager.js tương đương /account/manager
  • pages/account/admin.js tương đương /account/admin

Khi bạn tạo thư mục account thì cần xóa tập tin account.js (mặc dù /account và /account/ là khác nhau, nhưng đây là quy định mặc định của Next.js)

Chú ý: khi bạn tạo thêm tập tin hay thư mục mới thì phải restart lại (Ctrl+C & yarn run dev)

Kết nối dữ liệu

Do đặc tính của Universal Javascript App là kể từ khi server đã render một lần, thì sau đó, toàn bộ đều thực thi dưới client, nên sẽ sử dụng API riêng biệt với app, sửa file index.js như sau:

import React from 'react';
import 'isomorphic-fetch'

export default class App extends React.Component {
    static async getInitialProps () {
        const res = await fetch('https://api.pokemontcg.io/v1/cards?pages=1&pageSize=10')
        const data = await res.json()
        return data;
    }
    render(){
        return (
            <div>
            {this.props.cards.map((card, i) => (
                <div key={i} style={{width: 200, float: 'left'}}>
                <div  style={{margin: 10}}>
                        <img src={card.imageUrl} style={{width: 160}}/>
                    </div>
                </div>
            ))}
            </div>
        )
    }
}

Chúng ta sẽ được kết quả như sau:

Trang liệt kê thẻ bài

Trong đó, hàm getInitialProps sẽ được thực hiện đầu tiên và lấy dữ liệu từ một API khác, thông thường nếu dùng theo cấu trúc database, bạn sẽ kết nối máy chủ dữ liệu và lấy dữ liệu ở bước này. Nhưng với Universal Javascript App, thì ở đây nên được dùng là API, để hình dung rõ hơn chúng ta sẽ đi tiếp và mình sẽ giải thích kỹ hơn.

Để giao diện đẹp hơn chút, mình dùng spectre.css: thêm component Head để gắn spectre.css, với component này thì Next.js sẽ tự động di chuyển phần trong <Head> vào thẻ head của html

import React from 'react';
import 'isomorphic-fetch'

import Head from 'next/head'
import Link from 'next/link'
export default class App extends React.Component {
    static async getInitialProps () {
        const res = await fetch('https://api.pokemontcg.io/v1/cards?pages=1&pageSize=12')
        const data = await res.json()
        return data;
    }
    render(){
        return (
            <div className="container">
            <Head>
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="stylesheet" href="https://unpkg.com/purecss@0.6.1/build/pure-min.css" />
            </Head>
            <div className="columns">
            {this.props.cards.map((card, i) => (
                <div className="column col-3" key={i}>
                <div  style={{margin: 10}}>
                    <Link href={`/cards?id=${card.id}`} >
                        <img src={card.imageUrl} className="img-responsive"/>
                    </Link>
                    </div>
                </div>
            ))}
                
            </div>
            </div>
        )
    }
}

Ở đây, mình thêm vào component Link cho thẻ bài, để tiếp tục phần tiếp theo là xem chi tiết thẻ bài.

Trang chi tiết

Tạo thư mục pages/cards và file index.js như sau:

import React from 'react'
import 'isomorphic-fetch'

import Head from 'next/head'
import Link from 'next/link'
export default class App extends React.Component {
    static async getInitialProps({query}) {
        const res = await fetch('https://api.pokemontcg.io/v1/cards/' + query.id)
        const data = await res.json()
        return data
    }
    render() {
        return (
            <div className="container">
                <Head>
                    <meta name="viewport" content="width=device-width, initial-scale=1"/>
                    <link
                        rel="stylesheet"
                        href="//cdn.bootcss.com/spectre.css/0.1.29/spectre.min.css"/>
                </Head>
                <ul className="breadcrumb">
                    <li className="breadcrumb-item">
                        <Link href="/">Home</Link>
                    </li>
                    <li className="breadcrumb-item">
                        {this.props.card.name}
                    </li>
                </ul>
                <div className="columns">
                    <div className="column col-4">
                        <p><img src={this.props.card.imageUrl}/></p>
                    </div>
                    <div className="column col-8 empty">
                        <div className="form-horizontal">
                            <div className="form-group">
                                <div className="col-3">
                                    <label className="form-label">Name</label>
                                </div>
                                <div className="col-9">
                                    <span>{this.props.card.name}</span>
                                </div>
                            </div>
                            <div className="form-group">
                                <div className="col-3">
                                    <label className="form-label">HP</label>
                                </div>
                                <div className="col-9">
                                    <span>{this.props.card.hp}</span>
                                </div>
                            </div>
                            <div className="form-group">
                                <div className="col-3">
                                    <label className="form-label">Series</label>
                                </div>
                                <div className="col-9">
                                    <span>{this.props.card.series}</span>
                                </div>
                            </div>
                            <div className="form-group">
                                <div className="col-3">
                                    <label className="form-label">Set</label>
                                </div>
                                <div className="col-9">
                                    <span>{this.props.card.set}</span>
                                </div>
                            </div>
                        </div>
                    </div>

                </div>
            </div>

        )
    }
}
Trang chi tiết

Điểm đặc biệt ở component này là chúng ta sử dụng query từ URL để lấy id, sau đó thực hiện fetch lấy dữ liệu như ở trang liệt kê. Ở đây mình có thêm cái breadcrumbs để quay về trang chủ, thì khi quay lại trang chủ, bạn sẽ thấy tốc độ nhanh như khi xem 1 thẻ bài vậy, vì component chỉ load API và render lại trang hoàn toàn ở client, và không liên quan gì đến server đã render trang trước đó.

Phân trang

Để trang chủ hoặc là trang liệt kê lá bài có thể phân trang, chúng ta sẽ sửa lại pages/index.js để nhận tham số page và thêm đoạn mã phân trang, để ví dụ được đơn giản thì mình để tạm 3 trang:

import React from 'react';
import 'isomorphic-fetch'

import Head from 'next/head'
import Link from 'next/link'
export default class App extends React.Component {
  static async getInitialProps({query}) {
    let page = 1
    if (query.page != undefined && parseInt(query.page)) {
      page = query.page
    }
        const res = await fetch(`https://api.pokemontcg.io/v1/cards?page=${page}&pageSize=12`)
        const data = await res.json()
        return data;
    }
  render() {
        return (
            <div className="container">
            <Head>
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="stylesheet" href="//cdn.bootcss.com/spectre.css/0.1.29/spectre.min.css" />
            </Head>
            <div className="columns">
            {this.props.cards.map((card, i) => (
                <div className="col-md-3" key={i}>
                <div  style={{margin: 10}}>
                    <Link href={`/cards?id=${card.id}`} >
                    
                    {<img src={card.imageUrl} className="img-responsive"/>}
                    </Link>
                    </div>
                </div>
            ))}
                
            </div>
            <div className="divider"></div>
            <div className="container">
              <div className="float-right">
              <ul className="pagination">
                  <li className="page-item">
                      <Link href={`/?page=1`} >1</Link>
                  </li>
                  <li className="page-item">
                      <Link href={`/?page=2`} >2</Link>
                  </li>
                  <li className="page-item">
                      <Link href={`/?page=3`} >3</Link>
                  </li>
                </ul>
              </div>  
              </div>  
            </div>
        )
    }
}

Như vậy là chúng ta đã hoàn thành xong ứng dụng web đơn giản, với mục tiêu là giới thiệu khái niệm kỹ thuật Universal Javascript App với Next.js

Fullstack Station’s Tips

  • Mặc dù khi sử dụng component Link, Next.js sẽ tạo ra mã html, nhưng nếu bạn sử dụng mã html trực tiếp hay vì dùng component Link, thì tính năng Universal sẽ mất đi, có nghĩa là browser sẽ tải lại trang mới được tạo ra từ server.
  • Sử dụng Next.js vẫn có khuyết điểm lớn về vấn đề router, vì router được sử dụng là của Next.js nhưng bạn lại không sử dụng React Router để thay thế được. Trong khi tính năng “params matching” lại là điều bất khả thi tại thời điểm này, ví dụ “/cards/:cardSlug” là không được (ảnh hưởng SEO nếu bạn làm website).

Code sample & Demo

https://github.com/virusvn/demo-nextjs

Demo: http://fullstackstation.com:3333/

Có nên sử dụng Next.js không?

Theo mình thì Next.js khá tốt, ngoại trừ vấn đề router sẽ được khắc phục trong thời gian tới thì Next.js phù hợp cho bạn muốn xây dựng ứng dụng nhanh, có tốc độ nhanh mà không phải mất nhiều thời gian để thiết lập, cấu hình và lập trình phía server. Khi sử dụng Next.js, thì bạn thỏa sức làm việc với React, tách biệt phần API mà vẫn có tính năng SSR – đây là ưu điểm rất lớn cho các ứng dụng Javascript.

Như đã nói ở phần giới thiệu Universal Javascript là gì ở trên, thì với kiến trúc microservices, nếu bạn tách được phần dữ liệu, xác thực ra ở một ứng dụng riêng, thì Next.js rất phù hợp, và dù sau này bạn không thích Next.js nữa, thì vẫn có thể tái sử dụng React components – chiếm phần lớn thời gian lập trình.

Cập nhật: Next.js đã có bản beta version 2, cho tùy chỉnh route và khả năng tích hợp với nhiều thứ khác.

Comments

Comments

3 bình luận cho “Hướng dẫn xây dựng Universal Javascript App với Next.js (Fullstack React)”

  1. Ảnh đại diện Ly Nam
    Ly Nam

    Chào bạn mình đang nghiên cứu và rất thích nextjs, rất mong được làm quen và học hỏi về nextjs
    facebook của mình: https://www.facebook.com/lynam.chibi

  2. Nguyễn Nhân

    5

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *