deno v1.6.2
ใกล้จะสิ้นปีแล้ว ปีหน้ามีโปรเจคใหม่ๆ รออยู่เพียบ วันนี้เลยมาลองดูหน่อยว่า deno พร้อมที่จะเอามาแทน node.js ได้เลยรึยัง โดยจะเขียนโค้ดเปรียบเทียบระหว่าง Deno กับ Node.js ไว้ด้วย
Content
- deno คืออะไร
- การติดตั้ง
- ลองสร้างโปรเจคแรก
- เจาะลึกการทำงานของ deno
- สร้าง REST APIs
- สร้าง Docker Image
- สรุปใช้ หรือไม่ใช้
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);
- ทดสอบรัน
$deno run app.ts
Hi there!
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!');
});
- ทดสอบรัน
$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!');
});
- รัน
$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' });
}
การ 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/');
});
- รัน
$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 });
- รัน
$ 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}`;
};
- ใช้การ import
// app.ts
import { greeting } from './greeting.ts';
console.log(greeting('Ball'));
- รันโค้ด
deno run app.ts
Node.js
- ต้อง export แบบ CommonJS
// greeting.js
exports.greeting = (name) => {
return `Hi ${name}`;
};
- ใช้การ require
// app.js
const { greeting } = require('./greeting');
console.log(greeting('Ball'));
- รัน
$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);
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 });
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;
- แก้ไขไฟล์ app.js เพื่อเรียกใช้งาน route ที่สร้างมา
// app.js
const express = require('express');
// เพิ่มบรรทัดนี้
const todoRoutes = require('./routes/todos');
const app = express();
// เพิ่มบรรทัดนี้
app.use(todoRoutes);
app.listen(3000);
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;
- แก้ไขไฟล์ 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 });
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);
- แก้ไฟล์
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,
});
});
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 };
}
});
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!' });
});
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' };
});
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 });
});
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 };
});
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);
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());
จากโค้ดข้างบนจะแสดงคำว่า
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);
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());
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());
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
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
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
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)