Meteor Mantra 系列文章:
Meteor Mantra 介绍(一)- 基本概念
Meteor Mantra 介绍(二)- 前端架构详解
Meteor Mantra 介绍(三)- 后端架构解释
Meteor Mantra 介绍(四)- 博客例子前端代码解读
Meteor Mantra 介绍(五)- 博客例子后端代码解读
Meteor Mantra 介绍(六)- 使用 mantra-cli 命令行生成源码
这篇文章由两部分组成
基本介绍。对每部分源代码作用的介绍
工作流程举例。数据如何在各部分流动
基本介绍
这篇文章是对 Meteor Mantra 的官方博客例子的详细解读,相当于前面几篇文章的一个应用例子。
前端入口
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 的数据流动示意图,我们下面以它来说明 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 有帮助。
注意:
- 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
- Mantra 的这个博客例子里搭建好了 storybook, 大家也可以试试,它可以分离前后端开发,而且让你对前端界面的更新立即可见。所以如果你发现在开发前端时等待每次修改结果显示时间太长,要不换台更快的电脑,要不使用 storybook 立即看到你的修改。