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

27 阅读4分钟

先澄清一个关键点:

❗️ Vue 3 是前端框架,运行在浏览器;Express 是后端框架,运行在 Node.js。它们不能“共用模板”或“共用 EJS”。


✅ 正确理解你的需求:

你想构建一个:

  • 后端:使用 Express + pnpm + ESM,提供 API 读写 CSV 文件
  • 前端:使用 Vue 3(不是 EJS!),通过 AJAX 调用后端 API,渲染数据、提交表单
  • 数据存储:CSV 文件(由 Express 后端读写)

🚀 完整项目:Vue3 + Express + CSV 文件系统(前后端分离)


📁 项目结构

my-vue-express-csv-app/
├── backend/           👈 Express 后端(读写 CSV)
│   ├── index.js
│   ├── csvUtils.js
│   ├── package.json
│   └── data/users.csv
│
└── frontend/          👈 Vue 3 前端(调用 API)
    ├── package.json
    ├── vite.config.js
    └── src/
        ├── main.js
        ├── App.vue
        └── components/
            ├── UserList.vue
            └── UserForm.vue

✅ 第一部分:搭建 Express 后端(CSV 读写 API)

步骤1:创建 backend 项目

mkdir -p my-vue-express-csv-app/backend
cd my-vue-express-csv-app/backend
pnpm init

backend/package.json

{
  "name": "backend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "nodemon index.js"
  }
}

安装依赖:

pnpm add express cors
pnpm add -D nodemon

cors 用于允许前端跨域访问


步骤2:创建 csvUtils.js

// backend/csvUtils.js
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');

await fs.mkdir(DATA_DIR, { recursive: true });

export async function readUsers() {
  try {
    const data = await fs.readFile(CSV_FILE, 'utf8');
    const lines = data.trim().split('\n');
    if (lines.length <= 1) 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 created');
      return [];
    }
    throw error;
  }
}

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 ${user.name} written to CSV`);
  return { id: nextId, ...user, created_at: now };
}

步骤3:创建 index.js(Express API)

// backend/index.js
import express from 'express';
import cors from 'cors';
import { readUsers, writeUser } from './csvUtils.js';

const app = express();
const PORT = process.env.PORT || 3001;

app.use(cors()); // 允许前端访问
app.use(express.json()); // 解析 JSON 请求体

// 👇 GET /api/users → 读取所有用户
app.get('/api/users', async (req, res) => {
  try {
    const users = await readUsers();
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: 'Failed to read users' });
  }
});

// 👇 POST /api/users → 创建新用户
app.post('/api/users', async (req, res) => {
  const { name, email, age } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }

  try {
    const newUser = await writeUser({ name, email, age });
    res.status(201).json(newUser);
  } catch (error) {
    res.status(500).json({ error: 'Failed to write user' });
  }
});

app.listen(PORT, () => {
  console.log(`✅ Backend running at http://localhost:${PORT}`);
});

启动后端:

cd backend
pnpm dev

✅ 后端 API 已就绪:

  • GET http://localhost:3001/api/users → 获取用户列表
  • POST http://localhost:3001/api/users → 创建用户(JSON 格式)

✅ 第二部分:搭建 Vue 3 前端

步骤1:创建 frontend 项目(使用 Vite)

在项目根目录:

cd ..
pnpm create vite frontend --template vue
cd frontend

安装依赖:

pnpm install

步骤2:配置代理(解决开发环境跨域)

创建 frontend/vite.config.js

// frontend/vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001', // 后端地址
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '/api')
      }
    }
  }
});

✅ 这样前端 http://localhost:5173/api/users 会自动代理到 http://localhost:3001/api/users


步骤3:创建组件

📄 frontend/src/components/UserList.vue

<!-- frontend/src/components/UserList.vue -->
<template>
  <div>
    <h2>👥 用户列表</h2>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error" class="error">{{ error }}</div>
    <table v-else-if="users.length > 0">
      <thead>
        <tr>
          <th>ID</th>
          <th>姓名</th>
          <th>邮箱</th>
          <th>年龄</th>
          <th>创建时间</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="user in users" :key="user.id">
          <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>
    <p v-else>暂无用户</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const users = ref([]);
const loading = ref(false);
const error = ref('');

const fetchUsers = async () => {
  loading.value = true;
  error.value = '';
  try {
    const res = await fetch('/api/users');
    if (!res.ok) throw new Error('Failed to fetch');
    users.value = await res.json();
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};

onMounted(fetchUsers);
</script>

<style scoped>
table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 20px;
}
th, td {
  padding: 10px;
  border: 1px solid #ddd;
  text-align: left;
}
th {
  background-color: #f5f5f5;
}
.error {
  color: red;
}
</style>

📄 frontend/src/components/UserForm.vue

<!-- frontend/src/components/UserForm.vue -->
<template>
  <div>
    <h2>➕ 添加新用户</h2>
    <form @submit.prevent="handleSubmit" class="form">
      <div>
        <label>姓名:</label>
        <input v-model="formData.name" required />
      </div>
      <div>
        <label>邮箱:</label>
        <input v-model="formData.email" type="email" required />
      </div>
      <div>
        <label>年龄:</label>
        <input v-model.number="formData.age" type="number" min="1" max="150" />
      </div>
      <button type="submit" :disabled="loading">
        {{ loading ? '提交中...' : '✅ 提交' }}
      </button>
      <div v-if="error" class="error">{{ error }}</div>
      <div v-if="success" class="success">🎉 用户添加成功!</div>
    </form>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const formData = ref({
  name: '',
  email: '',
  age: null
});

const loading = ref(false);
const error = ref('');
const success = ref(false);

const handleSubmit = async () => {
  loading.value = true;
  error.value = '';
  success.value = false;

  try {
    const res = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData.value)
    });

    if (!res.ok) {
      const err = await res.json();
      throw new Error(err.error || '提交失败');
    }

    const newUser = await res.json();
    console.log('新用户:', newUser);

    success.value = true;
    formData.value = { name: '', email: '', age: null }; // 重置表单

    // 可选:3秒后自动刷新列表(或 emit 事件让父组件刷新)
    setTimeout(() => {
      success.value = false;
    }, 3000);
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};
</script>

<style scoped>
.form {
  max-width: 500px;
  margin: 20px 0;
  padding: 20px;
  background: #f9f9f9;
  border-radius: 8px;
}
.form div {
  margin-bottom: 15px;
}
.form label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}
.form input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}
button {
  padding: 10px 20px;
  background: #4a90e2;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
button:disabled {
  background: #ccc;
}
.error {
  color: red;
  margin-top: 10px;
}
.success {
  color: green;
  margin-top: 10px;
}
</style>

步骤4:修改 frontend/src/App.vue

<!-- frontend/src/App.vue -->
<template>
  <div class="container">
    <h1>Vue3 + Express + CSV 用户管理系统</h1>
    <UserForm @user-added="refreshUsers" />
    <UserList />
  </div>
</template>

<script setup>
import UserList from './components/UserList.vue';
import UserForm from './components/UserForm.vue';

const refreshUsers = () => {
  // 简单刷新:重新加载页面(或通过 provide/inject 优化)
  window.location.reload();
};
</script>

<style>
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}
h1 {
  color: #333;
  text-align: center;
  margin-bottom: 30px;
}
</style>

步骤5:运行前端

cd frontend
pnpm dev

访问 👉 http://localhost:5173


✅ 最终效果

  1. 后端 Express 在 http://localhost:3001 运行,读写 backend/data/users.csv
  2. 前端 Vue 3 在 http://localhost:5173 运行,通过 /api/users 代理访问后端
  3. 页面显示用户列表(从 CSV 读取)
  4. 表单提交 → POST → 写入 CSV → 页面刷新显示新数据

📂 最终项目结构

my-vue-express-csv-app/
├── backend/
│   ├── index.js
│   ├── csvUtils.js
│   ├── package.json
│   └── data/users.csv
│
└── frontend/
    ├── package.json
    ├── vite.config.js
    └── src/
        ├── main.js
        ├── App.vue
        └── components/
            ├── UserList.vue
            └── UserForm.vue

🎉 恭喜你!

你已成功构建:

✅ Vue 3 前端(现代 Composition API)
✅ Express + ESM + pnpm 后端
✅ CSV 文件持久化存储
✅ 前后端分离架构
✅ 无数据库、无编译依赖、100% 兼容 Windows