第一弹:VNEN 技术栈构建 MVP (Doc01 示例)
技术栈: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编辑器中含有的插件(别的编辑器或多或少也有,但是叫啥名就不晓得了)
| 插件名 | 主要作用 | 为何必需 |
|---|---|---|
| Volar | Vue 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 文档的增删改查操作。
五、验证与部署
-
在
back目录下运行npm run start:dev。 -
在
front目录下运行npm run dev。 -
在浏览器中访问前端地址,进行功能测试(添加、标记状态、删除、刷新页面数据持久化)。
六、功能扩展建议
-
分页:在前端使用
el-pagination组件,调用/doc01?page=2&limit=20接口。 -
批量操作:结合
el-table的多选功能,实现批量删除。 -
API 文档:使用
@nestjs/swagger为后端自动生成 OpenAPI 文档。 -
用户认证:使用 NestJS JWT 模块快速集成登录/注册功能。
-
容器化:编写
Dockerfile和docker-compose.yml,实现一键部署。
(写到最后发现这并达不到mvp的体量......)
如果大家在实践过程中出现问题,欢迎在评论区提问。
更多推荐



所有评论(0)