Author: Tanapoj Chaivanichanan

jQuery เป็นหนึ่งใน JavaScript Library ที่โด่งดังมาก (เมื่อ 10 ปีที่แล้ว) เรียกว่าในยุคนั้นแทบจะทุกเว็บจะต้องมีการติดตั้ง jQuery เอาไว้อย่างแน่นอน

แต่เมื่อยุคสมัยเปลี่ยนไป เบราเซอร์ใหม่ๆ ไม่มีปัญหาการรัน JavaScript ตั้งแต่ ES6 ขึ้นไปแล้ว การใช้งาน jQuery จึงลดน้อยลงเรื่อยๆ

แต่ก็มีบางโปรเจคเหมือนกัน ที่เราต้องเข้าไปแก้โค้ดเก่าๆ ซึ่งเขียนด้วย jQuery เอาไว้ ในบทความนี้จะมาเปรียบเทียบว่าทุกวันนี้เราสามารถแปลง jQuery ให้กลายเป็น Vanilla JavaScript (JavaScript เพียวๆ แบบไม่ต้องลง lib อะไรเพิ่ม เหมือนกับการกินไอติมรสเบสิกมากๆ แบบวนิลา) ได้เลยแทบจะทุกคำสั่งที่ใช้บ่อยๆ แล้วล่ะ

ในบทความนี้ก็เลยลองเอาคำสั่ง jQuery ที่ใช้บ่อยๆ มาลองเทียบดู ว่าถ้าอยากเขียนแบบนี้โดยไม่ใช้ jQuery เราจะต้องทำยังไง

Ajax

ในยุค jQuery กำลังรุ่งเรือง การเรียกข้อมูลจาก API ด้วย HTTP Request หรือที่ในยุคนั้นนิยมเรียกว่า Ajax ถ้าเขียนด้วย JavaScript แบบธรรมดาจะยากมาก

การที่ jQuery มีฟังก์ชันสำหรับเรียกใช้ Ajax ง่ายๆ ด้วยเป็นหนึ่งในเหตุผลที่ทำให้มันได้รับความนิยม ถึงขนาดว่าบางครั้งโหลด jQuery ติดเข้าไปเพราะต้องการใช้ฟังก์ชัน ajax() แค่นั้นเลยก็มีนะ

$.ajax({
  method: 'GET',
  url: '/api/data',
  data: {
    page: 1,
  },
  dataType: 'json',
  success: function(data){
    console.log('Success:', data)
  },
  error: function(error){
    console.error('Error:', error)
  },
})

แต่ข้อเสียของ jQuery.ajax() ก็คือตอนนี้ฝั่ง vanilla มีทั้ง fetch() หรือไลบรารี่อย่าง Axios ให้ใช้งาน ซึ่งทั้งสองวิธีนี้ถูกสร้างมาด้วย JavaScript ยุคใหม่โดยใช้ Promise แล้ว ไม่เหมือนกับ jQuery ที่ยังใช้สไตล์ callback function อยู่เลย

//fetch API
fetch('/api/data', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    page: 1,
  }),
})
.then(response => response.json())
.then(data => {
  console.log('Success:', data)
})
.catch((error) => {
  console.error('Error:', error)
})

//Axios
axios.get('/api/data', {
  params: {
    pafe: 1
  }
})
.then(function (response) {
  console.log('Success:', data)
})
.catch(function (error) {
  console.error('Error:', error)
})

Query Elements

First Match Element

หา element ตัวแรก

//jQuery
$('.ele').first()
$('.ele:first')

//Vanilla
document.querySelector('.box')

All Elements

หา element ทุกตัวที่ตรงกับ selector

//jQuery
$('.ele')

//Vanilla
document.querySelectorAll('.box')

สำหรับ jQuery แล้วการหา element แค่ 1 ตัวหรือหาทั้งหมด ผลที่ได้ไม่ต่างกันนั้นคืออ็อบเจคของ jQuery แต่สำหรับวนิลาแล้ว querySelector จะให้ค่ามาเป็น Element ส่วน querySelectorAll จะให้ค่าเป็น Array of Elements นะ

Nested Element

การหา element ภายใน element อีกทีหนึ่ง

//jQuery
let container = $('#container')
container.find('.box')

//Vanilla
let container = document.querySelector('.container')
container.querySelector('.box')

Action on Elements

สำหรับการสั่ง action อะไรบางอย่างกับ element ที่ได้มา ถ้าเลือกออกมาแค่ 1 element อันนี้ไม่ยาก

//jQuery
$(".box").first().doSomething()

//Vanilla
document.querySelector(".box").doSomething()

ปัญหาจะเกิดขึ้นเมื่อเรา select all กับ element ทุกตัว ซึ่งเรื่องนี้น่าจะง่ายกว่าถ้าเราใช้ jQuery เพราะการสั่ง action ของ jQuery นั้นจะวนลูปทำให้กับ element ทุกตัวอยู่แล้ว

//jQuery
$(".box").doSomething()

//Vanilla
document.querySelectorAll(".box").forEach(box => { 
    /* doSomething */ 
})

แต่สำหรับวนิลานั้น ค่าที่ได้ออกมาเป็น Array แปลว่าถ้าเราอยากสั่งให้มันทำอะไรให้ครบทุกตัว เราจะต้องวนลูปเอาเอง

เช่นสมมุติว่าเราอยาก hide ทุกอย่างที่มีคลาส box ถ้าเป็น jQuery ก็สั่งง่ายๆ เลยคือ .hide() แต่ถ้าเป็นวนิลาก็วนลูปยาวไป แล้วก็กำหนดที่ style ให้ display=none แทน

//jQuery
$(".box").hide()

//Vanilla
document.querySelectorAll(".box").forEach(box => { 
    box.style.display = "none" 
})

DOM Traversal

การวิ่งขึ้นๆ ลงๆ ใน DOM (โครงสร้างชั้นของ HTML ที่อยู่ในรูปแบบของ Tree) อันนี้ไม่ต่างกันมากเท่าไหร่ แค่คำสั่งของ jQuery จะสั้น/เขียนกระชับกว่านิดหน่อย

//jQuery
box.next()
box.prev()
box.parent()
box.children()
box.siblings()

//Vanilla
box.nextElementSibling
box.previousElementSibling
box.parentElement
box.children
box.parentNode.children.filter(el => el !== box) 

ตัวที่วนิลาไม่มีคือ siblings() ซึ่งเป็นคำสั่งเอาไว้เลือก Element ที่อยู่เลเวลเดียวกับเราทั้งหมด (แต่ไม่รวม Element ตัวเอง) ดังนั้นเลยต้องประยุกต์นิดหน่อยโดยวิ่งขึ้นไปที่ parent แล้วเลือก child ทุกตัวที่ไม่ใช่ตัวมันเองแทน

Event Handling

การกำหนด Event Listener แทบจะไม่ต่างกันแล้ว เพราะตอนหลังวนิลาก็คำสั่ง addEventListener ให้ใช้งานแล้ว

//jQuery
ele.click(function(event){})
ele.on('click', function(event){})
ele.on('mouseenter', function(event){})
ele.on('keyup', function(event){})
ele.off('click', function(event){})

//Vanilla
ele.addEventListener('click', (e) => {})
ele.addEventListener('mouseenter', (e) => {})
ele.addEventListener('keyup', (e) => {})
ele.removeEventListener('click', (e) => {})

Delegate

การดีลีเกตคือการกำหนด Event Listener ที่ Element ชั้นนอกแทนที่จะกำหนดที่ตัวมันเองตามปกติ

เช่นหากเราต้องการสร้าง Listener ที่ทำงานเมื่อกด <li> ในโครงสร้างแบบนี้

<ul>
    <li></li>
    <li></li>
    <li></li>
</ul>

เราก็ไปกำหนดให้ Listener อยู่ที่ <ul> แทน

สาเหตุที่ต้องทำแบบนี้มักจะเกิดกับกรณีที่ <li> นั้นอาจจะโดนสร้างแบบ dynamic โดย script ส่วนอื่น ทำให้อาจจะมี li เกิดขึ้นมาใหม่ได้เรื่อยๆ การไล่สั่งให้มัน Listener ทีละครั้งๆ ไปเป็นอะไรที่จัดการยากกมา เลยไปสั่งที่ ul แทน (แล้วพอเกิด Event ก็ให้ ul ส่งไปบอก li ภายในอีกที)

สำหรับวนิลาไม่มีคำสั่งให้ทำดีลีเกตได้เหมือน jQuery ดังนั้นก็ต้องประยุกต์เอาอีกแล้ว (ฮา)

//jQuery
container.on('click', '.box', function(event){
    //...
})

//Vanilla
container.addEventListener('click', function(event){
  if(event.target.matches('.box')){
      //...
  }
})

Create Event

อันนี้เป็นการสร้าง Event ขึ้นมาเอง .. ไม่ต่างกันมากนัก

//jQuery
ele.trigger('click')

//Vanilla
ele.dispatchEvent(new Event('click'))

Styling

การเซ็ต CSS ให้กับ Element

ของ jQuery จะใช้คำสั่ง .css() แต่วนิลาสามารถเซ็ตลงไปได้ผ่าน properties ที่ชื่อว่า .style

//jQuery
ele.css('color', '#000')
ele.css({
  'color': '#000',
  'background': '#FFF',
})

//Vanilla
ele.style.color = '#000'
ele.style.background = '#FFF'
ele.style.cssText = 'color: #000, background: #FFF'

บางครั้ง jQuery ก็มีฟังก์ชันที่จริงๆ เบื้องหลักก็ไปเซ็ตค่า css นั่นแหละแบบนี้ด้วยนะ

//jQuery
box.hide()
box.show()

//Vanilla
box.style.display = 'none'
box.style.display = 'block'

Document Ready

ในการรันสคริป บางทีเราต้องการจะให้หน้าเพจโหลดเสร็จซะก่อน สำหรับ jQuery นั้นเราใช้คำสั่ง ready() หรือส่งฟังก์ชันเข้าไปเลยก็ได้ โค้ดส่วนนี้จะทำงานเหมือน document ทั้งหมดโหลดเสร็จแล้ว

สำหรับ vanilla นั้นไม่มีคำสั่งสำเร็จขนาดนั้น แต่ถ้าอยากได้ก็เขียนได้ ... แต่เอาจริงๆ ส่วนใหญ่เราจะใช้วิธีเรียกฟังก์ชันที่ท้ายๆ ของหน้าเพจมากกว่า

//jQuery
$(document).ready(function(){ 
  /*...*/ 
})

$(function(){ 
  /*...*/ 
})

//Vanilla
(function(callback){
  if (document.readyState != "loading") callback()
  else document.addEventListener("DOMContentLoaded", callback)
})(function(){
   /*...*/
})

Class Properties

สำหรับสำหรับจัดการ class ซึ่งตอนนี้ฝั่ง vanilla ก็ทำได้ทั้งหมดเหมือน jQuery แล้วผ่าน properties ชื่อ classList

//jQuery
box.addClass('active focus')
box.removeClass('active')
box.toggleClass('active')
box.hasClass('active')

//Vanilla
box.classList.add('active', 'focus')
box.classList.remove('active')
box.classList.toggle('active')
box.classList.replace('active', 'blur')
box.classList.contains('active')

Create Virtual Element

Virtual Element เป็นการสร้าง Element HTML ขึ้นมาในฝั่ง JavaScript ซึ่งสามารถเอาไปปะให้แสดงผลใน document ได้ ซึ่งหลายๆ เฟรมเวิร์คตอนนี้ก็ใช้วิธีนี้แหละ ในการควบคุมการทำงานระหว่างสคริปกับ HTML

สำหรับ jQuery การสร้าง Virtual Element แค่เติม <> ครอบชื่อแท็กลงไปก็พอแล้ว ซึ่งเราจะได้ element ออกมา สามารถเอาไปใช้งานได้เลย

ทางฝั่ง vanilla ตอนนี้วิธีสร้าง Virtual Element ก็ไม่ยากล่ะ เพราะมีคำสั่ง createElement ให้ใช้

//jQuery
let div = $('<div>')
div.text('Hello World!')
div.html('Hello World!')

//Vanilla
let div = document.createElement('div')
div.textContent = 'Hello World!'
div.innerHTML = 'Hello World!'

DOM Manipulate

การจัดการ DOM อันนี้เราคิดว่าคำสั่งทางฝั่ง vanilla อ่านเข้าใจง่ายกว่า

คือแบ่งเป็น 2 เรื่อง การแก้ไข DOM ภายใน กับ ภายนอก (replace)

