สอนทำเว็บไซต์ด้วย Node.js, Express และ MongoDB ตอนที่ 3 - พื้นฐาน Node.js

FREEคอร์สสอนสร้างเว็บไซต์ HTML/CSS สำหรับมือใหม่ + พร้อม Workshop


มาต่อกันที่ ตอนที่ 3 กันครับ

เนื้อหาบทเรียน

Callback

ก่อนจะไปถึง Node.js ขอพูดึงเรื่องของ Callback ก่อนนะครับ เนื่องจากว่าในการเขียน Node.js เราจะเห็นการใช้งาน callback บ่อยมากๆ และจริงๆแล้ว callback ไม่ได้มีแค่ใน Node.js ส่วนที่เป็น client บน Browser ก็จะเห็นเช่นกัน หรือ library ดังๆเมื่อก่อน เช่น jQuery หรือ event listener ใน Browser ก็ล้วนมีการใช้ callback ทั้งสิ้น

ใน Node.js ตัวมันถูกออกแบบมาให้ทำงานแบบ Asynchronus คือไม่จำเป็นต้องทำงานพร้อมกัน หรือรอให้คำสั่งนึงเสร็จก่อน ค่อยทำคำสั่งถัดไป แต่มันสามารถทำพร้อมกัน 2 คำสั่ง และไม่ต้องรอผลลัพธ์ เรียกได้ว่า เหมือนทำใครทำมัน คำสั่งแรก อาจจะได้ผลลัพธ์ทีหลังก็ได้

Callback คือ function ที่เราส่งไปให้กับอีก function (ปกติเราส่งแค่ค่าตัวแปร แต่ใน JavaScript ตัว function เป็น first class citizen สามารถส่ง function ผ่าน function ได้)

ตัวอย่าง Callback ง่ายๆ

const hello = name => console.log(`Hello ${name}`)

const myCallback = callback => callback('Devahoy')

// เรียกใช้งาน
myCallback(hello) // Hello Devahoy
  • อธิบายคือ ฟังค์ชั่น myCallback รับ parameter เป็น function และ ทำการ return function โดยทำการ call function และส่ง Devahoy ไปเป็น argument ของ function
  • พอ myCallback(hello) ถูก call ตัว myCallback ส่ง hello ฟังค์ชั่น เป็น argument พอเข้า function มันก็เลย return Hello Devahoy (มองว่า callback('Devahoy') ก็คือ hello('Devahoy')) ต่างกันแค่ชื่อ function เพราะเราจะตั้งเป็นอะไรก็ได้ เวลาเราตั้ง parameter.

ทีนี้ใน Node.js การใช้ callback จะเป็น style แนวๆ Error Convention คือ callback function ของ Node.js จะต้องเป็น function ที่มี 2 parameter ครับ คือตัวแรก คือ Error และตัวที่สอง คือ data ผลลัพธ์

ตัวอย่างเช่น

const hello = (error, name) => {
  if (error) {
    throw error
  }
  console.log(`Hello ${name}`)
}

const myCallback = callback => {
  if (!callback) {
    callback(new Error('No callback provided'))
  }
  callback(null, 'Devahoy')
}

Promise

อธิบาย Promise ซักนิด จริงๆแล้ว ไม่ใช่ Node.js แต่มันเป็นส่วนนึงของ JavaScript จริงๆแล้ว Promise ก็คือ Object ที่เอาไว้ทำงานพวก async ครับ ซึ่งเมื่อก่อนอาจจะมีแค่ return callback หลังๆ อาจจะเริ่มมีการใช้ Promise มากขึ้น ตัวอย่างเช่น การใช้ Promise ก็จะมีการ chain method แบบ then() และ catch() ครับ ซึ่งส่วนนี้ถ้าใครเคยเห็น หรือเคยเขียน Node.js มาบ้าง อาจจะคุ้นๆ เช่น

getSomeData('my request')
  .then(data => {
    console.log('data', data)
  })
  .catch(error => console.log('error', error))

// เทียบกับแบบ callback
getSomeData('my request', (error, data) => {
  if (error) console.log('error', error)
  console.log('data', data)
})

ซึ่งเดี๋ยวพอเป็น Promise ในบทความถัดๆไป ผมจะพยายามอธิบายเพิ่ม หรือมีตัวอย่างเพิ่มเติมเรื่อยๆครับ เผื่อใครที่ยังไม่ค่อยเข้าใจ เพราะเวลาเห็นการใช้จริงแล้ว ก็อาจจะเข้าใจมากขึ้น แล้วค่อยย้อนกลับมาลง Detail ครับ

File System

มาลองดูตัวอย่าง Node Modules ของ Node.js ที่ built-in มาตอนที่เราติดตั้งกันครับ จะได้มองเห็นภาพ และตัวอย่างการใช้งาน และเขียน Node.js มากขึ้น

ชื่อ module ว่า fs นะครับ เป็น การอ่าน เขียน ไฟล์ ด้วย Node.js

const fs = require('fs')

วิธีการใช้งาน ให้ทุกคนสร้างไฟล์ขึ้นมาใหม่เลย ตั้งชื่อว่า main.js จากนั้น import fs เข้ามาครับ

readFile - อ่านไฟล์

การอ่านไฟล์ ด้วยฟังค์ชั่น readFile(path, callback) argument ที่เราจะต้องส่งไปคือ ตัวแรกเป็น path ที่อยู่ไฟล์ของเรา และ 2 คือ callback function นั่นเอง แบบนี้

const fs = require('fs')

fs.readFile('data.txt', (error, data) => {
  console.log('data', data)
})

จากนั้นลองสร้างไฟล์ ขึ้นมา data.txt และใส่ข้อมูลลงไปใน Text จะใส่อะไรก็ได้ครับ เช่น

My data.txt
this is line 1

จากนั้นลองรันคำสั่ง node main.js และดูผลลัพธ์ครับ เป็น Buffer แบบนี้ครับ

data <Buffer 4d 79 20 64 61 74 61 2e 74 78 74 0a 74 68 69 73 20 69 73 20 6c 69 6e 65 20>

ถ้าเราอยากให้เป็น String เราต้อง pass options ให้มันครับ เป็นแบบนี้

fs.readFile('data.txt', 'utf-8', (error, data) => {
  console.log('data', data)
})

ซึ่งปกติ options จะเป็น Object แต่ถ้าเป็น string ตัว readFile มันจะเข้าใจว่าคือ encoding นั่นเอง ลองรันใหม่ซิ node main.js

ได้ผลลัพธ์แล้ว

My data.txt
this is line 1

ทีนี้อย่างที่บอก Node.js เป็น async เราลองเรียก readFile 2 ครั้ง แล้วมี console.log แบบนี้ดู

console.log('--START---')

fs.readFile('data.txt', 'utf-8', (error, data) => {
  console.log('data 1 >>>>', data)
})

fs.readFile('data.txt', 'utf-8', (error, data) => {
  console.log('data 2 >>>>', data)
})

console.log('--END--')

เมื่อเรารัน node main.js เราจะเห็นผลลัพธ์เป็นแบบนี้

--START---
--END--
data 2 >>>> My data.txt
this is line
data 1 >>>> My data.txt
this is line

จะเห็นได้ว่า start แล้ว end เลย ส่วนค่าที่ readFile มันถึงแสดงทีหลัง และก็ไม่การันตีว่า data 1 ต้องมาก่อน data 2 ใครเสร็จก่อน ก็ได้ผลลัพธ์ก่อน ลองรันหลายๆรอบดูก็ได้ครับ เพราะเนื่องมาจากการเป็น async callback เลยไม่ต้องรอผลลัพธ์นั่นเอง ซึ่งหลายๆคน ที่มาเขียน Node.js เข้าใจผิดว่า และเขียนคล้ายๆแบบนี้ครับ

console.log('--START---')

const data1 = fs.readFile('data.txt', 'utf-8', (error, data) => {})

const data2 = fs.readFile('data.txt', 'utf-8', (error, data) => {})

