×

Meteor Mantra 介绍 (四)- 博客例子前端代码解读

96
荆雷
2016.08.06 04:56* 字数 2337

Meteor Mantra 系列文章:

Meteor Mantra 介绍(一)- 基本概念
Meteor Mantra 介绍(二)- 前端架构详解
Meteor Mantra 介绍(三)- 后端架构解释
Meteor Mantra 介绍(四)- 博客例子前端代码解读
Meteor Mantra 介绍(五)- 博客例子后端代码解读
Meteor Mantra 介绍(六)- 使用 mantra-cli 命令行生成源码


这篇文章由两部分组成

  • 基本介绍。对每部分源代码作用的介绍

  • 工作流程举例。数据如何在各部分流动

基本介绍

这篇文章是对 Meteor Mantra 的官方博客例子的详细解读,相当于前面几篇文章的一个应用例子。

博客例子代码, 博客的在线 demo

前端入口

Mantra app 的前端入口是 client/main.js,这是 Meteor 框架的约定,会首先被执行。它不应该有任何其他逻辑,只是初始化配置和加载必要模块。

例子利用 client/configs/context.js 把整个应用的配置初始化,还利用 mantra-core 加载了各个 UI 组件并初始化。代码很简短,见下面

import {createApp} from 'mantra-core';
import initContext from './configs/context';

// 引入 界面模块
import coreModule from './modules/core';
import commentsModule from './modules/comments';

// 初始化 context
const context = initContext();

// 创建整个 app,加载模块并初始化
const app = createApp(context);
app.loadModule(coreModule);
app.loadModule(commentsModule);
app.init();

这里有必要解释下初始化 context,就是 client/configs/context.js 这个文件。Context 的意思是上下文,这里是把全体环境使用到的第三方包和变量等引入,相当于全局变量,统一引入可以避免再在每个文件去重复 import,这样整个应用都能使用,只需在需要时从 context 引入就行。

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';

// 可以看到 Meteor 环境,数据库的集合,路由,还有本地的响应式变量等都引入了
export default function () {
  return {
    Meteor,
    FlowRouter,
    Collections,
    LocalState: new ReactiveDict(),
    Tracker
  };
}

在 main.js 初始化 context 后,需要创建整个 app,加载各个模块并初始化。参考 mantra-core 的源代码 可以看到这里主要是通过依赖注入方式,把context,actions 和 UI 连接起来的地方,这样你写代码的时候可以把它们分开写,达到 store、action 和 UI 解耦的目的,并让数据单向流动。

Modules

Mantra 使用的是模块化结构。这里的模块不是 ES2015 的模块,而是指结构上的模块,形式上就是一组 ES2015 exports 构成的一个文件夹,完成一个具体的功能。

我们这里以每个 Mantra app 都必须有的 core 模块为例。

index.js

如果是 import 一个文件夹的话,Node.js 的约定是从 index.js 开始,Manta 里大量使用到了这个约定,所以基本每个文件里都会发现一个 index.js 文件。

下面就是 index.js 的源码,基本上就是一个集成,就是把该文件夹里的除了 UI 组件的其他部分集中输出给 main.js 的 app 去加载。要注意的一点就是,这里的 configs 通常是 Meteor 的 method stubs 代码,目的是获得 optimistic updates 特性。和顶层的 configs 不太一样。

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

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

routes.js

前面提到 index.js 没有引入 UI 组件,那 UI 是怎么加载进入应用的呢?
因为 UI 组件会根据用户的交互和 URL 变化,所以很自然的就是根据 client/core/routes.js 和 url 决定 mount 那些组件。

这里注意两点,第一是 Mantra 在 routes.js 使用了 injectDeps 对 Layout 注入了依赖。从 mantra-core 的源代码 可以看到注入了 context 和 actions。第二是 mount 的 content 是一个函数,而不是通常的 React 组件。因为使用了 React context,需要在 layout 里 render,必须是函数。

    ...
export default function (injectDeps, {FlowRouter}) {
  const MainLayoutCtx = injectDeps(MainLayout);

  FlowRouter.route('/', {
    name: 'posts.list',
    action() {
      mount(MainLayoutCtx, {
        content: () => (<PostList />)
      });
    }
  });
    ...

configs

这里是模块级的配置。入口 index.js 文件输出一个缺省函数,这个函数的第一个参数通常就是 Application Context。这里通常是 Meteor 的 method stubs 代码,目的是获得 optimistic UI 特性。如果有的 method 有自己特别的逻辑不想公开,可以在这里实现和服务端不一样的代码,只要能预测用户交互的结果就行。

actions

和前面的文件夹一样,也是通过 index.jx export。下面的代码就是一个完整的 action。可以看到这个 action 修改了 LocalState 这个客户端的全局变量,还有通过 Meteor.call 更新了数据库,最后跳转到新的博客页面。

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();
    // 通过 method 更新数据库
    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);
  }
};

Action 是在 container 里通过 mapper 函数完成的依赖注入,然后在 UI 里通过 props 调用。

containers

