SummonBenz logo
(Updated on )

สร้าง Realtime Chat แบบง่าย ๆ ด้วย Nuxt.js + Ably

Serverless แปลว่า 0 บาท ฟรีโว้ย วู้วๆๆ ><’’ //โดนเตะ แปลตรง ๆ ตัว คือ ไร้ server สรุปคือเราไม่จำเป็นต้องสร้าง server ขึ้นมาดูแลเลย ให้ cloud ของเจ้าของผู้ให้บริการจัดการเอง เหมาะกับการทดลองสร้างงานเล็ก ๆ คนใช้ไม่มาก แต่ถ้าใช้ปริมาณเยอะในระดับนึง ยังไงเราก็ต้องเสียเงินอยู่ดี) ในบทความนี้ขอเลือกเป็นตัว Vercel ครับ

แล้วทำไมเราไม่ใช้ socket.io ใน Vercel ล่ะ?

จริง ๆ แค่จะทำโปรแกรม Chat ใช้แค่ socket.io ก็เพียงพอแล้วนี่นา ทำไมถึงต้องสมัครเจ้า Ably ล่ะ?

ตามที่ Vercel ได้แจ้งมาเลย ว่าบริการ Serverless Functions ของเขา ไม่ support ตัว WebSockets แนะนำให้ใช้ตัว 3rd Party แทน จึงเป็นต้องหาตัว WebSocket ตัวอื่นที่รองรับ สุดท้ายก็เลยลงเอยมาใช้ตัว Ably แทน

แนะนำตัวละครของเราก่อน

Nuxt.js มันก็คือ Vue.js + Framework นั่นเอง ในเมื่อเค้าทำมาให้พร้อมใช้งานแบบง่าย ๆ แล้ว เราก็เอามาใช้ได้เลย

Vercel คือเจ้า Serverless ที่จะใช้ในวันนี้ ข้อดีของตัวนี้คือสามารถ deploy ได้ทั้ง Frontend และ Backend (หรือที่เรียกว่า Serverless Functions ของ Vercel นั่นแหละ) วันนี้เราจะใช้ทั้งสองอย่าง

Ably บริการ WebSocket ที่ทำให้คุณสามารถส่งข้อมูลเพื่อ Update ได้แบบ Realtime แล้วข้อมูลแบบไหนจำเป็นต้อง Realtime ? เช่น ข้อมูลราคาหุ้น, IoT, Chatroom ของพวกนี้ยิ่งอัปเดทได้ไวยิ่งดีเลย

สิ่งที่จำเป็นต้องมี

เปิดใช้งานบริการ Ably

  1. ก่อนอื่นเราจำเป็นต้องใช้ API Key ของบริการ Ably เมื่อ Login เข้าสู่ระบบแล้ว เมื่อมาถึงหน้า Dashboard กด “Create New App”
  2. ตั้งชื่อ Client ID ให้เรียบร้อย
  3. จะได้ Private API มาให้บันทึกเก็บไว้ก่อน เดี๋ยวต่อไปจะเอาไปใช้กัน

สร้างโปรเจกต์ใหม่ใน Nuxt.js

รันคำสั่งสร้างโปรเจกต์ Nuxt.js ก่อน (ตัวอย่างใช้ yarn ใครถนัด npm, npx เปลี่ยนได้ตามสะดวก)

yarn create nuxt-app <project-name>

ส่วน Rendering mode ต้องกำหนดเป็น Universal (SSR / SSG) เท่านั้นครับ เพราะเราต้องใช้ส่วน API ทำหลังบ้าน ที่เหลือก็สามารถเลือกได้ตามสะดวกครับ

ตัวอย่างการสร้างโปรเจกต์

? Project name: nuxtjs-chat-app
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? Continuous integration: None
? Version control system: Git

ติดตั้งเสร็จก็เข้าไปทดสอบเว็บก่อนเลย

cd <project-name>
yarn dev

