หัดสร้าง web app แบบ universal javascript ด้วย NextJS กัน

วันนี้จะมาหัดใช้ Next.js มันก็คือ framework เอาไว้ทำ server side render ของ react อืมมม งั้นๆแล้วทำไมต้องใช้มันด้วยอะ ก็ต้องพูดไปถึงว่า เมื่อเราสร้างเว็บแล้วแยก Front-end กับ Back-end ออกจากกันแล้ว จะเจอปัญหาสำคัญเลยคือการทำ SEO นั้นจะยากขึ้นและไม่ดีเท่าที่ควรจะเป็น ก็ส่งผลถึงการทำ Business เรื่องใหญ่นะนี่ ดังนั้นจะทำ SEO ดีๆยังไงก็ต้องให้ server มาช่วย render อยู่ดี แต่ๆผมก็ชอบเว็บที่มัน ไม่ต้อง Refresh หน้าอะ ดังนั้นเจ้า Next.js จะมาช่วยเราตรงทำนี้

ข้อดีของเว็บแบบ universal javascript

  • แก้ปัญหา SEO
  • UX (ผมรู้สึกใช้เว็บที่มันไม่ Refresh แล้วมันชื่นใจกว่านะ)
  • ใช้ความรู้แค่ภาษาเดียว

NOTE : สำหรับใครอยากรู้รายละเอียดแบบพื้นฐานการทำ universal javascript กันเลย ผมแนะนำบทความนี้ http://www.siamhtml.com/build-isomorphic-apps-with-react/

ความรู้พื้นฐานที่ควรเคยทำ

  • React (ปานกลาง)
  • NodeJs (พื้นฐาน)
  • ES6

ถ้างั้นก็โค้ดกันเถอะ เริ่มจากลง package กันก่อน

yarn init -y
yarn add next

สร้าง Folder ชื่อ pages

NOTE : ต้องใช้ folder ชื่อว่า pages เท่านั้นเนื่องจาก nextjs มันกำหนดไว้

สร้าง File index.js ข้างใน

- pages // folder
- index.js // file

ใส่โค้ด Hello World ลงใน index.js

// index.js
import React from 'react'
export default () => <div>Hello world!</div>

ตอนนี้หน้าตา project จะเป็นแบบนี้

ไปที่ไฟล์ package.json เพิ่ม script นี้ลงไป

{
.....
"license": "MIT",
"dependencies": {
"next": "^1.2.3"
},
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}

}

dev คือ เอาไว้ใช้ run server สำหรับไว้ dev

build คือ ตอนเราจะเอาขึ้น production ก็ build มันก่อน มันก็จะ minify อะไรพวกนี้ให้

start คือ เอาไว้ตอนเราจะ custom การ create server เอง

จากนั้นใช้คำสั่ง

npm run dev

ผลลัพธ์จะประมาณนี้ Nextjs จะใช้ port 3000 ไว้ run server

terminal

เปิด browser เข้า url http://localhost:3000/

ผลลัพธ์ Hello world ออกมาแล้ว

ก่อนจะเริ่มแบบจริงจัง ดูโค้ด Hello World นี้ก่อน

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

จะเห็นว่ามัน import module ได้ด้วยเลย ซึ่งก็เพราะว่า NextJs นั้นมันบอกว่า “on top of React, Webpack and Babel” แปลว่า เราสามารถเขียน React และใช้ Syntax ES6 ได้เลย แล้วมันก็จัดการเรื่อง module ให้เราเองเลยเรียกได้ว่า ช่วยจัดการสภาพแวดล้อมให้เราได้ระดับหนึ่งโดยไม่ต้อง Config อะไรเลย แถมยังติด Hot reload ให้เราด้วย แค่ save โค้ด web จะ Refresh ให้เองชีวิตสบายขึ้นไปอีกระดับ

ทีนี้กลับไปที่ไฟล์ index.js เปลี่ยนเป็นโค้ดนี้

import React from 'react'
import Head from 'next/head'
export default class extends React.Component {
render () {
return (
<div>
<Head>
<title>Learn NextJs</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.1/css/bulma.min.css" />
</Head>
<div className="columns">
<div className="column">
<h1>Home Page</h1>
</div>
</div>
</div>
);
}
}

ผลลัพธ์

เริ่มคุ้นๆไหมว่ามันคือการเขียน html ที่เราเคยเขียนมานั่นแหละ แค่เปลี่ยนนามสกุลไฟล์เป็น js แต่เขียนอยู่ในรูปแบบ React

ดูโค้ด import ข้างบน มีการ Import Head มาใช้ เอามาใช้แบบ Head ของ html ธรรมดา เจ้า Nextjs นี่จะมี Module หลักๆ อยู่ 3 ตัวที่มันจัดมาให้คือ css, head, link

  • head ก็อย่างที่เห็นเอามาทำเป็น head เหมือน html
  • link คือปุ่มกดไปหน้าอื่นนั่นแหละ แต่ๆ link ตัวนี้มันจะทำให้ไม่ Refresh หน้าเว็บด้วยนะ
  • css ก็มาช่วยเรื่องการสร้าง css ซึ่งผมขอเขียนเอาเองแล้วกัน css นี่

ต่อไปมาออกแบบเว็บให้มันเป็นรูปร่างซะหน่อย โดยเว็บที่ผมจะทำเป็นเว็บอ่านข่าวธรรมดา

เริ่มจากเราจะแยก Module ที่คิดไว้ว่ามันคงเอาไปใช้หลายหน้าแน่ๆ ออกมาเป็น Component ก่อน

ในที่นี้คือ Header กับ Navbar

เริ่มจากสร้าง folder components ไว้เก็บ Component ต่างๆ

- components // folder
- Nav.js // file
- Header.js // file

โค้ด Nav.js

// Nav.js
import Link from 'next/link'
const Navbar = () => {
return (
<div>
<nav className="nav has-shadow">
<div className="container">
<div className="nav-left">
<Link href='/'>
<a className="nav-item"> Brand </a>
</Link>
<Link href='/about'>
<a className="nav-item is-tab is-hidden-mobile"> About Me </a>
</Link>
</div>
<span className="nav-toggle">
<span></span>
<span></span>
<span></span>
</span>
</div>
</nav>
</div>
)
}
export default Navbar

ต่อไปโค้ด Header.js

// Header.js
import Head from 'next/head'
const Header = () => {
return (
<div>
<Head>
<title>Learn NextJs</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.1/css/bulma.min.css" />
</Head>
</div>
)
}
export default Header

หน้า Project จะได้ประมาณนี้

กลับมาหน้า index.js

เปลี่ยนเป็นโค้ดนี้ ใส่ Module Header กับ Navbar เข้าไป

// index.js
import React, { Component } from 'react'
import Navbar from '../components/Nav'
import Header from '../components/Header'
export default class Homepage extends Component {
render () {
return (
<div>
<Header/>
<Navbar/>
<div className="columns">
<div className="column">
<h1>Home Page</h1>
</div>
</div>
</div>
);
}
}

กลับไปหน้าเว็บหน้าตาก็จะเป็นแบบนี้ละ

ตอนนี้ถ้าเรากด ปุ่ม About บน Navbar ละก็ มันก็จะโชว์ error 404 ให้เรา แต่เราก็สามารถ Custom หน้า error 404 เองได้ โดยสิ่งที่ต้องทำคือ สร้างไฟล์ _error.js ใน folder pages

โค้ดไฟล์ _error.js

// error.js
import React, { Component } from 'react'
import Header from '../components/Header'
export default class Error extends Component {
static getInitialProps ({ res, xhr }) {
const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
return { statusCode }
}
render () {
return (
<div className="message is-info">
<Header/>
<div className="message-header">
<p>Error</p>
</div>
<div className="message-body">
<p>{
this.props.statusCode
? `An error ${this.props.statusCode} occurred on server`
: 'An error occurred on client'
}</p>
</div>
</div>
)
}
}

Note : ต้องใช้ไฟล์ชื่อ _error.js เท่านั้นนะครับ NextJs มันกำหนดไว้

หน้า error เราจะได้แบบนี้ -*- แบบเดิมยังดูดีกว่าอีก

งั้นเรามาเพิ่มหน้า about กันดีกว่า สร้างไฟล์ about.js ใน Folder pages

โค้ด about.js

// index.js
import React, { Component } from 'react'
import Navbar from '../components/Nav'
import Header from '../components/Header'
export default class Homepage extends Component {
render () {
return (
<div>
<Header/>
<Navbar/>
<div className="columns" style={{'padding': '36px'}}>
<div className="column is-narrow">
<div className="box" style={{'width': '200px'}}>
<img src="http://bulma.io/images/placeholders/1280x960.png" />
</div>
</div>
<div className="column">
<div className="box">
<h1 className="title">Title</h1>
<p className="subtitle">Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas facilisis eget eros consectetur rutrum. Curabitur luctus mauris nisi, eu laoreet velit accumsan ac.
Morbi vitae tortor enim. Integer blandit eros quam, in facilisis tortor pulvinar ut.
Nam sit amet velit a sapien vestibulum egestas. Mauris venenatis eros sed neque venenatis, quis pharetra augue dictum.
Aliquam vitae elementum erat, eget convallis nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;
Phasellus gravida sodales sollicitudin. Nunc vel velit a enim blandit venenatis id vitae tortor.
Nunc rhoncus aliquet augue. Ut accumsan, sapien vel scelerisque posuere, sapien eros tincidunt massa, et consequat lorem mi vel tellus.
Vestibulum augue diam, cursus vel risus a, rutrum tincidunt mi. Vivamus velit neque, tincidunt non enim sed, pretium condimentum dolor.
Sed elementum est sapien, ut pulvinar nulla eleifend in. Vivamus ut lacinia leo, eu semper urna.
</p>
</div>
</div>
</div>
</div>
);
}
}

ลองกลับมาเปิดหน้า About ดู

ต่อไปกลับมาหน้า Homepage ที่ Index.js ต่อ

เปลี่ยนเป็นโค้ดใหม่เป็นแบบนี้

// index.js
import React, { Component } from 'react'
import Navbar from '../components/Nav'
import Header from '../components/Header'
import Link from 'next/link'
export default class Homepage extends Component {
render () {
const paddingRounding = {
'padding': '7px'
}
return (
<div>
<Header/>
<Navbar/>
<div className="columns">
<div className="column">
<figure class="image">
<img src="https://image.shutterstock.com/z/stock-vector-white-welcome-sign-over-confetti-background-vector-holiday-illustration-302906972.jpg"
width="100%" style={{'height': '500px'}} />
</figure>
</div>
</div>
<div className="container">
<div className="columns">
<div className="column is-half is-offset-one-quarter">
<center>
<h1 className="title"> News </h1>
</center>
</div>
</div>
<div className="columns is-multiline">
<div className="column is-one-quarter" >
<div class="card" style={{'border': '1px #ddd solid'}}>
<div class="card-image">
<figure class="image is-4by3">
<img src="http://bulma.io/images/placeholders/1280x960.png" alt="Image" />
</figure>
</div>
<div class="card-content">
<div class="content" style={paddingRounding}>
<Link href='/new'>
<a className="nav-item is-tab title is-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Phasellus nec iaculis mauris.
</a>
</Link>
<br/>
<small>11:09 PM - 1 Jan 2016</small>
</div>
</div>
</div>
</div>
<div className="column is-one-quarter" >
<div class="card" style={{'border': '1px #ddd solid'}}>
<div class="card-image">
<figure class="image is-4by3">
<img src="http://bulma.io/images/placeholders/1280x960.png" alt="Image" />
</figure>
</div>
<div class="card-content">
<div class="content" style={paddingRounding}>
<Link href='/new'>
<a className="nav-item is-tab title is-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Phasellus nec iaculis mauris.
</a>
</Link>
<br/>
<small>11:09 PM - 1 Jan 2016</small>
</div>
</div>
</div>
</div>
<div className="column is-one-quarter" >
<div class="card" style={{'border': '1px #ddd solid'}}>
<div class="card-image">
<figure class="image is-4by3">
<img src="http://bulma.io/images/placeholders/1280x960.png" alt="Image" />
</figure>
</div>
<div class="card-content">
<div class="content" style={paddingRounding}>
<Link href='/new'>
<a className="nav-item is-tab title is-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Phasellus nec iaculis mauris.
</a>
</Link>
<br/>
<small>11:09 PM - 1 Jan 2016</small>
</div>
</div>
</div>
</div>
<div className="column is-one-quarter" >
<div class="card" style={{'border': '1px #ddd solid'}}>
<div class="card-image">
<figure class="image is-4by3">
<img src="http://bulma.io/images/placeholders/1280x960.png" alt="Image" />
</figure>
</div>
<div class="card-content">
<div class="content" style={paddingRounding}>
<Link href='/new'>
<a className="nav-item is-tab title is-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Phasellus nec iaculis mauris.
</a>
</Link>
<br/>
<small>11:09 PM - 1 Jan 2016</small>
</div>
</div>
</div>
</div>
<div className="column is-one-quarter" >
<div class="card" style={{'border': '1px #ddd solid'}}>
<div class="card-image">
<figure class="image is-4by3">
<img src="http://bulma.io/images/placeholders/1280x960.png" alt="Image" />
</figure>
</div>
<div class="card-content">
<div class="content" style={paddingRounding} >
<Link href='/new'>
<a className="nav-item is-tab title is-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</a>
</Link>
<br/>
<small>11:09 PM - 1 Jan 2016</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}

เปิดหน้าเว็บใหม่ จะได้หน้าตาแบบนี้

เราสามารถกด Title เพื่อไปหน้า new ได้แต่ จะ 404

ตอนนี้ข้อมูลเรายังกำหนดไว้เองอยู่ ต่อไปผมจะต่อข้อมูลมาจาก API ที่ https://jsonplaceholder.typicode.com/posts

เราจะได้ข้อมูลมา 100 ชุด จากนั้นที่ไฟล์ index.js เพิ่มโค้ดส่วนยิง API และ Render ใหม่

โค้ดไฟล์ index.js จะเป็นแบบนี้

// index.js
import React, { Component } from 'react'
import Navbar from '../components/Nav'
import Header from '../components/Header'
import Link from 'next/link'
import 'isomorphic-fetch'
export default class extends Component {
static async getInitialProps () {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await res.json()
return {
posts
}
}
render () {
return (
<div>
<Header/>
<Navbar/>
<div className="columns">
<div className="column">
<figure className="image">
<img src="https://image.shutterstock.com/z/stock-vector-white-welcome-sign-over-confetti-background-vector-holiday-illustration-302906972.jpg"
width="100%" style={{'height': '500px'}} />
</figure>
</div>
</div>
<div className="container">
<div className="columns">
<div className="column is-half is-offset-one-quarter">
<center>
<h1 className="title"> News </h1>
</center>
</div>
</div>
<div className="columns is-multiline">
{this.props.posts.map( (post) => {
return (
<div className="column is-one-quarter" >
<div className="card" style={{'border': '1px #ddd solid'}}>
<div className="card-image">
<figure className="image is-4by3">
<img src="http://bulma.io/images/placeholders/1280x960.png" alt="Image" />
</figure>
</div>
<div className="card-content" style={{ 'height': '160px' }}>
<div className="content">
<Link href='/new'>
<a className="title is-4">
{post.title}
</a>
</Link>
<br/>
<small>11:09 PM - 1 Jan 2016</small>
</div>
</div>
</div>
</div>
)
})
}
</div>
</div>
</div>
);
}
}

กลับมาดูหน้าเว็บใหม่ จะเห็นว่าได้ข้อมูลมาโชว์ 100 ชุดเลย

อธิบายโค้ดซักนิดหนึ่ง หลักๆ ก็โค้ดนี้

static async getInitialProps () {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const posts = await res.json()
return {
posts
}
}

ใน function getInitialProps (ต้องเป็นชื่อนี้เท่านั้น nextjs กำหนดมา ไว้โหลดค่าเริ่มต้นสำหรับ page) มันก็คือโค้ดที่ไว้ยิง API นั่นแหละ ซึ่งมันเป็นท่า Async Await ท่าใหม่ของ javascripts ใน ES7 และเราใช้ fetch ในการเรียก API ซึ่งได้มาจากการ

import ‘isomorphic-fetch’ (NextJS มีมาให้แล้ว import ได้เลย)

พอได้ข้อมูลมาแล้ว ที่เหลือก็เป็นการ Render โค้ดแบบ React Style JSX ปกติ

เสร็จแล้วสำคัญที่สุดในจุดประสงค์ที่เรามาใช้ NextJS กัน เนื่องจากตอนนี้สิ่งที่เราทำนั้นเป็นการทำ Server Rendering ดังนั้นแล้วถ้าผม View Page Source ที่เว็บผมจะต้องเห็นข้อมูลทั้งหมดของหน้าเว็บ เรามามาตรวจสอบกัน

Copy ข้อความไว้ จากนั้น กดคลิกขวา ที่เว็บ -> View page source แล้วลองมาหาข้อความที่เราเรียกมาจาก API มากัน

จะเห็นได้ว่าจะเจอด้วยๆ ซึ่งถ้าเป็น Front-end เรียก API มาปกติมันจะไม่เจอ

ทีนี้พอมันมีข้อความแบบนี้แล้ว มันก็จะทำให้ SEO ของเราดีขึ้นอีกมาก เพราะตัว Robot Crawler ของ Google มันก็มาเก็บข้อมูลได้ง่ายขึ้น

จบแล้วครับ สำหรับการพาทัวร์ไปกับ Next.JS แต่ถ้าหากใครอยากศึกษามันเพิ่มเติมก็ไปดูได้ที่ https://github.com/zeit/next.js

ส่วนนี้อันนี้โค้ดของโปรเจ็คนี้ https://github.com/kenshero/learn-nextjs

และถ้าใครยังอยากต่อ เดี๋ยวผมจะทำ Part 2 ต่อ ผมจะทำ Back-end เอง ใช้ NodeJs, MongoDB มี ADD , SHOW , และทำ Pagination เพิ่มอีกแค่นั้นแหละครับ