Containers 文件夹里没有 index.js 文件,因为 container 都是通过 import 在 routes.js 单独引入。和普通的非 Mantra Meteor app 一样,在这里 subscribe 后端数据,并将数据通过 props 传递到 view 的 UI 组件。Mantra 用的 react-komposer 这个 npm 包来创建 container。和非 Mantra Meteor app 不一样的是,actions 是作为依赖注入到 container 的。这样 UI 部分的显示就和应用的状态改变分开了。以 client/modules/core/contianers/newpost.js 为例

...
export const depsMapper = (context, actions) => ({
  create: actions.posts.create,  // 修改数据库的 action 作为 props.create 被传递进了 UI 的 NewPost 组件。
  clearErrors: actions.posts.clearErrors,
  context: () => context
});

export default composeAll(
  composeWithTracker(composer),
  useDeps(depsMapper)
)(NewPost);

components

这里就是 UI 组件了。也没有 index.js, 因为 container 也是通过 import 引入用到的每个 UI 组件。UI 组件就没有什么特别之处了。Layout 和 css 文件也位于这个文件夹。

其他模块

这个博客例子还有一个 comments 模块,就是博客的评论部分。这个模块相当于 core 这个核心模块就是一个副模块了,所以它没有 routes.js, 也没有 layout 和 css,都是通过 core 模块来实现的。


工作流程举例

mantra_flow.png

上图是 Mantra 的数据流动示意图,我们下面以它来说明 Mantra 的工作流程。假设你点击了 http://mantra-sample-blog-app.herokuapp.com 这个博客的在线例子,然后接下来会发生

1 client/main.js

首先运行的代码是 client/main.js,在这个文件里,各个模块 module 的 route 和 action 被引入(详见 client/modules/core/index.js 的 export),同样 context 里的 FlowRouter,Collection 和 LocalState 等也被引入。

这里就是图中红色虚线框的左边两个框 context 和 states 就绪。States 就是 context 里的 Collection 和 LocalState。

2 client/modules/core/route.js

在 client/main.js 里由 mantra-core 包创建的 app.init() 初始化会调用各个 module 的 routes.js。在 routes.js 里先把前面提到的 context 注入到 layout,然后根据用户输入或点击正则匹配到前面列出的 routes.js 的根 url,接着挂载(mount)PostList 这个 container 到注入了依赖的 MainLayoutCtx。

这里就是图中红色虚线框的最右边的框 container 就绪。他们之所以在红色的虚线里,就是表面他们都是基于 Meteor 的 reactive tracker 机制工作的,就是 Meteor 会自动保证你的 states 的更新。

3 container & UI component

Container 和使用 React-komposer 的非 Mantra app 的没有太大区别,不一样的是如果包含的 UI 有用户交互的话,那么需要注入 context 和 action。可以参看上面的 container 栏列出的 mapper 函数。actions 就是这样通过 props 传递到 UI 组件的。因为例子里的首页没有用户输入的交互,所以我们以 newpost.js 这个 container 为例,它通过前述方式注入了 action 的 create 和 clearErrors 函数,然后在 UI 组件里的 newpost.js 通过 props 调用 create 函数,这就是浅蓝色的 User Action 框,它执行后会更改应用的数据 states,而这种更改行为是通过注入 context 里的 Meteor, LocalState 等实现的。

4 Web Pub Action

上图中最左边的 action 是 Meteor 的数据订阅 publication,当有数据更新时,Meteor 的 tracker 会自动接收到更新的 action 事件,然后启动相应 Meteor.subscribe 所在的 container 尽行组件的 re-render。而这一切也都是通过 context 和浏览器里的 minimongo 来实现的。

    ...
export const composer = ({context}, onData) => {
  const {Meteor, Collections} = context();
  if (Meteor.subscribe('posts.list').ready()) {
    const posts = Collections.Posts.find().fetch();
    onData(null, {posts});
  }
};
    ...

以上就是 Mantra 的数据流动方式。

小结

这就是 Mantra 博客例子的前端代码解释。建议多结合例子代码还有使用到包的源码来理解。和 Redux 类似,刚开始时可能不太容易理解,因为不直观,也不知道为什么非要绕一个很大的圈子来完成一件任务,最好是多和实际例子联系、应用,理解 Mantra 的目的是写出更易于理解和维护的代码,特别是对复杂的 app 有帮助。

注意:

  1. Mantra 官方文档里有的 JSX 文件例子还是使用 .jsx 后缀。现在因为解释器进步,Meteor 1.3+ 可以使用 .js 支持 JSX 语法,所以建议使用 .js 后缀。 Atwood's Law 再次发生作用 - Any application that can be written in JavaScript, will eventually be written in JavaScript
  2. Mantra 的这个博客例子里搭建好了 storybook, 大家也可以试试,它可以分离前后端开发,而且让你对前端界面的更新立即可见。所以如果你发现在开发前端时等待每次修改结果显示时间太长,要不换台更快的电脑,要不使用 storybook 立即看到你的修改。
日记本
Web note ad 1