彻底掌握Mojito框架:从MVC到模块化应用架构实战指南

彻底掌握Mojito框架:从MVC到模块化应用架构实战指南

【免费下载链接】mojito [archiving soon] Yahoo! Mojito Framework 【免费下载链接】mojito 项目地址: https://gitcode.com/gh_mirrors/mo/mojito

引言:Mojito MVC架构的痛点与解决方案

你是否在构建大型Web应用时面临以下挑战:

  • 代码组织混乱,业务逻辑与界面展示交织
  • 前后端分离不彻底,开发效率低下
  • 模块复用困难,团队协作成本高
  • 测试与维护复杂度随项目规模指数增长

Mojito框架(Yahoo!开发的JavaScript全栈框架)通过其独特的MVC(Model-View-Controller,模型-视图-控制器)架构模式为这些问题提供了优雅的解决方案。本文将深入剖析Mojito的MVC实现机制,通过实战案例展示如何利用这一架构构建高内聚低耦合的Web应用。

读完本文后,你将能够:

  • 理解Mojito框架中MVC架构的核心设计理念
  • 掌握Mojit组件(Mojito的模块化单元)的创建与使用
  • 实现前后端统一的MVC模式,提高代码复用率
  • 构建可扩展、易维护的大型Web应用

Mojito框架概述

Mojito是一个基于Node.js和YUI(Yahoo! User Interface)库的全栈JavaScript框架,旨在简化高性能Web应用的开发。其核心特性包括:

  • 全栈JavaScript:前后端统一的代码库,减少上下文切换成本
  • 模块化架构:通过Mojit组件实现功能封装与复用
  • 统一MVC模式:前后端一致的MVC实现,简化开发流程
  • 丰富的工具链:提供命令行工具、测试框架和调试工具

Mojito的架构设计遵循"一次编写,到处运行"的原则,允许开发者编写一次代码,同时在服务器和客户端执行,极大地提高了开发效率和代码复用率。

Mojito中的MVC架构详解

MVC架构概览

MVC(Model-View-Controller)是一种软件架构模式,将应用程序分为三个核心组件:

  • 模型(Model):管理应用程序数据和业务逻辑
  • 视图(View):负责数据展示和用户界面
  • 控制器(Controller):处理用户输入并协调模型和视图

在Mojito中,这三个组件通过Mojit(Mojito组件)进行封装,形成了一个高度模块化的应用架构。

mermaid

Mojito MVC与传统MVC的差异

Mojito的MVC实现与传统MVC相比有几个关键差异:

特性传统MVCMojito MVC
执行环境通常仅服务器端服务器端和客户端双重执行
组件封装松散耦合通过Mojit组件紧密封装
数据流单向或双向,因实现而异严格的单向数据流
路由机制集中式路由基于Mojit的分布式路由
模板引擎通常单一引擎支持多种模板引擎(Handlebars, Mustache等)

Mojito MVC核心实现

Mojito的MVC架构在代码层面通过以下关键文件实现:

  1. 控制器(Controller):处理请求、协调模型和视图

    • controller.server.js:服务器端控制器
    • controller.client.js:客户端控制器
    • controller.common.js:通用控制器逻辑
  2. 模型(Model):管理数据和业务逻辑

    • model.server.js:服务器端数据处理
    • model.client.js:客户端数据处理
    • model.common.js:通用数据逻辑
  3. 视图(View):负责数据展示

    • 模板文件(.html, .hb, .mu等)
    • CSS样式文件
    • 客户端JavaScript

Mojito控制器(Controller)深入解析

控制器的角色与职责

在Mojito中,控制器是Mojit的核心,负责:

  • 接收和处理用户请求
  • 协调模型和视图
  • 实现业务逻辑
  • 管理数据流

控制器实现示例

以下是一个典型的Mojito控制器实现:

// controller.server.js
YUI.add('example-mojit-server', function(Y, NAME) {
    Y.namespace('mojito.controllers')[NAME] = {
        init: function(config) {
            this.config = config;
        },
        
        index: function(ac) {
            // 获取请求参数
            var params = ac.params.getFromReq();
            
            // 调用模型获取数据
            ac.models.get('exampleModel').getData(params, function(err, data) {
                if (err) {
                    ac.error(err);
                    return;
                }
                
                // 将数据传递给视图
                ac.done({
                    title: 'Mojito MVC示例',
                    data: data,
                    user: ac.session.get('user')
                });
            });
        },
        
        submit: function(ac) {
            // 处理表单提交
            var formData = ac.params.getFromBody();
            
            // 验证数据
            if (!this._validateFormData(formData)) {
                ac.error('Invalid form data');
                return;
            }
            
            // 保存数据
            ac.models.get('exampleModel').saveData(formData, function(err, result) {
                if (err) {
                    ac.error(err);
                    return;
                }
                
                // 重定向到结果页面
                ac.redirect('/example/result/' + result.id);
            });
        },
        
        _validateFormData: function(data) {
            // 私有验证方法
            return data && data.name && data.email;
        }
    };
}, '0.0.1', {requires: ['mojito', 'mojito-params', 'mojito-models', 'example-model']});

Action Context(AC)对象

Action Context(AC)对象是Mojito控制器的核心,提供了丰富的API用于与框架交互:

// AC对象主要方法
ac.done(data);          // 完成请求处理并渲染视图
ac.error(err);          // 处理错误
ac.redirect(url);       // 重定向到指定URL
ac.models.get(model);   // 获取模型实例
ac.params;              // 获取请求参数
ac.session;             // 访问会话数据
ac.config;              // 获取配置信息
ac.assets;              // 管理资源

前后端控制器协同工作

Mojito的独特之处在于其前后端统一的控制器架构:

mermaid

Mojito模型(Model)深入解析

模型的职责与设计原则

Mojito模型负责:

  • 数据获取与存储
  • 业务逻辑实现
  • 数据验证
  • 与数据源交互(数据库、API等)

模型设计应遵循以下原则:

  • 单一职责:每个模型专注于特定业务领域
  • 可测试性:设计便于单元测试的接口
  • 松耦合:独立于控制器和视图
  • 数据封装:隐藏数据访问细节

模型实现示例

// model.server.js
YUI.add('example-model-server', function(Y, NAME) {
    Y.namespace('mojito.models')[NAME] = {
        init: function(config) {
            this.config = config;
            this.db = Y.mojito.util.db.getConnection();
        },
        
        getData: function(params, callback) {
            var query = 'SELECT * FROM examples WHERE category = ?';
            
            this.db.query(query, [params.category], function(err, results) {
                if (err) {
                    callback(err);
                    return;
                }
                
                // 处理数据
                var processedData = this._processResults(results);
                callback(null, processedData);
            }.bind(this));
        },
        
        saveData: function(data, callback) {
            // 验证数据
            if (!this._validateData(data)) {
                callback(new Error('Invalid data'));
                return;
            }
            
            var query = 'INSERT INTO examples SET ?';
            this.db.query(query, data, function(err, result) {
                if (err) {
                    callback(err);
                    return;
                }
                callback(null, {id: result.insertId});
            });
        },
        
        _processResults: function(results) {
            // 数据处理逻辑
            return results.map(item => ({
                id: item.id,
                name: item.name,
                formattedDate: Y.DataType.Date.format(new Date(item.date), {
                    format: "%Y-%m-%d"
                })
            }));
        },
        
        _validateData: function(data) {
            // 数据验证逻辑
            return Y.Lang.isObject(data) && 
                   Y.Lang.isString(data.name) && 
                   data.name.length > 0;
        }
    };
}, '0.0.1', {requires: ['mojito-util', 'datatype-date', 'db-connector']});

模型的数据源交互

Mojito模型支持多种数据源交互方式:

  1. 数据库交互:通过ORM或原生查询
  2. Web服务/API:使用YUI的IO模块
  3. 文件系统:读取本地文件系统
  4. YQL(Yahoo! Query Language):查询网络数据
// 使用YQL获取数据
getDataViaYQL: function(params, callback) {
    var query = 'SELECT * FROM rss WHERE url=@url LIMIT 10';
    
    Y.YQL(query, {url: params.feedUrl}, function(response) {
        if (response.error) {
            callback(new Error(response.error.description));
            return;
        }
        
        callback(null, response.query.results.item);
    });
}

Mojito视图(View)深入解析

视图的角色与实现方式

Mojito视图负责:

  • 数据展示
  • 用户界面渲染
  • 客户端交互处理
  • 响应式布局实现

