สร้างแอพแชตด้วย Node.js และ socket.io

สร้างแอพแชตด้วย Node.js และ socket.io Cover Image

Last Updated : 16 September 2017 : ปรับปรุงเนื้อหา และทำตัวอย่าง source code ใหม่ทั้งหมด

เนื่องจากบทความนี้เขียนไว้ค่อนข้างนานแล้ว ช่วงต้นปี 2015 และทุกวันนี้เทคโนโลยีเว็บก็ไปเร็วมากๆ ปัจจุบันมีทั้ง Firebase Database Realtime, Pusher, RethinkDB หรืออื่นๆมากมาย ที่ช่วยให้เรา implement เรื่อง Real time ทั้งใช้ socket.io, web socket อะไรก็แล้วแต่ และบทความนี้ก็ถือว่ามีคนเข้ามาอ่านกันพอสมควร ก็เลยถือโอกาสนี้ revision มันเล็กน้อย แต่ก็ยังคงเนื้อหาเดิมไว้นะครับ

บทความทำ App Chat ด้วย Node.js และ socket.io โดยบทความนี้ผมอ้างอิงจากต้นฉบับจากเว็บไซต์ของ socket.io ลิงค์นี้ครับ Get Started: Chat application มีการดัดแปลงนิดๆหน่อย โดยใช้ Plug template(Jade) และปรับแต่ง stylesheet โดยใช้ Bulma

ตัวอย่างแอพ Chat หลังจากเสร็จแล้ว

Demo App

Prerequisite

  • Node.js แนะนำเวอร์ชั่น v6 ขึ้นไป
  • socket.io v2
  • socket.io client v2
  • Express
  • Pug
  • Bulma (สำหรับ stylesheet)

และสำหรับความรู้ที่ต้องใช้คือ Node.js และ Express.js สำหรับใครที่ยังไม่รู้ แนะนำให้อ่านบทความด้านล่างครับ

Overview

ตัวโปรแกรมแชต จะถูกแบ่งออกเป็น 2 ส่วน คือ

  • ส่วน Server : ซึ่งจะถูกรันด้วย Node.js
  • ส่วน Client : จะรันผ่าน Browser ปกติ

ทีนี้ฟังค์ชันสำคัญๆของ socket.io จะมีด้วยกัน 2 ตัวคือ

  1. socket.on(event, callback) : ใช้สำหรับรับข้อความ
  2. socket.emit(event, message) : ใช้สำหรับส่งข้อความ

Step 1 : Create a project

เริ่มต้นสร้างโปรเจ็คด้วยคำสั่ง

npm init

จากนั้นทำการตั้งค่าโปรเจ็ค จะได้ไฟล์ package.json ประมาณนี้ (อันนี้ผมสร้างเอง เอาแค่ชื่อและเวอร์ชันเท่านั้น)

{
    "name": "simple-chat",
    "version": "2.0.0"
}

ติดตั้ง express, pug และ socket.io

npm install --save express pug socket.io

# หรือหากใช้ yarn ก็สั่ง
yarn add express pug socket.io

จะได้ไฟล์ package.json เป็นดังนี้

{
  "name": "simple-chat",
  "version": "2.0.0",
  "dependencies": {
    "express": "^4.15.4",
    "pug": "^2.0.0-rc.4",
    "socket.io": "^2.0.3"
  }
}

Step 2 : Create app.js

ทำการสร้างไฟล์ app.js ขึ้นมา ไฟล์นี้จะเป็นไฟล์หลักในส่วนของ Server

const express = require('express')
const app = express()
const path = require('path')

const APP_PORT = 5555

// // ตั้งค่า เพื่อให้ express ทำการ render view ที่โฟลเดอร์ views
// // และใช้ template engine เป็น pug
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'pug')

app.get('/', (req, res) => {
  res.render('index')
})

app.listen(APP_PORT, () => {
  console.log(`App running on port ${APP_PORT}`)
})

เนื่องจากว่าผมใช้ JavaScript แบบ Standard เลยไม่ได้ใส่ semicolon นะครับ

จากนั้นสร้างไฟล์ index.pug ไว้ที่โฟลเดอร์ views ดังนี้

h1 Hello World

ตอนนี้ไฟล์ทั้งโปรเจ็คจะมีดังนี้

├── app.js
├── node_modules
│   ├── express
│   ├── pug
│   └── socket.io
├── package.json
└── views
    └── index.pug

ทดสอบรันโปรแกรม แล้วเปิดบราวเซอร์ http://localhost:5555/ เพื่อดูว่าโปรแกรมทำงานโอเคหรือไม่ (ต้องแสดงคำว่า Hello World)

Step 3 : Update file index.pug

ต่อมาทำการแก้ไขไฟล์ index.pug โดยเพิ่มโค๊ดด้านล่างนี้ลงไป

