Mantra中文版

原版文档及版权说明参考https://kadirahq.github.io/mantra/

  1. 1简介
    1. 1.1Mantra包含什么?
    2. 1.2Mantra不是什么?
    3. 1.3Mantra是什么?
    4. 1.4我们为什么需要一个规范?
    5. 1.5预备知识
  2. 2核心内容
    1. 2.1侧重客户端
    2. 2.2ES2015语法和模块
    3. 2.3UI使用React
    4. 2.4Actions
    5. 2.5状态管理
    6. 2.6容器
    7. 2.7应用程序上下文
    8. 2.8依赖注入
      1. 2.8.1配置依赖注入
    9. 2.9路由和组件挂载
    10. 2.10
    11. 2.11测试
      1. 2.11.1UI测试
    12. 2.12Mantra模块
      1. 2.12.1应用程序上下文和模块
      2. 2.12.2模块定义
      3. 2.12.3隐性模块
      4. 2.12.4模块容器和UI组件
      5. 2.12.5模块Actions
      6. 2.12.6路由
      7. 2.12.7核心模块
      8. 2.12.8避免子模块
    13. 2.13单入口点
  3. 3目录结构
    1. 3.1顶层目录结构
      1. 3.1.1configs
      2. 3.1.2modules
        1. 3.1.2.1actions
        2. 3.1.2.2components
        3. 3.1.2.3containers
        4. 3.1.2.4configs
        5. 3.1.2.5libs
        6. 3.1.2.6routes.jsx
        7. 3.1.2.7index.js
      3. 3.1.3main.js
  4. 4未来工作
    1. 4.1服务端渲染
    2. 4.2通过NPM发布Mantra模块
    3. 4.3代码标准
    4. 4.4测试标准
    5. 4.5复用Composers
  5. 5为Mantra贡献
  6. A附录: 预备知识
    1. A.1ES2015
    2. A.2React
    3. A.3React容器
    4. A.4Meteor基础
  7. B附录: 服务端目录结构
    1. B.1methods
      1. B.1.1测试
    2. B.2publications
    3. B.3libs
    4. B.4configs
    5. B.5main.js
  8. C附录: 组织模块
    1. C.1单个核心模块
    2. C.2核心模块外加多个特性模块
    3. C.3多模块
    4. C.4页面模块
  9. D附录: 命名约定
    1. D.1源码文件命名
    2. D.2测试文件命名
      1. D.2.1后缀
  10. E附录: 词汇表

1简介

Mantra是一个基于Meteor的应用程序架构,我们试图通过它达成如下两个目的.

1. 可维护性

可维护性是大规模团队工作成功的关键因素。为了达到这一目标,需要为代码的每一部分添加单元测试,并为各方面内容制定全面的标准。通过这种方式,新的团队成员也可以更加容易的融入到团队之中。

2. 与时俱进

JavaScript生态系统丰富多样,往往每个问题都有多个优秀方案,很难说谁是当前唯一的最佳选择,以及未来会发生什么变化。

Mantra定义了一些可长期遵守的核心原则,其它部分则可按需变化。

1.1Mantra包含什么?

1.2Mantra不是什么?

1.3Mantra是什么?

1.4我们为什么需要一个规范?

Mantra是一个应用程序架构,它与很多人相关,包括应用程序开发者,工具链开发者,教程作者和项目经理,所以需要制定一个所有人能够遵循的共同标准。

1.5预备知识

如果您具备如下几方面的预备知识,学习mantra会更加方便。

更多相关知识可以参考附录A。

2核心内容

这里介绍Mantra的核心内容,以及它们的组织方式。

2.1侧重客户端

Mantra重点关注应用程序的客户端。它推荐代码共享,但并不像常规的meteor程序一样将客户端和服务端代码混合在一起。这里主要基于如下几点考虑:

基于上述的事实,将客户端和服务端代码混合不是一个好主意。这个规范里面讨论的内容主要是针对客户端的,服务端内容可以参考附录B。

2.2ES2015语法和模块

Mantra依赖于ES2015多个特性,特别是模块系统,而Meteor1.3则支持所有这些特性。

2.3UI使用React

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;

2.4Actions

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);
  }
};

2.5状态管理

在应用程序中,我们需要处理不同类型的状态,这些状态可以分为两大类型:

  1. 本地状态 - 仅存在于客户端的状态,不与服务端同步,比如错误信息,验证消息和当前页面等。
  2. 远端状态 - 需要从服务端获取并进行同步的状态。

应用程序里有多种管理状态的方法,包括:

JavaScript社区很多的关于状态管理的新方法,Mantra是很灵活的,您可以使用任何需要的方法。

比如,程序启动时您可以使用如下方法

然后,您可以使用其它方法

注意:Mantra有一些管理状态的强制规则

下面是一些状态管理的例子:

2.6容器

容器是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);

2.7应用程序上下文

应用程序上下文对所有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
  };
}

2.8依赖注入

Mantra使用依赖注入来区分应用程序的不同部分,包括UI组件和action。可以使用react-simple-di来实现依赖注入。

一旦配置完成,应用程序上下文会被自动注入到每个action里。

另外,应用程序上下文也可以在容器里访问。

2.8.1配置依赖注入

依赖会被注入到应用程序最高层的组件里,比如布局(Layout)组件,您可以在路由里面实现注入:

import React from 'react';
export default function (injectDeps) {
  // See: Injecting Deps
  const MainLayoutCtx = injectDeps(MainLayout);

  // Routes related code
}

2.9路由和组件挂载

在Mantra里,路由唯一的功能就是将组件挂载(mount)到UI上。可以有多种选择,比如Flow RouterReact 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 />)
      });
    }
  });
}

2.10

每个应用程序都需要一些完成不同功能的工具函数,您可以通过NPM来获得它们。这些库会导出函数,所以您可以在程序的任何地方导入它们,包括action,组件和容器。

当在一个组件里使用库函数时,库函数需要是纯函数.

2.11测试

测试是Mantra和核心内容,可以对程序的每部分进行测试。具体实现时可以使用诸如Mocha, Chai, 和 Sinon这样的常见工具。

在Mantra里,您可以对应用程序的三个核心部分进行单元测试,示例如下:

2.11.1UI测试

我们使用enzyme进行UI测试。通过这个例子来看一些测试样例。

2.12Mantra模块

Mantra遵循模块化架构,除了应用程序上下文,Mantra所有的组件都应该在某个模块里.

您可以在程序里创建很多模块,并通过import来进行集成。

2.12.1应用程序上下文和模块

应用程序上下文是应用程序的核心,它的定义应该不依赖任何模块。所有模块可以通过依赖来访问应用程序上下文,但是模块不应该去更新应用程序上下文。

2.12.2模块定义

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.
  }
};

2.12.3隐性模块

隐性模块(Implicit Modules)不需要定义文件,它没有action或者路由,也不需要做任何初始化操作,它可以包含下面这些内容:

  • UI组件
  • 容器

2.12.4模块容器和UI组件

模块容器和UI组件可以通过ES2015模块的方式导入。

2.12.5模块Actions

一个模块可以通过命名空间暴露action。这些命名空间对于应用程序而言是全局的,模块需要确保命名空间的唯一性。不过一个模块也可以暴露多个命名空间。

最后,每个模块的所有命名空间都会被合并,并可以在action和容器里面访问。

2.12.6路由

在Mantra里,您可以使用任何路由库,如果需要的话,可以在多个模块里面定义路由。

2.12.7核心模块

Mantra是百分之百模块化的,每个应用程序里至少有一个核心模块(core module)。 它仅是一个简单的模块,您需要在加载其它任何模块之前加载这个模块。核心模块是应用程序相关内容的最佳实现位置,包括:

  • 核心路由
  • 应用程序配置
  • 公共库
  • 公共action

根据程序的不同,有很多种组织模块的方法,具体方法可以参考附录C。

2.12.8避免子模块

