29、MVC项目实战(一):从零开始搭建一个完整的MVC项目

终于到了实战环节。说实话,前面讲了那么多理论,我自己都觉得有点「纸上谈兵」了。这一章,咱们真刀真枪干一场——从零开始,把一个完整的MVC项目搭起来。

我会带着你走完整个流程:需求分析、架构设计、编码实现。你想想看,这不就是平时工作中最真实的场景吗?

一、需求分析:我们要做什么?

先别急着写代码。我见过太多人,上来就敲键盘,结果写到一半发现需求没搞清楚,又得重来。嗯,咱们先花10分钟把需求理清楚。

这次我们要做一个「图书管理系统」的简化版。功能不多,但足够覆盖MVC的核心场景:

  • 图书列表展示:从后端获取数据,渲染到页面上
  • 添加图书:用户输入书名、作者,提交后更新列表
  • 删除图书:点击删除按钮,移除指定图书
  • 搜索图书:按关键字过滤列表

说白了,就是一个典型的CRUD应用。但你别小看它,麻雀虽小五脏俱全。我在项目中遇到过很多「看起来简单」的需求,最后都因为架构没设计好,改得焦头烂额。

核心原则:需求分析阶段,一定要明确「数据从哪里来」「数据到哪里去」「用户怎么操作」。这三个问题搞清楚了,架构设计就顺了。

二、架构设计:分层怎么分?

好,需求明确了。接下来咱们画一下架构图。我个人习惯先用一张图把整体结构定下来,再动手写代码。

MVC 图书管理系统架构图 View 层 bookList.html addBook.html searchView.html Controller 层 BookController.js 事件绑定 路由分发 Model 层 BookModel.js 数据获取/存储 API 通信 用户操作 请求数据 返回数据 更新视图 数据流向说明 1. 用户在 View 层操作(点击、输入) 2. Controller 层捕获事件,调用 Model 层方法 3. Model 层处理数据(请求 API 或本地存储) 4. Model 层将结果返回给 Controller 5. Controller 层更新 View 层展示

这张图你看懂了吗?数据是单向流动的:用户操作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