技术栈:Vue3 + Vite + Element-Plus (前端) / NestJS (后端)

一、项目骨架

mvp-project/
├─ back/                                      # NestJS 后端项目
│  ├─ src/
│  │  ├─ doc01/                           # Doc01 功能模块
│  │  ├─ app.module.ts
│  │  └─ main.ts
│  └─ package.json
└─ front/                                      # Vue3 前端项目
   ├─ src/
   │  ├─ api/                                  # 接口封装
   │  ├─ components/
   │  ├─ App.vue
   │  └─ main.ts
   └─ package.json


二、环境准备:从零搭建开发环境 (本文以编辑器VScode为例,其他编辑器类似)

(注:本节讲述如何安装node.js以及vscode中优化代码、提高效率的小插件;如果已经配置完成直接跳转至第三节)

1. 核心运行环境:Node.js 安装与配置

Node.js 是全栈项目的运行基础,我们必须首先安装它。

1.1 下载与安装
  • 访问 Node.js 官网https://nodejs.org

  • 关键:请下载并安装 LTS(Long Term Support)版本,此版本更稳定,兼容性更好。

  • 运行下载的安装程序(如 node-vxx.xx.x-x64.msi),基本上一路点击 "Next" 即可,安装程序会自动完成大部分配置。

1.2 验证安装

安装完成后,我们需要检查是否成功。

  • 按下 Win + R 键,输入 cmd 打开命令提示符。

  • 依次输入以下命令并回车:

node --version
npm --version

成功标志:如果两条命令都返回了具体的版本号(如下图所示),则说明安装成功。您的版本号可能更高,这是正常的。

node --version  

# 输出示例: v18.17.0
npm --version  

# 输出示例: 9.6.7

注意:如果提示“不是内部或外部命令”,重启vscode或其他编辑器,通常是因为环境变量未刷新;还是不行就重启电脑!(- v -)!

2. 插件推荐

本小节建议的是vscode编辑器中含有的插件(别的编辑器或多或少也有,但是叫啥名就不晓得了)

插件名主要作用为何必需
VolarVue 3 语言支持插件,提供语法高亮、智能感知、类型检查。Vue 3 官方推荐,替代 Vetur。
TypeScript Importer自动管理 TypeScript 文件的导入和导出。避免手动输入 import 语句,提高效率。
Auto Rename Tag自动重命名配对的 HTML/XML 标签。编写 Vue 模板时非常方便。
Path Intellisense文件路径自动补全。快速、准确地输入文件路径。
Prettier - Code formatter代码格式化工具。统一代码风格,让代码更整洁。
Thunder Client轻量级 API 测试工具。用于测试我们后面编写的后端接口。

(温馨提示:建议安装完之后再重启一下vscode以便确保能运行)


三、后端 (NestJS) 实现
1. 环境搭建与依赖安装
# 安装NestJS CLI并创建项目
npm i -g @nestjs/cli
nest new back --package-manager npm

cd back
# 安装ORM、数据库、验证器等依赖
npm i @nestjs/typeorm typeorm sqlite3 class-validator class-transformer
2. 核心代码
  • 实体定义 (src/doc01/doc01.entity.ts)

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class Doc01 {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column({ default: false })
  completed: boolean;
}
  • 数据传输对象 (src/doc01/doc01.dto.ts)

import { IsString, IsOptional, IsBoolean } from 'class-validator';

export class CreateDoc01Dto {
  @IsString()
  title: string;
}

export class UpdateDoc01Dto {
  @IsString()
  @IsOptional()
  title?: string;

  @IsBoolean()
  @IsOptional()
  completed?: boolean;
}
  • 业务逻辑层 (src/doc01/doc01.service.ts)

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Doc01 } from './doc01.entity';
import { CreateDoc01Dto, UpdateDoc01Dto } from './doc01.dto';

@Injectable()
export class Doc01Service {
  constructor(
    @InjectRepository(Doc01)
    private repo: Repository<Doc01>,
  ) {}

  findAll(page = 1, limit = 10) {
    return this.repo.find({
      skip: (page - 1) * limit,
      take: limit,
    });
  }

  create(dto: CreateDoc01Dto) {
    return this.repo.save(dto);
  }

  async update(id: number, dto: UpdateDoc01Dto) {
    await this.repo.update(id, dto);
    return this.repo.findOneBy({ id });
  }

  remove(id: number) {
    return this.repo.delete(id);
  }
}
  • 控制器层 (src/doc01/doc01.controller.ts)

import { Controller, Get, Post, Patch, Delete, Body, Param, Query } from '@nestjs/common';
import { Doc01Service } from './doc01.service';
import { CreateDoc01Dto, UpdateDoc01Dto } from './doc01.dto';

@Controller('doc01')
export class Doc01Controller {
  constructor(private readonly svc: Doc01Service) {}

  @Get()
  list(@Query('page') page = 1, @Query('limit') limit = 10) {
    return this.svc.findAll(+page, +limit);
  }