จากนั้นลองเข้าไปที่ http://localhost:3000/ ถ้าเปิดได้ตามรูปถือว่าติดตั้ง Nuxt.js สำเร็จ

1 Bua Z4zab7w Dt Cvrq J2w K Cw

ติดตั้ง Ably SDK

จากนั้นยกเลิกการรัน yarn dev ไปก่อน คราวนี้จะมาติดตั้ง Ably กันต่อ

yarn add ably

จากนั้นให้ติดตั้ง express เพื่อให้สามารถรับส่งหลังบ้านผ่าน API ได้

yarn add express

ติดตั้งเสร็จแล้ว เราไปลุยสร้างห้องแชทกันเลย

เขียน API เพื่อดึง Token จาก Ably

สร้างไฟล์ใหม่ server-middleware/server.js และเพิ่มโค้ดตามด้านล่าง
(อย่าลืมแก้ <client-id-ably> ให้เป็นของตัวเอง)

ถ้าสังเกตบรรทัดที่ 9 จะเห็นว่ามีว่า process.env.ABLY_API_KEY เราจะมาใส่ค่า ABLY_API_KEY ที่ไฟล์ .env ให้สร้างไฟล์ .env แล้วนำรหัสคีย์ที่ได้จากสมัครตอนแรกมาใส่เลย

ABLY_API_KEY="xxxxxxxxxxxxxxxxxx"

เพิ่ม serverMiddleware ใน Nuxt.js

จากนั้นเราจะทำให้ Nuxt.js สามารถอ่านหลังบ้านได้ เพียงแค่ /api โดยการเพิ่ม serverMiddleware ที่ไฟล์ nuxt.config.js บรรทัดก่อนสุดท้าย ตามนี้

serverMiddleware: [{ path: '/api', handler: '~/server-middleware/server.js' }]

อย่าลืม!! ใส่ , ให้ที่ build ด้วยนะถ้ายังไม่มี เดี๋ยว syntax จะพังนะเออ

จากนั้นให้เข้าไปที่ http://localhost:3000/api/createTokenRequest ถ้าได้ผลลัพธ์ตามรูป ถือว่าทำสำเร็จไปแล้วครึ่งหนึ่ง (เฮ~)

1 a Twm Onj6 Gdx Gf Gl Dea7 Bg

ทำไมถึงต้องทำเป็น Token Request API?
เพราะว่า Vercel นั้น ไม่สามารถทำงานเป็น server side code ได้ แต่สามารถเขียน API เพื่อทำเป็น serverless functions ได้ จึงใช้วิธีนี้เพื่อ generate เอาค่า Token มาก่อน หลังจากนั้นก็โยน Token ให้ Ably เพื่อสามารถนำ Token นี้ไปประมวลผลต่อเองได้

แก้ไขหน้าแรก

จากนั้นเราจะเคลียร์หน้าตาเดิมออกก่อน เข้าไปแก้ไขที่ pages/index.vue แล้วใส่โค้ดดังกล่าวทับได้เลย

สามารถแก้ไขได้ตามใจ สิ่งที่สำคัญต้องมี <AblyChat /> อยู่ในโค้ด

สร้าง Component สำหรับ Chat

จากนั้นไปสร้างไฟล์ใหม่ที่ components/AblyChat.vue

เพื่อไม่ให้เป็นการเสียเวลา เราได้ทำหน้าตาแชทเบื้องต้นไว้แล้ว สามารถคัดลอกแล้วไปแปะได้เลย

1 Ic M47 X D Yi a No Sr F Abx6 Qถ้าลองรันดูจะพบว่า ได้หน้าตาดังนี้

มาเริ่มเขียน Logic ให้กับ component

ข้อดีของการแก้ components ก็คือ ทุกอย่างจะแก้ไขจบที่ไฟล์เดียว คราวนี้ให้แก้ไขที่ไฟล์ components/AblyChat.vue เพราะได้รวม Template, Script, Styling ไว้หมดแล้ว

