pipehole
v1.0.0
Published
Pipehole Web framework support http websocket
Readme
pipehole
Framework Node.js ขนาดเล็กสำหรับ HTTP + WebSocket
ออกแบบมาให้เร็ว เบา และควบคุม flow ได้เองทั้งหมด
ใช้ CommonJS และรองรับทั้ง Node.js และ Bun.js
Created by R938
✨ ความสามารถหลัก
- สร้าง HTTP Server + WebSocket Server จาก instance เดียว
- Routing ครบ:
- static path
- dynamic params (
:id) - wildcard (
*)
- Middleware chaining ด้วย
next() - Domain-based routing (
app.domain(...)) - Parse body ได้หลายรูปแบบ:
jsonformdatawwwformbinary
- ระบบ Type & Validation ในตัว
- Static file serving (
directory) - Auto route loader จากไฟล์ (
routes) - Helper ครบ:
- cookie
- query
- redirect
- file response
- ใช้งานได้ทั้ง Node.js และ Bun.js
Quick Start
const pipehole = require('pipehole');
const app = new pipehole({
dev: true,
logger: true
});
app.get('/', (req) => {
req.html('<h1>Hello pipehole</h1>');
});
app.listen();โครงสร้างเริ่มต้น (แนะนำ)
project/
routes/
[email protected]
[email protected]
get@user-$id.js
public/
index.html
logo.png
app.jsตัวอย่าง app.js:
const pipehole = require('pipehole');
const app = new pipehole({ port: 3000, dev: true, logger: true });
app
.add('json', ({ load, next }) => load('json', next))
.directory('./public', { pathname: '/static', type: ['png', 'jpg', 'html', 'css', 'js'] })
.routes('./routes');
app.listen();การตั้งค่า new pipehole(options)
hostnamedefault:0.0.0.0portdefault:3000keepAlivedefault:truekeepAliveInitialDelaydefault:5000noDelaydefault:truetimeoutdefault:30000headerTimeoutdefault:10000bodyTimeoutdefault:30000maxHeaderSizedefault:16384maxHeaderCountdefault:100maxHeaderLineSizedefault:8192maxStartLineSizedefault:4096maxPathSizedefault:4096maxBodySizedefault:10485760maxWebSocketMessageSizedefault:10485760websocketOrigindefault:falsedevdefault:false(ถ้าtrueจะพิมพ์ข้อความตอน listen)loggerdefault:false(ถ้าtrueจะ log traffic)use: 'https'ใช้tlsแทนnet(ต้องส่ง options ที่จำเป็นสำหรับ TLS)
ตัวเลือกสำหรับงานจริง / ป้องกัน DDoS
headerTimeoutเวลาสูงสุดที่รอ header จาก client ก่อนตัดการเชื่อมต่อbodyTimeoutเวลาสูงสุดที่รอ body ระหว่าง upload ก่อนตัดการเชื่อมต่อmaxHeaderSizeจำกัดขนาด header รวมทั้งหมด ถ้าเกินจะตอบ431maxHeaderCountจำกัดจำนวน header lines เพื่อลด header abusemaxHeaderLineSizeจำกัดความยาวของ header ต่อบรรทัดmaxStartLineSizeจำกัดความยาว request line (GET /path HTTP/1.1)maxPathSizeจำกัดความยาว path/query ใน request linemaxBodySizeจำกัดขนาด body รวมทั้งหมด ถ้าเกินจะตอบ413maxWebSocketMessageSizeจำกัดขนาด WebSocket frame/message ที่ยอมรับwebsocketOriginกำหนด policy สำหรับOriginตอน handshake- รองรับ
string - รองรับ
string[] - รองรับ
RegExp - รองรับ
function(origin, req) => true | false
- รองรับ
ตัวอย่าง:
const app = new pipehole({
port: 3000,
headerTimeout: 8000,
bodyTimeout: 15000,
maxHeaderSize: 16 * 1024,
maxBodySize: 8 * 1024 * 1024,
maxWebSocketMessageSize: 2 * 1024 * 1024,
websocketOrigin: ['https://app.example.com']
});API หลักของ App
app.listen(callback?)app.type(id, rule)app.types(rules)app.add(id, middleware)app.domain(domain | string[])app.next(...middlewareOrName)app.all(path, ...handler)app.get/post/put/patch/delete/options/head(path, ...handler)app.websocket(path, onStart, onData?, onClose?, onPing?, onPong?)app.directory(dir, options)app.routes(dir)
API ใน Handler (HTTP)
- Response:
req.code(),req.set(),req.http(),req.send(),req.html(),req.json(),req.redirect(),req.file(),req.error() - Request:
req.get(headerKey),req.look(queryKeys),req.u(queryKey),req.load(),req.check(),req.q() - Cookie:
req.gcookie(),req.scookie(),req.rcookie() - EventSource (SSE):
req.event(start, close) - Middleware flow:
req.next() - Validation:
req.test({ type, data, core })
API ใน Handler (WebSocket)
req.start(uid)เริ่ม handshake + ลงทะเบียนผู้ใช้req.stop()req.send(),req.sends(),req.sendto(),req.sendc()req.json(),req.jsons(),req.jsonto(),req.jsonc()req.ping(),req.pong()req.userEach(),req.userHas(),req.userLength()
ระบบ Type/Validation
รองรับ type:
intstringbooleanarrayobjectfile(object ที่มีdataเป็น Buffer)timestamp(YYYY-MM-DDTHH:mm:ss.sssZ)
รองรับเงื่อนไขเสริม:
length: [min, max]หรือlength: exactformat: 'user' | 'email' | 'hex' | 'decimal'list: [...]mime: [...](ใช้กับfile)
ตัวอย่าง:
app
.type('id', { type: 'int', length: [1, 1000000] })
.type('email', { type: 'string', format: 'email', length: [6, 300] })
.type('avatar', { type: 'file', mime: ['image/png', 'image/jpeg'] });
.types({
userid:{ type: 'int', length: [1, 1000000] }
no:{ type: 'int', length: [1, 1000] }
});ตัวอย่างการใช้งานทุกเคสหลัก
1) GET/POST พื้นฐาน
app.get('/', (req) => req.send('ok'));
app.post('/echo', ({ load, json, body }) => {
load('json', () => json({ body }));
});2) Dynamic params และ wildcard
app.get('/user/:id', ({ params, json }) => json({userid: params.id}));
// [GET] /user/1
// {"userid":1}
app.get('/files/*', ({ wildcard, send }) => send(wildcard));
// [GET] /files/asset/logo.svg
// /asset/logo.svg3) Query
app.get('/search', (req) => {
if (!req.look('q')) return req.code(400).send('missing q');
req.json({ q: req.u('q') });
});
// [GET] /search?q=pipehole
// {"q":"pipehole"}4) Middleware chain
app.add('auth', (req) => {
if (!req.look('token')) return req.code(401).send('unauthorized');
req.next();
});
app.next('auth').get('/private', (req) => req.send('ok'));5) Parse body หลายประเภท
app.add('json', ({ load, next }) => load('json', next));
app.add('form', ({ load, next }) => load(['wwwform', 'formdata'], next));6) Validation ด้วย req.test
app.type('id', { type: 'int', length: [1, 999999] });
app.post('/user', 'json', (req) => {
const data = req.test({
type: 'json',
data: { id: { t: 'id', r: true } }
});
if (data !== false) req.json({ ok: true, data });
});7) Cookie
app.get('/cookie/set', (req) => {
req.scookie('sid', 'abc123', { path: '/', httponly: true, 'max-age': 3600 });
req.send('set');
});
app.get('/cookie/get', (req) => req.json({ sid: req.gcookie('sid') || null }));
app.get('/cookie/remove', (req) => { req.rcookie('sid', { path: '/' }); req.send('removed'); });8) Redirect
app.get('/go-doc', (req) => req.redirect('/docs')); // 301
app.get('/go-login', (req) => req.redirect(302, '/login'));9) ส่งไฟล์ / template parameter
app.get('/download', (req) => {
req.file({ file: './public/a.jpg', download: 'photo.jpg' });
});
app.get('/page', (req) => {
req.file({
file: './public/index.html',
parameter: { title: 'Hello', time: new Date().toISOString() }
});
});ตัวอย่างไฟล์ /public/index.html
<h1>{{title}}</h1>
<p>Time: {{time}}</p>10) Static directory
app.directory('./public', {
pathname: '/assets',
type: ['png', 'jpg', 'mp4', 'html', 'css', 'js']
});11) Auto routes จากไฟล์
app.routes('./routes');หลักการตั้งชื่อไฟล์ของ app.routes:
- อ่านเฉพาะไฟล์
.jsในโฟลเดอร์นั้น (ไม่อ่านโฟลเดอร์ย่อย) - รูปแบบหลัก:
[email protected] - ถ้าไม่มี
@จะถือเป็นGETอัตโนมัติ เช่นhome.js=>GET /home methodที่ใช้ได้ตาม API:get,post,put,patch,delete,options,head,all,websocketpathจะถูกแปลงอักขระพิเศษ:-=>/$=>:(path param)\"จะถูกลบออกจาก path- ถ้าชื่อไฟล์มี
@มากกว่า 1 ตัว (เช่นget@[email protected]) ระบบจะไม่ map route ไฟล์นั้น
ตัวอย่างชื่อไฟล์ -> endpoint:
[email protected]=>GET /[email protected]=>GET /home/adminget@user-$id.js=>GET /user/:id[email protected]=>POST /auth/loginhealth.js=>GET /healthall@api-v1-*.js=>ALL /api/v1/*
ตัวอย่างไฟล์ route:
// routes/[email protected]
module.exports = (req) => {
req.json({ ok: true, body: req.body });
};
module.exports.next = 'json'; // ใช้ middleware ที่ add ไว้แล้ว
// module.exports.next = ['auth', 'json'];
// module.exports.next = ()=>{};12) Domain-based routing
app.domain(['api.local', 'localhost'])
.get('/health', (req) => req.json({ ok: true }));
// [GET] http://api.local/health
// [GET] http://localhost/health
// {"ok":true}13) WebSocket
app.websocket('/ws', async (req) => {
const ok = await req.start(req.u('uid') || String(Date.now()));
if (!ok) return req.stop();
req.send('connected');
}, (req) => {
req.send(req.body.payload); // echo
}, (req) => {
console.log('socket closed', req.uid);
});
// ws://127.0.0.1/ws?uid=pipehole14) EventSource (SSE) ด้วย req.event
app.get('/event', (req) => {
let timer;
req.event(() => {
req.write('event: ready\n');
req.write('data: {"status":"connected"}\n\n');
timer = setInterval(() => {
req.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
}, 2000);
}, () => {
clearInterval(timer); // เรียกเมื่อ client ปิดการเชื่อมต่อ
});
});ตัวอย่างฝั่ง client:
const source = new EventSource('/event');
source.addEventListener('ready', (e) => {
console.log('ready:', e.data);
});
source.onmessage = (e) => {
console.log('message:', JSON.parse(e.data));
};
source.onerror = () => {
source.close();
};รูปแบบข้อมูล SSE ที่ต้องส่ง:
- แต่ละ message ควรลงท้ายด้วย
\n\n - ใช้
req.write(...)ส่งข้อมูลตามฟอร์แมตevent:และdata:
หมายเหตุสำคัญ
- ค่า
Hostต้องตรงกับ domain ที่ลง route (หรือใช้*ถ้าต้องการรับทุก host) - ถ้า
req.load(...)ประเภทไม่ตรงกับContent-Typeจะตอบ 400 req.test(...)จะตอบ error อัตโนมัติเมื่อ type/format ไม่ผ่าน- ถ้าใช้
app.routes(dir)โฟลเดอร์ต้องมีไฟล์.jsตาม naming convention