//jQuery
el.replaceWith('x')
el.html('x')

//Vanilla
el.outerHTML = 'x'
el.innserHTML = 'x'

ส่วนการเพิ่ม element เข้าไป ก็มี 2 คำสั่งคือเพิ่มต่อท้าย กับเพิ่มแทรกเข้าไปเป็นตัวแรก ซึ่งฝั่ง vanilla นั้นไม่มีคำสั่งเพิ่มแบบแทรกเข้าไปเป็นตัวแรก ต้องหา child ตัวแรกให้ได้ก่อน แล้วใช้ insertBefore ต่ออีกที

//jQuery
ul.append($('<li>'))
ul.prepend($('<li>'))

//Vanilla
ul.appendChild(document.createElement('li'))
ul.insertBefore(document.createElement('li'), parent.firstChild)

เรื่องการลบทิ้ง, การแทรกในตำแหน่งต่างๆ, และการ clone อันนี้ไม่ยากเท่าไหร่

//jQuery
el.remove()

//Vanilla
el.parentNode.removeChild(el)
//jQuery
$(target).after(element)
$(target).before(element)

//Vanilla
target.insertAdjacentElement('afterend', element)
target.insertAdjacentElement('beforebegin', element)
//jQuery
$(el).clone()

//Vanilla
el.cloneNode(true)

Attribute

เรื่องการจัดการ attribute ฝั่ง jQuery จะพิเศษหน่อยตรงมี data() ให้ใช้งานด้วย

//jQuery
el.prop('data-x')
el.attr('data-x')
el.data('x')

//Vanilla
el['data-x']
//jQuery
el.removeAttr('data-x')

//Vanilla
el.removeAttribute('data-x')

Effect

สำหรับ jQuery นั้นมีฟังก์ชันที่ทำให้เราโชว์/ซ่อน element แบบมีอนิเมชันได้ด้วย เช่น fadeOut, fadeIn, slideUp, slideDown (ซึ่งเบื้องหลังเป็นการสั่งเปลี่ยนค่าพวกความสูงหรือ opacity ด้วยลูปเอานะ)

สำหรับการเขียนแบบใหม่ ตอนนี้เราไม่นิยมให้ JavaScript เป็นคนจัดการอนิเมชันแล้ว แต่โยนไปเป็นหน้าที่ของ css แทน เช่นการใช้ transition

ฝั่ง JavaScript มีหน้าที่แค่ระบุคลาสของ css เข้าไปเท่านั้นพอ

//jQuery
$(el).fadeOut()

//Vanilla
el.classList.add('hide')
el.classList.remove('show')
/* With this CSS */
.show {
  opacity: 1;
}
.hide {
  opacity: 0;
  transition: opacity 400ms;
}

จริงๆ น่าจะยังมีอีกหลายคำสั่ง

Vue 3 เพิ่งเปิดตัวมาเมื่อเดือนที่แล้ว ซึ่งมาพร้อมกับฟีเจอร์ใหม่ๆ และสิ่งที่เปลี่ยนแปลงไป เรามาดูกันดีกว่า

เขียนใหม่ด้วย TypeScript

ภาษา JavaScript นั้นไม่มี Type ของตัวแปรทำให้เวลาเขียนโปรแกรมมีโอกาสเกิดข้อผิลพลาดเยอะ ดังนั้นการเขียนงานโปรเจคใหญ่ๆ คนเลยนิยมเปลี่ยนไปใช้ TypeScript แทน (ถ้ายังไม่รู้จัก TypeScript อ่านต่อได้ที่นี่)

สำหรับ Vue 3.0 นี้ก็เป็นการเขียนใหม่ด้วย TypeScript แทน แต่เวลาเราเอามาใช้งาน เราสามารถเลือกได้ว่าจะใช้แบบ JavaScript ตามปกติ หรือจะใช้แบบ TypeScript ก็ได้

interface Book {
  title: string
  author: string
  year: number
}

const Component = defineComponent({
  data() {
    return {
      book: {
        title: 'Vue 3 Guide',
        author: 'Vue Team',
        year: 2020
      } as Book
    }
  }
})

อ่านเรื่อง TypeScript ต่อได้ที่นี่

JSX

เป็นฟีเจอร์ที่เอามาจาก React นั้นคือแทนที่จะใช้ Hyperscript แบบนี้

Vue.h(
  Vue.resolveComponent('anchored-heading'),
  {
    level: 1
  },
  [Vue.h('span', 'Hello'), ' world!']
)

Vue.h ย่อมาจาก Hyperscript ซึ่งหมายถึง script ที่เอาไว้สร้าง HTML structures นั่นเอง

การเขียน JSX ได้จะทำให้เราเขียน HTML ลงไปในโค้ด JavaScript ตรงๆ ได้เลย

import AnchoredHeading from './AnchoredHeading.vue'

