DEV Community

Cover image for ถ้าจะเปลี่ยนจาก Node.js มา Deno เริ่มยังไง?
Somprasong Damyos
Somprasong Damyos

Posted on

ถ้าจะเปลี่ยนจาก Node.js มา Deno เริ่มยังไง?

deno v1.6.2

ใกล้จะสิ้นปีแล้ว ปีหน้ามีโปรเจคใหม่ๆ รออยู่เพียบ วันนี้เลยมาลองดูหน่อยว่า deno พร้อมที่จะเอามาแทน node.js ได้เลยรึยัง โดยจะเขียนโค้ดเปรียบเทียบระหว่าง Deno กับ Node.js ไว้ด้วย

Content

  1. deno คืออะไร
  2. การติดตั้ง
  3. ลองสร้างโปรเจคแรก
  4. เจาะลึกการทำงานของ deno
  5. สร้าง REST APIs
  6. สร้าง Docker Image
  7. สรุปใช้ หรือไม่ใช้

1. Deno คืออะไร

deno นั้นเกิดจาก node.js นั้นถูกมาว่ายังไม่จุดอ่อนอยู่หลายๆ จุด จึงนำเอามาเขียนใหม่เพื่อกำจัดจุดอ่อนเหล่านั้น ตัวอย่างเช่น

Deno Node.js
รองรับทั้ง TypeScript และ JavaScript เลย รองรับแค่ JavaScript แต่สามารถใช้ TS compiler ได้
พัฒนาบน Modern JS Features เช่น Promise Core Modules ยังมี JS แบบเก่าอยู่
ใช้ ES Module (import) ใช้ CommonJS (require)
import โดยใช้ URL (ไม่มี package.json) มี npm และ package.json
การรันต้องต้องระบุ Permissions เช่น allow-net ไม่สนใจเรื่อง Permissions

2. การติดตั้ง

Shell (Mac, Linux):

curl -fsSL https://deno.land/x/install/install.sh | sh

PowerShell (Windows):

iwr https://deno.land/x/install/install.ps1 -useb | iex

3. ลองสร้างโปรเจคแรก

  • สร้าง app.ts
  • ลองเขียนโค้ด typescript
let message: string;

message = 'Hi there!';

console.log(message);
Enter fullscreen mode Exit fullscreen mode
  • ทดสอบรัน
$deno run app.ts
Hi there!
Enter fullscreen mode Exit fullscreen mode

4. เจาะลึกการทำงานของ deno

ถ้าดูจากหน้าเวบของ deno จะเห็นว่า Runtime API, Standard Library และ Third Party Modules แต่ละอันคืออะไรมาดูกัน

Runtime API

Runtime API คือ built-in utilities ที่ทาง Deno เตรียมไว้ให้ซึ่งสามารถเรียกใช้งานได้เลย เช่น Deno.writeFile() ไม่ต้อง import เข้ามาก่อนเหมือน Node.js

สำหรับ VS Code
ให้ลง Plugin ชื่อ Deno เพื่อช่วยให้เขียนโค้ดสะดวกขึ้น

ตัวอย่าง โปรแกรมเขียน Text file
Deno
// app.ts
let message: string;

message = 'Hi there!';

// เนื่องจาก Deno.writeFile รับค่าเป็น Uint8Array จึงต้องแปลงค่าก่อน
const encoder = new TextEncoder();
const data = encoder.encode(text);

// เป็น Promise
Deno.writeFile('message.txt', data).then(() => {
  console.log('Wrote to file!');
});
Enter fullscreen mode Exit fullscreen mode
  • ทดสอบรัน $deno run app.ts จะพบว่ามี Error เกี่ยวกับ Permission เนื่องจาก Deno จะมี Security มาตั้งแต่แรก
  • แก้ไขโดยรัน $deno run --allow-write app.ts เพื่ออนุญาตให้เขียนไฟล์ได้
Node.js
// app.js
const fs = require('fs');

const message = 'Hi there!';

