1. 项目背景与核心价值
去年在做一个智能推荐系统时,第一次接触到MCP(Model-Controller-Presenter)架构。当时团队为了赶进度直接套用了现成框架,结果后期维护时发现视图逻辑和业务逻辑完全纠缠在一起,每次修改功能都像在拆炸弹。这个惨痛教训让我下定决心从头实现一套MCP架构,结果意外发现这比想象中简单得多——只要理解三个核心组件的职责边界,用200行基础代码就能搭建出可扩展的架构原型。
MCP本质上是一种前端架构模式,它通过强制分离业务逻辑(Model)、控制流(Controller)和视图渲染(Presenter)来解决代码混乱问题。与常见的MVC相比,它的Presenter层完全解耦了UI框架,使得单元测试覆盖率能轻松达到90%以上。我在电商后台、数据看板等6个项目中验证过这套架构,迭代效率比传统模式提升至少40%。
2. 架构原理深度解析
2.1 三权分立的职责划分
Model层要像瑞士银行一样严守数据底线。以用户登录场景为例,它只负责:
typescript复制class AuthModel {
private users: Map<string, string> = new Map();
async validate(credentials: {username: string, password: string}) {
const storedPassword = this.users.get(credentials.username);
return storedPassword === credentials.password;
}
}
注意这里绝对不包含跳转页面或提示错误等视图逻辑,它只返回布尔值这个原始事实。
Controller层是交通警察,它的黄金法则是:
- 绝不直接操作DOM
- 绝不包含业务规则
- 只做三件事:
- 接收视图事件
- 调用Model方法
- 选择对应的Presenter
Presenter才是真正的"视觉设计师"。它从Controller获取Model处理后的原始数据,转换成视图所需的格式。比如登录失败的提示:
typescript复制class AuthPresenter {
static showError(element: HTMLElement, error: Error) {
element.innerHTML = `
<div class="alert" style="color:red">
${error.message}
</div>
`;
}
}
2.2 数据流向的闭环设计
典型的登录流程演示了单向数据流:
- 视图触发submit事件 →
- Controller捕获事件并提取表单数据 →
- 调用Model.validate() →
- 根据返回结果选择Presenter.renderSuccess()或Presenter.renderError()
这种设计带来两个关键优势:
- 可预测性:每个环节输入输出明确,没有隐藏状态
- 可测试性:Presenter可以完全mock掉真实DOM
3. 手把手实现核心架构
3.1 基础框架搭建
先定义类型约束保证架构纯洁:
typescript复制interface IModel {
// 业务方法声明
}
interface IPresenter {
// 渲染方法声明
}
class BaseController {
private model: IModel;
private presenter: IPresenter;
constructor(model: IModel, presenter: IPresenter) {
this.model = model;
this.presenter = presenter;
}
// 事件路由方法
}
3.2 用户登录完整实现
Model层纯业务逻辑:
typescript复制class AuthModel implements IModel {
async login(username: string, password: string): Promise<{token?: string, error?: string}> {
if(!username) return {error: '用户名不能为空'};
// 实际项目这里会调用API
return {token: 'mock_token'};
}
}
Controller的流转控制:
typescript复制class AuthController extends BaseController {
async handleLogin(formData: FormData) {
try {
const result = await this.model.login(
formData.get('username'),
formData.get('password')
);
if(result.error) {
this.presenter.showError(result.error);
} else {
this.presenter.redirect('/dashboard');
}
} catch(e) {
this.presenter.showSystemError();
}
}
}
Presenter的视图渲染:
typescript复制class AuthPresenter implements IPresenter {
constructor(private rootElement: HTMLElement) {}
showError(message: string) {
this.rootElement.querySelector('.error-box').innerHTML = message;
}
redirect(path: string) {
window.location.href = path;
}
}
4. 进阶优化技巧
4.1 依赖注入实现
用IoC容器解决组件耦合:
typescript复制class Container {
static resolve<T>(identifier: symbol): T {
// 实际项目会用Inversify等库
if(identifier === AUTH_MODEL) return new AuthModel() as T;
throw new Error('未注册的依赖');
}
}
const controller = new AuthController(
Container.resolve<AuthModel>(AUTH_MODEL),
new AuthPresenter(document.getElementById('app'))
);
4.2 性能优化策略
- Presenter层采用虚拟DOM差异更新:
typescript复制class HighPerfPresenter {
private lastVDOM: VNode;
update(viewState: ViewState) {
const newVDOM = this.render(viewState);
patch(this.lastVDOM, newVDOM);
this.lastVDOM = newVDOM;
}
}
- Model层实现数据缓存:
typescript复制class CachedModel {
private cache = new LRU(100);
async getData(key: string) {
if(this.cache.has(key)) {
return this.cache.get(key);
}
const data = await fetch(key);
this.cache.set(key, data);
return data;
}
}
5. 实战中的避坑指南
5.1 典型错误模式
- 在Model中操作DOM:
typescript复制// 错误示范!
class WrongModel {
validate() {
document.getElementById('error').innerText = 'Invalid'; // 污染Model
}
}
- Presenter包含业务逻辑:
typescript复制// 错误示范!
class WrongPresenter {
showProfile(user) {
if(user.age < 18) { // 业务判断属于Model
this.showWarning();
}
}
}
5.2 调试技巧
- 在Controller中添加日志标记:
typescript复制class LoggableController extends BaseController {
private logStream = new EventEmitter();
async handleLogin(data) {
this.logStream.emit('before_validate', data);
const result = await super.handleLogin(data);
this.logStream.emit('after_validate', result);
return result;
}
}
- 使用中间件捕获异常:
typescript复制function errorHandler(target: any, method: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
try {
return await original.apply(this, args);
} catch(e) {
this.presenter.showSystemError(e);
throw e;
}
}
}
6. 架构扩展方向
6.1 状态管理集成
将Redux融入MCP体系:
- Model层作为Redux的reducer
- Controller监听store变化
- Presenter对接React/Vue组件
typescript复制class ReduxController {
private unsubscribe: () => void;
init() {
this.unsubscribe = store.subscribe(() => {
this.presenter.render(store.getState());
});
}
}
6.2 微前端适配
每个微应用作为独立MCP单元:
code复制app-shell (主控制器)
│
├── product-mcp (商品模块)
│ ├── ProductModel.ts
│ └── ProductPresenter.svelte
│
└── order-mcp (订单模块)
├── OrderModel.ts
└── OrderPresenter.vue
主控制器通过CustomEvent与子应用通信:
javascript复制// 子应用发送事件
window.dispatchEvent(new CustomEvent('add-to-cart', {detail: {sku: '123'}}));
// 主控制器监听
window.addEventListener('add-to-cart', (e) => {
cartController.handleAddItem(e.detail);
});