Meteor Mantra 系列文章:
Meteor Mantra 介绍(一)- 基本概念
Meteor Mantra 介绍(二)- 前端架构详解
Meteor Mantra 介绍(三)- 后端架构解释
Meteor Mantra 介绍(四)- 博客例子前端代码解读
Meteor Mantra 介绍(五)- 博客例子后端代码解读
Meteor Mantra 介绍(六)- 使用 mantra-cli 命令行生成源码
Mantra 是一种基于 Meteor 1.3+、React 和 ES2015 的 Meteor 应用架构,主要作用让 Meteor 应用代码架构标准化,特别是前端部分,当然它对后端代码的组织也有要求。注意 Mantra 不是一个框架,而是一套如何构建 Meteor App 的说明,同时也有配套的开源库来提高代码编写效率。
如果你熟悉 React,Mantra 类似于 Flux,讲究的是对数据流的控制,但是规定得更加细致。
目的
Mantra 的目的是写出更易于理解和维护的代码。它对几乎所有的情况都有一个标准,另外还为 Meteor App 增加单元测试覆盖率。
和 Perl 类似,JavaScript 的一个难点就是同样一个问题有太多实现方式,而且可能都是最佳解决方案。所以经常是不同的人使用不同的方法。Mantra 让 Meteor App 有一个统一的结构,遵循相同的标准,就像设计模式一样,降低大家理解代码结构的难度,确保模块之间解耦,像 Flux 一样让数据单向流动,这样维护代码更加容易。
Mantra 使用的原则很有前瞻性,能够很长时间不会过时,同时也允许其他人做必要的改变。
偏重前端
现在的 Web App 的大部分代码都是在前端。后端的代码逻辑相对简单也好管理,后端的难点在于性能优化,特别是大并发的处理,数据库等。
Mantra 的核心在如何组织客户端代码。它倡导前后端代码分离,前端不用知道后端代码是如何实现的,但是可以代码共享。因为是基于 React 又侧重前端,所以 Mantra 很类似 React 的那些标准,例如 Flux,Redux 等,解决的问题也类似,都是控制数据流 data flow,让代码更易理解维护。如果你对 React 熟悉,理解 Mantra 就不难。如果理解有困难,建议多看看 React 的高级用法,例如 stateless/pure function,Higher Order Components 等。
Mantra 不相信 Universal App,就是不相信一套前端代码适应所有终端平台。它鼓励一套后端代码,但是为每个前端平台开发单独的 app 来提高用户体验,尽量通过模块化来共享代码。
其他 Mantra 的基本介绍可以参看这篇中文翻译 http://www.jianshu.com/p/96d6b8e64c3a
下面我来详细解释 Mantra 的各个部件。
这里介绍的顺序和文档里的不一样,主要是先从新的概念介绍。下图是一个典型的 Mantra App 的 work flow。
Application Context
应用上下文 context 对所有 action 和 container 开放读取,所以这是你分享变量的地方。
import * as Collections from '/lib/collections';
import {Meteor} from 'meteor/meteor';
import {FlowRouter} from 'meteor/kadira:flow-router';
import {ReactiveDict} from 'meteor/reactive-dict';
import {Tracker} from 'meteor/tracker';
export default function () {
return {
Meteor,
FlowRouter,
Collections,
LocalState: new ReactiveDict(),
Tracker
};
}
从上面例子中可以看出,context 可以让大家少写重复的代码,又可以在不同模块之间分享变量。
Actions
处理业务逻辑的模块。包括验证,状态管理和远程数据交互。
Action 就是一个简单的函数而已,第一个参数必须是应用的上下文 Context。Action 不得使用引入除了参数以外的任何变量和模块,甚至全局变量,但是可以使用库函数。
export default {
create({Meteor, LocalState, FlowRouter}, title, content) {
if (!title || !content) {
return LocalState.set('SAVING_ERROR', 'Title & Content are required!');
}
LocalState.set('SAVING_ERROR', null);
const id = Meteor.uuid();
// There is a method stub for this in the config/method_stubs
// That's how we are doing latency compensation
Meteor.call('posts.create', id, title, content, (err) => {
if (err) {
return LocalState.set('SAVING_ERROR', err.message);
}
});
FlowRouter.go(`/post/${id}`);
},
clearErrors({LocalState}) {
return LocalState.set('SAVING_ERROR', null);
}
};
UI
Mantra 只使用 React 作为 UI 组件。
在 UI 组件内部不需要知道 App 的其他任何内容,也不应该读取和修改应用的 state。UI 使用到的数据和事件应该由 props 从 container 传入,或者通过事件作为 action props 传入。如果 UI 组件使用到本地 state,那么这个 state 不应该被外部的任何组件使用,仅限于组件内部使用。
Mantra 文档里给出的代码示例:
import React from 'react';
const PostList = ({posts}) => (
<div className='postlist'>
<ul>
{posts.map(post => (
<li key={post._id}>
<a href={`/post/${post._id}`}>{post.title}</a>
</li> ))}
</ul>
</div>
);
export default PostList;
上面的例子代码就是 React 里的无状态纯函数实现,UI 只负责展示界面,没有逻辑、状态等处理。
State 管理
有两种状态:本地状态(客户端)和远程状态(服务器)。本地状态不和外界发生联系;远程状态需要和外界,例如数据库同步数据。
类似 Flux 里的 store 概念 (可参考 使用 Meteor 和 React 开发 Web App
),Meteor 有不同的方式实现,例如 MiniMongo,ReactiveDict 等。Mantra 在这方面很灵活,没有要求用哪一种。但是还是有一些规则
- Action 里可以读写 state
- Container 里只能读 state
- UI 组件里既不能读也不能写 state,只能由 props 传入
Dependency Injection 依赖注入
首先,什么是依赖?Mantra 有两种依赖
- context - 通常就是配置,models 和各种数据
- actions - 业务逻辑。每个 action 都以 context 为第一个参数
例如:
const context = {
DB,
Router,
appName: 'My Blog'
};
const actions = {
posts: {
create({DB, Router}, title, content) {
const id = String(Math.random());
DB.createPost(id, title, content);
Router.go(`/post/${id}`);
}
}
};
然后注入依赖。Mantra 使用 react-simple-di 这个包来进行依赖注入。背后其实就是 React context。这个包接受 Context 和 Actions 作为依赖。
import {injectDeps} from 'react-simple-di';
import Layout from './layout.jsx';
// 上面定义的 context 和 actions 定义在这里
const LayoutWithDeps = injectDeps(context, actions)(Layout);
现在 LayoutWithDeps
就可以在 app 里随意使用了。
如何使用依赖?
首先创建一个 UI 组件。可以看到这个组件的依赖是通过 props 传入的
class CreatePost extends React.Component {
render() {
const {appName} = this.props;
return (
<div>
Create a blog post on app: ${appName}. <br/>
<button onClick={this.create.bind(this)}>Create Now</button>
</div>
);
}
create() {
const {createPost} = this.props;
createPost('My Blog Title', 'Some Content');
}
}
使用依赖
const {useDeps} from 'react-simple-di';
// 前面定义的 CreatePost react 组件
const depsToPropsMapper = (context, actions) => ({
appName: context.appName,
createPost: actions.posts.create
});
const CreatePostWithDeps = useDeps(depsToPropsMapper)(CreatePost);
如果你没有定义自己的 mapper 函数(就是上面的 depsToPropsMapper), useDeps 将使用下面的默认 mapper 函数,这样就可以直接使用 context 和 actions 了。
const mapper = (context, actions) => ({
context: () => context,
actions: () => actions
});
Mantra 使用依赖注入的目的是隔离代码。例如隔离 UI 组件和 actions。
一旦配置好,Applicaton Context 就会被注入到把 Context 作为第一参数的 action。
Container 同样也能读取 Application Context。
不能在子组件里注入依赖,只能是最上层组件,通常就是 Layout Component,例如下面代码。
import React from 'react';
export default function (injectDeps) {
// See: Injecting Deps
const MainLayoutCtx = injectDeps(MainLayout);
// Routes related code
}
Container
Container 的作用是集成、组装数据。它的中文意思是容器,里面包裹的就是 UI 组件。主要功能:
- 处理 state,处理后把值通过 props 传入 UI 组件
- 把 action 传入 UI 组件
- 把应用 Context 传入 UI 组件
Container 是一个 React 组件。如这篇文章所述的 Controller-View
如上图所示,使用一个父组件,就是 Mantra 的 container 来监听数据的变化,子组件 UI Component 负责界面渲染和互动。�Controller 就是高阶组件 (Higher Order Components) HOC 来包裹 UI 组件。高阶组件负责数据查询,子组件负责渲染等。
Mantra 使用 react‐komposer 来作为 container 获取数据状态。
container 的规则
- 每个 jsx 文件只能有一个 container,而且这个 container 应该是默认 export
- composer 和 mapper 函数应该从 container 模块输出
- composer 函数只能使用从 props 输入的值
- mapper 应该是纯函数
Note: 基于 Mantra Draft 0.2.0