Mojito支持多种视图实现方式:

  • 服务器端渲染:初始页面生成
  • 客户端渲染:动态内容更新
  • 混合渲染:结合前两种方式的优势

视图模板示例

Handlebars模板示例:

<!-- views/index.hb -->
<div class="example-container">
    <h1>{{title}}</h1>
    
    {{#if user}}
    <div class="user-info">
        <p>欢迎回来,{{user.name}}!</p>
    </div>
    {{/if}}
    
    <ul class="data-list">
        {{#each data}}
        <li class="data-item">
            <h3>{{this.title}}</h3>
            <p>{{this.description}}</p>
            <span class="date">{{this.formattedDate}}</span>
        </li>
        {{/each}}
    </ul>
    
    {{#unless data}}
    <p class="no-data">没有找到相关数据</p>
    {{/unless}}
    
    <form action="/example/submit" method="post" class="example-form">
        <input type="text" name="name" placeholder="输入名称" required>
        <button type="submit">提交</button>
    </form>
</div>

客户端视图交互

Mojito使用Binder实现客户端视图交互:

// binders/example.client.js
YUI.add('example-binder-client', function(Y, NAME) {
    Y.namespace('mojito.binders')[NAME] = {
        init: function(mojitProxy) {
            this.mojitProxy = mojitProxy;
        },
        
        bind: function(node) {
            this.node = node;
            
            // 绑定点击事件
            this.node.one('.data-list').delegate('click', function(e) {
                var itemId = e.currentTarget.getAttribute('data-id');
                this._handleItemClick(itemId);
            }, '.data-item', this);
            
            // 绑定表单提交
            this.node.one('.example-form').on('submit', function(e) {
                e.preventDefault();
                this._handleFormSubmit(e.currentTarget);
            }, this);
        },
        
        _handleItemClick: function(itemId) {
            // 显示详情
            this.mojitProxy.invoke('getDetails', {
                params: {id: itemId}
            }, function(err, html) {
                if (!err) {
                    this.node.one('.details-container').setHTML(html);
                }
            }.bind(this));
        },
        
        _handleFormSubmit: function(form) {
            // 处理表单提交
            var formData = new FormData(form);
            
            Y.io(form.get('action'), {
                method: 'POST',
                data: formData,
                on: {
                    complete: function(id, response) {
                        if (response.status === 200) {
                            this.mojitProxy.refreshView();
                        }
                    }.bind(this)
                }
            });
        }
    };
}, '0.0.1', {requires: ['node', 'event', 'io-form']});

Mojit组件:MVC的模块化封装

Mojit结构详解

Mojit是Mojito应用的基本构建块,是MVC组件的封装单元。一个典型的Mojit结构如下:

example-mojit/
├── assets/              # 静态资源
│   ├── css/             # 样式表
│   ├── js/              # 客户端JavaScript
│   └── images/          # 图片资源
├── binders/             # 客户端绑定器
│   └── example.client.js
├── controllers/         # 控制器
│   ├── controller.common.js
│   ├── controller.server.js
│   └── controller.client.js
├── models/              # 模型
│   ├── model.common.js
│   ├── model.server.js
│   └── model.client.js
├── views/               # 视图模板
│   ├── index.hb
│   └── details.hb
├── config.json          # Mojit配置
└── spec.json            # Mojit元数据

Mojit配置详解

Mojit配置文件(config.json)示例:

{
    "root": {
        "viewEngine": "handlebars",
        "layout": {
            "template": "layouts/main.hb"
        },
        "assets": {
            "js": [
                "http://libs.baidu.com/jquery/2.1.4/jquery.min.js",
                "{{mojito_base}}/js/app.js"
            ],
            "css": [
                "{{mojito_base}}/css/style.css"
            ]
        }
    },
    "server": {
        "cache": {
            "enabled": true,
            "ttl": 300
        },
        "models": {
            "exampleModel": {
                "params": {
                    "cache": true
                }
            }
        }
    }, 
    "client": {
        "debug": false,
        "lazyLoad": true
    }
}

Mojit通信机制

Mojit之间通过以下方式进行通信:

  1. Parent-Child通信:父Mojit与子Mojit之间的直接通信
  2. Pub/Sub机制:基于事件的发布-订阅模式
  3. Mojit Proxy:通过代理对象调用其他Mojit的方法
// 父Mojit调用子Mojit
this.children.exampleChild.invoke('update', {
    params: {data: newData}
}, function(err, response) {
    // 处理响应
});

// Pub/Sub通信示例
// 发布事件
Y.Global.fire('example:dataUpdated', {data: updatedData});

// 订阅事件
Y.Global.on('example:dataUpdated', function(e) {
    this._handleDataUpdate(e.data);
}, this);

路由与请求分发

Mojito路由系统

Mojito的路由系统将URL映射到Mojit的特定Action,支持多种路由配置方式:

  1. JSON配置文件:routes.json
  2. YAML配置文件:routes.yaml
  3. 编程方式:通过代码动态定义路由

路由配置示例

# routes.yaml
"/":
  "index":
    "mojit": "example-mojit"
    
"/example/:id":
  "view":
    "mojit": "example-mojit"
    "action": "view"
    
"/example/:category":
  "list":
    "mojit": "example-mojit"
    "action": "list"
    "params":
      "limit": 20
      
"/admin/*":
  "admin":
    "mojit": "admin-mojit"
    "action": "index"
    "conditions":
      "method": ["GET", "POST"]
      "https": true

请求处理流程

Mojito的请求处理流程如下:

mermaid

实战案例:构建Mojito MVC应用

项目结构设计

我们将构建一个简单的博客应用,展示Mojito MVC架构的实际应用:

blog-app/
├── application.json     # 应用配置
├── routes.json          # 路由配置
├── mojits/              # Mojit组件
│   ├── header-mojit/    # 头部导航Mojit
│   ├── post-mojit/      # 文章Mojit
│   ├── comment-mojit/   # 评论Mojit
│   └── footer-mojit/    # 页脚Mojit
├── models/              # 共享模型
│   ├── blog-model/
│   └── user-model/
├── assets/              # 全局资源
└── server.js            # 应用入口

数据模型实现

// mojits/post-mojit/models/model.server.js
YUI.add('post-mojit-model-server', function(Y, NAME) {
    Y.namespace('mojito.models')[NAME] = {
        init: function(config) {
            this.config = config;
            this.db = Y.mojito.util.db.getConnection();
        },
        
        getPosts: function(params, callback) {
            var sql = 'SELECT * FROM posts WHERE status = "published"';
            var queryParams = [];
            
            if (params.category) {
                sql += ' AND category = ?';
                queryParams.push(params.category);
            }
            
            sql += ' ORDER BY created DESC LIMIT ? OFFSET ?';
            queryParams.push(params.limit || 10);
            queryParams.push(params.offset || 0);
            
            this.db.query(sql, queryParams, function(err, results) {
                if (err) {
                    callback(err);
                    return;
                }
                
                callback(null, results.map(this._formatPost));
            }.bind(this));
        },
        
        getPostById: function(id, callback) {
            var sql = 'SELECT p.*, u.name as author_name FROM posts p ' +
                      'JOIN users u ON p.author_id = u.id ' +
                      'WHERE p.id = ? AND p.status = "published"';
            
            this.db.query(sql, [id], function(err, results) {
                if (err || !results.length) {
                    callback(err || new Error('Post not found'));
                    return;
                }
                
                var post = this._formatPost(results[0]);
                
                // 获取评论
                this._getComments(id, function(err, comments) {
                    if (err) {
                        callback(err);
                        return;
                    }
                    
                    post.comments = comments;
                    callback(null, post);
                });
            }.bind(this));
        },
        
        _formatPost: function(post) {
            return {
                id: post.id,
                title: post.title,
                slug: post.slug,
                content: post.content,
                excerpt: post.content.substring(0, 200) + '...',
                author: post.author_name,
                category: post.category,
                created: Y.DataType.Date.format(new Date(post.created), {
                    format: "%Y-%m-%d %H:%M"
                }),
                tags: post.tags ? post.tags.split(',') : []
            };
        },
        
        _getComments: function(postId, callback) {
            var sql = 'SELECT c.*, u.name as author_name FROM comments c ' +
                      'JOIN users u ON c.author_id = u.id ' +
                      'WHERE c.post_id = ? AND c.approved = 1 ' +
                      'ORDER BY created ASC';
            
            this.db.query(sql, [postId], function(err, results) {
                if (err) {
                    callback(err);
                    return;
                }
                
                callback(null, results.map(comment => ({
                    id: comment.id,
                    content: comment.content,
                    author: comment.author_name,
                    created: Y.DataType.Date.format(new Date(comment.created), {
                        format: "%Y-%m-%d %H:%M"
                    })
                })));
            });
        }
    };
}, '0.0.1', {requires: ['mojito-util', 'datatype-date']});

控制器实现

// mojits/post-mojit/controllers/controller.server.js
YUI.add('post-mojit-server', function(Y, NAME) {
    Y.namespace('mojito.controllers')[NAME] = {
        init: function(config) {
            this.config = config;
        },
        
        index: function(ac) {
            var params = {
                category: ac.params.getFromReq().category,
                limit: ac.params.getFromReq().limit || 10,
                offset: ac.params.getFromReq().offset || 0
            };
            
            ac.models.get('post-mojit-model').getPosts(params, function(err, posts) {
                if (err) {
                    ac.error(err);
                    return;
                }
                
                ac.done({
                    title: '博客首页',
                    posts: posts,
                    categories: this._getCategories()
                });
            }.bind(this));
        },
        
        view: function(ac) {
            var postId = ac.params.getFromReq().id;
            
            if (!postId) {
                ac.error('Post ID is required');
                return;
            }
            
            ac.models.get('post-mojit-model').getPostById(postId, function(err, post) {
                if (err) {
                    ac.error(404, 'Post not found');
                    return;
                }
                
                ac.done({
                    title: post.title,
                    post: post
                });
            });
        },
        
        _getCategories: function() {
            // 返回博客分类列表
            return [
                {id: 'tech', name: '技术'},
                {id: 'life', name: '生活'},
                {id: 'travel', name: '旅行'},
                {id: 'food', name: '美食'}
            ];
        }
    };
}, '0.0.1', {requires: ['mojito', 'mojito-params', 'mojito-models']});

视图实现

<!-- mojits/post-mojit/views/index.hb -->
{{> header-mojit}}

<div class="blog-container">
    <div class="sidebar">
        <div class="categories">
            <h3>分类</h3>
            <ul>
                {{#each categories}}
                <li>
                    <a href="/?category={{this.id}}" 
                       {{#if params.category '===' this.id}}class="active"{{/if}}>
                        {{this.name}}
                    </a>
                </li>
                {{/each}}
            </ul>
        </div>
    </div>
    
    <div class="main-content">
        <h1>{{title}}</h1>
        
        <div class="post-list">
            {{#each posts}}
            <article class="post-item">
                <h2>
                    <a href="/post/{{this.id}}">{{this.title}}</a>
                </h2>
                
                <div class="post-meta">
                    <span class="author">作者: {{this.author}}</span>
                    <span class="category">分类: {{this.category}}</span>
                    <span class="date">日期: {{this.created}}</span>
                </div>
                
                <div class="post-excerpt">{{this.excerpt}}</div>
                
                <a href="/post/{{this.id}}" class="read-more">阅读全文</a>
            </article>
            {{/each}}
            
            {{#unless posts}}
            <p class="no-posts">没有找到相关文章</p>
            {{/unless}}
        </div>
    </div>
</div>

{{> footer-mojit}}

客户端交互实现

// mojits/post-mojit/binders/post.client.js
YUI.add('post-mojit-binder-client', function(Y, NAME) {
    Y.namespace('mojito.binders')[NAME] = {
        init: function(mojitProxy) {
            this.mojitProxy = mojitProxy;
        },
        
        bind: function(node) {
            this.node = node;
            
            // 绑定评论提交事件
            this._bindCommentSubmit();
            
            // 实现平滑滚动
            this._setupSmoothScroll();
            
            // 图片懒加载
            this._setupLazyLoad();
        },
        
        _bindCommentSubmit: function() {
            var commentForm = this.node.one('.comment-form');
            if (!commentForm) return;
            
            commentForm.on('submit', function(e) {
                e.preventDefault();
                
                var formData = new FormData(commentForm.getDOMNode());
                
                Y.io(commentForm.get('action'), {
                    method: 'POST',
                    data: formData,
                    on: {
                        complete: function(id, response) {
                            try {
                                var result = Y.JSON.parse(response.responseText);
                                
                                if (result.success) {
                                    // 显示成功消息
                                    this.node.one('.comment-success').setStyle('display', 'block');
                                    commentForm.reset();
                                    
                                    // 刷新评论列表
                                    this.mojitProxy.invoke('getComments', {
                                        params: {postId: result.postId}
                                    }, function(err, html) {
                                        if (!err) {
                                            this.node.one('.comments-list').setHTML(html);
                                        }
                                    }.bind(this));
                                } else {
                                    // 显示错误消息
                                    this.node.one('.comment-error').setHTML(result.message);
                                }
                            } catch (e) {
                                this.node.one('.comment-error').setHTML('提交评论失败,请重试');
                            }
                        }.bind(this)
                    }
                });
            }, this);
        },
        
        _setupSmoothScroll: function() {
            this.node.all('a[href^="#"]').on('click', function(e) {
                e.preventDefault();
                var targetId = this.get('href').substring(1);
                var targetNode = Y.one('#' + targetId);
                
                if (targetNode) {
                    Y.one('win').scrollTo(targetNode.getY(), {
                        duration: 0.5,
                        easing: 'easeOut'
                    });
                }
            });
        },
        
        _setupLazyLoad: function() {
            // 实现图片懒加载
            var images = this.node.all('img.lazy-load');
            
            if (Y.LazyLoad) {
                new Y.LazyLoad({
                    container: this.node,
                    images: images
                });
            } else {
                // 降级处理:立即加载所有图片
                images.each(function(img) {
                    img.setAttribute('src', img.getAttribute('data-src'));
                });
            }
        }
    };
}, '0.0.1', {requires: ['node', 'event', 'io-form', 'json-parse', 'anim-scroll']});

性能优化与最佳实践

MVC性能优化策略

  1. 控制器优化

    • 减少Action复杂度,遵循单一职责原则
    • 合理使用缓存,减少重复计算
    • 异步处理耗时操作
  2. 模型优化

    • 优化数据库查询,添加适当索引
    • 实现数据缓存层(Redis/Memcached)
    • 批量处理数据操作
  3. 视图优化

    • 减少DOM操作,使用文档片段
    • 实现视图片段缓存
    • 压缩和合并静态资源

最佳实践总结

  1. 代码组织

    • 按功能模块组织Mojit,而非技术层次
    • 合理划分Mojit粒度,避免过大或过小
    • 提取共享逻辑到公共模块或服务
  2. 前后端分离

    • 保持业务逻辑在前后端的一致性
    • 明确定义前后端通信接口
    • 实现前后端可独立测试的架构
  3. 错误处理

    • 实现全局错误处理机制
    • 详细日志记录,便于问题诊断
    • 友好的用户错误提示
  4. 测试策略

    • 编写单元测试覆盖核心业务逻辑
    • 实现集成测试验证Mojit交互
    • 进行性能测试识别瓶颈

结论与展望

Mojito的MVC架构通过模块化的Mojit组件、前后端统一的代码库和灵活的视图渲染机制,为构建复杂Web应用提供了强大的支持。本文深入剖析了Mojito MVC的核心实现,包括控制器、模型和视图的设计与实现,以及它们如何通过Mojit组件进行封装。

随着Web技术的不断发展,Mojito架构也在不断演进。未来可能的发展方向包括:

  1. 更好的TypeScript支持:提供类型定义,增强开发体验
  2. React/Vue集成:结合现代前端框架的优势
  3. 微服务架构适配:支持更细粒度的服务拆分
  4. Serverless部署:适应云原生应用的部署需求

无论技术如何变化,MVC架构的核心思想(关注点分离、模块化、单一职责)仍然是构建高质量软件的基础。掌握Mojito的MVC实现不仅有助于当前项目开发,更能培养良好的软件设计思维,为应对未来的技术挑战打下坚实基础。

学习资源与扩展阅读

  • Mojito官方文档:http://developer.yahoo.com/cocktails/mojito/
  • Mojito GitHub仓库:https://gitcode.com/gh_mirrors/mo/mojito
  • YUI库文档:http://yuilibrary.com/yui/docs/
  • 《Mojito: Web Development with JavaScript and YUI 3》
  • 《JavaScript Web Applications》by Alex MacCaw
  • 《Single Page Web Applications》by Michael S. Mikowski

【免费下载链接】mojito [archiving soon] Yahoo! Mojito Framework 【免费下载链接】mojito 项目地址: https://gitcode.com/gh_mirrors/mo/mojito

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值