เริ่มจากประกาศตัวแปรเพื่อให้เก็บข้อมูลที่อยู่ใน data() {….}

..
data () {
return {
msg: '',
receivedMessages: [],
channel: null,
connectionId: null
}
},
...
  • ตัวแปร msg สำหรับเก็บค่า textarea (เพราะเราได้เขียนไว้ก่อนหน้าแล้วว่า v-model="msg")
  • ตัวแปร receivedMessages สำหรับเก็บข้อความใน chat (บทความนี้จะขอเก็บตั้งแต่ช่วงเวลาที่ออนไลน์เท่านั้น ไม่มีการเก็บย้อนหลังเพื่อความง่าย) ซึ่งรูปแบบในการเก็บแต่ละตัวจะเป็น objects จะเก็บสองค่า ได้แก่ text เก็บข้อความ กับ type เก็บชนิด มี 2 ชนิด คือ me , other ตัวอย่างการเก็บอยู่ในรูปแบบดังนี้
receivedMessages: [
{ text: 'ข้อความจากตัวเองพิมพ์', type: 'me' },
{ text: 'ข้อความจากคนอื่นส่งเข้ามา', type: 'other' }
{ text: 'ทดสอบ1', type: 'me' }
]
  • ตัวแปร channel ใช้สำหรับเก็บค่า get.channel ของ Ably
  • ตัวแปร connectionId ใช้สำหรับเก็บค่า connectionId ของ Ably

เปลี่ยนข้อความตัวอย่างให้แสดงตาม receivedMessages

ให้ลบ span ที่อยู่ใน chat-area ทั้งหมด แล้วใส่คำสั่งนี้แทน

...
<div id="chat-area">
<span v-for="(item, key) in receivedMessages" :key="key" :class="['bubble', item.type]">
{{ item.text }}
</span>
</div>
...

ความหมายของส่วนนี้ก็คือโปรแกรมจะ loop ค่า receivedMessages แต่ละตัวใส่ในตัวแปร item
ชื่อ class จะแสดงชื่อ bubble แล้วตามด้วยชื่อ item.type ที่เก็บไว้ เช่น bubble me, bubble other
ข้อความที่เก็บไว้จะถูกแสดงใน {{ item.text }}

(การทำ v-for ของ Vue.js จะต้องกำหนด key ที่ค่าไม่ซ้ำกันเสมอ ในที่นี้จะเก็บเป็น index ของ array นั่นก็คือ key นั่นเอง)

ใส่ Event Click ให้กับปุ่ม SEND และ Enter ใน Textarea

หลังจากนั้นทำให้ปุ่มทำงาน sendMessage() เมื่อคลิกและเมื่อกด Enter ใน textarea ให้เพิ่มคำสั่งเฉพาะตัวหนาจากเดิมครับ

...
<div class="control">
<textarea v-model="msg" name="" rows="4" placeholder="Type Message Here..." @keydown.prevent.enter="sendMessage()" />
<button id="sendBtn" type="button" @click="sendMessage()">
SEND
</button>
</div>
...

ได้เวลาเขียนฟังก์ชัน sendMessage แล้ว

หลังจากจัดการหน้าบ้านแล้ว ถึงเวลาใส่คำสั่งสำหรับหลังบ้านแล้ว ให้ใส่คำสั่งนี้ใน methods ครับ

...
methods: {
sendMessage () {
this.receivedMessages.push({
text: this.msg,
type: 'me'
})
this.msg = ''
}
}
...

คำสั่งส่วนนี้จะ push array ของ receivedMessages นั่นเอง โดยส่ง text เป็นข้อความที่อยู่ใน textarea ส่วน type = ‘me’ นั่นเอง

หลังจาก push เสร็จ ให้ทำการเคลียร์ textarea โดยเพียงแค่ใส่ว่า this.msg = ‘’ ก่อนจบการทำงานเท่านั้นเอง

หลังจากนั้นก็ run local ทดสอบการทำงานได้เลย

เมื่อรันทำงานถูกต้อง ก็จะสามารถทำงานได้แบบนี้

เมื่อรันทำงานถูกต้อง ก็จะสามารถทำงานได้แบบนี้

เขียนคำสั่ง connection เพื่อเชื่อมต่อ Ably

เริ่มจากใส่คำสั่ง import library Ably ไว้ด้านล่าง <script> ก่อนเลย

...
<script>
import Ably from 'ably/promises'
export default {
...

จากนั้นใส่คำสั่งใน mounted() ตามดังนี้

...
mounted () {
const self = this
const ably = new Ably.Realtime.Promise({ authUrl: '/api/createTokenRequest' })
ably.connection.on('connected', function () {
self.connectionId = ably.connection.id
console.log('successful connection')
})
this.channel = ably.channels.get('chat-demo')
},
...

บรรทัดที่ 3 เราสร้างตัวแปรชื่อ self เก็บรับค่า this (เพราะบรรทัดที่ 5 ใส่คำสั่ง this ไม่ได้ จึงต้องสร้างตัวแปรมาใช้แทน)
บรรทัดที่ 4 คำสั่ง Ably.Realtime.Promise ทำหน้าที่นำ Token ที่ได้จาก API มาทำ connection นั่นเอง
บรรทัดที่ 5 คำสั่ง ably.connection.on(‘connected’) ทำหน้าที่ตรวจสอบว่าถ้าขึ้นสถานะ connected แปลว่าเชื่อมต่อเรียบร้อย ให้เก็บค่า connection id เก็บใส่ตัวแปร connectionId และให้ทำการ log ‘successful connection’ ออกมา (ถ้าไม่ขึ้นตามนี้ อาจจะเพราะ token / .env ใส่ค่าไม่ถูกต้อง ลองตรวจสอบกันดู)
บรรทัดที่ 6 คำสั่ง ably.channels.get เอาใช้เก็บพวก connection ที่อยู่ภายใต้ channel chat-demo ใส่ตัวแปร channel (เตรียมเอาไปใช้ต่อไป)

1 G80i E8w E  7 U8y S5fs V Pbg

ถ้าข้อมูลถูกต้อง จะปรากฏข้อความ successful connection แปลว่าพร้อมไปต่อได้

แก้ไข sendMessage ให้สร้างตัวส่งข้อมูลเข้า Ably (publish)

จากเดิมที่เราจะเพิ่มข้อความใส่ไปตัวแปร receivedMessages ไปตรง ๆ ให้ทำการลบทิ้ง เพราะว่าเราจะส่งข้อความไปให้ server Ably แทน

sendMessage () {- this.receivedMessages.push({
- text: this.msg,
- type: 'me'
- })
+ this.channel.publish('chat-message', { msg: this.msg })this.msg = ''
}

จากตัวอย่างนี้เมื่อพิมพ์ข้อความแล้วกด SEND ระบบจะส่งข้อความที่ event ที่ชื่อ chat-message โดยสิ่งที่ส่งไปด้วยได้แก่ msg ที่เก็บข้อความไว้

สร้างตัวรับข้อความจาก Ably (subscribe)

เมื่อมีส่งแล้ว ก็ต้องมีตัวรับข้อมูล เพิ่มเติมคำสั่งต่อไปนี้ใน mounted()

mounted () {
...
this.channel.subscribe('chat-message', function (message) {
self.callbackOnMessage(message)
})
}
...

บรรทัดที่ 4–5 คำสั่ง this.channel.subscribe ทำหน้าที่รับข้อความจาก Ably เมื่อ Ably ส่งข้อมูลมา event ชื่อ chat-message จะส่งต่อไปฟังก์ชัน callbackOnMessage ต่อไป

หมายเหตุ: ทั้งตัว publish และ subscribe จำเป็นต้องใช้ชื่อ event ว่า chat-message เหมือนกัน ถ้าอยากเปลี่ยนไปตั้งชื่ออื่น ก็อย่าลืมแก้ให้ครบทั้งสองที่นะ

เขียน callbackOnMessage เพื่อ render ข้อความแชท

ขั้นตอนสุดท้ายแล้วเขียนฟังก์ชันนี้เพิ่มใน methods: {…} เป็นการเพิ่มข้อมูลลง array และ scroll ไปยังข้อความล่างสุด

async callbackOnMessage (message) {
const author = message.connectionId === this.connectionId ? 'me' : 'other'
await this.receivedMessages.push({
text: message.data.msg,
type: author
})
const container = this.$el.querySelector('#chat-area')
container.scrollTop = container.scrollHeight
}

บรรทัดที่ 1 สาเหตุที่ใช้ async เพราะต้องการทำให้โปรแกรมทำงานแบบ asynchronous อ่านทีละบรรทัด

บรรทัดที่ 2 เอาไว้ตรวจว่าเป็นข้อความของตัวเองหรือไม่? เมื่อ callbackOnMessage ทำงาน จะตรวจว่า connectionId ที่ส่งเข้ามา ตรงกับ connectionId ของตัวเองหรือเปล่า ถ้าใช่ เก็บค่าเป็น me ไม่ใช่ก็เก็บค่าเป็น other

บรรทัดที่ 3 ให้ push array ตัว receivedMessages โดยแนบ text เป็น msg ที่ได้จาก Ably (message.data) และ type เป็นตัวแปร author ที่เก็บ เมื่อทำงานเสร็จแล้วจึงจะทำงานที่บรรทัดต่อไป

บรรทัดที่ 4 ให้สร้างตัวแปร container เก็บค่า div id ชื่อ chat-area

บรรทัดที่ 5 ให้ scroll ไปยังด้านล่างสุด

ทดสอบการทำงานระบบแชท

ก่อนจะเอาโค้ดขึ้น vercel ลองทดสอบในเครื่องตัวเองก่อนได้

โค้ดหลังแก้ไขเสร็จจะเป็นแบบนี้

ขั้นตอนสุดท้าย Deploy to Vercel !!

ก่อนจะเอาขึ้นเว็บ Vercel แนะนำให้สร้างไฟล์ vercel.json แล้ว คัดลอกคำสั่งนี้ลงไป

จากนั้นก็ Import Project นี้ไว้ใน Vercel ได้เลย พวก build and output settings ไม่ต้องกำหนดอะไรเพิ่มเติมเลย เพราะ Preset ของเค้าเขียนไว้ดีแล้ว (ขอไม่สอนแบบละเอียดนะครับ น่าจะใช้งานง่ายพอสมควรอยู่)

ส่วน Environment Variables ก็อย่าลืมใส่ ABLY_API_KEY และ Key ลงไป

ถ้ายังไม่ได้ตั้งตอนแรก! สามารถไปตั้งค่า ENV เพิ่มเติมได้ที่ Settings -> Environment Variables แล้วใส่ค่า Name, Value ลงไป แล้วกด Build อีกรอบได้เลย

อย่าลืม แก้ xxx ให้เป็น key ของตัวเองด้วยนะ

อย่าลืม แก้ xxx ให้เป็น key ของตัวเองด้วยนะ

เสร็จแล้ว!!! ลองไปเล่นดูได้ ถ้าอยากทดสอบทั้งสองฝั่ง เปิดลิงก์เดียวกัน 2 Tab ได้นะ

1 3 Fs Y Kg Wuf H6 Wj Y36k3 Udq

Demo Link : https://nuxt-js-chat-app.vercel.app/

ส่วนใครที่อยากได้ทั้งไฟล์ไปศึกษา ลองเข้ามาอ่าน github ได้นะ

GitHub Link

หากบทความนี้มีข้อผิดพลาดประการใดก็ขออภัยด้วย แล้วเจอกันบทความหน้าครับ :)

Subscribe to Ovidius Newsletter

One update per week. All the latest news directly in your inbox.