原版文档及版权说明参考https://kadirahq.github.io/mantra/
Mantra是一个基于Meteor的应用程序架构,我们试图通过它达成如下两个目的.
1. 可维护性
可维护性是大规模团队工作成功的关键因素。为了达到这一目标,需要为代码的每一部分添加单元测试,并为各方面内容制定全面的标准。通过这种方式,新的团队成员也可以更加容易的融入到团队之中。
2. 与时俱进
JavaScript生态系统丰富多样,往往每个问题都有多个优秀方案,很难说谁是当前唯一的最佳选择,以及未来会发生什么变化。
Mantra定义了一些可长期遵守的核心原则,其它部分则可按需变化。
Mantra是一个应用程序架构,它与很多人相关,包括应用程序开发者,工具链开发者,教程作者和项目经理,所以需要制定一个所有人能够遵循的共同标准。
如果您具备如下几方面的预备知识,学习mantra会更加方便。
更多相关知识可以参考附录A。
这里介绍Mantra的核心内容,以及它们的组织方式。
Mantra重点关注应用程序的客户端。它推荐代码共享,但并不像常规的meteor程序一样将客户端和服务端代码混合在一起。这里主要基于如下几点考虑:
基于上述的事实,将客户端和服务端代码混合不是一个好主意。这个规范里面讨论的内容主要是针对客户端的,服务端内容可以参考附录B。
Mantra依赖于ES2015多个特性,特别是模块系统,而Meteor1.3则支持所有这些特性。
Mantra使用React实现UI表现层。
UI组件不依赖程序,也不能读取程序状态,用来渲染UI组件的数据和事件处理函数是通过props传入的。有时在UI组件里会使用临时本地状态,但是这样的状态永远不会被外部的内容引用。
当编写UI组件时,可以使用任意其它React组件。下面这些地方可以导入React组件
您可以直接从NPM组件中导入任何库函数,并在UI组件中使用。这些函数应该是纯函数.
下面是一个简单的UI组件:
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;
Actions是在应用程序中写 业务逻辑 的地方,包括:
Action是一个函数,它的第一个参数是应用程序上下文,其它参数是基于调用场景的。
注意:
下面是一个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);
}
};
在应用程序中,我们需要处理不同类型的状态,这些状态可以分为两大类型:
应用程序里有多种管理状态的方法,包括:
JavaScript社区很多的关于状态管理的新方法,Mantra是很灵活的,您可以使用任何需要的方法。
比如,程序启动时您可以使用如下方法
然后,您可以使用其它方法
注意:Mantra有一些管理状态的强制规则
下面是一些状态管理的例子:
容器是Mantra里的集成层,它主要执行如下操作:
容器也是一个React组件,它通过react‐komposer进行集成,支持不同的数据源,包括Meteor/Tracker, Promises, Rx.js Observable等。
容器内需要写如下这些函数:
创建一个容器时需要遵循如下规则:
注意:如果您需要将应用程序上下文传递给一个组件,要使用mapper通过props传递。
下面是一个容器的例子:
import PostList from '../components/postlist.jsx';
import {useDeps, composeWithTracker, composeAll} from 'mantra-core';
export const composer = ({context}, onData) => {
const {Meteor, Collections} = context();
if (Meteor.subscribe('posts.list').ready()) {
const posts = Collections.Posts.find().fetch();
onData(null, {posts});
}
};
export default composeAll(
composeWithTracker(composer),
useDeps()
)(PostList);
应用程序上下文对所有action和容器都是可见的,所以可以将应用程序共享变量放在这个地方,包括
下面是一个简单的例子:
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
};
}
Mantra使用依赖注入来区分应用程序的不同部分,包括UI组件和action。可以使用react-simple-di
来实现依赖注入。
一旦配置完成,应用程序上下文会被自动注入到每个action里。
另外,应用程序上下文也可以在容器里访问。
依赖会被注入到应用程序最高层的组件里,比如布局(Layout)组件,您可以在路由里面实现注入:
import React from 'react';
export default function (injectDeps) {
// See: Injecting Deps
const MainLayoutCtx = injectDeps(MainLayout);
// Routes related code
}
在Mantra里,路由唯一的功能就是将组件挂载(mount)到UI上。可以有多种选择,比如Flow Router和React Router。
下面例子使用FlowRouter作为路由:
import React from 'react';
import {FlowRouter} from 'meteor/kadira:flow-router';
import {mount} from 'react-mounter';
import MainLayout from '/client/modules/core/components/main_layout.jsx';
import PostList from '/client/modules/core/containers/postlist';
export default function (injectDeps) {
const MainLayoutCtx = injectDeps(MainLayout);
FlowRouter.route('/', {
name: 'posts.list',
action() {
mount(MainLayoutCtx, {
content: () => (<PostList />)
});
}
});
}
每个应用程序都需要一些完成不同功能的工具函数,您可以通过NPM来获得它们。这些库会导出函数,所以您可以在程序的任何地方导入它们,包括action,组件和容器。
当在一个组件里使用库函数时,库函数需要是纯函数.
测试是Mantra和核心内容,可以对程序的每部分进行测试。具体实现时可以使用诸如Mocha, Chai, 和 Sinon这样的常见工具。
在Mantra里,您可以对应用程序的三个核心部分进行单元测试,示例如下:
Mantra遵循模块化架构,除了应用程序上下文,Mantra所有的组件都应该在某个模块里.
您可以在程序里创建很多模块,并通过import来进行集成。
应用程序上下文是应用程序的核心,它的定义应该不依赖任何模块。所有模块可以通过依赖来访问应用程序上下文,但是模块不应该去更新应用程序上下文。
Mantra的模块需要一个定义文件‘index.js’,用来暴露action和路由并可以接受应用程序上下文。
一个简单的模块定义如下图所示:
export default {
// 可选
load(context, actions) {
// 模块初始化
},
// 可选
actions: {
myNamespace: {
doSomething: (context, arg1) => {}
}
},
// 可选
routes(injectDeps) {
const InjectedComp = injectDeps(MyComp);
// load routes and put `InjectedComp` to the screen.
}
};
隐性模块(Implicit Modules)不需要定义文件,它没有action或者路由,也不需要做任何初始化操作,它可以包含下面这些内容:
模块容器和UI组件可以通过ES2015模块的方式导入。
一个模块可以通过命名空间暴露action。这些命名空间对于应用程序而言是全局的,模块需要确保命名空间的唯一性。不过一个模块也可以暴露多个命名空间。
最后,每个模块的所有命名空间都会被合并,并可以在action和容器里面访问。
在Mantra里,您可以使用任何路由库,如果需要的话,可以在多个模块里面定义路由。
Mantra是百分之百模块化的,每个应用程序里至少有一个核心模块(core module)。 它仅是一个简单的模块,您需要在加载其它任何模块之前加载这个模块。核心模块是应用程序相关内容的最佳实现位置,包括:
根据程序的不同,有很多种组织模块的方法,具体方法可以参考附录C。
在一个模块内不可以有子模块。这个决定是为了不必要的复杂性,因为多层嵌套的模块结构是非常难以维护的。
我们希望Mantra程序的运行是可预期的,这样我们会在程序里放置唯一的入口点client/main.js
。它会初始化程序上下文并加载程序中所有的模块,下面是一个示例:
import {createApp} from 'mantra-core';
import {initContext} from './configs/context';
// modules
import coreModule from './modules/core';
import commentsModule from './modules/comments';
// init context
const context = initContext();
// create app
const app = createApp(context);
app.loadModule(coreModule);
app.loadModule(commentsModule);
app.init();
在Mantra里强制目录结构规则,这是可维护性的关键。
这里只讨论客户端目录结构,服务端目录结构可以参考附录B。
所有Mantra相关代码都在应用程序的 client
目录下,这个目录有2个子目录和1个js文件,它们是:
* configs
* modules
* main.js
这个目录包含应用程序的顶层配置信息,针对所有模块通用的配置信息放在这里。
这个目录下所有的js文件应该都具备一个默认导出函数,它完成初始化工作并返回一些必要的内容。
我们一般用这里的context.js
文件放置应用程序上下文。
这个目录下的不同子目录中放置不同的模块,至少有一个命名为core
的核心模块,内部结构如下:
* actions
* components
* configs
* containers
* libs
* routes.jsx
* index.js
这个目录包含模块里所有的actions:
* posts.js
* index.js
* tests
- posts.js
posts.js
是一个ES2015模块,它可以导出一个具有action的js对象。
下面是一个简单的action模块:
export default {
create({Meteor, LocalState, FlowRouter}, title, content) {
//...
},
clearErrors({LocalState}) {
//...
}
};
在index.js
里导入所有的action模块并聚合所有action,并给每个模块一个命名空间。
import posts from './posts';
export default {
posts
};
在上面例子中,我们给posts.js
action模块一个posts
的命名空间。
在应用程序中命名空间需要唯一性,这是我们在编写模块时需要注意的。
在tests目录下,我们针对每个action模块编写它们的测试,可以参考附录D以获取更多的关于测试文件命名规则的问题。
components目录包含模块的UI组件:
* main_layout.jsx
* post.jsx
* style.css
* tests
- main_layout.js
- post.js
.jsx
文件都需要有一个默认的export。它应该是一个React类。这里也有一个tests目录,具体命名习惯可以参考附录D。
这个目录包含一些 .js
文件, 每个文件代表一个容器,并具有一个默认导出的React容器类:
* post.js
* postlist.js
* tests
- post.js
- postlist.js
这里也有一个测试目录tests
,具体命名习惯可以参考附录D。
这个目录包含模块的配置信息。
这个目录下所有的js文件都需要导出一个默认函数,完成初始化并返回一些内容,这些函数都采用应用程序上下文作为第一个参数。
下面是一个示例的配置文件:
export default function (context) {
// do something
}
在加载模块时,这些配置信息会被导入和调用。
这个目录包含一系列能够导出工具函数的js文件,这也被称作为库,您可以在tests目录下为这些库编写测试函数。
这个文件包含模块的路由定义,它有一个默认导出函数:
import React from 'react';
import {FlowRouter} from 'meteor/kadira:flow-router';
import {mount} from 'react-mounter';
import MainLayout from '/client/modules/core/components/main_layout.jsx';
import PostList from '/client/modules/core/containers/postlist';
export default function (injectDeps) {
const MainLayoutCtx = injectDeps(MainLayout);
FlowRouter.route('/', {
name: 'posts.list',
action() {
mount(MainLayoutCtx, {
content: () => (<PostList />)
});
}
});
}
这里默认导出一个函数,函数里在加载模块时使用injectDeps
向React组件里面注入依赖。
这是模块定义文件,如果不考虑下面这些工作就不需要这个定义文件了:
下面是一个标准的模块定义:
import methodStubs from './configs/method_stubs';
import actions from './actions';
import routes from './routes.jsx';
export default {
routes,
actions,
load(context) {
methodStubs(context);
}
};
在这个模块定义里.load()
方法在模块加载时被调用。所以,这是调用配置的地方。
这里是Mantra应用程序的入口,初始化程序上下文并加载模块,这些工作是由一个被称作mantra-core
的库完成的。
下面是一个例子:
import {createApp} from 'mantra-core';
import initContext from './configs/context';
// modules
import coreModule from './modules/core';
import commentsModule from './modules/comments';
// init context
const context = initContext();
// create app
const app = createApp(context);
app.loadModule(coreModule);
app.loadModule(commentsModule);
app.init();
Mantra目前处于草案阶段,未来还会有很多改进和补充。下面这些内容相对重要,会在近期补充。
Mantra可以进行服务端渲染,目前的参考实现可以查看FlowRouter SSR.
未来需要通过NPM发布Mantra的模块,一旦完成这个工作,我们就可以极大地共享代码。
未来需要一个编写代码的标准。
未来需要一个编写测试的标准。
未来需要一个相应的规范,从而可以在很多地方针对同样的功能复用composer。
这是Mantra标准的一个草本,下面这个的示例程序充分展示了Mantra的特性
对Mantra的讨论在这里进行。
下面这些资源有助于您更加清晰地了解Mantra.
ES2015是2015年最新版的JavaScript语言标准,由于Meteor已经内置了ES2015支持,所以您不需要做任何额外的工作就可以直接使用。
ES2015是JavaScript世界最激动人心的事件,它引入了很多新特性,并解决了很多公共问题。
React是一个基于JavaScript的UI框架。您可以在JavaScript里创建基于HTML内容,这个特性一开始看起来比较奇怪,不过适应以后会发现非常有用。下面是一些必要的资源:
我们目前很少使用React组件的状态,而是通过props获取数据,React的无状态组件使这项工作非常容易。
我们使用React容器从不同的数据源获取数据并加载到UI组件时,react‐komposer使这项工作更加容易,下面这篇文章可以让您获取更多知识。
您需要对Meteor有一个相对深入的了解。可以遵循Meteor的官方教程.
Mantra在使用上述技术时略有不同,比如Meteor的React教程建议使用mixin获取Mongo集合数据。但是Mantra使用容器这种更加时髦的方式来使用React。
服务端目录结构不是Mantra的核心内容,不过它类似客户端的目录结构,有一个main目录以及一个 main.js
的js文件。
* methods
* publications
* libs
* configs
* main.js
应用程序的Method放在这个目录下,文件结构如下:
* posts.js
* index.js
* tests
- posts.js
这里有一个posts.js
文件,实现了应用程序里关于posts
特性的一些method。这里有一个默认的导出函数,Meteor的Method就定义在这个函数里。
命名method名称的时候,需要一个前缀,前缀是文件名和一个点,比如 posts.
项目是一些 posts.js
内定义的method的例子:
import {Posts, Comments} from '/lib/collections';
import {Meteor} from 'meteor/meteor';
import {check} from 'meteor/check';
export default function() {
Meteor.methods({
'posts.create'(_id, title, content) {
// method body
}
});
Meteor.methods({
'posts.createComment'(_id, postId, text) {
// method body
}
});
}
最后,需要一个 index.js
文件, 它可以导入目录下的其它模块并在默认导出里调用。 所以当导入方法时,我们可以用单个导入实现。
下面是一个index.js
的例子:
import posts from './posts';
import admin from './admin';
export default function () {
posts();
admin();
}
我们可以为tests目录下的method编写测试,这时最好使用集成测试而不是单元测试。
具体内容可参考Gagarin.
这个目录和methods
一致,我们写发布而不是method。
这个目录包含一些在服务端使用的工具函数。
我们在这里写用用程序的配置信息,这些配置需要一个默认的导出函数,可以被导入和调用,配置代码需要在这个函数里编写。
下面是一个示例:
export default function() {
// invoke the configuration here
}
这里是整个应用程序启动的入口点,我们在这里导入method,发布和配置代码。
下面是一个main.js
的示例:
import publications from './publications';
import methods from './methods';
import addInitialData from './configs/initial_adds.js';
publications();
methods();
addInitialData();
注意: 可以看这个样例代码来学习具体的实现方式。
Mantra是一个百分之百基于模块的应用程序架构,至少应该有一个模块。我们已经讨论了如何在模块里面组织代码以及如何使用它们,但是我并没有讨论过如何组织模块。下面是一些常见的组织模块的方式。
简单程序可以将所有代码放到一个模块里,并命名为 core
,这比较适用于客户端代码比较少的简单程序。
这是在上述“单个核心模块”模式的扩充:
在多模块方式下,没有单个核心模块。
注意: 这种方式可以与上述任何方式一起使用。
有时候,我们需要显示一些UI页面。但是它们并没有action,路由和配置。它们只包含UI代码,可能是UI组件或者其它容器,这时可以使用隐含模块。
目录结构一节讨论了不同组件组织文件的方式,这里我们讨论命名文件的方式。
我们从文件名除去扩展名后需要满足如下条件:
_
符号。相应的正则表达式是:
/^[a-z]+[a-z0-9_]+$/
我们用如下规则命名tests
目录下的文件名:
大多数情况下,每个源码文件都有一个测试文件。当需要为一个源码文件创建多个测试文件时,就需要编写后缀了。
如果源码文件名是 posts.js
, 那么添加后缀以后将会如下的样子:
posts-part1.js
posts-part2.js
相应的正则表达式是:
/^([a-z]+[a-z0-9_]+)(\-[a-z]+[a-z0-9_]+)*$/
中英翻译及核心概念解释。