fs.writeFile('node-message.txt', message).then(() => {
  console.log('Wrote to file!');
});
Enter fullscreen mode Exit fullscreen mode
  • รัน $node app.js ได้เลย

Standard Library

Standard Library คือ lib ที่ทาง Core Team ของ Deno สร้างขึ้นมาให้เพื่อให้ใช้งานง่ายขึ้น โดยการใช้งานจะต้อง import เข้ามาก่อน

ตัวอย่าง ทดลองสร้าง HTTP Server
Deno
// app.ts
import { serve } from 'https://deno.land/std@0.81.0/http/server.ts';

const server = serve({ port: 8000 });
console.log('HTTP server listening on http://localhost:8000/');

for await (const req of server) {
  req.respond({ body: 'Hello World\n' });
}
Enter fullscreen mode Exit fullscreen mode

การ import จะใช้วิธีดึงมาจาก URL ซึ่งเมื่อดาวน์โหลดมาแล้วจะถูก cache ไว้ที่เครื่องของเรา

  • รันโค้ด deno run --allow-net app.ts

การรันโค้ดจำเป็นที่จะต้องอนุญาตให้ใช้งาน network ก่อน

Node.js
// app.js
const http = require('http');

const server = http.createServer((req, res) => {
  res.end('Hello World from Nodejs');
});

server.listen(3000, () => {
  console.log('HTTP server listening on http://localhost:3000/');
});
Enter fullscreen mode Exit fullscreen mode
  • รัน $node app.js

Third Party Modules

Third Party Modules คือ lib ที่ทาง Community Teams สร้างขึ้นมาให้เพื่อให้ใช้งานง่ายขึ้น โดยการใช้งานจะต้อง import เข้ามา

เนื่องจาก deno ไม่มี package management จึงไม่มี npm และ package.json การ import จะ import มาจาก url

ตัวอย่างใช้ oak framework
// app.ts
import { Application } from 'https://deno.land/x/oak@v6.4.0/mod.ts';

const app = new Application();

app.use((ctx) => {
  ctx.response.body = 'Hello World!';
});

await app.listen({ port: 8000 });
Enter fullscreen mode Exit fullscreen mode
  • รัน $ deno run --allow-net app.ts

deno จะทำการ cache remote file เอาไว้ที่ local
ถ้าต้องการให้ deno ทำการ re-fetch remote file ให้ใส่ --reload เช่น $ deno run --reload --allow-net app.ts
สามารถกำหนด version ได้ โดยใส่ @version เช่น import { Application } from "https://deno.land/x/oak@v6.4.0/mod.ts";

Custom Modules

เนื่องจาก deno ใช้ ES Module ดังนั้นจะใช้วิธีการ import แทนการ require

ตัวอย่าง
Deno
  • ต้อง export แบบ ES Module
// greeting.ts
export const greeting = (name: String) => {
  return `Hi ${name}`;
};
Enter fullscreen mode Exit fullscreen mode
  • ใช้การ import
// app.ts
import { greeting } from './greeting.ts';

console.log(greeting('Ball'));
Enter fullscreen mode Exit fullscreen mode
  • รันโค้ด deno run app.ts
Node.js
  • ต้อง export แบบ CommonJS
// greeting.js
exports.greeting = (name) => {
  return `Hi ${name}`;
};
Enter fullscreen mode Exit fullscreen mode
  • ใช้การ require
// app.js
const { greeting } = require('./greeting');

console.log(greeting('Ball'));
Enter fullscreen mode Exit fullscreen mode
  • รัน $node app.js

5. สร้าง REST APIs

ในส่วนจะมาลองสร้าง CRUD REST APIs ง่ายๆ โดยจะเปรียบเทียบไปทีละขั้นตอนระหว่าง Node.js ที่ใช้ Express กับ Deno ที่ใช้ Oak

5.1 สร้าง HTTP Server

เริ่มสร้าง HTTP Server ง่ายๆ

Node.js
  • ต้องติดตั้ง express ก่อน npm install express
  • สร้างไฟล์ app.js
// app.js
const express = require('express');

const app = express();

app.use((req, res, next) => {
  res.send('Hello World from Node.js');
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode
Deno
  • Deno สามารถใช้งาน Oak ได้โดยไม่ต้องติดตั้งก่อน
  • สร้างไฟล์ app.ts
// app.ts
import { Application } from 'https://deno.land/x/oak@v6.4.1/mod.ts';

const app = new Application();

app.use((ctx) => {
  ctx.response.body = 'Hello World from Deno';
});

await app.listen({ port: 3000 });
Enter fullscreen mode Exit fullscreen mode

5.2 สร้าง Router

สร้าง route /todos ขึ้นมา เพื่อทำ CRUD แบบง่ายๆ

Node.js
  • สร้างไฟล์ routes/todos.js
// routes/todos.js
const express = require('express');

const router = express.Router();

let todos = [];

// C - Create
router.post('/todos', (req, res, next) => {
  res.send('create');
});

// R - Read
router.get('/todos', (req, res, next) => {
  res.json({ todos: todos });
});

// R - Read by Id
router.get('/todos/:id', (req, res, next) => {
  res.send('read by id');
});

// U - Update by Id
router.put('/todos/:id', (req, res, next) => {
  res.send('update');
});

// D - Delete by Id
router.delete('/todos/:id', (req, res, next) => {
  res.send('delete');
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode
  • แก้ไขไฟล์ app.js เพื่อเรียกใช้งาน route ที่สร้างมา
// app.js
const express = require('express');

// เพิ่มบรรทัดนี้
const todoRoutes = require('./routes/todos');

const app = express();

// เพิ่มบรรทัดนี้
app.use(todoRoutes);

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode
Deno
  • สร้างไฟล์ routes/todos.ts
// routes/todos.ts
import { Router } from "https://deno.land/x/oak@v6.4.1/mod.ts";

const router = new Router();

// เนื่องจากใช้ TypeScript จำเป็นต้องระบุ type ของ todo
interface Todo {
  id: string;
  text: string;
}

let todos: Todo[] = [];

router.get('/todos', (ctx) => {
  ctx.response.body = { todos: todos };
});

// C - Create
router.post('/todos', (ctx) => {
  ctx.response.body = 'create';
});

// R - Read
router.get('/todos', (ctx) => {
  ctx.response.body = { todos: todos };
});

// R - Read by Id
router.get('/todos/:id', (ctx) => {
  ctx.response.body = 'read by id';
});

// U - Update by Id
router.put('/todos/:id', ((ctx) => {
  ctx.response.body = 'update';
});

// D - Delete by Id
router.delete('/todos/:id', (ctx) => {
  ctx.response.body = 'delete';
});

export default router;
Enter fullscreen mode Exit fullscreen mode
  • แก้ไขไฟล์ app.ts เพื่อเรียกใช้งาน route ที่สร้างมา
// app.ts
import { Application } from 'https://deno.land/x/oak@v6.4.1/mod.ts';

// เพิ่มบรรทัดนี้
import todosRoutes from './routes/todos.ts';

const app = new Application();

// เพิ่มบรรทัดนี้
app.use(todosRoutes.routes());
app.use(todosRoutes.allowedMethods());

await app.listen({ port: 3000 });
Enter fullscreen mode Exit fullscreen mode

5.3 การอ่านค่าจาก Body

สำหรับการสร้างข้อมูลใหม่นั้นโดยปกติจะส่งข้อมูลมาในรูปแบบของ JSON ซึ่งจะถูกแนบมากับ body ของ method POST ดังนั้นเราจะอ่านค่าออกมาจาก body ก่อน แล้วนำไปใช้งานต่อ

Node.js

เนื่องจาก Express ตัวมันเองไม่สามารถอ่านค่าจกา body ได้ จึงจำเป็นที่จะต้องใช้ middleware ที่ชื่อว่า body-parser มาช่วยแปลงค่าให้ก่อน โดยต้องติดตั้ง npm install body-parser จึงจะสามารถสามารถอ่านค่าจาก body ได้จาก req.body

  • แก้ไขที่ไฟล์ app.js
// app.js
const express = require('express');
// เพิ่มบรรทัดนี้
const bodyParser = require('body-parser');

const todoRoutes = require('./routes/todos');

const app = express();

// เพิ่มบรรทัดนี้
app.use(bodyParser.json()); // for parsing application/json

app.use(todoRoutes);

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode
  • แก้ไฟล์ routes/todos.js โดยต้องแก้ที่ router.post
// routes/todos.js
router.post('/todos', (req, res, next) => {
  const newTodo = {
    id: new Date().toISOString(),
    text: req.body.text,
  };

  todos.push(newTodo);

  res.status(201).json({
    message: 'Todo created!',
    todo: newTodo,
  });
});
Enter fullscreen mode Exit fullscreen mode
Deno
  • แก้ไฟล์ routes/todos.ts โดยต้องแก้ที่ router.post
// routes/todos.ts

router.post('/todos', async (ctx) => {
  // ตรวจสอบว่ามี body หรือไม่
  if (ctx.request.hasBody) {
    // สามารถใส่ option type เพื่อระบุประเภทของ body ที่ส่งมา
    const result = ctx.request.body({ type: 'json' });
    // ประเภท json -> result.value จะเป็น promise
    const body = await result.value;

    const newTodo: Todo = {
      id: new Date().getTime().toString(),
      text: body.text,
    };

    todos.push(newTodo);

    ctx.response.status = 201;
    ctx.response.body = { message: 'Created todo!', todo: newTodo };
  }
});
Enter fullscreen mode Exit fullscreen mode

5.4 การอ่านค่าจาก Path Parameters

Path Parameters คือ url endpoint ที่ใช้ดึงข้อมูลตามที่ระบุไปใน url เช่น /todos/:id ซึ่ง :id คือค่าที่เปลี่ยนแปลงได้ ตัวอย่าง ต้องการอ้างถึง id ที่ 1 จะเรียกไปที่ url endpoint /todos/1 หรือ ถ้าต้องการอ้างถึง id ที่ 2 จะเรียกไปที่ url /todos/2 เป็นต้น

ดังนั้นจะนำมาใช้ในการทำ R (Read), U (Update) และ D (Delete) กับข้อมูลเฉพาะ id ที่ต้องการ

Node.js
  • Express สามารถอ่านค่า Path Parameters ได้จาก req.params โดยชื่อจะต้องกันกับที่ระบไว้ที่ url endpoint เช่น ค่าของ id จะอ่านได้จาก req.params.id

  • แก้ไฟล์ routes/todos.js /todos/:id

// routes/todos.js
router.get('/todos/:id', (req, res, next) => {
  const { id } = req.params;

  const todoIndex = todos.findIndex((todo) => {
    return todo.id === id;
  });

  res.status(200).json({ todo: todos[todoIndex] });
});

router.put('/todos/:id', (req, res, next) => {
  const { id } = req.params;

  const todoIndex = todos.findIndex((todo) => {
    return todo.id === id;
  });

  todos[todoIndex] = { id: todos[todoIndex].id, text: req.body.text };

  res.status(200).json({ message: 'Updated todo!' });
});

router.delete('/todos/:id', (req, res, next) => {
  const { id } = req.params;

  todos = todos.filter((todo) => todo.id !== id);

  res.status(200).json({ message: 'Todo deleted!' });
});
Enter fullscreen mode Exit fullscreen mode
Deno
  • Oak กำหนด url เหมือน Express แต่จะอ่านค่าออกมาจาก ctx.params
  • แก้ไฟล์ routes/todos.ts
// routes/todos.ts
router.get('/todos/:id', (ctx) => {
  const { id } = ctx.params;

  const todoIndex = todos.findIndex((todo) => {
    return todo.id === id;
  });

  ctx.response.body = { todo: todos[todoIndex] };
});

router.put('/todos/:id', async (ctx) => {
  if (ctx.request.hasBody) {
    const result = ctx.request.body({ type: 'json' });
    const body = await result.value;

    const id = ctx.params.id;

    const todoIndex = todos.findIndex((todo) => {
      return todo.id === id;
    });

    todos[todoIndex] = { id: todos[todoIndex].id, text: body.text };
    ctx.response.body = { message: 'Updated todo' };
  }
});

router.delete('/todos/:id', (ctx) => {
  const { id } = ctx.params;

  todos = todos.filter((todo) => todo.id !== id);

  ctx.response.body = { message: 'Deleted todo' };
});
Enter fullscreen mode Exit fullscreen mode

5.5 รับค่าจาก Query string

ถ้าต้องการค้นหา todos จากคำที่ต้องการ จะส่งค่าไปคำค้นหาไปกับ Query string เช่น /todos?q=deno ตัวอย่างโค้ด

Node.js
  • Express สามารถอ่านค่า Query string ได้จาก req.query โดยค่าของ q จะอ่านได้จาก req.query.q

  • แก้ไฟล์ routes/todos.js

// routes/todos.js

// แก้ให้รับค่า q มาค้นหาได้
router.get('/todos', (req, res, next) => {
  const { q } = req.query;

  if (q) {
    const results = todos.filter((todo) => {
      return todo.text.toLowerCase().includes(q.toLowerCase());
    });
    return res.json({ todos: results });
  }
  res.json({ todos: todos });
});
Enter fullscreen mode Exit fullscreen mode
Deno
  • Oak จะต้องใช้ฟังก์ชัน helpers.getQuery() มาช่วย
  • แก้ไฟล์ routes/todos.ts
// routes/todos.ts

// เพิ่ม import
import { getQuery } from 'https://deno.land/x/oak@v6.4.1/helpers.ts';

// แก้ให้รับค่า q มาค้นหาได้
router.get('/todos', (ctx) => {
  const { q } = getQuery(ctx);

  if (q)
    const results = todos.filter((todo) => {
      return todo.text.toLowerCase().includes(q.toLowerCase());
    });
    ctx.response.body = { todos: results };
    return;
  }
  ctx.response.body = { todos: todos };
});
Enter fullscreen mode Exit fullscreen mode

5.6 สร้าง Middleware

เราสามารถสร้าง middleware ขึ้นมาเพื่อให้ทำงานบางอย่างการที่เข้าสู่ route ที่เรียกมาจริงๆ ได้

Node.js
  • Express สร้าง middleware ได้จาก app.use((req, res, next) => {next()}) โดยเมื่อเรียกใช้งาน next() จะเป็นการส่งไปทำที่ middleware ถัดไป

  • แก้ไฟล์ app.js

// app.js

app.use(bodyParser.json());

// เพิ่มบรรทัดนี้
app.use((req, res, next) => {
  console.log('Middleware A');
  next();
});

// เพิ่มบรรทัดนี้
app.use((req, res, next) => {
  console.log('Middleware B');
  next();
});

app.use(todoRoutes);
Enter fullscreen mode Exit fullscreen mode
Deno
  • Oak สร้าง middleware ได้จาก app.use((ctx, next) => {next()}) โดยเมื่อเรียกใช้งาน next() จะเป็นการส่งไปทำที่ middleware ถัดไป

  • แก้ไฟล์ app.js

// app.ts
const app = new Application();

// เพิ่มบรรทัดนี้
app.use(async (ctx, next) => {
  console.log('Middleware A');
  next();
});

// เพิ่มบรรทัดนี้
app.use(async (ctx, next) => {
  console.log('Middleware B');
  next();
});

app.use(todosRoutes.routes());
Enter fullscreen mode Exit fullscreen mode

จากโค้ดข้างบนจะแสดงคำว่า Middleware A แล้วแสดง Middleware B ก่อนที่จะเข้าไปทำงานที่ todo route ที่เรียกมา

  • ซึ่งทั้ง Express และ Oak จะทำงานแบบ Stack คือ เมื่อมี request เข้ามาจะทำงานลงไปตามลำดับ และเมื่อตอบ respone กลับไปจะย้อนกลับเข้ามาใน middleware จากล่างขึ้นบน ตัวอย่างทำ logger แสดงระยะเวลาการทำงานของแต่ละ route
Node.js
  • แก้ไฟล์ app.js
// app.js

app.use(bodyParser.json());

// เพิ่มบรรทัดนี้
// Logger
app.use(async (req, res, next) => {
  const start = Date.now();
  await next();
  const rt = Date.now() - start;
  console.log(`${req.method} ${req.url} - ${rt} ms`);
});

app.use(todoRoutes);
Enter fullscreen mode Exit fullscreen mode
Deno
  • แก้ไฟล์ app.ts
// app.ts
const app = new Application();

// เพิ่มบรรทัดนี้
// Logger
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const rt = Date.now() - start;
  console.log(`${ctx.request.method} ${ctx.request.url} - ${rt} ms`);
});

app.use(todosRoutes.routes());
Enter fullscreen mode Exit fullscreen mode

5.7 Enable CORS

Node.js
  • ต้องติดตั้ง npm install cors ก่อน

  • แก้ไฟล์ app.js

// app.js
const express = require('express');
const bodyParser = require('body-parser');
// เพิ่มบรรทัดนี้
const cors = require('cors');

const todoRoutes = require('./routes/todos');

const app = express();

// เพิ่มบรรทัดนี้
app.use(cors()); // Enable All CORS Requests

app.use(bodyParser.json());
Enter fullscreen mode Exit fullscreen mode
Deno
  • ต้อง import oakCors มาใช้งาน

  • แก้ไฟล์ app.ts

// app.ts
import { Application } from 'https://deno.land/x/oak@v6.4.1/mod.ts';
// เพิ่มบรรทัดนี้
import { oakCors } from 'https://deno.land/x/cors@v1.2.1/mod.ts';

import todosRoutes from './routes/todos.ts';

const app = new Application();

// เพิ่มบรรทัดนี้
app.use(oakCors()); // Enable All CORS Requests

// Logger
Enter fullscreen mode Exit fullscreen mode

6. สร้าง Docker Image

ตัวอย่างการสร้าง Dockerfile ของทั้ง Nodejs และ Deno

Deno version 1.6.1 สามารถ build เป็นไฟล์ binary ได้แล้ว

Node.js

  • สร้างไฟล์ Dockerfile
FROM node:14-alpine

ENV NODE_ENV=production

WORKDIR /usr/app

COPY ./package*.json ./

RUN npm ci && \
    npm cache clean --force

COPY ./src ./src

CMD node ./src/app.js
Enter fullscreen mode Exit fullscreen mode
  • Build Docker Image จากคำสั่ง docker image build -t api-todo-express .

  • Run จากคำสั่ง docker container run -p 3000:3000 api-todo-express

Deno

  • สร้างไฟล์ Dockerfile
FROM hayd/deno:alpine-1.6.2

WORKDIR /usr/app

COPY ./src ./src

CMD deno run --allow-net src/app.ts
Enter fullscreen mode Exit fullscreen mode
  • Build Docker Image จากคำสั่ง docker image build -t api-todo-deno .

  • Run จากคำสั่ง docker container run -p 3000:3000 api-todo-deno

7. สรุปใช้ หรือไม่ใช้

ส่วนตัวมองว่า Deno นั้นยังใหม่อยู่ มี Bug อยู่เยอะ และที่สำคัญ Ecosystem ยังไม่เยอะเท่า Node.js ส่วน Node.js นั้นสร้างมานานแล้วไม่มี Major Bugs และ Ecosystem ก็แข็งแรงกว่า

สรุปว่า ปีหน้าก็ยังขึ้นโปรเจคใหม่ด้วย Node.js ต่อไป ส่วน Deno คงเอามาใช้ทำพวก side-project เอาครับ ^_^

Top comments (0)