在一个模块内不可以有子模块。这个决定是为了不必要的复杂性,因为多层嵌套的模块结构是非常难以维护的。

2.13单入口点

我们希望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();

3目录结构

在Mantra里强制目录结构规则,这是可维护性的关键。

这里只讨论客户端目录结构,服务端目录结构可以参考附录B。

3.1顶层目录结构

所有Mantra相关代码都在应用程序的 client 目录下,这个目录有2个子目录和1个js文件,它们是:

* configs
* modules
* main.js

3.1.1configs

这个目录包含应用程序的顶层配置信息,针对所有模块通用的配置信息放在这里。

这个目录下所有的js文件应该都具备一个默认导出函数,它完成初始化工作并返回一些必要的内容。

我们一般用这里的context.js文件放置应用程序上下文。

3.1.2modules

这个目录下的不同子目录中放置不同的模块,至少有一个命名为core的核心模块,内部结构如下:

* actions
* components
* configs
* containers
* libs
* routes.jsx
* index.js
3.1.2.1actions

这个目录包含模块里所有的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.jsaction模块一个posts的命名空间。

在应用程序中命名空间需要唯一性,这是我们在编写模块时需要注意的。

在tests目录下,我们针对每个action模块编写它们的测试,可以参考附录D以获取更多的关于测试文件命名规则的问题。

点击这里查看action的目录结构

3.1.2.2components

components目录包含模块的UI组件:

* main_layout.jsx
* post.jsx
* style.css
* tests
  - main_layout.js
  - post.js
  • 这个目录下所有的.jsx文件都需要有一个默认的export。它应该是一个React类。
  • 您可以针对这些React组件编写CSS文件,Meteor会帮您打包。

这里也有一个tests目录,具体命名习惯可以参考附录D。

点击这里查看components的目录结构

3.1.2.3containers

这个目录包含一些 .js 文件, 每个文件代表一个容器,并具有一个默认导出的React容器类:

* post.js
* postlist.js
* tests
    - post.js
    - postlist.js

这里也有一个测试目录tests,具体命名习惯可以参考附录D。

点击这里查看容器的目录结构

3.1.2.4configs

这个目录包含模块的配置信息。

这个目录下所有的js文件都需要导出一个默认函数,完成初始化并返回一些内容,这些函数都采用应用程序上下文作为第一个参数。

下面是一个示例的配置文件:

export default function (context) {
  // do something
}

在加载模块时,这些配置信息会被导入和调用。

点击这里查看configs的目录结构

3.1.2.5libs

这个目录包含一系列能够导出工具函数的js文件,这也被称作为库,您可以在tests目录下为这些库编写测试函数。

3.1.2.6routes.jsx

这个文件包含模块的路由定义,它有一个默认导出函数:

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组件里面注入依赖。

3.1.2.7index.js

这是模块定义文件,如果不考虑下面这些工作就不需要这个定义文件了:

  • 加载路由
  • 定义action
  • 加载模块时运行配置信息

下面是一个标准的模块定义:

import methodStubs from './configs/method_stubs';
import actions from './actions';
import routes from './routes.jsx';

export default {
  routes,
  actions,
  load(context) {
    methodStubs(context);
  }
};

在这个模块定义里.load() 方法在模块加载时被调用。所以,这是调用配置的地方。

3.1.3main.js

这里是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();

4未来工作

Mantra目前处于草案阶段,未来还会有很多改进和补充。下面这些内容相对重要,会在近期补充。

4.1服务端渲染

Mantra可以进行服务端渲染,目前的参考实现可以查看FlowRouter SSR.

4.2通过NPM发布Mantra模块

未来需要通过NPM发布Mantra的模块,一旦完成这个工作,我们就可以极大地共享代码。

4.3代码标准

未来需要一个编写代码的标准。

4.4测试标准

未来需要一个编写测试的标准。

4.5复用Composers

未来需要一个相应的规范,从而可以在很多地方针对同样的功能复用composer。

5为Mantra贡献

这是Mantra标准的一个草本,下面这个的示例程序充分展示了Mantra的特性

对Mantra的讨论在这里进行。

A附录: 预备知识

下面这些资源有助于您更加清晰地了解Mantra.

A.1ES2015

ES2015是2015年最新版的JavaScript语言标准,由于Meteor已经内置了ES2015支持,所以您不需要做任何额外的工作就可以直接使用。

ES2015是JavaScript世界最激动人心的事件,它引入了很多新特性,并解决了很多公共问题。

A.2React

React是一个基于JavaScript的UI框架。您可以在JavaScript里创建基于HTML内容,这个特性一开始看起来比较奇怪,不过适应以后会发现非常有用。下面是一些必要的资源:

A.3React容器

我们目前很少使用React组件的状态,而是通过props获取数据,React的无状态组件使这项工作非常容易。

我们使用React容器从不同的数据源获取数据并加载到UI组件时,react‐komposer使这项工作更加容易,下面这篇文章可以让您获取更多知识。

A.4Meteor基础

您需要对Meteor有一个相对深入的了解。可以遵循Meteor的官方教程.

Mantra在使用上述技术时略有不同,比如Meteor的React教程建议使用mixin获取Mongo集合数据。但是Mantra使用容器这种更加时髦的方式来使用React。

B附录: 服务端目录结构

服务端目录结构不是Mantra的核心内容,不过它类似客户端的目录结构,有一个main目录以及一个 main.js 的js文件。

* methods
* publications
* libs
* configs
* main.js

B.1methods

应用程序的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();
}

B.1.1测试

我们可以为tests目录下的method编写测试,这时最好使用集成测试而不是单元测试。

具体内容可参考Gagarin.

B.2publications

这个目录和methods一致,我们写发布而不是method。

B.3libs

这个目录包含一些在服务端使用的工具函数。

B.4configs

我们在这里写用用程序的配置信息,这些配置需要一个默认的导出函数,可以被导入和调用,配置代码需要在这个函数里编写。

下面是一个示例:

export default function() {
  //  invoke the configuration here
}

B.5main.js

这里是整个应用程序启动的入口点,我们在这里导入method,发布和配置代码。

下面是一个main.js的示例:

import publications from './publications';
import methods from './methods';
import addInitialData from './configs/initial_adds.js';

publications();
methods();
addInitialData();

注意: 可以看这个样例代码来学习具体的实现方式。

C附录: 组织模块

Mantra是一个百分之百基于模块的应用程序架构,至少应该有一个模块。我们已经讨论了如何在模块里面组织代码以及如何使用它们,但是我并没有讨论过如何组织模块。下面是一些常见的组织模块的方式。

C.1单个核心模块

简单程序可以将所有代码放到一个模块里,并命名为 core,这比较适用于客户端代码比较少的简单程序。

C.2核心模块外加多个特性模块

这是在上述“单个核心模块”模式的扩充:

C.3多模块

在多模块方式下,没有单个核心模块。

C.4页面模块

注意: 这种方式可以与上述任何方式一起使用。

有时候,我们需要显示一些UI页面。但是它们并没有action,路由和配置。它们只包含UI代码,可能是UI组件或者其它容器,这时可以使用隐含模块。

D附录: 命名约定

目录结构一节讨论了不同组件组织文件的方式,这里我们讨论命名文件的方式。

D.1源码文件命名

我们从文件名除去扩展名后需要满足如下条件:

相应的正则表达式是:

/^[a-z]+[a-z0-9_]+$/

D.2测试文件命名

我们用如下规则命名tests目录下的文件名:

D.2.1后缀

大多数情况下,每个源码文件都有一个测试文件。当需要为一个源码文件创建多个测试文件时,就需要编写后缀了。

如果源码文件名是 posts.js, 那么添加后缀以后将会如下的样子:

posts-part1.js
posts-part2.js

相应的正则表达式是:

/^([a-z]+[a-z0-9_]+)(\-[a-z]+[a-z0-9_]+)*$/

E附录: 词汇表

中英翻译及核心概念解释。