Node.js 构建简单应用
把路由、静态文件、模板、表单拼成一个能跑的待办应用。
Key Points一个 Web 应用的最小闭环:路由分发 → 数据读写 → 页面渲染 → 表单提交。
项目结构
Text
todo-app/
├── package.json
├── server.js
├── store.js ← 内存存储,之后可换数据库
├── views/
│ └── index.html
└── public/
└── style.css数据层
JavaScript
// store.js
let seq = 0;
const items = [];
module.exports = {
list: () => items.slice(),
add(text) {
const item = { id: ++seq, text, done: false };
items.push(item);
return item;
},
toggle(id) {
const it = items.find((i) => i.id === Number(id));
if (it) it.done = !it.done;
return it;
},
remove(id) {
const i = items.findIndex((x) => x.id === Number(id));
if (i === -1) return false;
items.splice(i, 1);
return true;
},
};服务层
JavaScript
// server.js
const express = require('express');
const store = require('./store');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));
app.get('/', (req, res) => {
const items = store.list();
const rows = items
.map(
(i) => `<li class="${i.done ? 'done' : ''}">
<form method="post" action="/toggle/${i.id}" style="display:inline">
<button>${i.done ? '✓' : '○'}</button>
</form>
${i.text}
<form method="post" action="/delete/${i.id}" style="display:inline">
<button>×</button>
</form>
</li>`
)
.join('');
res.send(`<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>待办</title>
<link rel="stylesheet" href="/style.css"></head>
<body>
<h1>待办事项</h1>
<form method="post" action="/add">
<input name="text" required placeholder="要做什么">
<button>添加</button>
</form>
<ul>${rows || '<li>还没有事项</li>'}</ul>
</body></html>`);
});
app.post('/add', (req, res) => {
const text = (req.body.text || '').trim();
if (text) store.add(text);
res.redirect(303, '/');
});
app.post('/toggle/:id', (req, res) => {
store.toggle(req.params.id);
res.redirect(303, '/');
});
app.post('/delete/:id', (req, res) => {
store.remove(req.params.id);
res.redirect(303, '/');
});
app.listen(3000, () => console.log('http://127.0.0.1:3000'));跑起来
Bash
npm init -y
npm install express
node server.jsOutput
http://127.0.0.1:3000Open the browser, type "buy milk" and submit: the page reloads with the item. Click ○ to mark done, × to delete. Zero client-side JS — it is all form posts and server-side re-rendering.
下一步
| Improvement | How |
|---|---|
| 数据持久化 | 把 store.js 换成 SQLite 或 JSON 文件 |
| 模板分离 | 用 EJS / Handlebars,别在 JS 里拼 HTML |
| 表单校验 | 服务端校验必填和长度,前端只做体验 |
| 防重复提交 | PRG 模式(Post-Redirect-Get),本项目已用 |
res.redirect(303, '/') rather than 302 — 303 explicitly tells the client to switch to GET, which is the PRG standard. With 302 some clients re-post.项目结构
Text
todo-app/
├── package.json
├── server.js
├── store.js ← 内存存储,之后可换数据库
├── views/
│ └── index.html
└── public/
└── style.css数据层
JavaScript
// store.js
let seq = 0;
const items = [];
module.exports = {
list: () => items.slice(),
add(text) {
const item = { id: ++seq, text, done: false };
items.push(item);
return item;
},
toggle(id) {
const it = items.find((i) => i.id === Number(id));
if (it) it.done = !it.done;
return it;
},
remove(id) {
const i = items.findIndex((x) => x.id === Number(id));
if (i === -1) return false;
items.splice(i, 1);
return true;
},
};服务层
JavaScript
// server.js
const express = require('express');
const store = require('./store');
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));
app.get('/', (req, res) => {
const items = store.list();
const rows = items
.map(
(i) => `<li class="${i.done ? 'done' : ''}">
<form method="post" action="/toggle/${i.id}" style="display:inline">
<button>${i.done ? '✓' : '○'}</button>
</form>
${i.text}
<form method="post" action="/delete/${i.id}" style="display:inline">
<button>×</button>
</form>
</li>`
)
.join('');
res.send(`<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>待办</title>
<link rel="stylesheet" href="/style.css"></head>
<body>
<h1>待办事项</h1>
<form method="post" action="/add">
<input name="text" required placeholder="要做什么">
<button>添加</button>
</form>
<ul>${rows || '<li>还没有事项</li>'}</ul>
</body></html>`);
});
app.post('/add', (req, res) => {
const text = (req.body.text || '').trim();
if (text) store.add(text);
res.redirect(303, '/');
});
app.post('/toggle/:id', (req, res) => {
store.toggle(req.params.id);
res.redirect(303, '/');
});
app.post('/delete/:id', (req, res) => {
store.remove(req.params.id);
res.redirect(303, '/');
});
app.listen(3000, () => console.log('http://127.0.0.1:3000'));跑起来
Bash
npm init -y
npm install express
node server.jsOutput
http://127.0.0.1:3000打开浏览器,输入「买牛奶」点添加,页面刷新后出现一条待办;点 ○ 变成 ✓;点 × 删除。整个过程没有一行前端 JS——全部靠表单提交加服务端重渲染。
下一步
| 改进 | 怎么做 |
|---|---|
| 数据持久化 | 把 store.js 换成 SQLite 或 JSON 文件 |
| 模板分离 | 用 EJS / Handlebars,别在 JS 里拼 HTML |
| 表单校验 | 服务端校验必填和长度,前端只做体验 |
| 防重复提交 | PRG 模式(Post-Redirect-Get),本项目已用 |
res.redirect(303, '/') 用 303 而不是 302 —— 303 明确要求浏览器把后续请求改成 GET,这是 PRG 模式的标准做法。用 302 时某些客户端会重发 POST。