console.log('data1', data1)
console.log('data2', data2)

console.log('--END--')

มันจะได้เป็น undefined แบบนี้

--START---
data1 undefined
data2 undefined
--END--

ซึ่งถ้าหากใครเจอปัญหา undefined เวลาที่รับค่าจาก node.js หรือพวก function ต่างๆ ให้พึงนึกเสมอครับ ว่าเป็น async เราต้องใช้ callback ในการรับค่า หรืออาจจะอยู่ในรูปแบบ Promise ก็ได้ครับ

แต่ถ้าหากใครอยาก readFile แบบให้มันเป็น sync รอผลลัพธ์ก่อน ค่อยทำต่อ ก็ใช้ readFileSync ได้ครับ แบบนี้

console.log('--START---')

const data1 = fs.readFileSync('data.txt', 'utf-8')
const data2 = fs.readFileSync('data.txt', 'utf-8')

console.log('data1', data1)
console.log('data2', data2)

console.log('--END--')

แต่ข้อควรระวัง Node.js จุดขายของมันคือการทำงานแบบ Async ครับ หากเราใช้แต่ sync เพื่อรอผลลัพธ์ คิดดูเล่นๆ หาก 1 request เราต้องทำงานหลายๆ อย่าง และต้องรอคิวแรกเสร็จ คนที่สองค่อยทำงาน มันก็จะช้ามากๆ

writeFile - การเขียนไฟล์

การเขียนไฟล์ใน Node.js ใช้ writeFile(FILE, data, callback) แบบเดียวกันกับตอนอ่านไฟล์เลยครับ เช่น

fs.writeFile('hello.txt', 'Ahoy Node.js', 'utf8', (error, data) => {
  console.log('file saved')
})

เช่นเดียวกับ readFile ตัว writeFile ก็มีแบบ synchronous ครับ คือใช้ writeFileSync() นั่นเอง โดยไม่ต้องส่ง callback และไม่มีผลลัพธ์กลับมานะครับ

ตัวเขียนไฟล์ ทุกครั้งที่สั่ง node main.js มันจะเขียนไฟล์ทับลงไปในไฟล์เดิม และเซฟทุกครั้ง แต่ถ้าเราอยากเขียนไฟล์ โดยที่ไม่ต้องลบเนื้อหาเก่าละ คือเขียนเพิ่มลงไปในไฟล์ต่อกันเลย

เราจะใช้ appendFile() ครับ เหมือนกับ writeFile ทุกอย่างเลย ต่างกันที่ appendFile มันเขียนข้อมูลต่อกัน ขณะที่ writeFile จะเขียนใหม่ทุกครั้ง

fs.appendFile('hello2.txt', 'Ahoy Node.js\n', 'utf8', (error, data) => {
  console.log('file saved')
})

Module scope

นอกเหนือจาก Module แล้ว มีอีก 2 อันที่อยากจะแนะนำครับ คือ

  • __dirname เป็นตัวแปรเพื่อดึงค่า ตำแหน่ง folder หรือ directory ของเราครับ
  • __filename จะได้ชื่อไฟล์ที่โปรแกรมมันทำงานอยู่

เช่นลองเพิ่มลงไปที่ main.js ดูครับ

console.log('dir', __dirname)
console.log('file', __filename)
dir /Users/xxx/node-express-tutorial/
file /Users/xxx/node-express-tutorial/main.js

Path

ขอแนะนำอีก Module เอาไว้แก้ปัญหาครับ จะเห็นว่าตัวอย่างด้านบน การอ่านไฟล์ การเขียนไฟล์ จะไม่มีปัญหาเลย ถ้าเรารันคำสั่ง ในโฟลเดอร์เดียวกัน เช่น เราอยู่โฟลเดอร์ /my-app/node-tutorial และรัน node main.js ที่โฟลเดอร์นั้น มันก็จะอ่านไฟล์จากโฟลเดอร์ถูก แต่ถ้าเรา อยู่ที่โฟลเดอร์ my-app แล้วรัน node node-tutorial/main.js

