29、MVC项目实战(一):从零开始搭建一个完整的MVC项目
终于到了实战环节。说实话,前面讲了那么多理论,我自己都觉得有点「纸上谈兵」了。这一章,咱们真刀真枪干一场——从零开始,把一个完整的MVC项目搭起来。
我会带着你走完整个流程:需求分析、架构设计、编码实现。你想想看,这不就是平时工作中最真实的场景吗?
一、需求分析:我们要做什么?
先别急着写代码。我见过太多人,上来就敲键盘,结果写到一半发现需求没搞清楚,又得重来。嗯,咱们先花10分钟把需求理清楚。
这次我们要做一个「图书管理系统」的简化版。功能不多,但足够覆盖MVC的核心场景:
- 图书列表展示:从后端获取数据,渲染到页面上
- 添加图书:用户输入书名、作者,提交后更新列表
- 删除图书:点击删除按钮,移除指定图书
- 搜索图书:按关键字过滤列表
说白了,就是一个典型的CRUD应用。但你别小看它,麻雀虽小五脏俱全。我在项目中遇到过很多「看起来简单」的需求,最后都因为架构没设计好,改得焦头烂额。
核心原则:需求分析阶段,一定要明确「数据从哪里来」「数据到哪里去」「用户怎么操作」。这三个问题搞清楚了,架构设计就顺了。
二、架构设计:分层怎么分?
好,需求明确了。接下来咱们画一下架构图。我个人习惯先用一张图把整体结构定下来,再动手写代码。
这张图你看懂了吗?数据是单向流动的:用户操作View → Controller处理 → Model更新 → 通知View刷新。为什么要这么设计?说白了,就是为了「解耦」。
我曾经在一个项目里,把业务逻辑直接写在HTML的事件回调里。刚开始觉得挺方便,后来需求一变更,改一个功能要翻三个文件。嗯,从那以后我再也不敢这么干了。
三、目录结构:代码怎么放?
架构定好了,接下来就是目录结构。我个人习惯这样组织:
book-manager/
├── index.html # 入口页面
├── css/
│ └── style.css # 样式文件
├── js/
│ ├── app.js # 应用入口,初始化
│ ├── controller/
│ │ └── BookController.js # 控制器
│ ├── model/
│ │ └── BookModel.js # 数据模型
│ └── view/
│ └── BookView.js # 视图渲染
└── data/
└── books.json # 模拟数据(实际项目用API)
小提示:实际项目中,data目录通常不存在,数据来自后端API。这里用JSON文件模拟,方便咱们本地调试。
四、编码实现:动手写代码
好,终于到编码环节了。咱们按「Model → View → Controller」的顺序来写。为什么是这个顺序?因为Model是核心,View依赖Model的数据,Controller是胶水层。
4.1 Model 层:数据管理
Model层负责两件事:获取数据、更新数据。它不关心数据怎么展示,也不关心用户点了哪里。
// js/model/BookModel.js
class BookModel {
constructor() {
this.books = [];
this.apiUrl = './data/books.json'; // 模拟API
}
// 获取所有图书
async fetchBooks() {
try {
const response = await fetch(this.apiUrl);
this.books = await response.json();
return this.books;
} catch (error) {
console.error('获取图书失败:', error);
return [];
}
}
// 添加图书(模拟)
addBook(book) {
// 实际项目这里会调用POST API
this.books.push(book);
return this.books;
}
// 删除图书(模拟)
deleteBook(id) {
this.books = this.books.filter(book => book.id !== id);
return this.books;
}
// 搜索图书
searchBooks(keyword) {
return this.books.filter(book =>
book.title.includes(keyword) ||
book.author.includes(keyword)
);
}
}
你看,Model层就是纯数据操作。没有DOM操作,没有事件绑定。这样做的好处是:如果以后换数据源(比如从本地JSON换成REST API),只需要改Model层,View和Controller完全不用动。
4.2 View 层:界面渲染
View层负责把数据变成HTML。它只做一件事:渲染。不处理业务逻辑,不发起网络请求。
// js/view/BookView.js
class BookView {
constructor() {
this.bookList = document.getElementById('book-list');
this.searchInput = document.getElementById('search-input');
}
// 渲染图书列表
renderBooks(books) {
if (books.length === 0) {
this.bookList.innerHTML = '<p>暂无图书数据</p>';
return;
}
let html = '';
books.forEach(book => {
html += `
<div class="book-item" data-id="${book.id}">
<span>${book.title}</span>
<span>${book.author}</span>
<button class="delete-btn" data-id="${book.id}">删除</button>
</div>
`;
});
this.bookList.innerHTML = html;
}
// 获取表单输入
getFormData() {
const title = document.getElementById('book-title').value.trim();
const author = document.getElementById('book-author').value.trim();
return { title, author };
}
// 清空表单
clearForm() {
document.getElementById('book-title').value = '';
document.getElementById('book-author').value = '';
}
}
注意:View层里不要写业务逻辑。比如「如果书名超过10个字就报错」这种判断,应该放在Controller或Model里。View只负责「展示」和「收集」。
4.3 Controller 层:协调者
Controller是MVC的大脑。它监听用户操作,调用Model的方法,然后通知View更新。
// js/controller/BookController.js
class BookController {
constructor(model, view) {
this.model = model;
this.view = view;
// 初始化
this.init();
}
async init() {
// 1. 获取数据
const books = await this.model.fetchBooks();
// 2. 渲染视图
this.view.renderBooks(books);
// 3. 绑定事件
this.bindEvents();
}
bindEvents() {
// 添加图书
document.getElementById('add-btn').addEventListener('click', () => {
this.handleAddBook();
});
// 删除图书(事件委托)
this.view.bookList.addEventListener('click', (e) => {
if (e.target.classList.contains('delete-btn')) {
const id = parseInt(e.target.dataset.id);
this.handleDeleteBook(id);
}
});
// 搜索图书
this.view.searchInput.addEventListener('input', (e) => {
this.handleSearch(e.target.value);
});
}
handleAddBook() {
const formData = this.view.getFormData();
if (!formData.title || !formData.author) {
alert('请填写完整信息');
return;
}
const newBook = {
id: Date.now(), // 简单生成ID
title: formData.title,
author: formData.author
};
const updatedBooks = this.model.addBook(newBook);
this.view.renderBooks(updatedBooks);
this.view.clearForm();
}
handleDeleteBook(id) {
const updatedBooks = this.model.deleteBook(id);
this.view.renderBooks(updatedBooks);
}
handleSearch(keyword) {
const filteredBooks = this.model.searchBooks(keyword);
this.view.renderBooks(filteredBooks);
}
}
4.4 入口文件:组装起来
// js/app.js
import BookModel from './model/BookModel.js';
import BookView from './view/BookView.js';
import BookController from './controller/BookController.js';
// 实例化MVC三件套
const model = new BookModel();
const view = new BookView();
const controller = new BookController(model, view);
你看,入口文件就三行代码。这就是MVC的魅力——各层独立,组装简单。
五、避坑指南
实战中容易踩的坑,我帮你列出来:
- Model里不要操作DOM:我曾经犯过这个错,后来改得想哭。Model只处理数据,DOM操作全交给View。
- Controller不要直接修改数据:Controller只负责「调用」,不负责「实现」。修改数据是Model的事。
- View里不要写业务逻辑:比如「如果用户没输入书名就提示错误」,这个判断应该在Controller里做。
- 事件绑定用事件委托:动态生成的元素,直接用事件委托绑定,省心又高效。
核心总结:MVC的核心思想就是「各司其职」。Model管数据,View管展示,Controller管协调。记住这个原则,你的代码就会越来越清晰。
好了,这一章咱们从需求分析到架构设计,再到编码实现,完整走了一遍。你跟着敲一遍代码,应该就能体会到MVC的好处了。下一章咱们继续深入,看看更复杂的场景怎么处理。
公众号:蓝海资料掘金营,微信deep3321