const app = createApp({
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

app.mount('#demo')

อ่านเรื่อง JSX ต่อได้ที่นี่

API ที่เปลี่ยนไปใน Vue 3

ในหัวข้อนี้ขอยกตัวอย่างโค้ดข้างล่างนี่ ซึ่งรันได้ปกติใน Vue 2

<div id="app">
    <h1>{{ title }}</h1>
    <my-button @whenclick="changeTitleText"></my-button>
</div>

<script src="https://unpkg.com/[email protected]"></script>
<script>
Vue.component('my-button', {
    template: `
        <button @click="handleClick">Click Me!</button>
    `,
    methods: {
        handleClick(){
            this.$emit('whenclick')
        }
    },
})

let app = new Vue({
    el: '#app',
    data: {
        title: 'นี่คือ Title แบบ Vue 2',
    },
    methods: {
        changeTitleText(){
            this.title = 'โค้ดนี้ทำงานได้ใน Vue 2'
        }
    },
})
</script>

See the Pen
Vue3-change-1
by Ta Tanapoj Chaivanichanan (@tanapoj)
on CodePen.

ในตัวอย่างนี้มีการสร้าง component ชื่อ my-button ขึ้นมาหนึ่งตัว แล้วตั้งค่าไว้ว่าถ้ามีการคลิก ให้เปลี่ยนข้อความ title ซะ

ทีนี้ ถ้าเราเปลี่ยนไปใช้ Vue 3 ตัวนี้แทน https://unpkg.com/[email protected] เราจะพบว่าโค้ดของเรารันไม่ได้ไปซะแล้ว

สาเหตุก็เพราะว่า API การเรียกใช้งานมีการเปลี่ยนแปลง

วิธี createApp

จากเดิมใน Vue 2 ถ้าเราจะสร้าง Application ขึ้นมา ก็จะใช้วิธีการ new Vue ขึ้นมาตรงๆ เลย

const app = new Vue({
    el: '#app',
    //TODO
})

แต่สำหรับ Vue 3 จะเปลี่ยนไปใช้คำสั่ง createApp แทน (ไม่สามารถสร้างเองได้แล้ว ต้องสร้างผ่าน factory function ที่ Vue เตรียมไว้ให้เท่านั้น)

const app = Vue.createApp({
    //TODO
})

app.mount('#app')

หรือ

import { createApp } from 'vue'

const app = createApp({
    //TODO
})

app.mount('#app')

อีกเรื่องหนึ่งคือการ mount แอพเข้ากับ html โดยใช้ el นั้นโดนตัดออกไปแล้ว

ไปใช้วิธีการ mount ด้วยเมธอด .mount() หลังจากสร้างแอพไปแล้วแทน ซึ่งเป็นการแยกส่วนโลจิคออกจากการกำหนดส่วนแสดงผล (HTML) ออกจากกัน ทำให้ตอนนี้เราสามารถสร้างแอพเปล่าๆ โดยยังไม่ต้อง mount ได้แล้ว ถ้าอยากเอาไปทำเทสเพิ่มก็ทำได้ง่ายขึ้น

data ต้องเป็น method เท่านั้น

ปกติ data ของ Vue 2 นั้นสามารถกำหนดเป็น object หรือ function ที่รีเทิร์น object กลับมาแบบไหนก็ได้

new Vue({
    data: {
        title: 'นี่คือ Title แบบ Vue 2',
    },
})

แต่สำหรับ Vue 3 ถูกกำหนดว่า data จะต้องเป็น function เท่านั้น

Vue.createApp({
    data(){
        return {
            title: 'นี่คือ Title แบบ Vue 2',
        }
    },
})

Component ผูกกับ app

ใน Vue 2 เวลาเราสร้างคอมโพเนนท์ เราจะสั่งผ่านตัวแปร Vue ที่เป็นระดับ global แปลว่าคอมโพเนนท์นี้สามารถเรียกใช้จาก Application ของ Vue ตัวไหนก็ได้

Vue.component('my-button', {
    template: `
        <button @click="handleClick">Click Me!</button>
    `,
    methods: {
        handleClick(){
            this.$emit('whenclick')
        }
    },
})

แต่สำหรับ Vue 3 แล้วการจะสร้างคอมโพเนนท์จะต้องสร้างให้แอพตัวใดตัวหนึ่งไปเลย

const app = Vue.createApp({
    //TODO
})

app.component('my-button', {
    emits: ['whenclick'], //optional
    template: `
        <button @click="handleClick">Click Me!</button>
    `,
    methods: {
        handleClick(){
            this.$emit('whenclick')
        }
    },
})

emits เป็น properties ที่จะกำหนด หรือไม่กำหนดก็ได้ หน้าที่ของมันคือเอาไว้เก็บชื่อเมธอดทั้งหมดที่คอมฑโพเนนท์ตัวนี้ทำได้ (มันก็คือ $emit อะไรได้บ้างนั่นแหละ) แต่การใส่ใน emits นั้นจะทำให้เวลาเรามาอ่านว่าคอมโพเนนท์ตัวนี้มีอะไรให้เรียกใช้ได้บ้างก็จะดูจากตรงนี้ได้เลย ไม่ต้องเสียเวลาไปไล่โค้ดข้างในอีกที

See the Pen
OJNGLEV
by Ta Tanapoj Chaivanichanan (@tanapoj)
on CodePen.

Vue Router ต้องสร้างด้วย createRouter()

เร้าเตอร์ก็เป็นอีกตัวที่มีการเปลี่ยนรูปแบบการเขียน

import Vue from 'vue'
import VueRouter from 'vue-router'

import App from './App.vue'
import FirstPage from './pages/FirstPage.vue'
import Page from './pages/FirstPage.vue'

Vue.use(VueRouter)

new Vue({
    render: (h) => h(App),
    router: new VueRouter({
        mode: 'history',
        routers: [
            { path: '/', component: FirstPage },
            { path: '/second', component: SecondPage },
        ],
    }),
}).$mount('#app')

โดยการสร้าง router จะต้องสร้างแยกตั้งหากด้วยคำสั่ง createRouter()

รวมถึงโหมด history ที่ตอนแรกเป็นค่าคอนฟิก, ใน Vue 3 ก็แยกออกมาเป็น createWebHistory() แล้วเหมือนกัน

import {createApp} from 'vue'
import {createRouter, createWebHistory} from 'vue-router'

import App from './App.vue'
import FirstPage from './pages/FirstPage.vue'
import Page from './pages/FirstPage.vue'

const router = createRouter({
    history: createWebHistory(),
    routers: [
        { path: '/', component: FirstPage },
        { path: '/second', component: SecondPage },
    ],
})

const app = createApp(App)
app.use(router)

router.isReady().then(() => {
    app.mount('#app')
})

Vuex ต้องสร้าง store ด้วย createStore()

คล้ายๆ กับข้อเมื่อกี้คือถ้าในโค้ดเรามีการใช้ Vuex การจะสร้าง store ขึ้นมาแทนที่จะ new Vuex.Store ตรงๆ ก็จะเปลี่ยนไปสร้างผ่าน createStore() แทน

import Vue from 'vue'
import Vuex from 'vuex'

import App from './App.vue'

const store = new Vuex.Store({
    //...
})

new Vue({
    store: store,
    render: (h) => h(App),
}).$mount('#app')

แบบนี้

import {createApp} from 'vue'
import {createStore} from 'vuex'

import App from './App.vue'

const store = createStore({
    //...
})

const app = createApp(App)
app.use(store)
app.mount('#app')

พวกค่าต่างๆ ของ store เช่น state, mutations, getters, actions ก็เซ็ตเหมือนเดิมทุกอย่าง ไม่มีอะไรเปลี่ยนแปลง

...

โดยรวมแล้ว Vue 3 มีการเปลี่ยน API ให้แยกส่วนกันมากขึ้น ทำให้โครงสร้างโค้ดถูกแบ่งเป็นโมดูลๆ จัดการและเทสง่ายขึ้น

Fragment

สำหรับใช้ที่เคยใช้ React มาก่อน Fragment ใน Vue ก็ใช้คอนเซ็ปเดียวกันนั่นแหละ

ตามปกติแล้วเวลาเราสร้าง template จำเป็นจะต้องมี root element 1 ตัวครอบทุกอย่างเอาไว้ ซึ่งหลายๆ ครั้งมันก็ไม่สะดวกเอาซะเลย

<!-- Layout.vue -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

สำหรับ Vue 3 นั้นอนุญาตให้เราสร้าง root element หลายตัวใน template ได้แล้ว

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

Teleport Component

teleport เป็นฟีเจอร์ที่ทำให้คอมโพเนนท์บางส่วนที่เราสร้างขึ้นมาใน Application สามารถ "เทเลฟอร์ท" หรือ "วาร์ป" ออกไปข้างนอกได้!

ตัวอย่างที่เข้าใจง่ายที่สุดน่าจะเป็น Modal หรือการที่มีกล่องเหรือเฟรมอะไรสักอย่างเด้งขึ้นมาซ้อนทับ content ของเราเป็นอีกเลเยอร์หนึ่ง

สำหรับ Vue เวอร์ชันก่อนๆ ที่ต้องเขียนทุกอย่างอยู่ในขอบเขตของ Application เท่านั้น

ถ้าเราจะสร้าง Modal ขึ้นมาก็จะต้องสร้างไว้ข้างในแอพนั้นแหละ

แต่สำหรับ Vue 3 เราสามารถกำหนดสิ่งที่เรียกว่า teleport เพื่อบอกว่าโค้ดตรงนี้นะ เราจะส่งมันออกไปแสดงผลข้างนอกแอพ!

จากเดิมเป็นโค้ดแบบนี้

const app = Vue.createApp({});

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal!
    </button>

    <div v-if="modalOpen" class="modal">
      <div>
        I'm a modal! 
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

ให้เราเพิ่ม <teleport to="body"> ลงไปครอบคอมโพเนนท์ส่วนที่ต้องการส่งออกไปข้างนอก (เช่นเคสนี้ส่งไปแสดงผลที่ชั้น body)

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

See the Pen
gOPNvjR
by Vue (@Vue)
on CodePen.


จริงๆ ยังมีอีกหลายฟีเจอร์ที่ Vue 3 มีการเพิ่มฝเปลี่ยนแปลง แต่อันนี้เลือกมาเฉพาะๆ ตัวที่คิดว่าสำคัญ

ป.ล. Vue 3 นั้นออกเวอร์ชัน 3.0.0 เรียบร้อยแล้ว แต่เฟรมเวิร์คหลัๆ บางตัวก็ยังไม่เสร็จดี เช่น router หรือ Vuex และสำหรับการติดตั้งจะต้องใช้ npm i [email protected] หากต้องการจะใช้เวอร์ชัน 3 (ถ้าใช้ npm i vue จะได้เวอร์ชัน 2) ซึ่งการเปลี่ยนแปลงให้ vue 3 เป็นตัวหลักแทนน่าจะดำเนินการเสร็จภายในสิ้นปี 2020 นี้

อ่านเพิ่มเติมได้ที่ Release Note นะ

Node.js กับ TypeScript

Node.js เป็นหนึ่งในเฟรมเวิร์คยอดนิยมสำหรับเขียนโปรแกรมฝั่ง Backend แต่เพราะมันสร้างมาตั้งแต่ปี 2009 ยุคที่ JavaScript ยังเป็นเวอร์ชัน ES5 อยู่เลย

โดยดีฟอลต์แล้ว Node.js เลยไม่ซัพพอร์ท TypeScript เลย ไม่เหมือนกับ Deno ที่เพิ่งสร้างขึ้นมาโดยซัพพอร์ท TypeScript เป็นค่า default ตั้งแต่แรก

เพื่อชีวิตที่ดีกว่า มาเซ็ตโปรเจค Node.js + Express ให้ใช้ TypeScript ได้กันเถอะ!

Step 1: ติดตั้ง TypeScript

สำหรับใครที่มี TypeScript อยู่แล้วข้ามสเต็ปนี้ไปได้เลยนะ

แต่ถ้าใครยังไม่มี ดูวิธีติดตั้งแบบง่ายๆ ได้ที่บทความ สอนวิธีเซ็ตโปรเจค TypeScript / มาใส่ไทป์ให้ JavaScript เพื่อลดความผิดพลาดในการเขียนโค้ดกันดีกว่า

Step 2: เริ่มสร้างโปรเจค

ปกติเราจะเขียนโค้ด Express หน้าตาประมาณนี้

const express = require('express')

const app = express()

app.get('/', (req, res) => {
    res.send('Hello World!')
})

app.listen(3000)

สมมุติว่าเซฟเป็นไฟล์ชื่อ app.js ละกัน วิธีที่จะเปลี่ยนมันเป็น TypeScript อย่างแรกเลยคือเปลี่ยนนามสกุลให้กลายเป็น app.ts

Step 3: @types

ปัญหาเมื่อเรานำโค้ด JavaScript ธรรมดามาเปลี่ยนเป็น TypeScript นั่นคือการที่ไทป์หายไป!

TypeScript Compiler หรือ tsc ไม่มีทางรู้ได้เลยว่าโค้ดที่เขียนๆ ขึ้นมาใน JavaScript แต่ละตัวมีไทป์เป็นอะไรบ้าง

ดังนั้นเลยต้องมีการ define ให้ TypeScript รู้จัก ซึ่งการทำแบบนั้นไม่ต้องถึงขั้นเขียนโค้ดใหม่ทั้งหมด แต่แค่เพิ่ม @types เข้าไปก็พอแล้ว!

ส่วนการติดตั้งก็ไม่ยาก มี npm ใช้ npm, มี yarn ใช้ yarn

//npm
npm i @types/express --save-dev

//yarn
yarn add @types/express --dev

ข้อสังเกตคือ library ยอดนิยมส่วนใหญ่จะมีการทำ types ไว้ให้อยู่แล้ว โดยชื่อของแพกเกจไทป์ส่วนใหญ่จะชื่อเดียวกับชื่อแพกเกจหลัก แต่เติม @types/ ไว้ข้างหน้า

จากนั้นเราก็กลับไปแก้โค้ดของเราอีกที

สำหรับ TypeScript นั้นจะต้องกำหนดไทป์ให้ตัวแปรทุกตัว เช่น Application, Request, Response ซึ่ง import มาจาก express ได้ตรงๆ เลย

import express, { Application, Request, Response } from 'express'

const app: Application = express()

app.get('/', (req: Request, res: Response) => {
    res.send('Hello World!')
})

app.listen(3000)

เวลาจะใช้งานก็ต้องคอมไพล์ให้ออกมาเป็น .js ซะก่อนนะ

Step 4: Watch File

จากสเต็ปที่แล้ว จะพบว่าระหว่างการ develop จะมีขั้นตอนเยอะมากในการรัน

  • ต้องคอมไพล์ TypeScript ด้วยคำสั่ง tsc ก่อน
  • รัน Node ด้วยคำสั่ง node
  • แล้วถ้ามีการแก้ไข้โค้ดอะไร ก็ต้อง kill process แล้วเริ่มคอมไพล์ตั้งแต่ TypeScript อีกรอบหนึ่งเลย
  • ซึ่งเป็นอะไรที่น่ารำคาญและทำให้ Productivity ต่ำมากๆ

ดังนั้นในสเต็ปสุดท้ายนี้เรามาดูวิธีการเซ็ตคำสั่ง ให้มัน watch file ให้เรากันดีกว่า

การ watch file หมายความว่า ให้ node คอยดูถ้าไฟล์ source code ของโปรแกรมมีการเปลี่ยนแปลง (เช่นมีการ save โค้ดตัวใหม่เข้ามา) ให้เริ่มรันโปรแกรมใหม่ด้วยตัวมันเอง เราจะได้ไม่ต้องมาคอยสั่งทุกครั้ง

ตามปกติถ้าเราเขียน Node.js ธรรมดา เราอาจจะใช้แพ็กเกจ เช่น nodemon

nodemon app.js

แต่ถ้าเราเขียนโค้ดในไฟล์ TypeScript อยู่ เราก็ต้องสั่งให้มัน watch file ด้วยอีกต่อหนึ่ง

tsc --watch

ซึ่งถ้าใช้วิธีนี้จะต้องเปิด Terminal ไว้ 2 อัน อันนึงรัน nodemon อีกอันรัน tsc นะ

หรือเราอาจจะใช้ ts-node แทนก็ได้ (อ่านวิธีใช้ ts-node และวิธีเซ็ต structure ของโปรเจคได้ที่ บทความสอนวิธีเซ็ตโปรเจค TypeScript)

nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec 'ts-node' src/app.ts

นั่นคือเราสั่งให้ nodemon คอยดูไฟล์ทั้งหมดในโฟลเดอร์ src ที่เป็นนามสกุล .ts ถ้าไฟล์มีการเปลี่ยนแปลง (มีการแก้ไขหรือมีการเซฟใหม่) ให้รันคำสั่ง ts-node ใหม่ทันที

หรือถ้าไม่อยากเขียน command ยาวๆ ก็ไปเขียน config ในไฟล์ nodemon.json แทนก็ได้

แบบนี้

{
  "watch": ["src"],
  "ext": "ts",
  "ignore": ["src/**/*.spec.ts"],
  "exec": "ts-node ./src/app.ts" 
  #or 
  "exec": "npx ts-node ./src/app.ts"
}

ใช้ ts-node-dev

หรือใช้อีกวิธีคือลงแพ็กเกจ ts-node-dev

# npm
yarn add ts-node-dev --dev

# yarn
npm i ts-node-dev --save-dev

แล้วก็รันด้วยคำสั่ง

ts-node-dev app.ts

หนึ่งในคำถามที่เว็บโปรแกรมเมอร์มือใหม่ถามกันเยอะมากจนน่าจัดเก็บไว้เป็น FAQ เลยก็คือ

"จะทำยังไงให้เราเรียกใช้ฟังก์ชันภาษา PHP จากสคริป JavaScript ได้"

เช่นแบบนี้...

<button onclick="<?php functionInPhp1(); ?>">
    คลิกฉันสิ!
</button>

หรือแบบนี้...

function functionInJs(){
    let x = <?php functionInPhp2(); ?>
}

คำตอบคือ ด้วยการทำงานของเว็บที่ทำงานบนโปรโตคอล http นั้น...มันทำไม่ได้!! (แต่มีวิธีแก้ ถ้าอยากทำจริงๆ อ่านต่อไป!)

จริงๆ แล้วไม่จำกัดว่าต้องเป็นภาษา PHP เท่านั้นนะ เคสนี้เป็นกับทุกภาษาฝั่งเซิร์ฟเวอร์เลย

เพื่อจะตอบว่าทำไมมันทำไม่ได้ เราจะต้องอธิบายการทำงานของ http กันซะก่อน

http และ Stateless

http เป็นโปรโตคอลมาตราฐานสำหรับการเขียนเว็บ ซึ่งการทำงานของมันสรุปง่ายๆ ได้ดังนี้

โปรโตคอล (Protocol) = กฎที่ตั้งขึ้นมาเพื่อให้คอมพิวเตอร์ในเน็ตเวิร์คสามารถคุยกันด้วยภาษาเดียวกัน ไม่งั้นมันจะคุยกันไม่รู้เรื่อง

  • Client ส่งคำขอหรือ request ไปหา Server
  • เมื่อ Server ได้รับ request แล้วก็จะรันสคริปที่เตรียมไว้ เพื่อหาว่าจะตอบอะไรกลับไปให้ Client, ในสเต็ปนี้ อาจจะมีการเชื่อมต่อเพื่อขอข้อมูลจาก database ด้วย
  • หลังจาก Server คิดเสร็จ ก็จะส่งคำตอบ (อยู่ในรูปแบบของ String) กลับไปให้ Client [มาถึงตรงนี้ถือว่า Server ทำงานเสร็จแล้ว!]
  • Client รับข้อมูลไป (ส่วนใหญ่จะอยู่ในรูปแบบ html) ก็เอาไปแสดงผล แต่ถ้ามีการแนบสคริปมาด้วย ก็จะจัดการรันสคริปตัวนั้นซะในสเต็ปนี้
  • จบจ้า!

สำหรับฝั่ง Client นั้นไม่ค่อยมีปัญหาเพราะมีแค่ภาษาเดียวเท่านั้นที่ครอบครองอยู่ นั่นคือ JavaScript แต่ฝั่ง Server นี่มีหลายภาษามากๆ เช่น PHP, Node.js. Go, .NET

แต่ไม่ว่าฝั่งเซิร์ฟเวอร์จะเขียนด้วยภาษาอะไร หน้าที่ของมันคือส่ง HTML, CSS, JS กลับมาให้ Client เพื่อโปรเซสต่อ

จะเห็นว่า http เป็นโปรโตคอลง่ายๆ ถามมา-ตอบกลับ แล้วก็จบงาน

ซึ่งเราเรียกการทำงานแบบนี้ว่า "Stateless" คือทำงานเป็นรอบ พอเรียกใช้งานและทำงานนั้นเสร็จแล้ว ก็จบการทำงาน (เว็บส่วนใหญ่ทำงานแบบนี้)

ตรงข้ามกับ "Stateful" ที่ตัวโปรแกรมรันค้างอยู่ตลอดเวลา (ให้นึกถึงแอพพลิเคชันมือถือหรือโปรแกรมที่รันใน Desktop)

Server ทำงานก่อน Client เสมอ!

เราอธิบายให้ฟังแล้วว่า http นั้นเริ่มทำงานที่ Server รันเพื่อโปรเซสข้อมูลที่จะส่งกลับมาให้ Client ก่อน ... แล้วพอ Client ได้รับข้อมูล (ซึ่งอาจจะมี JavaScript แนบติดมาให้ด้วย) ก็จะเอาสคริปตัวนั้นมารันต่อ

แสดงว่าในจังหวะที่ Client เริ่มทำงานนั้น ... โค้ดฝั่ง Server ทำงานเสร็จไปแล้ว !

นี่เลยเป็นเหตุผลว่าทำไมเราไม่สามารถทำให้ JavaScript เรียกให้ PHP ทำงานได้

เช่น

<?php
    function registerMeeting(){
        //ต้องการบันทึกว่าเข้าร่วมประชุมแล้ว
    }

    function getOnlines(){
        //ต้องการดึงรายชื่อคนที่กำลังออนไลน์อยู่ในขณะนี้
    }
?>

<button onclick="<?php registerMeeting(); ?>">
    ลงชื่อเข้าประชุม
</button>

<script>
    function getWhoOnlineNow(){
        let onlines = <?php getOnlines(); ?>
    }
</script>

ในโค้ดตัวอย่างนี้เรามีฟังก์ชันในภาษา PHP อยู่ 2 ตัว คือ registerMeeting() และ getOnlines()

จากนั้นเราไปเขียนโค้ด JavaScript เพื่อเรียกใช้งานฟังก์ชัน PHP ทั้งสองตัวนี้ต่อ

แต่ปัญหาก็คือ PHP เป็น Server-Side-Script มันทำงานก่อนเสมอ (ไม่ต้องรอให้ JavaScript เรียกหรอก ฉันทำงานเองตอนนี้เลย!)

แต่ด้วยโลจิคแล้ว เราต้องการให้ฟังก์ชันพวกนี้ทำงานเมื่อเรามี event เท่านั้น (เชื่อ ปุ่มโดน onclick หรือฟังก์ชัน JavaScript ทำงาน)

ดังนั้นโค้ดนี้เลยผิด!

Asynchronous คือทางแก้

หากต้องการให้ JavaScript เรียกให้ PHP ทำงานได้ มีอยู่วิธีเดียวนั้นคือใช้เทคนิคการเขียนโปรแกรมแบบ Asynchronous

หรือถ้าจะเรียกด้วยชื่อแบบดังเดิมคือเทคนิคที่เรียกว่า "Ajax" (Asynchronous JavaScript and XML) ... แต่ปัจจุบันไม่ค่อยเรียกชื่อนี้กันแล้วนะ และเราก็ไม่ได้ใช้แค่ XML เป็นตัวกลางส่งข้อมูลกันอีกตัวไป มีการใช้ JSON เพิ่มเข้ามาด้วย

ในบทความนี้จะไม่สอน Ajax แบบละเอียดนะ ถ้าอยากรู้ตามไปอ่านได้ที่ Ajax คืออะไร แล้วมันใช้ยังไง?

สำหรับ PHP ...

ให้แยกโค้ดส่วนที่ต้องการให้ JavaScript เรียกใช้งานได้ออกมาอยู่อีกไฟล์หนึ่ง เช่นกรณีนี้ตั้งชื่อไฟล์ว่า async-handler.php

<?php
//FILE: async-handler.php

function registerMeeting(){
    //ต้องการบันทึกว่าเข้าร่วมประชุมแล้ว
}

function getOnlines(){
    //ต้องการดึงรายชื่อคนที่กำลังออนไลน์อยู่ในขณะนี้
}

switch($_GET['action']){
    case 'register-meeting': 
        registerMeeting(); 
        break;
    case 'get-online-user': 
        getOnlines(); 
        break;
}

และเนื่องจากเรามี action ที่ต้องการให้ทำงานมากกว่า 1 ตัว ก็เลยใช้ query-string ชื่อว่า action เป็นตัวแยกว่า request ครั้งนี้ต้องการให้ฟังก์ชันไหนทำงาน (ตรงนี้ตั้งชื่อว่าอะไรก็ได้นะ แล้วแต่เลย)

<button onclick="registerMeeting()">
    ลงชื่อเข้าประชุม
</button>

<script>
    function registerMeeting(){
        fetch('/async-handler.php?action=register-meeting')
    }

    function getWhoOnlineNow(){
        fetch('/async-handler.php?action=get-online-user')
        .then(onlines => {
            ...
        })
    }
</script>

จากนั้น ในฝั่ง JavaScript ก็ใช้การ request กลับไปยังไฟล์ PHP async-handler.php ที่เตรียมไว้อีกรอบ จะใช้คำสั่ง fetch() หรือไลบรารี่ Axios หรือถ้าเก่าหน่อยก็ใช้ $.ajax() ของ jQuery ก็ตามสะดวกเลย

สรุป

การจะให้ JavaScript เรียกคำสั่ง PHP ตรงๆ นั้นทำไม่ได้ เพราะกว่า JavaScript จะเริ่มทำงาน PHP ก็ทำงานจนเสร็จไปก่อนแล้ว

แต่มีวิธีการแก้ทางคือสร้าง request ขึ้นมาอีกครั้งเพื่อเรียกไปหา Server ให้ปลุก PHP ขึ้นมาทำงานอีกรอบหนึ่ง

คือต้อง request 2 ครั้ง

  • ครั้งแรก - ให้ Server ส่ง HTML, CSS, JS มาให้ก่อน
  • ครั้งที่สอง - JavaScript สร้าง request อีกครั้ง เรียกไปยังไฟล์ PHP ที่เตรียมเป็น asynchronous handler เอาไว้

ภาษา JavaScript นั้นเป็นภาษาที่เริ่มเขียนได้ไม่ยาก ยิ่งถ้าใครเขียนภาษาสาย Structure/OOP เช่น C/C++, C#, Java มาก่อน อาจจะชอบในความเป็น Dynamic Type เราสร้างตัวแปรแล้วเก็บค่าอะไรก็ได้ ทำให้มีความคล่องตัวในการเขียนมากๆ ก่อนที่พอเขียนไปเรื่อยๆ เราจะพบความแปลกของมัน ยิ่งเขียนไปนานๆ เราก็พบว่ามันเป็นภาษาที่ทำให้เกิดบั๊กได้ง่ายมาก และเป็นหนึ่งในภาษาที่น่าปวดหัวที่สุดที่คนโหวตๆ กันเลย ด้วยเหตุผลแรกคือการที่ตัวแปรไม่มี Type นั่นเอง (อีกเหตุผลหนึ่งคือส่วนใหญ่ของคนที่เปลี่ยนมาเขียน JavaScript เคยชินกับภาษาแนว OOP มาก่อน พอมาเขียนภาษาแนว Functional เลยงงหน่อยๆ)

หลักการคือคอมไพล์ออกมาเป็น JavaScript

เอาจริงๆ ตัวภาษา TypeScript เอาไปรันอะไรไม่ได้เลย

แต่เราใช้วิธีการคอมไพล์ไฟล์ .ts ให้ออกมาเป็นไฟล์ .js อีกที หน้าที่ของมันมีแค่นั้นเอง

หรือบางกรณีถ้าเราเขียน React เราอาจจะรู้จักกับ .jsx สำหรับ TypeScript ก็มีไฟล์นามสกุล .tsx ให้ใช้เช่นกัน

Installation มาติดตั้ง TypeScript กัน

สำหรับ TypeScript Compiler นั้นสามารถติดตั้งได้ทั้งผ่าน npm หรือ yarn ด้วยคำสั่งประมาณนี้

npm install -g typescript

พอติดตั้งแล้วเราสามารถใช้คำสั่ง tsc สำหรับคอมไพล์ไฟล์ TypeScript ได้เลย

วิธีนี้เป็นการติดตั้งแบบ global คือติดตั้งครั้งเดียว ใช้ได้ทุกที่เลย แต่ถ้าอยากติดตั้งแต่ในโปรเจคๆ ไปก็สามารถทำได้ด้วย

npm install typescript --save-dev

หรือจะใช้งานผ่าน npx ก็ได้เช่นกัน

npx tsc

flag --save-dev หรือตัวย่อ -D ใส้ไว้เพื่อระบุว่าเราใช้แพ็กเกจนี้เฉพาะตอน develop เท่านั้น เมื่อไหร่ที่จะ build โปรเจคเอาไปใช้จริง ไม่ต้องเอาติดไปด้วยนะ เพราะอย่างที่บอกคือ TypeScript นั้นตัวมันไม่ได้เอาไปรัน แต่คอมไพล์เป็น JavaScript เมื่อไหร่ที่เขียนเสร็จแล้ว ก็ไม่ต้องใช้ต่อแล้ว

คอมไพล์ไฟล์ TypeScript

เริ่มแรกลองมาสร้างไฟล์ TypeScript กันดีกว่า

//app.ts

function sum(n: number): number {
    let sum: number = 0
    for(let i = 0; i < n; i++){
        sum += i
    }
    return sum
}

const total: number = sum(100)

console.log(total)

เขียนโค้ดนี้แล้วเซฟเป็นไฟล์ชื่อ app.ts ไว้ จะเห็นว่า syntax ทั้งหมดนั้นเหมือนกัน JavaScript ทุกอย่าง ที่เพิ่มเติมมาก็แค่หลังชื่อตัวแปร ตอนที่ประกาศตัวแปรและ parameter เราจะต้องกำหนดชนิดตัวแปรลงไปด้วย โดยใช้ตัว : ตามแพทเทิร์นนี้

variable-name: type

ไทป์ที่ใช้ได้ก็เช่น

let decimal: number = 6
let hex: number = 0xf00d
let binary: number = 0b1010
let octal: number = 0o744

let color: string = "blue"

let isDone: boolean = false

สำหรับตัวแปรประเภท Array นั้นประกาศได้ 2 แบบคือ

let list: number[] = [1, 2, 3]
let list: Array<number> = [1, 2, 3]

ส่วนตัวแปรแบบ Object นั้นจะต้องสร้าง interface เพื่อกำหนดไทป์ขึ้นมาก่อน

interface Point {
    x: number
    y: number
}

let myPoint: Point = { x: 10, y: 20 }

สำหรับ TypeScript นั้น เราไม่สามารถสร้างตัวแปรแบบไม่กำหนดไทป์ได้ แต่ถ้าเราไม่รู้ไทป์จริงๆ เราสามารถประกาศไทป์ any ได้ (แต่ก็ไม่แนะนำให้ทำนะ)


จากนั้นเราใช้คำสั่ง tsc ในการคอมไพล์ไฟล์ TypeScript ของเรา

tsc app.ts

หลังจากคอมไพล์เสร็จแล้ว เราจะได้อีกไฟล์เพิ่มมาในชื่อเดียวกันแต่เป็นนามสกุล .js แทน

เมื่อเราเอามาเปิดดูก็จะพบว่ามันมีหน้าตาคล้ายๆ กับโค้ด TypeScript ที่เราเขียนไป แต่ชนิดตัวแปรที่เคยประกาศจะหายไป และอาจจะมีอะไรอีกนิดหน่อยด้วย (เช่นมี ; เพิ่มเข้ามาให้ จากที่เคยใช้ let, const ก็เปลี่ยนเป็น var) ซึ่งโดยร่วมไม่ได้ทำให้โลจิคเปลี่ยนไปนะ

// app.js

function sum(n) {
    var sum = 0;
    for (var i = 0; i < n; i++) {
        sum += i;
    }
    return sum;
}
var total = sum(100);
console.log(total);

ซึ่งจุดนี้อาจจะทำให้คนที่เพิ่งเริ่มใช้ TypeScript สงสัยว่าถ้ามันแค่ตัด Type ทิ้งไป แล้วทำไมเราไม่เขียน JavaScript ตรงๆ ไปเลยล่ะ?

เช่นผลคือ TypeScript สามารถช่วยเราในส่วนของการเช็ก Compiler-Time Error ได้

เช่นโค้ดนี้

เราประกาศฟังก์ชันที่ต้องรับ number แต่เวลาเรียกใช้งาน ดันใส่ string เข้าไป ถ้าเป็น JavaScript จะสามารถนำไปรันได้ (แต่ผลออกมาจะเกิด Runtime-Error หรือมีโลจิคผิดพลาดตรงไหนจะไม่สามารถรู้ได้เลย)

แต่สำหรับ TypeScript นั้นจะแจ้งออกมาตั้งแต่ตอนสั่งคอมไพล์เลยว่าเรามีการเขียนผิดพลาดเกิดขึ้นนะ และ Editor ส่วนใหญ่ก็สามารถแสดงผลข้อผิดพลาดพวกนี้แบบ real time ได้อยู่แล้วด้วย ทำให้ไม่ต้องเสียเวลาพิมพ์ไปซะนาน แต่พอเอามารันเพิ่งจะรู้ว่าพิมพ์ผิดไป

เริ่มสร้างโปรเจคกันเถอะ

ในโปรเจคขนาดใหญ่ แน่นอนว่าเราไม่ได้มีแค่ไฟล์ sourcecode แค่ไฟล์สองไฟล์ แต่อาจจะมีเป็นร้อยๆ ไฟล์ การจะมานั่งคอมไพล์ TypeScript ทีละไฟล์นั้นทำไม่ได้แน่นอน (จริงๆ ก็ทำได้ แต่ใครจะทำนะ !)

การสร้างโปรเจคเลยเป็นทางเลือกที่ดีกว่า เช่นเราอาจจะสร้างโฟลเดอร์ /src สำหรับเอาไว้เก็บไฟล์ .ts ทั้งหมด และสร้างโฟลเดอร์ /dist หรือ distribute เอาไว้เก็บไฟล์ JavaScript

.
├── src
│   ├── app1.ts
│   ├── app2.ts
│   ├── app3.ts
│   └── ...
└── dist
    ├── app1.js
    ├── app2.js
    ├── app3.js
    └── ...

ซึ่งสำหรับ TypeScript เราสามารถใช้คำสั่งนี้

tsc --init

เพื่อกำหนดว่าโฟลเดอร์นั้นจะเป็นโปรเจค TypeScript ได้ หลังจากสั่งคำสั่งนั้นแล้ว เราจะได้ไฟล์ tsconfig.json มา ภายในไฟล์จะมี config สำหรับโปรเจคอยู่ หน้าตาประมาณนี้

รายละเอียดการเซ็ตค่า tsconfig สามารถดูได้ที่เว็บไซต์ของ TypeScript

ปกติการตั้งค่าหลักๆ ก็จะใช้ของที่ TypeScript สร้างมาให้ ยกเว้นบางตัวที่เราต้องเซ็ตเอง เช่น

{
  "compilerOptions": {

    // สั่งให้คอมไพล์ออกมาเป็น JavaScript ES6
    "target": "es6",

    // ชื่อโหลเดอร์ที่ output ไฟล์ JavaScript ที่คอมไพล์แล้ว
    "outDir": "./dist",

    // ชื่อโฟลเดอร์ sourcecode ไฟล์ TypeScript
    "rootDir": "./src",

    // หากใช้งานกับ React คือมีไฟล์ .tsx ให้คอมไพล์เป็น .jsx ด้วย
    "jsx": "react",

    // หากใช้กับ node
    "moduleResolution": "node",
  },

  // กำหนดขอบเขตของไฟล์ที่จะให้คอมไพล์
  // เช่น ทุกไฟล์ที่นามสกุล .ts ในโฟลเดอร์ไหนก็ได้ใต้ /src
  "include": [
      "src/**/*.ts"
  ],

  // กำหนดไฟล์และโฟลเดอร์ที่ไม่ต้องคอมไพล์ 
  // เช่นโฟลเดอร์ node_modules หรือไฟล์ spec
  "exclude": [
      "node_modules",
      "**/*.spec.ts"
  ]
}

จากนั้นเวลาเราจะคอมไพล์ ก็สั่งแค่

tsc

มันก็จะเป็นการคอมไพล์ทุกไฟล์ในทั้งโฟลเดอร์ /src/ ที่เรากำหนดไว้

แต่นี่ก็เป็นการคอมไพล์แค่ครั้งเดียว ถ้าเรามีการแก้ไขไฟล์ TypeScript เพิ่ม เราก็ต้องสั่งคอมไพล์อีกรอบ ถ้าไม่อยากจะมาสั่งคอมไพล์ทุกรอบที่แก้ไขโค้ด เราสามารถสั่งคอมไพล์แบบ watch ได้

tsc --watch

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

ในบทความนี้จะเล่าถึงที่มาที่ไปของ TypeScript อย่างเดียวนะ ส่วนเรื่องสอนว่าใช้งานยังไงได้บ้าง จะเขียนอีกทีในบล็อกต่อๆ ไป

สำหรับภาษาโปรแกรมทุกวันนี้ ถ้าแบ่งคร่าวๆ ด้วยชนิดของตัวแปร เราจะแบ่งได้ 2 อย่าง คือ Static Type: ต้องกำหนดชนิดตัวแปร เช่น int, string ตั้งแต่สร้างตัวแปร และ Dynamic Type ตัวแปรประเภทนี้ไม่ต้องบอกว่าเก็บค่าชนิดไหน เปลี่ยนไปได้เรื่อยๆ

สำหรับภาษาแบบ Dynamic Type ที่ไม่ต้องกำหนดชนิดตัวแปรให้แน่นอน จะเซ็ตค่าเป็นอะไรก็ได้นั้นอาจจะทำให้เขียนง่าย แถมบางภาษาเช่น Python ไม่ต้องประกาศตัวแปรก่อนใช้ด้วยซ้ำ อยากใช้ก็ใช้เลย

Static Type

int x;
x = 10;
x = "str"; //Type Error!

Dynamic Type

x = 10
x = "str" #Ok!

แต่สำหรับระยะยาวและโปรเจคขนาดใหญ่ที่มีความซับซ้อนสูงมากๆ การที่ไม่ต้องประกาศ/กำหนดชนิดตัวแปรทำให้เกิดปัญหาตามมามากมาย ยิ่งถ้าต้องทำงานเป็นทีมร่วมกันแล้ว ถ้าเราไม่รู้ว่าตัวแปรตัวนี้เป็นชนิดอะไร จะทำให้เสียเวลาไล่โค้ดนานมาก

ลองดูตัวอย่างต่อไปนี้

def sum_product_price(data):
  total = 0
  for product in data['products']:
    total += product.price
  return total

จะเห็นว่าการที่ตัวแปรสร้างเป็นไทป์อะไรก็ได้ทำให้เวลาอ่าน/แก้ไขโค้ดทำได้ยากมาก เพราะไม่รู้ structure ของตัวแปร เช่น data ที่เราต้องมาจำเองว่ามันมีฟิลด์ชื่อ products และเป็นอ็อบเจคที่มี .price

โค้ดที่เขียนเองอาจจะไม่เท่าไหร่ แต่ถ้าเป็นโค้ดที่เขียนต่อจากคนอื่น อันนี้มีงงแน่นอน

ช่วงหลังๆ มีหลายภาษาที่เริ่มต้นจากการเป็นภาษา Dynamic Type แต่ก็ต้องเพิ่มฟีเจอร์ให้กำหนดตัวแปรได้มาภายหลัง เช่น PHP (กำหนดไทป์ได้ตั้งแต่เวอร์ชัน 7) และ Python (กำหนดไทป์ได้ตั้งแต่เวอร์ชัน 3.6)

JavaScript ก็เป็นหนึ่งในนั้น แต่สำหรับ JavaScript นั้นมีความแปลกกว่าภาษาอื่นนิดหน่อย เพราะถ้าเราต้องการใช้ไทป์ในภาษานี้ เราจะต้องเขียนมันด้วย TypeScript ซึ่งเป็นภาษาอีกตัวหนึ่งจากทาง Microsoft ที่พอเขียนเสร็จแล้วมันจะคอมไพล์กลับมาเป็น JavaScript อีกทีหนึ่ง

ดังนั้นคำแนะนำในการเขียน JavaScript ข้อแรกคือ ..

อย่าเขียน JavaScript แต่ให้เขียน TypeScript แทน !!

กำเนิด TypeScript

TypeScript นั้นถูกสร้างโดย Microsoft หลังจากที่ VBScript ของตัวเองแพ้ให้กับ JavaScript

Microsoft ใช้หลักการว่า "ถ้าต้านไม่อยู่ ก็เข้าร่วมซะ" โดยมอบหมายงานนี้ให้กับ นักออกแบบภาษาโปรแกรมที่เคยออกแบบ C# และ .NET Framework มาแล้วอย่าง Anders Hejlsberg ซึ่งคอนเซ็ปที่เขาใช้ก็คือเพิ่ม Type ให้กับ JS ซะ

TypeScript = JavaScript + Type

แนวคิดนี้เหมือนกับตอนที่ C++ ถูกสร้างขึ้นมาคือเป็นการเพิ่มความสามารถให้ภาษา C อีกที (ในเวอร์ชันแรก C++ นั้นคอมไพล์ออกมาเป็นโค้ดภาษา C)

ครั้งนี้ก็เช่นกัน ภาษาใหม่ที่ออกแบบจะไม่ฉีกแนวไปจาก JavaScript เลย แต่เพิ่ม syntax ที่บวกความสามารถเรื่อง Type และอื่นๆ เข้าไปแทน โดยตัว TypeScript นี้ไม่ใช่ภาษาที่เอาไปรันได้ แต่ตัวมันคอมไพล์แล้ว output ออกมาเป็น JavaScript นั้นเอง

จริงๆ ต้องบอกว่า Alternative Language หรือภาษาที่เอามาเขียนแทน JavaScript ได้นั้นมีเยอะมาก เช่น CoffeeScript หรือภาษา Functional อย่าง Clojure

แต่ทำไมอยู่ๆ TypeScript ถึงดังขึ้นมาล่ะ? ทั้งที่ตอนนั้นกระแสโปรแกรมเมอร์ไม่ชอบ Microsoft เยอะมาก (ย้อนกลับไปช่วงต้นปี 2010 นะ) อะไรที่ Microsoft ทำในช่วงนั้น คนจะหนีกันหมด

จริงๆ แล้วตอนแรก TypeScript ก็ไม่ได้รับความนิยมอะไรมากหรอก

จน Google มีโปรเจคสร้างเฟรมเวิร์คสำหรับเขียน Frontend ตัวใหม่ที่ชื่อว่า AngularJS ออกมานั่นแหละ

ตอนนั้น AngularJS กำลังอยู่ในเวอร์ชัน 1 (เขียนด้วย JavaScript เพียวๆ เลย) และทาง Google ก็พบปัญหามากมาย จนอยากจะรื้อเขียนใหม่ให้เป็นเวอร์ชัน 2 ซะ แล้วไหนๆ ก็จะเขียนใหม่แล้ว เขียนมันด้วย ES6 เลยดีกว่า! (ในปีนั้นมาตราฐาน ES6 ยังไม่ออก) จะรอให้มาตราฐานออก ก็ไม่รู้ว่าจะมาเมื่อไหร่ งั้นก็ลองไปค้นๆ ในตลอดดูซิว่าตอนนี้มีภาษาอะไรเอามาใช้แทนก่อนได้มั้ย?

และหวยก็ไปออกที่ TypeScript นั่นเอง !!

คือตอนนั้นทีม Angular สร้างภาษาของตัวเองชื่อ ATScript ขึ้นมาเพื่อครอบ TypeScript อีกที แต่ก็เปลี่ยนใจยกเลิกไป แล้วหันมาใช้ TypeScript เพียวๆ แทน

และเมื่อ Angular ดังขึ้น --> TypeScript ก็เลยดังขึ้นมาพร้อมๆ กัน

สรุป

TypeScript เป็นแค่ภาษาที่เพิ่ม Type เข้ามาให้ภาษา JavaScript ซึ่งก็อาจจะถือว่าเป็นภาษาดวงดี ออกมาถูกจังหวะ จนทุกวันนี้ได้รับความนิยมสูงมาก โปรเจคใหญ่ๆ ไม่ว่าจะเป็น Node.js, React, Vue.js ล้วนแต่เอา TypeScript ไปผสมได้ทั้งนั้น (ถ้าเป็น Deno, Angular นั้นซัพพอร์ท TypeScript ตั้งแต่แรกอยู่แล้ว)

บล็อกต่อไปเราจะสอนวิธีการเซ็ตโปรเจค TypeScript ในโปรเจค Node.js กัน

This entry is part 5 of 5 in the series Async-in-Dart-Series

เนื้อหาในบทนี้ เป็นคลาสเฉพาะที่มากับ Flutter Framework เท่านั้น .. ถ้าเขียน Dart ธรรมดาจะไม่มีให้ใช้นะ!!

เอาจริงๆ 99% ของคนที่ศึกษาภาษา Dart ก็เพื่อเอาไปเขียนแอพ (หรือเว็บ/เด็กส์ท็อป) แบบ cross-platform ด้วยเฟรมเวิร์ก Flutter นั่นแหละ

ตามความคิดของเรา จริงๆ Flutter น่าจะเลือกภาษา Kotlin มาใช้แทนมากกว่า แต่ก็มีเหตุผลหลายๆ อย่างนั่นแหละที่ทำให้ทำไม่ได้

สำหรับการเขียน Flutter นั้น ส่วนประกอบในแต่ละส่วนของแอพนั้นจะพูดออกแบบมาเป็น Component เอามาประกอบเข้าด้วยกันเป็นชั้นๆ เรียกว่า "Widget"

มี Widget อยู่ 2 ประเภทหลักๆ คือ

  • StatelessWidget: เป็น Widget ที่ไม่สามารถเปลี่ยนแปลงได้หลังจาก render ไปแล้ว
  • StatefulWidget: เป็น Widget ที่สามารถ render หน้าตา UI ของแอพใหม่ได้ถ้ามีการเปลี่ยนแปลงข้อมูล

ในบทความนี้เราจะโฟกัสกันที่ StatefulWidget เป็นหลักนะ หน้าตาของ StatefulWidget ก็จะเป็นประมาณนี้ (StatefulWidget จะมาพร้อมกับคลาส State ของมันเสมอ)

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('This is my App.'),
    );
  }
}

เมธอดหลักที่ทำหน้าที่ในการกำหนดส่วนแสดงผลหรือ UI คือเมธอด build()

แน่นอน เวลาเราเขียนแอพจริงๆ โครงสร้างมันจะเป็นการเอา Widget มาซ้อนกันหลายๆๆๆ ชั้นมากๆ เช่น

Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Column(
        children: [
          Container(
            child: Row(
              children: [
                Text('This is my App.'),
              ]
            ),
          ),          
        ],
      ),
    ),
  );
}

ทีนี้ ปัญหามันก็อยู่ตรงนี้แหละ เพรายิ่งโครงสร้างซ้อนกันหลายชั้นมากๆ ถ้ามีการเปลี่ยนแปลงข้อมูล ก็ต้อง render หน้านี้ใหม่ (เรียกคำสั่ง build() ใหม่) แม้ว่าเราจะเปลี่ยนแปลงข้อมูลแค่ไม่กี่จุดเล็กๆ ก็ตาม

ลองมาดูตัวอย่างแอพมาตราฐานคือ Counter กัน (แอพตัวอย่างนี้เป็นแอพที่เวลาเราสั่ง new Flutter project มันจะสร้างให้เป็น default เลย)

แต่เราจะขอตัดมาแค่ส่วนที่จะใช้อธิบายนะ และขอแยก Widget ออกเป็นชิ้นๆ ด้วย

class MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('render - Scaffold');
    return Scaffold(
      body: columnWidget(),
    );
  }

  Widget columnWidget(){
    print('render - Column Widget');
    return Column(
      children: [
        incrementButtonWidget(),
        counterWidget(),
      ],
    );
  }

  Widget incrementButtonWidget(){
    print('render - Increment Button Widget');
    return RaisedButton(
      child: incrementButtonTextWidget(),
      onPressed: _incrementCounter,
    );
  }

  Widget incrementButtonTextWidget(){
    print('render - Increment Button Text Widget');
    return Text('Increment');
  }

  Widget counterWidget(){
    print('render - Counter Widget');
    return Text('count is $_counter');
  }
}

เมื่อเราเปิดหน้าแอพขึ้นมาครั้งแรก เราจะพบว่า build() เริ่มทำงาน โดยมันจะเรนเดอร์ Widget ทุกชิ้นขึ้นมาก่อน

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget

จากนั้น เมื่อเรากด increment จะทำให้เมธอด _incrementCounter() ทำงาน ไปทำการ setState() เพื่อเปลี่ยนค่า _counter

ผลที่ได้คือเมื่อ state เปลี่ยนไป ทั้งหน้าจะต้องทำการเรนเดอร์ใหม่อีกครั้ง เมธอด build() ก็ต้องเริ่มทำงานใหม่ตั้งแต่ต้นอีกครั้ง ทำให้เราได้ผลแบบนี้ออกมาอีกที

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget

แต่จริงๆ แล้วส่วนที่มีการเปลี่ยนแปลงจริงๆ มีแค่วิดเจ็ต Text('count is $_counter') เท่านั้น

ทางทีม Flutter ของ Google เลยแนะนำมาว่าการเขียนแอพแบบนี้จะทำให้ประสิทธิภาพไม่ดี ถ้ามีข้อมูลแปลี่ยนเป็นบางตัว เราควรจะเรนเดอร์ใหม่เฉพาะวิดเจ็ตที่จำเป็นน่าจะดีกว่า

นั่นเลยเป็นที่มาของวิดเจ็ตพิเศษที่ชื่อว่า FutureBuilder และ StreamBuilder

FutureBuilder / StreamBuilder

มีวิธีการสร้างแบบนี้

FutureBuilder(
    future: _future,
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        return ...
    },
)

StreamBuilder(
    stream: _stream,
    builder: (BuildContext context, AsyncSnapshot snapshot) {
        return ...
    },
)

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

แล้วเมื่อไหร่จะใช้ Future / Stream

ถ้าข้อมูลของเราเปลี่ยนแปลงได้แค่ครั้งเดียว เราจะใช้ FutureBuilder เช่นต้องการโหลดข้อมูลจาก API แค่ครั้งเดียว

แต่ถ้าข้อมูลของเราเปลี่ยนได้เรื่อยๆ มากกว่า 1 ครั้ง เราจะใช้ StreamBuilder เช่นการกดปุ่มที่กดได้มากกว่า 1 ครั้ง

แน่นอนว่าเราสามารถใช้ StreamBuilder แทน FutureBuilder ได้แทบจะทุกกรณีเลย

ทีนี้ลองเอา StreamBuilder มาใช้กับโค้ด Counter ดูบ้าง

ในเคสนี้ เราเปลี่ยน Text ตรงที่แสดงตัวนับให้กลายเป็น StreamBuilder จากนั้นสร้าง StreamController ขึ้นมาหนึ่งตัว ซึ่งจะอัพเดทค่า counter แทนการสั่ง setState()

class MyHomePageState extends State<MyHomePage> {

  int _counter = 0;
  StreamController<int> controller = new StreamController<int>();

  @override
  void initState(){
    super.initState();
    controller.add(_counter);
  }

  @override
  void dispose(){
    super.dispose();
    controller.close();
  }

  void _incrementCounter() {
    //แทนที่จะใช้ setState ก็เซ็ตค่าผ่าน StreamController แทน
    controller.add(++_counter);
  }

  @override
  Widget build(BuildContext context) {
    print('render - Scaffold');
    return Scaffold(
      body: columnWidget(),
    );
  }

  Widget columnWidget(){
    print('render - Column Widget');
    return Column(
      children: [
        incrementButtonWidget(),
        counterWidget(),
      ],
    );
  }

  Widget incrementButtonWidget(){
    print('render - Increment Button Widget');
    return RaisedButton(
      child: incrementButtonTextWidget(),
      onPressed: _incrementCounter,
    );
  }

  Widget incrementButtonTextWidget(){
    print('render - Increment Button Text Widget');
    return Text('Increment');
  }

  Widget counterWidget(){
    return StreamBuilder(
      stream: controller.stream,
      builder: (context, snapshot){
        print('render - Counter Widget');
        return Text('count is ${snapshot.data}');
      },
    );
  }
}

ในกรณีนี้วิดเจ็ตทั้งหมดจะมีการเรนเดอร์แค่ครั้งแรกครั้งเดียว หลังจากนั้นถ้ามีการกดปุ่ม วิดเจ็ตตัวอื่นที่ไม่เกี่ยวข้องด้วย จะไม่มีการเรนเดอร์ใหม่ เรนเดอร์เฉพาะตัวของ StreamBuilder เท่านั้น

render - Scaffold
render - Column Widget
render - Increment Button Widget
render - Increment Button Text Widget
render - Counter Widget
render - Counter Widget
render - Counter Widget
render - Counter Widget

จะเห็นว่าวิธีนี้ทำให้ประสิทธิภาพของแอพเพิ่มขึ้นเยอะมาก เพราะไม่จำเป็นต้องเรนเดอร์ทั้งหน้าใหม่ทุกครั้งที่มีข้อมูลเปลี่ยนแปลงเพียงเล็กน้อย

ซึ่งก็แลกมากับการที่เราต้องเขียนโค้ดเยอะขึ้นล่ะนะ (ฮา)

AsyncSnapshot

ในการใช้ทั้ง FutureBuilder และ StreamBuilder จะมีสิ่งที่เรียกว่า AsyncSnapshot ส่งมาให้ตัว เจ้าตัวนี้เป็นเหมือนกับตัวที่บอกว่าตอนนี้ข้อมูลของเรามีสถานะเป็นยังไงบ้างแล้ว

// สถานะของ future/stream ในตอนนั้น
snapshort.connectionState

// มี error เกิดขึ้นไหม
snapshop.hasError
// error คืออะไร
snapshop.error

// ได้รับ data มาแล้วรึยัง
snapshop.hasData
// data คืออะไร
snapshop.data

ConnectionState

สำหรับ Future

  • waiting: ขณะกำลังรอข้อมูล
  • done: เมื่อได้รับข้อมูลมาแล้ว

สำหรับ Stream

  • waiting: ขณะกำลังรอข้อมูล
  • active: เมื่อได้รับข้อมูลมาแล้ว แต่ stream ยังไม่ close
  • done: เมื่อสั่งให้ stream close
(BuildContext context, AsyncSnapshot snapshot) {

  // เช็กก่อนว่ามี error มั้ย
  if(snapshot.hasError){
    return Text('เกิดข้อผิดพลาดในการโหลดข้อมูล ${snapshot.error}');
  }

  // ถ้าไม่มี ตอนนี้โหลดข้อมูลเป็นยังไงบ้างแล้ว
  switch(snapshort.connectionState){
      // ข้อมูลยังไม่มา กำลังโหลดอยู่
      case ConnectionState.waiting:
          return Text('กำลังโหลดข้อมูลอยู่');

      // ข้อมูลมาเรียบร้อยแล้ว แสดงผลได้
      case ConnectionState.done:
      case ConnectionState.active:
          return Text('ข้อมูลคือ ${snapshot.data}');
  }
}

ทิ้งท้าย...

การใช้ FutureBuilder / StreamBuilder ใน Flutter เป็นสิ่งที่ช่วยเพิ่มประสิทธิภาพให้ตัวแอพไม่ต้องเรนเดอร์วิดเจ็ตที่ไม่ได้เปลี่ยนแปลงซ้ำๆ

ซึ่งคอนเซ็ปนี้จะถูกเอาไปใช้ต่อในแพทเทิร์นที่ทาง Google แนะนำมาให้ใช้กับการวางโครงสร้าง UI ใน Flutter ที่ชื่อว่า BLoC หรือ Business Logic Component ซึ่งเดี๋ยวเราจะเอามาสอนกันต่อในบทความซีรีส์ Flutter ต่อไป

ส่วนบทความซีรีส์ Async in Dart ก็ขอจบลงแค่ตอนนี้ก่อนล่ะ

Series Navigation<< Async in Dart (4) ควบคุมข้อมูลในstreamอย่างเหนือชั้นด้วย StreamController

ในโลกของ OOP เรามักจะสร้างคลาสต่างๆ ที่มีการเรียกใช้กันต่อเป็นทอดๆ มากมาย

เช่นโค้ดข้างล่างนี่

class Car {

    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }

}

Car car = new Car();

เราสร้างคลาส Car และ Engine คือรถและเครื่องยนต์ขึ้นมา โดยการจะสร้างรถได้ เราจะต้องสร้างเครื่องยนต์ขึ้นมาด้วย

ในกรณีแบบนี้เราเรียกว่าคลาส Car depend on Engine (Car ต้องการ Engine) หรือแปลว่า เราไม่สามารถสร้างคลาส Car ได้ถ้าไม่มีคลาส Engine หรือ Engine นั้นเป็น "Dependency" ของคลาส Car นั่นเอง

แต่ตามหลักการออกแบบ OOP แล้วเราไม่ควรเขียนแบบโค้ดด้านบน คือไม่ควร new อ็อบเจคตรงๆ ในคลาสอื่น เพราะจะทำให้เราเปลี่ยนแปลงพฤติกรรมต่างๆ ของโค้ดทำได้ยาก

เช่นถ้าเราต้องการจะเปลี่ยน Engine ไปเป็น EcoEngine เครื่องยนต์แบบอีโค่เราจะต้องแก้โค้ดในคลาส Car เท่านั้น

วิธีการแก้แบบมาตราฐานก็คือถ้าเราต้องการใช้อ็อบเจคจากคลาสอื่น อย่าnewเอง แต่ให้รับอ็อบเจคเข้ามาแทน อาจจะทาง constructure แบบนี้ก็ได้

class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

}

แต่สำหรับมือใหม่หัดเขียน OOP นั้นมักจะไม่เขียนโค้ดในแพทเทิร์นแบบนี้ ด้วยเหตุผลว่าเวลาเราจะสร้าง Car ขึ้นมาใช้นั้น จะต้องไปสร้าง Dependencies ทั้งหมดของ Car ขึ้นมาซะก่อน (คือ new ตรงๆ ทันทีไม่ได้นั่นแหละ)

Engine engine = new Engine();
Car car = new Car();

ถ้ามี Dependencies ไม่เยอะก็ยังไม่เท่าไหร่ แต่ถ้าเยอะๆ นี่ก็มีความปวดหัวเหมือนกัน

เช่น ถ้าเราต้องการสร้างบ้านขึ้นมา แต่บ้านก็ต้องมี ประตู หน้าต่าง กำแพง บลาๆๆ

Door door = new Door();
Window window = new Window();
Wall wall = new Wall();
Ceil ceil = new Ceil();
Floor floor = new Floor();
House house = new House(door, window, wall, ceil, floor);

ซึ่งถ้าเราย้ายโค้ดพวกนี้เข้าไปไว้ในคลาส การจะสร้าง House ขึ้นมาก็จะเหลือแค่นี้

House house = new House();

DI Service แบบคร่าวๆ

หลักการ DI (หรือ Dependency Injection) แบบคร่าวๆ คือเราจะไม่ new อ็อบเจคในคลาสแบบที่เล่าไปข้างต้น แต่เพราะวิธีการนี้มันยุ่งยากกว่า คนส่วนใหญ่เลยชอบสร้างคลาสกันแบบ new ตรงๆ ในคลาสเลย ไม่ทำการรับอ็อบเจคเข้ามาทาง Constructure

ดังนั้นเลยมีคนเห็นปัญหานี้ ในเมื่อ DI เป็นคอนเซ็ปที่ดี แต่เขียนยุ่งยากมาก ... จะดีกว่าถ้าเราสามารถสร้างอ็อบเจคที่มี dependencies เต็มไปหมดด้วยการสั่ง new ง่ายๆ

DI Service คือตัวช่วยที่ว่า ซึ่งเป็นเหมือนตัวที่จะคอยจัดการและสร้างอ็อบเจคให้เราแทน

ซึ่งการใช้งานส่วนใหญ่จะแบ่งออกเป็น 2 เฟส นั่นคือการกำหนดค่าว่า Dependencies แต่ละตัวมีวิธีสร้างยังไง หลังจากนั้นคือการสั่งให้มันสร้างอ็อบเจคกลับมาให้เรา

// Setup
DIService di = ...
di.set<Engine>(new Engine());

// Create Object
Car car = di.newObject<Car>();

แต่เอาจริงๆ บางครั้ง ในบางภาษา การจะใช้งาน DI Service พวกนี้ก็ยุ่งยากกว่าการสร้างเองตรงๆ ซะดี (ฮา) ตัวอย่างเช่น Dagger2 สำหรับภาษา Java

แต่..เอ๊ะ! นี่มัน Service Locator ตั้งหาก

ในบางภาษา หากเราเสิร์จหา library เพื่อมาช่วยเราทำ DI Service เราอาจจะเจอตัวที่ใช้แพทเทิร์นแบบ Container

แบบนี้

class Car {

    private Engine engine;

    public Car() {
        this.engine = Container.get<Engine>();
    }

}

นั่นคือ แทนที่จะรับอ็อบเจคเข้ามาผ่าน Constructure ตรงๆ แต่จะใช้วิธีการขอจากสิ่งที่เรียกว่า Container แทน (ไม่ได้ new เองนะ แต่ขออ็อบเจคมาจากคอนเทนเนอร์)

วิธีใช้งานก็คล้ายๆ กับ DI Service นั่นแหละ คือต้องทำการกำหนดค่า dependencies ซะก่อน แล้วถึงจะเรียกใช้งานได้

// Setup
Container.set<Engine>(new Engine());

// Create Object
Car car = new Car();

เอาล่ะ! ถึงผลลัพธ์จะออกมาเหมือนกันเป๊ะ! แถมคนส่วนใหญ่ยังเรียกวิธีการทั้ง 2 ตัวนี้ว่า DI ทั้งคู่อีกตั้งหาก

แต่เนื่องจากโค้ดมันเขียนไม่เหมือนกัน ดังนั้นเขาเลยตั้งชื่อแยกมันออกเป็น 2 แพทเทิร์น นั่นคือ "Dependency Injection" และ "Service Locator"

ข้อแตกต่างล่ะ?

Service Locator

  • คอนเซ็ปคือคลาสเป้าหมาย ขออ็อบเจคจาก Service Locator ซึ่งจะไปตามหามาให้
  • คลาสเป้าหมายเรียกใช้ Service Locator
  • มีสิ่งที่เรียกว่า Container สำหรับเก็บอ็อบเจค
  • คลาสจะขออ็อบเจคจาก Container แทนการ new ขึ้นมาเอง
  • ใช้งานและเข้าใจได้ง่าย เพราะอยากได้อ็อบเจคอะไรก็ขอเอาจาก Container ตรงๆ ได้เลย (ส่วนใหญ่ Container จะเป็นแบบ static คือเรียกจากตรงไหนในโปรแกรมเราก็ได้)
  • ปัญหาคือมันจะทำให้มี dependency เพิ่มขึ้นมาในระบบเราอีก 1 ตัวคือ Container นั่นแหละ แถมคลาสแทบทุกคลาสในระบบเรายัง depend on เจ้าคอนเทนเนอร์นี้ซะอีก
  • ถ้าเกิดอยากเปลี่ยน library สำหรับทำ Service Locator จะต้องแก้โค้ดแทบจะทั้งโปรแกรม!

Dependency Injection

  • คอนเซ็ปคือ DI Service จะเช็กดูว่าคลาสเป้าหมาย เวลาจะสร้างต้องการ dependencies อะไรไป ก็จะไปหามาให้ แล้วเอาไป new อ็อบเจคเป้าหมายขึ้นมา
  • คลาสเป้าหมายไม่ได้เรียกใช้ DI Service, แต่ DI Service เป็นฝ่ายที่จะ new อ็อบเจคขึ้นมาเอง
  • ใช้วิธี inject อ็อบเจคเข้าไปตรงๆ
  • สำหรับคลาสเป้าหมายจะไม่ต้องเปลี่ยนอะไรมากนัก เพราะ dependencies ทั้งหมดถูกส่งเข้ามาทาง Constructure อยู่แล้ว
  • เขียนเทสได้ง่ายโดยการสร้าง mock dependencies ขึ้นมาเองไม่ต้องผ่าน DI Service เลยก็ยังได้

เขียนเทสยังไง?

จริงๆ เป้าหมายของการทำ DI ทั้งหมดคือเพื่อเอามาเขียนเทสหรือการทดสอบโปรแกรมได้ง่ายๆ นะ

class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

}

Car testCar = new Car(new TestEngine());

ซึ่งในเคสนี้ Service Locator จะ mock อ็อบเจคต่างๆ ได้ยากมากเพราะเป็นการเรียกใช้งานแบบ static คือเรียกตรงๆ ที่ Container ในคลาสเลย

ดังนั้นคำแนะนำสำหรับคนใช้แพทเทิร์น Service Locator คืออย่าเรียก Container ตรงๆ แต่ให้ inject Container นี้เข้าไปแทน

แบบนี้

class Car {

    private Engine engine;

    public Car(Container container) {
        this.engine = container.get<Engine>();
    }

}

เพราะจะทำให้เราสร้าง container สำหรับเทสอ็อบเจคขึ้นมาได้

// Develop
Container container = new Container();
container.set<Engine>(new Engine());

Car car = new Car(container);

// Testing
Container testContainer = new Container();
testContainer.set<Engine>(new TestEngine());

Car testCar = new Car(testContainer);
This entry is part 4 of 5 in the series Async-in-Dart-Series

Async in Dart (4) ควบคุมข้อมูลในstreamอย่างเหนือชั้นด้วย StreamController

ในบทที่แล้วเราสอนการสร้าง Stream แบบง่ายๆ ไปแล้ว แต่ในบางครั้งเราต้องการควบคุมข้อมูลใน Stream แบบคัสตอมมากๆ ซึ่งข้อมูลอาจจะเข้ามาจากหลายทางมากๆ การที่เรามีแค่ yield จากฟังก์ชันเดียวอาจจะไม่เพียงพอ

ดังนั้นเลยเป็นที่มาของคลาสที่ชื่อว่า StreamController ซึ่งเป็นคลาสที่เอาไว้ควบคุม Stream อีกที

StreamController

การสร้าง Stream แบบธรรมดาก็จะเป็นประมาณนี้

Stream<int> getNumberStream() async* {
    ...
}

Stream<int> numberStream = getNumberStream();

ส่วนใหญ่เราจะสร้าง Stream จากฟังก์ชันประเภท async* ซึ่งภายในฟังก์ชันก็จะมีการ yield ข้อมูลกลับมา

ทีนี้ ถ้าเราจะเปลี่ยนไปใช้ StreamController แทน ก็จะเป็นแบบนี้

StreamController<int> controller = StreamController<int>();

นั่นคือ จากปกติที่เราจะสร้างข้อมูลในฟังก์ชันเท่านั้น การสร้างด้วย StreamController คือเราไม่ต้องกำหนดว่า Stream ตัวนี้จะสร้างอะไรเลย (แค่ new ตัวคอนโทรเลอร์ขึ้นมาเฉยๆ ก็พอแล้ว)

StreamController ไม่ใช่ Stream นะ ไม่สามารถเอาไปใช้แทน Stream ได้ แต่สามารถดึง Stream ออกมาจากตัวมันได้

ให้คิดง่ายๆ ว่ามันคือ Controller ที่ถือ Stream เอาไว้ข้างในอีกทีก็ได้

สำหรับอ็อบเจค StreamController ที่เราสร้างขึ้นมา จะมี Stream อยู่ข้างใน เราสามารถดึงออกมาใช้งานได้ด้วยการสั่ง .stream ตรงๆ เลย

Stream stream = controller.stream;
stream.listen((value) {
    print('value: $value');
});

//หรือ

controller.stream.listen((value) {
    print('value: $value');
});

แล้วข้อมูลจะมาจากไหน? ใช้คำสั่ง add ยังไงล่ะ

StreamController นั้นไม่ได้เป็นฟังก์ชัน ดังนั้นมันจะไม่มีโลจิคการสร้างข้อมูลอะไรอยู่กับตัวมันทั้งนั้น!

StreamController เป็นแค่ตัวกลางสำหรับใส่ข้อมูลเข้า Stream เท่านั้น

import 'dart:async';

var controller = StreamController<int>();

