Meteor Mantra 介绍 (一)- 基本概念

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。

mantra_flow.png

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 有两种依赖

  1. context - 通常就是配置,models 和各种数据
  2. 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

Flux

如上图所示,使用一个父组件,就是 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容