  @Post()
  create(@Body() dto: CreateDoc01Dto) {
    return this.svc.create(dto);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() dto: UpdateDoc01Dto) {
    return this.svc.update(+id, dto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.svc.remove(+id);
  }
}
  • 模块配置 (src/app.module.ts)

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Doc01 } from './doc01/doc01.entity';
import { Doc01Service } from './doc01/doc01.service';
import { Doc01Controller } from './doc01/doc01.controller';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db.sqlite',
      entities: [Doc01],
      synchronize: true,
    }),
    TypeOrmModule.forFeature([Doc01]),
  ],
  controllers: [Doc01Controller],
  providers: [Doc01Service],
})
export class AppModule {}
  • 应用入口 (src/main.ts)

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
  app.enableCors();
  await app.listen(3000);
  console.log('NestJS running on http://localhost:3000');
}
bootstrap();
3. 启动后端
npm run start:dev

访问 http://localhost:3000/doc01(被占用就换一个端口),若返回空数组 [] 则表示 API 运行正常。


四、前端 (Vue3) 实现
1. 创建项目与安装依赖
npm create vite@latest front --template vue-ts
cd front
npm i element-plus axios
npm i -D unplugin-vue-components
2. 配置与代码
  • Vite 配置 (vite.config.ts)

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    Components({ resolvers: [ElementPlusResolver()] })
  ],
  server: {
    proxy: {
      '/api': 'http://localhost:3000'
    }
  }
})
  • API 接口封装 (src/api/doc01.ts)

import axios from 'axios';

const http = axios.create({ baseURL: '/api' });

export interface Doc01 {
  id: number;
  title: string;
  completed: boolean;
}

export const getDoc01List = (page = 1, limit = 10) =>
  http.get<Doc01[]>('/doc01', { params: { page, limit } }).then(r => r.data);

export const addDoc01 = (title: string) =>
  http.post<Doc01>('/doc01', { title }).then(r => r.data);

export const updateDoc01 = (id: number, dto: { completed?: boolean; title?: string }) =>
  http.patch<Doc01>(`/doc01/${id}`, dto).then(r => r.data);

export const deleteDoc01 = (id: number) => http.delete(`/doc01/${id}`);
  • 主页面组件 (src/App.vue)

注意因为vue基于Javascript,但是注意文件后缀是.vue

<template>
  <el-container style="padding: 24px">
    <el-header>
      <h2>Doc01 MVP — VNEN 技术栈</h2>
    </el-header>
    <el-main>
      <el-input
        v-model="title"
        @keyup.enter="handleAdd"
        placeholder="输入文档标题,按回车添加"
        style="margin-bottom: 20px;"
      />
      <el-table :data="list" v-loading="loading">
        <el-table-column prop="id" label="ID" width="60" />
        <el-table-column prop="title" label="文档标题" />
        <el-table-column label="状态" width="100">
          <template #default="{ row }">
            <el-checkbox
              v-model="row.completed"
              @change="(v: boolean) => handleToggle(row.id, v)"
            >
              {{ row.completed ? '已完成' : '进行中' }}
            </el-checkbox>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80">
          <template #default="{ row }">
            <el-button
              link
              type="danger"
              @click="handleDelete(row.id)"
            >
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-main>
  </el-container>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getDoc01List, addDoc01, updateDoc01, deleteDoc01, type Doc01 } from './api/doc01';

const list = ref<Doc01[]>([]);
const loading = ref(false);
const title = ref('');

const loadData = async () => {
  loading.value = true;
  list.value = await getDoc01List();
  loading.value = false;
};

const handleAdd = async () => {
  if (!title.value.trim()) return;
  await addDoc01(title.value);
  title.value = '';
  await loadData();
};

const handleToggle = async (id: number, completed: boolean) => {
  await updateDoc01(id, { completed });
  await loadData();
};

const handleDelete = async (id: number) => {
  await deleteDoc01(id);
  await loadData();
};

onMounted(loadData);
</script>
3. 启动前端
npm run dev

访问 http://localhost:5173 即可进行 Doc01 文档的增删改查操作。


五、验证与部署
  1. 在 back 目录下运行 npm run start:dev

  2. 在 front 目录下运行 npm run dev

  3. 在浏览器中访问前端地址,进行功能测试(添加、标记状态、删除、刷新页面数据持久化)。


六、功能扩展建议
  • 分页:在前端使用 el-pagination 组件,调用 /doc01?page=2&limit=20 接口。

  • 批量操作:结合 el-table 的多选功能,实现批量删除。

  • API 文档:使用 @nestjs/swagger 为后端自动生成 OpenAPI 文档。

  • 用户认证:使用 NestJS JWT 模块快速集成登录/注册功能。

  • 容器化:编写 Dockerfile 和 docker-compose.yml,实现一键部署。

(写到最后发现这并达不到mvp的体量......)

如果大家在实践过程中出现问题,欢迎在评论区提问。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