ฟอร์มแจ้งปัญหาบนเว็บ static ที่ไม่มีแบ็กเอนด์สักตัว
ฟอร์มแจ้งปัญหาที่ใช้งานได้จริง ไม่ได้วัดกันที่หน้าตา แต่วัดกันที่ว่าคนแจ้งกดส่งแล้วมันถึงมือคนแก้จริงหรือเปล่า และบอตยิงมั่วแล้วมันเงียบจริงหรือเปล่า
พอดีผมทำเว็บคำนวณโครงสร้างไว้ตัวหนึ่ง เป็นไฟล์นิ่งล้วนทั้งเว็บ คือ HTML กับ JavaScript ที่คำนวณในเบราว์เซอร์ของคนใช้เอง ไม่มีฐานข้อมูล ไม่มี PHP ไม่มีอะไรรันฝั่งเซิร์ฟเวอร์เลยสักบรรทัด วางไว้บนบริการโฮสต์ไฟล์นิ่งของคลาวด์แฟลร์แล้วจบ ค่าโฮสต์เป็นศูนย์ ความเร็วดีมาก
ปัญหามาตอนที่มีคนทักมาว่าสูตรข้อหนึ่งให้ค่าแปลกๆ ผมถามกลับว่าหน้าไหน กรอกอะไรไว้ ได้คำตอบกลับมาไม่ครบสักที สุดท้ายผมเลยตัดสินใจว่าต้องมีปุ่มแจ้งปัญหาในเว็บ ที่กดแล้วส่งข้อความ ส่งรูปจอ และบอกให้ผมรู้ว่าอยู่หน้าไหน มาถึงผมโดยตรง
แต่เว็บมันไม่มีแบ็กเอนด์ ทางเลือกก็เหลือไม่กี่ทาง และผมเลือกทางที่ไม่ต้องเพิ่มเซิร์ฟเวอร์อีกเครื่อง
ชื่อโดเมน คีย์ อีเมลปลายทาง และเลขบัญชีทั้งหมดในบทความนี้ผมสมมติขึ้นใหม่ ส่วนโครงสร้างโค้ด ลำดับการตรวจ และกับดักที่เจอ เป็นของจริงจากงานนั้น
เว็บนิ่งล้วน แต่ต้องมีปลายทางรับหนึ่งจุด
โจทย์ตั้งต้นสั้นมาก คือทั้งเว็บต้องยังเป็นไฟล์นิ่งเหมือนเดิม ยกเว้นเส้นทางเดียวที่ต้องมีโค้ดทำงานฝั่งเซิร์ฟเวอร์ คือเส้นทางที่รับฟอร์มแจ้งปัญหา
ทางเลือกที่มีอยู่ตอนนั้นคือสามทาง หนึ่ง ไปเช่าโฮสต์ PHP เล็กๆ มาวางไฟล์รับฟอร์มไฟล์เดียว สอง ไปใช้บริการฟอร์มสำเร็จรูปของเจ้าอื่น สาม เขียนโค้ดตัวเล็กๆ ให้รันอยู่ที่ขอบเครือข่ายของผู้ให้บริการโฮสต์เดิม
ส่วนตัวผมตัดทางที่หนึ่งทิ้งทันที เพราะการมีเซิร์ฟเวอร์เพิ่มอีกเครื่องแปลว่ามีของที่ต้องอัปเดต ต้องต่ออายุใบรับรอง และต้องคอยดูอีกเครื่องหนึ่ง เพื่อฟอร์มเดียว ส่วนทางที่สองผมตัดเพราะไม่อยากส่งข้อความของคนใช้ผ่านมือที่สามโดยไม่จำเป็น เหลือทางที่สาม ซึ่งกลายเป็นคำตอบสุดท้าย
โฟลเดอร์ functions ที่ไม่ยอมคอมไพล์ กับทางออกเป็นไฟล์เดียว
บริการโฮสต์ไฟล์นิ่งตัวนี้มีสองวิธีให้ใส่โค้ดฝั่งเซิร์ฟเวอร์ วิธีแรกคือสร้างโฟลเดอร์ชื่อ functions/ แล้ววางไฟล์ตามเส้นทางที่อยากได้ ระบบจะคอมไพล์ให้เองตอนดีพลอย วิธีที่สองเรียกว่าโหมดขั้นสูง คือวางไฟล์ _worker.js ไว้ที่รากของโฟลเดอร์ที่จะดีพลอย แล้วไฟล์นั้นจะกลายเป็นตัวรับทุกคำขอของทั้งเว็บ
ผมเริ่มจากวิธีแรกเพราะมันดูเป็นทางหลัก แล้วก็ติดอยู่ตรงนั้นนานเกินควร
ตอนที่โฟลเดอร์ฟังก์ชันคอมไพล์ไม่ผ่าน ผลลัพธ์ที่ได้ไม่ใช่ดีพลอยล้มเหลว แต่เป็นดีพลอยสำเร็จโดยที่ไม่มีเส้นทางนั้นอยู่จริง พอยิงเข้าไปก็ได้หน้า 404 ของเว็บนิ่งกลับมา ซึ่งหน้าตาเหมือนเขียนเส้นทางผิดทุกประการ ผมไล่แก้ชื่อไฟล์กับตำแหน่งโฟลเดอร์อยู่นาน กว่าจะกลับไปอ่านบันทึกการดีพลอยแล้วเห็นว่ามันข้ามขั้นตอนคอมไพล์ไปเงียบๆ ตั้งแต่แรก ถ้าเจออาการเส้นทางหาย ให้ไปอ่านบันทึกการดีพลอยก่อนเสมอ อย่าเพิ่งไปแก้โค้ด
พอเปลี่ยนมาใช้โหมดขั้นสูง ทุกอย่างจบในไฟล์เดียวและไม่มีขั้นตอนคอมไพล์ให้พัง แต่มีข้อแลกเปลี่ยนที่ต้องรู้ก่อน
สะดวกตอนมีหลายเส้นทาง
- แต่ละเส้นทางเป็นไฟล์ของตัวเอง อ่านง่าย
- ไฟล์นิ่งถูกเสิร์ฟให้เองโดยที่เราไม่ต้องเขียนอะไร
- มีขั้นตอนคอมไพล์ตอนดีพลอย ซึ่งพังเงียบได้
- เหมาะกับงานที่มีสิบกว่าเส้นทางขึ้นไป
ไฟล์เดียว คุมทุกคำขอเอง
- ไม่มีขั้นตอนคอมไพล์ ไฟล์ที่เขียนคือไฟล์ที่รัน
- ต้องส่งคำขอที่เหลือไปหาไฟล์นิ่งเองด้วยมือ
- ถ้าลืมบรรทัดนั้น ทั้งเว็บจะกลายเป็นหน้าว่าง
- เหมาะกับงานที่มีเส้นทางเดียวสองเส้นทางแบบนี้
โครงของไฟล์ทั้งไฟล์สั้นแค่นี้ ตัวแยกทางอยู่ข้างบนสุด
// _worker.js — โหมดขั้นสูง: ไฟล์นี้รับทุกคำขอของทั้งเว็บ
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/report') {
if (request.method !== 'POST') return json({ ok: false, error: 'method' }, 405);
return handleReport(request, env);
}
// ทุกอย่างที่ไม่ใช่ /report ส่งต่อให้ตัวเสิร์ฟไฟล์นิ่ง
// บรรทัดนี้หายเมื่อไร ทั้งเว็บกลายเป็นหน้าว่างทันที
return env.ASSETS.fetch(request);
}
};
function json(o, s = 200) {
return new Response(JSON.stringify(o), {
status: s,
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
});
}
บรรทัด env.ASSETS.fetch(request) คือบรรทัดที่คนลืมกันบ่อยที่สุด เพราะในโหมดนี้ไม่มีใครเสิร์ฟไฟล์นิ่งให้เราอีกแล้ว เราคือคนเสิร์ฟเอง ผมแนะนำให้เขียนบรรทัดนั้นก่อนเขียนอย่างอื่นเลย แล้วดีพลอยทดสอบว่าเว็บเดิมยังปกติ ค่อยมาเติมเส้นทางใหม่ทีหลัง
ตรวจกล่องกันบอตที่ฝั่งเซิร์ฟเวอร์ ไม่ใช่ที่หน้าเว็บ
เรื่องนี้เป็นเรื่องที่พลาดกันเยอะที่สุดในฟอร์มสาธารณะ คือติดกล่องกันบอตไว้ในหน้าเว็บ เห็นเครื่องหมายถูกขึ้นเขียวแล้วก็ปล่อยผ่าน โดยไม่เคยเอาผลลัพธ์ไปตรวจกับผู้ให้บริการเลย
สิ่งที่กล่องกันบอตคืนมาให้หน้าเว็บคือสายอักขระหนึ่งชุด ที่ต้องเอาไปถามผู้ให้บริการว่าของจริงหรือเปล่า ถ้าเราไม่ถาม ใครก็ยิงตรงเข้าเส้นทางรับฟอร์มด้วยคำสั่งบรรทัดเดียวโดยไม่ต้องเปิดหน้าเว็บเลยด้วยซ้ำ แล้วส่งค่าอะไรก็ได้มาในช่องนั้น การตรวจต้องเกิดที่ฝั่งเซิร์ฟเวอร์เสมอ และต้องตรวจก่อนทำงานอย่างอื่นทั้งหมด
จุดที่สองที่คนพลาดคือไอพีที่ส่งไปให้ผู้ให้บริการตรวจ ตอนที่โค้ดเรารันอยู่ที่ขอบเครือข่าย ไอพีที่เห็นในระดับการเชื่อมต่อไม่ใช่ไอพีของคนกรอกฟอร์ม ต้องอ่านจากหัวข้อมูลที่ตัวแทนใส่มาให้แทน
async function verifyBot(token, ip, env) {
const p = new URLSearchParams();
p.set('secret', env.TURNSTILE_SECRET || ''); // ค่าเก็บเป็นความลับของโปรเจกต์ ไม่อยู่ในซอร์ส
p.set('response', token);
p.set('remoteip', ip); // ไอพีจริงของคนกรอก ไม่ใช่ของตัวแทน
const r = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: p
});
const vr = await r.json();
return vr.success === true;
}
แล้วในตัวรับฟอร์ม ลำดับต้องเป็นแบบนี้เท่านั้น คืออ่านฟอร์ม ตรวจความยาวข้อความขั้นต่ำ ตรวจกล่องกันบอต แล้วค่อยแตะเรื่องรูปกับเรื่องเมล
async function handleReport(request, env) {
try {
const form = await request.formData();
const message = (form.get('message') || '').toString().trim();
const page = (form.get('page') || '').toString().slice(0, 300);
const contact = (form.get('contact') || '').toString().slice(0, 160);
const token = (form.get('cf-turnstile-response') || '').toString();
const img = form.get('image');
if (message.length < 3) return json({ ok: false, error: 'empty' }, 400);
const ip = request.headers.get('CF-Connecting-IP') || '';
if (!await verifyBot(token, ip, env)) return json({ ok: false, error: 'captcha' }, 403);
const attachments = [];
if (img && typeof img.arrayBuffer === 'function' && img.size > 0) {
if (img.size > 5 * 1024 * 1024) return json({ ok: false, error: 'image_too_big' }, 413);
attachments.push({
filename: (img.name || 'screenshot.png'),
content: toB64(await img.arrayBuffer())
});
}
return sendMail({ message, page, contact, ip, attachments }, env);
} catch (e) {
return json({ ok: false, error: 'server', detail: String(e).slice(0, 200) }, 500);
}
}
สังเกตว่าผมตัดความยาวของทุกช่องที่รับมาจากผู้ใช้ตั้งแต่บรรทัดที่อ่านค่า ไม่ใช่ตอนเอาไปใช้ ช่องหน้าเว็บจำกัดสามร้อยตัวอักษร ช่องติดต่อกลับจำกัดร้อยหกสิบ เพราะสองช่องนี้เป็นช่องที่บอตชอบใช้ยัดข้อความยาวๆ เข้ามามากที่สุด
กล่องกันบอตในหน้าต่างลอย กับการเรนเดอร์ที่ไม่ควรไปยุ่ง
ฟอร์มของผมอยู่ในหน้าต่างลอยที่เปิดขึ้นมาทับหน้าเดิม ไม่ได้อยู่ในหน้าแยก ตอนแรกผมทำตามแบบที่ดูฉลาดกว่า คือสั่งเรนเดอร์กล่องกันบอตเองตอนที่เปิดหน้าต่างลอย เพราะคิดว่าไม่ควรโหลดของที่ยังไม่ได้ใช้
ผลคือมันพังแบบกวนที่สุด คือครั้งแรกทำงานปกติ พอปิดหน้าต่างแล้วเปิดใหม่ กล่องจะไม่ขึ้น หรือขึ้นมาแล้วค้างอยู่แบบนั้นโดยไม่คืนค่าอะไรให้เลย บางครั้งได้กล่องซ้อนกันสองอัน
ทางที่ผมลงเอยคือปล่อยให้สคริปต์ของผู้ให้บริการเรนเดอร์กล่องเองตั้งแต่โหลดหน้า โดยวางกล่องไว้ในหน้าต่างลอยที่ซ่อนอยู่ พอเปิดหน้าต่างก็แค่สั่งล้างค่าเดิมทิ้งหนึ่งครั้ง และสั่งล้างอีกครั้งทุกครั้งที่ส่งไม่สำเร็จ ค่าที่กล่องคืนมาใช้ได้ครั้งเดียวเสมอ ถ้าไม่ล้างแล้วผู้ใช้กดส่งซ้ำ รอบที่สองจะไม่ผ่านการตรวจแน่นอน
<!-- กล่องอยู่ในหน้าต่างลอยตั้งแต่แรก ปล่อยให้เรนเดอร์เองตามปกติ -->
<div class="ts-wrap"><div class="cf-turnstile" data-sitekey="YOUR_SITEKEY_HERE" data-theme="light"></div></div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
function openReport() {
document.getElementById('reportModal').classList.add('on');
document.body.style.overflow = 'hidden';
try { if (window.turnstile) turnstile.reset(); } catch (e) {} // ค่าเก่าใช้ซ้ำไม่ได้
document.addEventListener('paste', onPaste);
}
function closeReport() {
document.getElementById('reportModal').classList.remove('on');
document.body.style.overflow = '';
document.removeEventListener('paste', onPaste); // ถอดตัวดักออกทุกครั้ง
}
จุดเล็กๆ ที่อยากให้สังเกตคือตัวดักเหตุการณ์วางจากคลิปบอร์ด ผมผูกมันตอนเปิดหน้าต่าง และถอดออกตอนปิดเสมอ ถ้าผูกทิ้งไว้ตลอดทั้งหน้า คนที่กดวางอะไรก็ตามในเว็บจะไปโดนตัวดักนี้กินไปหมด ซึ่งเป็นพฤติกรรมที่คนใช้ไม่ได้ขอ
รับรูปจากการกดวาง ไม่ต้องสั่งให้ใครไปเซฟไฟล์ก่อน
ของที่ทำให้ฟอร์มแจ้งปัญหามีประโยชน์ขึ้นสิบเท่าคือรูปจอ แต่ถ้าให้คนกดปุ่มเลือกไฟล์ แปลว่าเขาต้องกดปุ่มจับภาพจอ ไปเปิดโปรแกรมวาดรูป วางลงไป เซฟเป็นไฟล์ แล้วค่อยมาเลือก ซึ่งคนส่วนใหญ่เลิกกลางทาง
ทางที่สั้นที่สุดคือให้เขากดจับภาพจอแล้วมากดวางในฟอร์มได้เลย
function onPaste(e) {
const items = (e.clipboardData || {}).items || [];
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') === 0) {
setImage(items[i].getAsFile()); // ได้ File มาตรงๆ ใช้กับ FormData ได้เลย
e.preventDefault();
return;
}
}
}
function submitReport() {
const fd = new FormData();
fd.append('message', document.getElementById('rMsg').value);
fd.append('contact', document.getElementById('rContact').value);
fd.append('page', location.pathname + location.search);
fd.append('cf-turnstile-response', window.turnstile ? turnstile.getResponse() : '');
if (currentImage) fd.append('image', currentImage, currentImage.name || 'screenshot.png');
fetch('/report', { method: 'POST', body: fd })
.then(r => r.json().catch(() => ({ ok: r.ok })))
.then(showResult);
}
ฝั่งเซิร์ฟเวอร์ต้องแปลงไบต์ของรูปเป็นข้อความฐานหกสิบสี่ก่อนยิงเข้าเมล ซึ่งถ้าเขียนแบบตรงไปตรงมาด้วยการกาง array ทั้งก้อนเข้าฟังก์ชันเดียว รูปขนาดสองสามเมกะไบต์จะทำให้ล้มด้วยอาการกองซ้อนเรียกเกิน ต้องซอยเป็นก้อนย่อย
function toB64(buf) {
let b = '';
const u = new Uint8Array(buf);
const chunk = 0x8000; // ซอยทีละ 32768 ไบต์ ไม่งั้นกองซ้อนเรียกเกิน
for (let i = 0; i < u.length; i += chunk) {
b += String.fromCharCode.apply(null, u.subarray(i, i + chunk));
}
return btoa(b);
}
เพดานห้าเมกะไบต์ผมตั้งจากขนาดรูปจอความละเอียดสูงที่ยังไม่บีบอัด บวกเผื่อไว้นิดหน่อย ถ้าเกินผมตอบรหัสสถานะที่บอกว่าเนื้อหาใหญ่เกินไปกลับไปตรงๆ แล้วให้หน้าเว็บแปลเป็นข้อความไทยให้คนอ่านเข้าใจ ไม่ใช่ปล่อยให้หมุนค้าง
ยิงเมลจากเครื่องตัวเองโดนบล็อก แต่ยิงจากขอบเครือข่ายผ่าน
ตอนทดสอบว่า API ของผู้ให้บริการเมลใช้งานได้ไหม ผมเขียนสคริปต์สั้นๆ บนเครื่องตัวเองยิงไปก่อน เพราะมันเร็วกว่าการดีพลอยแล้วค่อยลอง ผลคือโดนปฏิเสธด้วยรหัสความผิดพลาดของด่านหน้าเว็บ ที่หมายถึงคำขอถูกกันด้วยกฎของเจ้าของโดเมนปลายทาง
ผมเสียเวลาไปพอสมควรกับการเดาว่าคีย์ผิดหรือเปล่า จนกระทั่งลองดีพลอยขึ้นไปแล้วยิงจากโค้ดที่รันอยู่ที่ขอบเครือข่ายจริง ปรากฏว่าผ่านฉลุยตั้งแต่ครั้งแรก
ผู้ให้บริการหลายเจ้ามีกฎกันการเรียกใช้จากไอพีบ้านหรือไอพีที่ไม่รู้จัก ซึ่งเป็นกฎที่ถูกต้องของเขา ไม่ใช่บั๊ก เวลาทดสอบ API ที่จะเรียกใช้จริงจากโครงสร้างพื้นฐานตัวไหน ให้ทดสอบจากที่นั่นตั้งแต่แรก อย่าทดสอบจากเครื่องตัวเองแล้วสรุปว่าคีย์ใช้ไม่ได้ ผมเสียเวลาไปกับเรื่องนี้จนดึกโดยไม่จำเป็น
ตัวส่งเมลทั้งก้อนหน้าตาแบบนี้ จุดที่ต้องระวังคือการหนีอักขระพิเศษก่อนเอาข้อความของผู้ใช้ไปวางใน HTML ของเมล เพราะมีคนส่งวงเล็บมุมมาแน่นอน
function esc(s) {
return String(s).replace(/[&<>"]/g, c =>
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
}
async function sendMail({ message, page, contact, ip, attachments }, env) {
if (!env.MAIL_API_KEY) return json({ ok: false, error: 'mail_unconfigured' }, 503);
const html = `<div style="font-family:system-ui,sans-serif;color:#1e293b">
<h3>แจ้งข้อผิดพลาดจากหน้าเว็บ</h3>
<p><b>หน้า:</b> ${esc(page)}</p>
${contact ? `<p><b>ผู้แจ้ง:</b> ${esc(contact)}</p>` : ''}
<pre style="white-space:pre-wrap;background:#f1f5f9;padding:12px;border-radius:8px">${esc(message)}</pre>
<p style="color:#94a3b8;font-size:12px">ไอพี ${esc(ip)} · ${new Date().toISOString()}</p>
</div>`;
const r = await fetch('https://api.example-mail.com/v1/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.MAIL_API_KEY}`,
'content-type': 'application/json'
},
body: JSON.stringify({
from: env.MAIL_FROM,
to: [env.MAIL_TO],
reply_to: contact && contact.includes('@') ? contact : undefined,
subject: 'แจ้งปัญหาจากเว็บ',
html,
attachments
})
});
if (!r.ok) return json({ ok: false, error: 'send', detail: (await r.text()).slice(0, 200) }, 502);
return json({ ok: true });
}
อีกจุดที่ผมยืนยันว่าต้องทำคือ อีเมลที่ผู้แจ้งกรอกเข้ามาต้องไปอยู่ในช่องตอบกลับเท่านั้น ห้ามเอาไปใส่ช่องผู้ส่งเด็ดขาด เพราะการส่งเมลโดยอ้างชื่อโดเมนของคนอื่นคือทางตรงที่สุดที่จะทำให้โดเมนของเราติดบัญชีดำ ช่องผู้ส่งต้องผูกกับโดเมนของเราเสมอ
ค่าลับอยู่ที่ไหน และอย่าให้มันเข้าไปอยู่ในซอร์ส
โค้ดทั้งไฟล์ไม่มีคีย์ ไม่มีความลับ ไม่มีอีเมลปลายทางแม้แต่ตัวเดียว ทุกอย่างอ่านจากตัวแปรสภาพแวดล้อมของโปรเจกต์ ซึ่งตั้งผ่านเครื่องมือบรรทัดคำสั่งหรือหน้าตั้งค่าของบริการ
# ตั้งค่าลับของโปรเจกต์ (พิมพ์ค่าตอนที่มันถาม ไม่ใส่ในบรรทัดคำสั่งให้ติดประวัติเชลล์)
npx wrangler pages secret put TURNSTILE_SECRET
npx wrangler pages secret put MAIL_API_KEY
# ค่าที่ไม่ลับ ใส่เป็นตัวแปรธรรมดาได้
npx wrangler pages deploy ./public --project-name my-static-site
# ทดสอบว่าเส้นทางมีอยู่จริง ต้องได้ 403 เพราะยังไม่ผ่านกล่องกันบอต ไม่ใช่ 404
curl -s -o /dev/null -w '%{http_code}\n' -X POST -F 'message=ทดสอบ' https://example.com/report
ได้ 404 แปลว่าเส้นทางยังไม่มีอยู่จริง ดีพลอยไม่ติด ได้ 200 แปลว่ากล่องกันบอตไม่ได้ทำงาน ใครก็ยิงเข้ามาได้ ต้องได้ 403 เท่านั้นถึงจะแปลว่าเส้นทางมีอยู่และด่านตรวจทำงานอยู่ ผมเขียนคำสั่งบรรทัดนี้ไว้ในบันทึกการดีพลอยของโปรเจกต์เลย เพราะเป็นการตรวจที่เร็วที่สุดว่าของยังดีอยู่ไหมหลังแก้อะไรก็ตาม
ลำดับที่ผมทำ ถ้าจะทำใหม่อีกรอบ
- วางไฟล์ _worker.js ที่มีแค่บรรทัดส่งต่อไปหาไฟล์นิ่ง แล้วดีพลอย ยืนยันว่าเว็บเดิมไม่เปลี่ยนอะไรเลย นี่คือด่านที่พังแล้วเห็นทันที
- เพิ่มเส้นทางรับฟอร์มที่ตอบกลับมาว่ายังไม่พร้อม แล้วยิงด้วยคำสั่งบรรทัดเดียวให้เห็นว่าเส้นทางมีอยู่จริง
- ใส่การตรวจกล่องกันบอตเป็นอย่างแรกในตัวรับ ทดสอบว่ายิงโดยไม่มีค่าแล้วได้ 403
- ต่อหน้าเว็บ ใส่กล่องกันบอตแบบเรนเดอร์เอง และสั่งล้างค่าทุกครั้งที่เปิดหน้าต่างและทุกครั้งที่ส่งไม่ผ่าน
- ต่อ API เมล โดยทดสอบจากโค้ดที่ดีพลอยแล้วเท่านั้น ไม่ทดสอบจากเครื่องตัวเอง
- ค่อยมาเติมเรื่องรูปแนบทีหลัง เพราะเป็นส่วนที่พังแล้วไม่กระทบการแจ้งข้อความธรรมดา
ลำดับนี้สำคัญตรงที่ทุกขั้นมีตัวชี้วัดของตัวเองว่าผ่านหรือไม่ผ่าน ไม่ต้องเดา ผมเคยทำสลับลำดับแล้วเจอสภาพที่ไม่รู้ว่าปัญหาอยู่ที่การดีพลอย ที่กล่องกันบอต หรือที่ผู้ให้บริการเมล ซึ่งไล่ยากกว่ากันหลายเท่า
สรุป
ทั้งระบบนี้คือไฟล์ JavaScript ไฟล์เดียวประมาณเจ็ดสิบบรรทัด บวกกับ HTML ในหน้าต่างลอยอีกไม่กี่สิบบรรทัด ไม่มีเซิร์ฟเวอร์เพิ่ม ไม่มีฐานข้อมูล ไม่มีอะไรต้องต่ออายุ และค่าใช้จ่ายยังเป็นศูนย์เหมือนเดิม
ส่วนตัวผมคิดว่าบทเรียนที่เอาไปใช้ต่อได้มากที่สุดมีสองข้อ ข้อแรกคือด่านกันบอตที่ไม่ได้ตรวจซ้ำที่ฝั่งเซิร์ฟเวอร์ ไม่ได้กันอะไรเลย มันกันแค่คนที่ใช้ฟอร์มตามที่เราออกแบบ ซึ่งไม่ใช่กลุ่มที่เรากลัว ข้อที่สองคือทดสอบการเรียก API จากที่ที่มันจะรันจริง ไม่ใช่จากเครื่องที่สะดวกที่สุด เพราะกฎกันของผู้ให้บริการมองที่มาของคำขอด้วย ไม่ได้มองแค่คีย์
ใครจะเลือกใช้โฟลเดอร์ฟังก์ชันแทนไฟล์เดียวก็ตามสะดวกครับ ถ้าเส้นทางเยอะมันอ่านง่ายกว่าจริง แค่ต้องไปอ่านบันทึกการดีพลอยทุกครั้งว่ามันคอมไพล์ผ่านจริงหรือเปล่า
ปล. เรื่องที่ผมยังไม่ได้ทำในรอบนี้คือการจำกัดจำนวนครั้งต่อไอพีต่อชั่วโมง ตอนนี้พึ่งกล่องกันบอตอย่างเดียว ซึ่งพอสำหรับปริมาณที่เว็บนี้มี แต่ถ้าวันไหนโดนยิงถี่ๆ จากคนที่ผ่านด่านได้จริง ผมต้องมีที่เก็บตัวนับสักตัวที่ขอบเครือข่าย เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย