太棒了!你希望在 Express + pnpm + ESM + EJS(不使用 expressLayouts) 项目基础上:
✅ 从 CSV 文件读取数据 → 渲染到 EJS 模板
✅ 添加表单 → POST 提交 → 追加写入 CSV 文件
✅ 不用数据库,纯文件操作,轻量简单
✅ 保持 ESM + pnpm + 现代语法
🚀 项目目标:CSV 用户管理系统
- 数据存储在
data/users.csv
- 首页
/
→ 读取 CSV → 渲染表格 - 表单页
/add
→ 填写用户 → POST → 追加到 CSV - 无需数据库,无需编译,100% 兼容 Windows!
✅ 第一步:初始化项目 & 安装依赖
bash
mkdir express-ejs-csv
cd express-ejs-csv
pnpm init -y
修改 package.json
:
json
{
"name": "express-ejs-csv",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "nodemon index.js"
}
}
安装依赖:
bash
pnpm add express ejs
pnpm add -D nodemon
✅ 注意:不需要 sqlite3 或任何数据库模块!
✅ 第二步:创建 CSV 工具模块 csvUtils.js
js
// csvUtils.js ------ 读写 CSV 文件的工具函数
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DATA_DIR = path.join(__dirname, 'data');
const CSV_FILE = path.join(DATA_DIR, 'users.csv');
// 确保 data 目录存在
await fs.mkdir(DATA_DIR, { recursive: true });
// 👇 读取 CSV → 返回对象数组
export async function readUsers() {
try {
const data = await fs.readFile(CSV_FILE, 'utf8');
const lines = data.trim().split('\n');
if (lines.length === 0) return [];
const headers = lines[0].split(',');
const users = [];
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue;
const values = lines[i].split(',');
const user = {};
headers.forEach((header, index) => {
user[header] = values[index] || '';
});
users.push(user);
}
return users;
} catch (error) {
if (error.code === 'ENOENT') {
// 文件不存在,创建并写入表头
await fs.writeFile(CSV_FILE, 'id,name,email,age,created_at\n', 'utf8');
console.log('✅ users.csv 文件已创建');
return [];
}
throw error;
}
}
// 👇 写入新用户到 CSV(追加)
export async function writeUser(user) {
const users = await readUsers(); // 读取现有数据(确保表头存在)
const nextId = users.length > 0 ? Math.max(...users.map(u => parseInt(u.id))) + 1 : 1;
const now = new Date().toISOString();
const line = `${nextId},${user.name},${user.email},${user.age || ''},${now}\n`;
await fs.appendFile(CSV_FILE, line, 'utf8');
console.log(`✅ 用户 ${user.name} 已写入 CSV`);
}
✅ 第三步:创建入口文件 index.js
js
// index.js
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import { readUsers, writeUser } from './csvUtils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3000;
// 设置 EJS
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// 解析表单
app.use(express.urlencoded({ extended: true }));
// 静态文件
app.use(express.static(path.join(__dirname, 'public')));
// ========== 路由 ==========
// 👇 首页:读取 CSV 并渲染
app.get('/', async (req, res) => {
const users = await readUsers();
res.render('index', {
title: 'CSV 用户列表',
users: users
});
});
// 👇 显示添加表单
app.get('/add', (req, res) => {
res.render('add', {
title: '添加新用户'
});
});
// 👇 处理表单提交
app.post('/users', async (req, res) => {
const { name, email, age } = req.body;
if (!name || !email) {
return res.status(400).send('姓名和邮箱为必填项');
}
try {
await writeUser({ name, email, age });
res.redirect('/'); // 成功后跳转回首页
} catch (error) {
console.error('❌ 写入失败:', error);
res.status(500).send('服务器错误,请重试');
}
});
app.listen(PORT, () => {
console.log(`✅ Server running at http://localhost:${PORT}`);
});
✅ 第四步:创建 EJS 模板
📄 views/index.ejs
ejs
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>👥 <%= title %></h1>
<a href="/add" class="btn">➕ 添加新用户</a>
<% if (users.length === 0) { %>
<p>暂无用户数据,请添加。</p>
<% } else { %>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>年龄</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
<% users.forEach(user => { %>
<tr>
<td><%= user.id %></td>
<td><%= user.name %></td>
<td><%= user.email %></td>
<td><%= user.age || '---' %></td>
<td><%= new Date(user.created_at).toLocaleString() %></td>
</tr>
<% }) %>
</tbody>
</table>
<% } %>
</div>
</body>
</html>
📄 views/add.ejs
ejs
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>➕ <%= title %></h1>
<form action="/users" method="POST" class="form-box">
<div class="form-group">
<label>姓名:</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>邮箱:</label>
<input type="email" name="email" required>
</div>
<div class="form-group">
<label>年龄(可选):</label>
<input type="number" name="age" min="1" max="150">
</div>
<button type="submit" class="btn-submit">✅ 提交</button>
<a href="/" class="btn-back">⬅️ 返回</a>
</form>
</div>
</body>
</html>
✅ 第五步:创建静态资源
📁 创建目录:
bash
mkdir -p views public/css data
📄 public/css/style.css
(复用之前的样式)
css
body {
font-family: Arial, sans-serif;
background-color: #f5f7fa;
margin: 0;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #4a90e2;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #4a90e2;
color: white;
text-decoration: none;
border-radius: 4px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f8f9fa;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f8f9fa;
}
.form-box {
max-width: 500px;
margin: 30px auto;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
}
.btn-submit {
padding: 10px 20px;
background: #4a90e2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.btn-submit:hover {
background: #357abD;
}
.btn-back {
display: inline-block;
margin-top: 15px;
color: #4a90e2;
text-decoration: none;
}
.btn-back:hover {
text-decoration: underline;
}
✅ 第六步:运行项目
bash
pnpm dev
首次运行时,会自动创建:
bash
data/users.csv
内容:
bash
id,name,email,age,created_at
✅ 第七步:浏览器访问测试
-
→ 显示空表格(首次运行)
-
👉 点击"添加新用户" → 填写表单 → 提交
→ 自动跳转回首页,新用户已显示!
-
👉 查看
data/users.csv
文件,数据已追加!
📂 最终项目结构
csharp
express-ejs-csv/
├── index.js
├── csvUtils.js
├── package.json
├── pnpm-lock.yaml
├── public/
│ └── css/
│ └── style.css
├── views/
│ ├── index.ejs
│ └── add.ejs
└── data/
└── users.csv 👈 自动生成 + 数据持久化
🧠 为什么选 CSV?
- ✅ 无需数据库,零配置
- ✅ 纯文本,可直接用 Excel 打开编辑
- ✅ 适合学习、原型、小型项目
- ✅ 100% 兼容所有系统(Windows/macOS/Linux)
- ✅ 无编译依赖,安装即用
🚨 注意事项
- CSV 不适合大数据量(>1000 行)或高并发
- 不支持复杂查询(如 WHERE、JOIN)
- 无事务支持(并发写入可能冲突)
- 但 ------ 对学习和小项目来说,完美够用!
🎁 Bonus:添加简单搜索功能
修改 index.js
:
js
app.get('/', async (req, res) => {
const users = await readUsers();
const search = req.query.search?.toLowerCase() || '';
const filteredUsers = search
? users.filter(user =>
user.name.toLowerCase().includes(search) ||
user.email.toLowerCase().includes(search)
)
: users;
res.render('index', {
title: 'CSV 用户列表',
users: filteredUsers,
search
});
});
在 views/index.ejs
中添加搜索框:
ejs
<form method="GET" style="margin-bottom: 20px;">
<input
type="text"
name="search"
placeholder="搜索姓名或邮箱..."
value="<%= search || '' %>"
style="padding: 8px; width: 300px;"
>
<button type="submit">🔍 搜索</button>
</form>
🎉 恭喜你!
你已成功构建:
✅ Express + pnpm + ESM + EJS + CSV 文件系统
✅ 从 CSV 读取 → 渲染到模板
✅ 表单提交 → POST → 追加写入 CSV
✅ 无数据库、无编译、无兼容问题
✅ 数据持久化在 data/users.csv
📌 现在你可以用 Excel 打开 data/users.csv
直接编辑数据 ------ 修改后刷新页面立即生效!
这是你第一个"文件驱动"的全栈应用 ------ 继续扩展它吧!