มันก็จะอ่าน และเขียนไฟล์ จากโฟลเดอร์ my-app ไม่ใช่ my-app/node-tutorial แล้ว

วิธีแก้ไขปัญหานี้คือใช้ __dirname เพื่อบอกให้ Node รู้ว่า __dirname ของไฟล์ มันคือ path ไหน เพราะเรารัน node จากที่ไหนก็ได้ __dirname ค่าเดิม

สิ่งที่ path มีคือ path.join() ครับ คือเหมือน path มาต่อกันนั้นเอง เช่น

path.join('/foo', 'bar')
// ผลลัพธ์: '/foo/bar/'

เราก็เลยใช้ร่วมกับ __dirname เป็นแบบนี้

path.join(__dirname, 'myfile.txt')

สุดท้าย ลองแก้ appendFile ด้านบน โดยใช้ path ด้วย จะได้เป็นแบบนี้ครับ

const fs = require('fs')
const path = require('path')

fs.appendFile(
  path.join(__dirname, 'hello2.txt'),
  'Ahoy Node.js\n',
  'utf8',
  (error, data) => {
    console.log('file saved')
  }
)

HTTP

ต่อมาสุดท้ายของบทความนี้ละครับ คือ Module HTTP ที่ทำให้เราสามารถนำ Node.js มาเป็น Web Server ได้นั้นเอง ข้อดีของ HTTP คือทำตัวเป็น server พร้อมรับ request และส่ง response กลับไปที่ client ได้ครับ

ลองสร้างไฟล์ขึ้นมาใหม่ ชื่อ app.js

const http = require('http')

const hostname = '127.0.0.1'
const port = 3000

// ทำการสร้าง server โดยใช้ `http.createServer()`
// ซึ่ง createServer รับ listener function ที่มี request และ response
// ใช้ตัวย่อ เป็น req และ res (จริงๆจะใช้ชื่ออื่นก็ได้)
const server = http.createServer((req, res) => {
  res.statusCode = 200
  res.setHeader('Content-Type', 'text/plain')
  res.end('Hello World')
})

// server.listen() เพื่อระบุ port และ hostname ถ้าเราไม่กำหนด
// ตัว server ก็ยังไม่ start นั่นเอง
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`)
})

เมื่อเรามีโค๊ด ด้านบน ลองสั่ง node app.js แล้วเปิดหน้าเว็บ http://127.0.0.1:3000/ เราก็จะเห็นคำว่า Hello World นั่นเอง ที่เป็น String จริงๆ เพราะถูก set content-type เป็น text/plain

ทีนี้ลองแก้นิดหน่อย เปลี่ยนตรง createServer เป็นแบบนี้

const handler = (req, res) => {
  res.end(`
    <div>
      <h2 style="color: #ff3456;">Ahoy!</h2>
      <p style="color: #23dd55;">This is node.js tutorial</p>
    </div>
  `)
}

const server = http.createServer(handler)

เนื่องจากผมย้าย listener function ไปเป็น function ชื่อ handler เพื่อให้ดูง่ายๆ แล้วเวลา createServer() ค่อยส่ง function ไป จะได้ไม่ต้องเขียน function ซ้อนๆกันครับ มือใหม่หลายคนอาจจะงงได้

และตอนนี้ผมเอา setHeader ออกแล้ว เนื่องจากไม่ต้องให้เป็น text/plain เพราะไม่งั้นมันจะมอง html tag เป็น string (หรือจริงๆ เป็น content type text/html ก็ได้ครับ)

จะได้ผลลัพธ์เป็นแบบนี้ครับ เพิ่มสี และปรับ style ได้เหมือน html ปกติเลย (และเนื่องจากผมใช้ ES6 String literal ทำให้ผมสามารถทำ string หลายบรรทัดโดยใช้ backtick ``` ได้นั่นเอง อ่านง่ายกว่า string ยาวๆ)

Ahoy Node.js

ตอนนี้เราก็สามารถสร้าง Server ด้วย Node.js ง่ายๆ ได้แล้ว