doctype html
html
  head
    title Chat Application Example by DEVAHOY
    link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.2/css/bulma.css')
    link(rel='stylesheet', href='css/main.css')
    script(src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js')
    script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js')
  body
    #chat-app
      #chat.has-text-centered
        section.hero.is-success
          .hero-body
            .container
              h1.title Chat
              h2 Chat Application with Node.js + socket.io
        section.section.chat-container
          .container
            .columns
              .box.column.is-8.is-offset-2
                h2.title Chatbox
                .chat-messages.has-text-left
                  ul#chat-messages
                form
                  .field.has-addons
                    p.control
                      input(type='text', class='input', value='Chuck Norris', id='name')
                    p.control.is-expanded
                      input(type='text', class='input', placeholder='Try to say something', id='message')
                    p.control
                      input(type='submit', class='button is-success', value='Post')
        footer
          .container
            .content
              p
                | Powered by
                a(href='https://devahoy.com') DEVAHOY

จากนั้นสร้างไฟล์ main.css และเซฟไว้ที่โฟลเดอร์ใหม่ชื่อ public/css/ ดังนี้ (CSS ใครไม่ใส่ก็ได้นะครับ ผมเพิ่ม custom นิดเดียวเอง เพราะว่าใช้ Bulma ไป

body {
    font-family: Avenir, Helvetica, Arial, sans-serif;
}

form {
    margin-top: 12px;
}

#chat-messages li {
    padding: 12px;
}

เพิ่มโค๊ดนี้ลงไปที่ไฟล์ app.js ก่อน app.get('/') เพื่อให้ express ทำการลิงค์ไฟล์ public/css/main.css ได้ถูกต้อง

app.use(express.static('public'))

โครงสร้างโปรเจ็ค เมื่อถึงขั้นตอนนี้จะเป็นดังนี้

├── app.js
├── node_modules
│   ├── express
│   ├── pug
│   └── socket.io
├── package.json
├── public
│   └── css
│       └── main.css
└── views
    └── index.jade

ลองสั่งรันโปรแกรมใหม่

node app.js

Step 4 : Integrate Socket.io

ต่อมาเราจะทำการเชื่อมต่อกับ socket.io ทำการเพิ่ม module socket.io ที่ไฟล์ app.js (ต้องแยกระหว่าง socket.io ที่อยู่ฝั่ง server (node.js) เราจะใช้ตัวที่ติดตั้งผ่าน npm ส่วน socket ผั่ง client(browser) จะเห็นว่าผมใช้เรียกจาก cdn เอาเลย

const server = app.listen(APP_PORT, () => {
  console.log(`App running on port ${APP_PORT}`)
})

const io = require('socket.io').listen(server)

โดยให้ออปเจ็ค app.listen() เก็บไว้ที่ตัวแปร server เพื่อใช้สำหรับส่งเป็น argument ให้กับ socket.io

จากนั้นทำการเพิ่ม

io.on('connection', function(socket) {
    console.log('a user connected');
});

เพื่อให้ socket ทำการรับ event ที่ชื่อ connection

ตอนนี้ตัวไฟล์ app.js จะเป็นดังนี้

const express = require('express')
const app = express()
const path = require('path')

const APP_PORT = 5555

const server = app.listen(APP_PORT, () => {
  console.log(`App running on port ${APP_PORT}`)
})

const io = require('socket.io').listen(server)

// ตั้งค่า เพื่อให้ express ทำการ render view ที่โฟลเดอร์ views
// และใช้ template engine เป็น pug
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'pug')

app.use(express.static('public'))

app.get('/', (req, res) => {
  res.render('index')
})

io.on('connection', (socket) => {
  console.log('a user connected')
})

จากนั้นที่ไฟล์ index.pug ทำการเพิ่ม script ของ socket.io.js ฝั่ง client และทำโหลด socket.io ดังนี้

script(src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js')
script.
  const socket = io();

ตอนนี้ไฟล์ index.pug จะเป็นแบบนี้

doctype html
html
  head
    title Chat Application Example by DEVAHOY
    link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.2/css/bulma.css')
    link(rel='stylesheet', href='css/main.css')
    script(src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js')
  body
    #chat-app
      #chat.has-text-centered
        section.hero.is-success
          .hero-body
            .container
              h1.title Chat
              h2 Chat Application with Node.js + socket.io
        section.section.chat-container
          .container
            .columns
              .box.column.is-8.is-offset-2
                h2.title Chatbox
                .chat-messages.has-text-left
                  ul#chat-messages
                form
                  .field.has-addons
                    p.control
                      input(type='text', class='input', value='Chuck Norris', id='name')
                    p.control.is-expanded
                      input(type='text', class='input', placeholder='Try to say something', id='message')
                    p.control
                      input(type='submit', class='button is-success', value='Post')
        footer
          .container
            .content
              p
                | Powered by
                a(href='https://devahoy.com') DEVAHOY
      script.
        const socket = io();

io(); ที่ฝั่ง client โดย default แล้วคือการ connect ไปที่ server ฉะนั้นเราก็ไม่จำเป็นต้องระบุ event

ทดสอบรันโปรแกรม ทีนี้ลองเปิดบราวเซอร์ แล้วดูที่ console จะเห็นว่า มีขึ้นว่า a user connected ตามจำนวนครั้งที่เราเปิดบราวเซอร์

a user connected

Step 5 : ส่ง Event ไปที่ Server

จุดเด่นของ socket.io คือเราสามารถที่่จะส่งและรับข้อความได้ทุกๆ event ที่เราต้องการ สามารถส่งอะไรไปก็ได้ ไม่ว่าจะเป็น String, JSON หรือแม้แต่ binary data

ทีนี้ลองมาแก้ไขไฟล์ index.jade โดยการเพิ่ม socket.emit(event, message) ดูกันครับ พิมพ์ที่ส่วน script ล่างสุดของไฟล์

var socket = io();
socket.emit('chatter', 'Hello from client');

และไฟล์ app.js แก้ไขโดยให้ socket ทำการรับ event ที่ชื่อ chatter ชื่อเดียวกันกับฝั่ง client

io.on('connection', function(socket) {
    socket.on('chatter', function(message) {
        console.log('message : ' + message);
    });
});

จะเห็นว่าเราต้องทำการระบุชื่อของ event เป็นอะไรก็ได้ แต่ต้องให้เหมือนกับ กับฝั่ง client และ server เพื่อให้รับส่งข้อมูลกันได้ถูกต้อง ลองนึกถึงเวลาเราแชตกับเพื่อน คนนึงใช้ LINE คนนึงใช้ Facebook, event ไม่เหมือนกัน ข้อความก็ไปไม่ถึง :)

ทีนี้ที่ไฟล์ index.pug เราระบุข้อความไปโต้งๆเลย ซึ่งจริงๆแล้วเรามี form อยู่นี่นา ก็ให้ user ทำการพิมพ์ข้อความ แล้วกด Send ถึงส่งข้อความไปที่ server ก็ทำการเพิ่มโค๊ดนี้ลงไป (ใช้ jquery)

script.
  const socket = io();
  $('form').submit(function() {
    const name = $('#name').val();
    const message = $('#message').val();

    socket.emit('chatter', `${name} : ${message}`);
    $('#message').val('');
    return false;
  });

อธิบายคือ

  • เมื่อเราทำการ submit form ก็จะทำการอ่านค่า จาก input เก็บไว้ที่ตัวแปร name และ message
  • socket.emit('chatter', value) : คือการส่งค่าผ่าน socket ผ่านท่อชื่อ chatter โดยส่ง name และ message ไป
  • จากนั้นเคลียร์ค่า form input ซะ ถ้าส่งเสร็จแล้ว

Step 6 : Broadcasting

ทีนี้เมื่อ client สามารถที่จะส่ง message ผ่าน event ที่ระบุไว้ไปที่ server ได้แล้ว จะทำยังไงให้ server ส่ง event กลับมาที่ client ได้ (เราไม่ได้พูดถึง client เฉพาะที่ส่ง message ไปที่ server นะครับ แต่พูดถึง client ทุกๆ client เลย) นึกถึงกรณีที่คนนึงพิมพ์ข้อความ สมมติชื่อ client1

  1. ข้อความจะถูกส่งจาก client1 ไปที่ server
  2. จากนั้น server ก็จะส่งข้อความกลับไป ทุกๆ client โดยระบุชื่อ event ผ่านเมธอด io.emit('event', 'message')
  3. client อื่นๆ รวมถึง client1 ก็จะรับ message เดียวกัน ผ่าน event ด้วยการ implement method io.on('event', callback)

มาที่ไฟล์ app.js ทำการเพิ่ม io.emit() ลงไปในฟังค์ชัน socket.io แบบนี้

io.on('connection', (socket) => {
  socket.on('chatter', (message) => {
    console.log('message : ', message)
    io.emit('chatter', message)
  })
})

อธิบายคือ เมื่อทางฝั่ง server ได้รับข้อความจาก event chatter แล้ว ก็จะทำการส่งไปกลับไปให้กับทุกๆ client ผ่าน io.emit()

จากนั้นที่ไฟล์ index.pug ทางฝั่ง client ก็ทำการเพิ่มโค๊ดเพื่อทำการรอรับข้อความจาก server ดังนี้

script.
  socket.on('chatter', function(message) {
    $('#chat-messages').append($('<li>').text(message));
  });
  • socket.on('chatter', fn) : เมื่อได้รับข้อความจาก server ก็ทำการ append ค่าไปใส่ ช่อง ul#chat-messages นั่นเอง

ทดสอบรันโปรแกรม ก็จะได้ดังภาพ เป็นอันเรียบร้อย :)

Demo App

ดาวน์โหลดหรือดูตัวอย่าง source code ประกอบได้บน Github ครับ

Source Code

Reference

Chai Chai Phonbopit : Web Developer @Nimbl3 • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด