AI教你常识之 ESM + Express + EJS + 表单POST + CSV文件

太棒了!你希望在 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

✅ 第七步:浏览器访问测试

  1. 👉 http://localhost:3000

    → 显示空表格(首次运行)

  2. 👉 点击"添加新用户" → 填写表单 → 提交

    → 自动跳转回首页,新用户已显示!

  3. 👉 查看 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 直接编辑数据 ------ 修改后刷新页面立即生效!

这是你第一个"文件驱动"的全栈应用 ------ 继续扩展它吧!

相关推荐
xiaopengbc3 小时前
在Webpack中,如何在不同环境中使用不同的API地址?
前端·webpack·node.js
Gogo8163 小时前
java与node.js对比
java·node.js
王蛋1114 小时前
前端工作问题或知识记录
前端·npm·node.js
Swift社区5 小时前
为什么 socket.io 客户端在浏览器能连上,但在 Node.js 中报错 transport close?
javascript·node.js
萌萌哒草头将军10 小时前
Node.js v24.8.0 新功能预览!🚀🚀🚀
前端·javascript·node.js
Adorable老犀牛10 小时前
可遇不可求的自动化运维工具 | 2 | 实施阶段一:基础准备
运维·git·vscode·python·node.js·自动化
若无_10 小时前
npm 与 pnpm 深度对比:从依赖管理到实际选型
npm·node.js
GISer_Jing10 小时前
Next系统学习(二)
前端·javascript·node.js
BillKu11 小时前
vue3 中 npm install mammoth 与 npm install --save mammoth 的主要区别说明
前端·npm·node.js