ทีนี้ลองอีกแบบ เราจะส่ง string กลับไป แต่เราจะไม่มานั่งพิมพ์ในไฟล์เดียวกัน แต่เราจะอ่านจาไฟล์แทน ลองสร้างไฟล์ index.html ขึ้นมาไฟล์นึง

มีข้อมูลแบบนี้

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Ahoy</title>

    <style>
      .container {
        display: block;
        max-width: 840px;
        margin: 0 auto;
      }

      h2 {
        font-size: 6rem;
        color: #03484b;
      }

      .subheading {
        color: #222;
        font-size: 2rem;
      }

      p {
        color: #222;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h2>Ahoy!</h2>

      <p class="subheading">This is node.js tutorial by devahoy</p>

      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptas quia
        provident repellendus natus possimus, vitae consequuntur aliquam.
        Tempore neque libero eius molestiae. Vel impedit exercitationem magnam
        vero repudiandae eligendi a!
      </p>
    </div>
  </body>
</html>

จากนั้น เราก็ใช้ fs.readFile ครับ เพื่ออ่านข้อมูลจาไฟล์ index.html แล้วส่งกลับไป พวก response ด้วย res.end() เลยแบบนี้

const handler = (req, res) => {
  const filename = path.join(__dirname, 'index.html')
  const indexData = fs.readFileSync(filename)
  res.end(indexData)
}

หรืออีกวิธีคือ การอ่านไฟล์ด้วย fs.createReadStream ครับ จะอ่าน text ได้ไวกว่า readFile ซึ่งถ้าข้อมูลมากๆ เราจะเห็นผลชัดเจนครับ

const handler = (req, res) => {
  res.writeHeader(200, { 'Content-Type': 'text/html' })

  const filename = path.join(__dirname, 'index.html')
  const readSream = fs.createReadStream(filename, 'utf8')
  readSream.pipe(res)
}

Node.js HTTP Example

ตอนนี้เราก็สามารถทำเว็บด้วย Node.js แบบง่ายๆได้แล้วเนาะ ตอนนี้ก็พอมองเห็นภาพได้มากขึ้นแล้วใช่มั้ยครับ ในบทความถัดๆไป จะเริ่มไปส่วน Express ที่เป็น Web Framework มาช่วยให้เราทำเว็บได้ไวขึ้น และสะดวกง่ายขึ้นมากๆ

ซึ่งจริงๆแล้ว Node.js ยังมี built-in อื่นๆอีกเพียบเลยนะครับ แต่ตัวอย่างหรือบทความนี้ขอยกตัวอย่างคร่าวๆ ละกันเนาะ เพราะมันค่อนข้างใช้งานบ่อยเลย และ module อื่นๆ จริงๆแล้ว มันก็ไม่ยากเลยครับ อ่าน Document และลักษณะการใช้ก็เหมือนกับ fs เลย

สุดท้ายใครมีปัญหา ติดปัญหาตรงไหน สามารถสอบถามเพิ่มเติมได้ตลอดครับ แล้วพบกันบทความถัดไป ติดตามอ่านได้เลยครับ

อ่านต่อ ตอนที่ 4 - เริ่มต้นทำเว็บด้วย Node.js และ Express.js

ส่วน Source Code (อยู่ part3) สามารถเข้าไปดาวน์โหลด หรือ clone ผ่าน Github ได้เลย หากใครไม่รู้จัก Git สามารถอ่านบทความนี้เพิ่มได้ครับ Git คืออะไร ? + พร้อมสอนใช้งาน Git และ Github

Source Code

❤️ Happy Coding

ติดตามข่าวสารใหม่ๆจาก Devahoy

กด Like เพจ Devahoy

สมัครรับข่าวสารผ่านทาง Email

Chai Phonbopit: Software Engineer แห่งหนึ่ง • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript, React และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football นอกจากเขียนบล็อคที่เว็บนี้แล้ว ก็มีเขียนบล็อกแนว Lifestyle สบายๆ ที่ indyROVER.com ครับ

แสดงความคิดเห็น