void main() {
    controller.stream.listen((x) {
        print(x);
    });

    ...
}

void addNumberToStream(int x) {
    controller.add(x);
}

จากโค้ดด้านบน เรามีการสร้าง StreamController ขึ้นมา 1 ตัว แล้วจัดการ listen() มันใน main (listen ไว้ก่อน ยังไม่มีข้อมูลอะไรเข้ามาทั้งนั้น)

ทีนี้ เราก็ไปสร้างฟังก์ชันอีกตัวหนึ่งชื่อ addNumberToStream(int x) ซึ่งจะมีการเพิ่มค่าเข้า Stream ผ่านคำสั่ง add()

เมื่อฟังก์ชัน addNumberToStream(int x) ถูกเรียก StreamController ก็จะส่งค่านั้นไปให้ listener ที่รอค่าอยู่นั่นเอง

เราสามารถส่งข้อมูลกี่ครั้งก็ได้ผ่านคำสั่ง add()

ส่วนใหญ่ use case ที่เราต้องใช้ StreamController แทนการสร้างฟังก์ชัน async* แบบธรรมดาจะเป็นการเขียนโปรแกรมแบบ Event-Driving เพราะเราไม่รู้ว่าข้อมูลจะเข้ามาเมื่อไหร่ ตอนไหนที่ผู้ใช้จะกดปุ่ม อะไรแบบนั้น เราเลยสร้างฟังก์ชันเตรียมไว้ก่อนไม่ได้ ทำได้แค่ให้ controller คอยส่งข้อมูลมาให้เมื่อเกิด Event เท่านั้น

คำเตือน! StreamController มักจะไม่รู้จุดสิ้นสุด!

การใช้งาน Stream ธรรมดา เราสามารถเปลี่ยนจากการใช้ callback ด้วย listen() ไปใช้ await เพื่อเปลี่ยนโค้ดให้อยู่ในรูปแบบ sync แทนได้

Stream<int> numberStream = getNumberStream();

await for(var number in numberStream) {
    print('Receive: $number');
}

ซึ่งมีข้อควรระวังในการใช้ await คือ Stream ตัวนั้นจะต้องเป็น "Finite Stream" หรือสตรีมที่รู้ว่ามีจุดสิ้นสุดแน่ๆ !

ไม่งั้นโค้ดของเราจะถูก await บล็อกการทำงานอยู่ที่ลูปนั้น ไปต่อไม่ได้ เช่น

import 'dart:async';

var controller = StreamController<void>();

void main() {

    print('start');

    await for(var _ in controller.stream) {
        print('clicked!');
    }

    print('end');

    ...
}

void onClick() {
    controller.add(null);
}

ในกรณีแบบนี้ โค้ดจะถูกบล็อกอยู่ที่ลูป (โปรแกรมจะไม่หลุดไปยังคำสั่งปริ้น end เลย) เพราะไม่มีการปิดสตรีม

ดังนั้นถ้าฟังก์ชันเรามีคำสั่งให้ทำอะไรบ้างอย่างต่อไป เราไม่ควรเขียนมันในรูป await นะ ใช้ listen() จะดีกว่า

ยังมี addError และ close อีกนะ

นอกจากการ add ข้อมูลเข้า Stream แล้วเรายังสามารถสั่งอีก 2 คำสั่งกับคอรโทรเลอร์ได้ คือการโยน Exception หรือสั่งปิด Stream นั่นเอง

import 'dart:async';

var controller = StreamController<int>();

void main() {
    controller.stream.listen(
        (x) {
            print(x);
        },
        onDone: () {
            print('จบแล้วจ้า!');
        },
        onError: (e){
            print('error is $e');
        },
        cancelOnError: false,
    );

    print('start');
    controller.add(1);
    controller.add(2);
    controller.add(3);
    controller.addError(Exception('ตู้มมม'));
    controller.add(4);
    controller.add(5);
    controller.close();
    print('end');
}

output:

start
end
1
2
3
error is Exception: ตู้มมม
4
5
จบแล้วจ้า!

การสั่ง addError ไม่ได้ทำให้ Stream หยุดทำงานนะ ไม่งั้นต้องเพิ่มอ็อบชัน cancelOnError: true ลงไปด้วย (ถ้าไม่เซ็ต ดีฟอลต์จะเป็น false คือถึงเจอ Exception ฉันก็ไม่หยุดนะ)

ข้อสังเกตอีกอย่างคือ listen() นั้นเป็น Isolates นะ มันจะปริ้น start และ end (อยู่ใน Isolates ของ ฟังก์ชัน main) ก่อน

ใครงงว่า Isolates คืออะไร ย้อนกลับไปอ่านบทความแรกของซีรีส์นี้อีกทีนะ

สุดท้าย การใช้งาน StreamController นั้นมีอีกเรื่องที่ต้องระวัง คือ...

Stream อาจจะไม่ได้ถูก subscription ในทันที

ลองดูตัวอย่างโค้ดนี้

Stream<int> counter1(int maxCount) {
    var controller = StreamController<int>();
    int counter = 0;

    Timer.periodic(Duration(seconds: 1), (Timer timer) {
        counter++;
        controller.add(counter);
        if (counter >= maxCount) {
            timer.cancel();
            controller.close();
        }
    });

    return controller.stream;
}

เราตั้งให้ฟังก์ชัน counter1 ทำการ add ตัวเลขเข้าไปเรื่อยๆ ทุกๆ 1 วินาที (ฟังก์ชัน Timer.periodic() เอาไว้สำหรับสั่งงานที่ทำงานซ้ๆ กันทุกๆ ช่วงเวลาหนึ่ง คล้ายๆ กับ setInterval ใน JavaScript)

เวลาใช้งาน ก็ใช้ listen() ตามปกติหรือใช้ for loop แล้วเติม await ลงไปก็ได้

void main() {
    //start!
    var counterStream = counter1(5);

    //listen
    counterStream.listen((n){
        print(n);
    });
}

แบบนี้ถ้าเอาไปรัน ก็จะได้ output เป็นเลข 1, 2, 3, 4, 5 ตัวใน 5 วินาที ... อันนี้มาตราฐาน ตามคอมมอนเซ้นส์ ไม่มีอะไรแปลก

แต่ถ้าเรามีการสั่ง delay ก่อนที่เราจะ listen() แบบนี้

void main() {
    //start!
    var counterStream = counter1(5);

    //waiting 2 sec.
    await Future.delayed(const Duration(seconds: 2));

    //listen
    counterStream.listen((n){
        print(n);
    });
}

คำถามคือถ้าเขียนโค้ดแบบนี้ เราคาดหวังว่า output จะเป็นยังไง?

  • โปรแกรมก็น่าจะหยุดรอ 2 วินาที
  • แล้วก็ค่อยๆ ปริ้นเลข 1, 2, 3, 4, 5 ทีละวิ

แต่มันไม่ใช่แบบนั้นน่ะสิ!

การทำงานของโปรแกรมคือหลังจาก delay 2 วินาทีแล้ว มันจะทำการปริ้นเลข 1, 2 ออกมาในทันที (ดูรูปข้างล่างประกอบนะ)

สำหรับการใช้งาน Stream นั้นมันจะทำงานทันที แม้ว่าจะยังไม่มีใครมารอ listen มันเลยก็ตาม

แบบในเคสนี้ เราเรียกฟังก์ชัน counter1() ให้ทำงาน แล้วทำการ delay มันซะ 2 วินาทีแล้วค่อย listen แต่ตัว counter นั้นดันทำงานก่อนไปแล้ว ไม่ได้เริ่มทำงานตอนที่เรา listen

แต่ถ้าเราต้องการให้มันทำงานเมื่อสั่ง listen จังหวะนั้นเลย (แบบ counter2 ในรูปข้างบน) เราจะต้องเซ็ตค่าตอนสร้าง StreamController

ในการสร้าง StreamController เราสามารถกำหนดอ็อบชันเพิ่มได้คือ

  • onListen
  • onPause
  • onResume
  • onCancel

วิธีการก็คือเราจะต้องสร้าง Stream ที่ตอนแรกยังไม่เริ่มนับ counter แล้วสั่งให้มันเริ่มนับตอน onListen นั่นเอง

Stream<int> counter2(int maxCount) {
    StreamController<int> controller;
    Timer timer;
    int counter = 0;

    //ฟังก์ชันสำหรับเริ่มรับเลข
    void startTimer() {
        timer = Timer.periodic(
            Duration(seconds: 1), 
            (_) {
                counter++;
                controller.add(counter);
                if (counter == maxCount) {
                    timer.cancel();
                    controller.close();
                }
            },
        );
    }

    //ฟังก์ชันสำหรับหยุดนับเลข
    void stopTimer() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    controller = StreamController<int>(

        //เริ่มทำงาน startTimer() เมื่อ listen นะ
        onListen: startTimer,

        onPause: stopTimer,
        onResume: startTimer,
        onCancel: stopTimer,
    );

    return controller.stream;
}
Series Navigation<< Async in Dart (3) ตอบค่าเป็นกระแสข้อมูลด้วย StreamAsync in Dart (5) รู้จักกับ FutureBuilder/StreamBuilder, ตัวช่วยสร้าง Async-Widget ใน Futter >>

Deno เป็นเอ็นจิน runtime ที่เอาไว้รันภาษา JavaScript (และ TypeScript) คล้ายๆ กับ V8-Engine ที่ใช้ใน Node.js

สร้างโดย Ryan Dahl ที่เป็นผู้เริ่มโครงการ Node.js ตั้งแต่ปี 2009
ตอนแรกพัฒนาด้วยภาษา Go แต่เปลี่ยนไปเขียนด้วยภาษา Rust แทนในภายหลัง (V8 ใช้ C++)

ฟีเจอร์เด่นๆ ของ Deno

  • Support TypeScript: นี่น่าจะเป็นข้อดีอันดับต้นๆ เลยล่ะ เพราะตามกฎการเขียน JavaScript ที่กล่าวไว้ว่า "คุณไม่ควรเขียน JavaScript!...ให้เขียน TypeScript แทน" (ฮา)
  • Security by default: แอพพลิเคชันที่รัน จะไม่สามารถเข้าถึงระบบ File System, Network, หรือพวก Environment ได้เลยถ้ายังไม่มีการกำหนดสิทธิตอนรันให้ เช่นต้องใส่ flag --allow-net เพิ่ม
  • มี build-in utilities ให้ใช้ เช่น dependency inspector, code formatter
  • สำหรับ standard modules นั้นมีกลุ่ม reviewer คอยเช็กความถูกต้องและเทสเพื่อการันตีว่ามันทำงานได้เวิร์คจริงๆ นะ

มี Node.js อยู่แล้ว จะสร้าง Deno มาทำไม?

Ryan Dahl ผู้สร้าง Deno บอกเอาไว้ว่า
JavaScript นั้นเปลี่ยนไปมากหลังจาก Node.js ถูกสร้างขึ้นตั้งแต่ปี 2009
อารมณ์ก็คล้ายๆ กับภาษา PHP ที่ไม่ได้ออกแบบโครงสร้างที่ดีมาตั้งแต่ต้น พอจะต่อเติมอะไรเข้าไปเพิ่มก็ทำได้ยาก

เช่น Module System นั้นออกแบบมาไม่ดีตั้งแต่แรก, มี API เก่าที่ควรเลิกใช้ได้แล้วแต่ก็ยังต้องทำงานอยู่, ความปลอดภัย

Ryan Dahl บอกว่าถึงแม้ Deno มันจะหน้าตาดูคล้ายๆ Node.js แต่มันถือว่าเป็นคนละตัวกันเลยนะ ตอนนี้ Deno ยังใหม่มาก อาจจะยังไม่เหมาะกับการใช้งานในบางโปรเจค โดยเฉพาะโปรเจคที่ยังต้องพึ่ง library จาก npm อยู่

installation

เข้าไปดูจาก https://deno.land/ ได้เลย

Hello World

ตัวอย่างการสร้าง server แบบง่ายๆ จะเห็นว่ามันเป็น JavaScript นั่นแหละ แต่มันไม่ใช่ Node.js

import { serve } from "https://deno.land/[email protected]/http/server.ts";

const s = serve({ port: 8000 });
console.log("http://localhost:8000/");

for await (const req of s) {
  req.respond({ 
    body: "Hello World\n" 
  });
}

Deno เพิ่งออกเวอร์ชัน 1.0 ตอนนี้ก็ต้องรอดูกันต่อไป ว่าไดโนเสาร์ตัวนี้จะมาแทน Node.js ได้ในอนาคตหรือไม่?