Meteor Mantra 系列文章:
Meteor Mantra 介紹(一)- 基本概念
Meteor Mantra 介紹(二)- 前端架構(gòu)詳解
Meteor Mantra 介紹(三)- 后端架構(gòu)解釋
Meteor Mantra 介紹(四)- 博客例子前端代碼解讀
Meteor Mantra 介紹(五)- 博客例子后端代碼解讀
Meteor Mantra 介紹(六)- 使用 mantra-cli 命令行生成源碼
這篇文章由兩部分組成
基本介紹云挟。對(duì)每部分源代碼作用的介紹
工作流程舉例。數(shù)據(jù)如何在各部分流動(dòng)
基本介紹
這篇文章是對(duì) Meteor Mantra 的官方博客例子的詳細(xì)解讀舞萄,相當(dāng)于前面幾篇文章的一個(gè)應(yīng)用例子荠瘪。
前端入口
Mantra app 的前端入口是 client/main.js淘捡,這是 Meteor 框架的約定,會(huì)首先被執(zhí)行。它不應(yīng)該有任何其他邏輯锦积,只是初始化配置和加載必要模塊假哎。
例子利用 client/configs/context.js 把整個(gè)應(yīng)用的配置初始化瞬捕,還利用 mantra-core 加載了各個(gè) UI 組件并初始化。代碼很簡(jiǎn)短舵抹,見(jiàn)下面
import {createApp} from 'mantra-core';
import initContext from './configs/context';
// 引入 界面模塊
import coreModule from './modules/core';
import commentsModule from './modules/comments';
// 初始化 context
const context = initContext();
// 創(chuàng)建整個(gè) app肪虎,加載模塊并初始化
const app = createApp(context);
app.loadModule(coreModule);
app.loadModule(commentsModule);
app.init();
這里有必要解釋下初始化 context,就是 client/configs/context.js 這個(gè)文件惧蛹。Context 的意思是上下文扇救,這里是把全體環(huán)境使用到的第三方包和變量等引入刑枝,相當(dāng)于全局變量,統(tǒng)一引入可以避免再在每個(gè)文件去重復(fù) import迅腔,這樣整個(gè)應(yīng)用都能使用装畅,只需在需要時(shí)從 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 環(huán)境沧烈,數(shù)據(jù)庫(kù)的集合掠兄,路由,還有本地的響應(yīng)式變量等都引入了
export default function () {
return {
Meteor,
FlowRouter,
Collections,
LocalState: new ReactiveDict(),
Tracker
};
}
在 main.js 初始化 context 后锌雀,需要?jiǎng)?chuàng)建整個(gè) app蚂夕,加載各個(gè)模塊并初始化。參考 mantra-core 的源代碼 可以看到這里主要是通過(guò)依賴注入方式腋逆,把context婿牍,actions 和 UI 連接起來(lái)的地方,這樣你寫(xiě)代碼的時(shí)候可以把它們分開(kāi)寫(xiě)闲礼,達(dá)到 store牍汹、action 和 UI 解耦的目的,并讓數(shù)據(jù)單向流動(dòng)柬泽。
Modules
Mantra 使用的是模塊化結(jié)構(gòu)慎菲。這里的模塊不是 ES2015 的模塊,而是指結(jié)構(gòu)上的模塊锨并,形式上就是一組 ES2015 exports 構(gòu)成的一個(gè)文件夾露该,完成一個(gè)具體的功能。
我們這里以每個(gè) Mantra app 都必須有的 core 模塊為例第煮。
index.js
如果是 import 一個(gè)文件夾的話解幼,Node.js 的約定是從 index.js 開(kāi)始,Manta 里大量使用到了這個(gè)約定包警,所以基本每個(gè)文件里都會(huì)發(fā)現(xiàn)一個(gè) index.js 文件撵摆。
下面就是 index.js 的源碼,基本上就是一個(gè)集成害晦,就是把該文件夾里的除了 UI 組件的其他部分集中輸出給 main.js 的 app 去加載特铝。要注意的一點(diǎn)就是,這里的 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 沒(méi)有引入 UI 組件稻轨,那 UI 是怎么加載進(jìn)入應(yīng)用的呢灵莲?
因?yàn)?UI 組件會(huì)根據(jù)用戶的交互和 URL 變化,所以很自然的就是根據(jù) client/core/routes.js 和 url 決定 mount 那些組件殴俱。
這里注意兩點(diǎn)政冻,第一是 Mantra 在 routes.js 使用了 injectDeps 對(duì) Layout 注入了依賴枚抵。從 mantra-core 的源代碼 可以看到注入了 context 和 actions。第二是 mount 的 content 是一個(gè)函數(shù)赠幕,而不是通常的 React 組件俄精。因?yàn)槭褂昧?React context,需要在 layout 里 render榕堰,必須是函數(shù)竖慧。
...
export default function (injectDeps, {FlowRouter}) {
const MainLayoutCtx = injectDeps(MainLayout);
FlowRouter.route('/', {
name: 'posts.list',
action() {
mount(MainLayoutCtx, {
content: () => (<PostList />)
});
}
});
...
configs
這里是模塊級(jí)的配置。入口 index.js 文件輸出一個(gè)缺省函數(shù)逆屡,這個(gè)函數(shù)的第一個(gè)參數(shù)通常就是 Application Context圾旨。這里通常是 Meteor 的 method stubs 代碼,目的是獲得 optimistic UI 特性魏蔗。如果有的 method 有自己特別的邏輯不想公開(kāi)砍的,可以在這里實(shí)現(xiàn)和服務(wù)端不一樣的代碼,只要能預(yù)測(cè)用戶交互的結(jié)果就行莺治。
actions
和前面的文件夾一樣廓鞠,也是通過(guò) index.jx export。下面的代碼就是一個(gè)完整的 action谣旁〈布眩可以看到這個(gè) action 修改了 LocalState 這個(gè)客戶端的全局變量,還有通過(guò) Meteor.call 更新了數(shù)據(jù)庫(kù)榄审,最后跳轉(zhuǎn)到新的博客頁(yè)面砌们。
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();
// 通過(guò) method 更新數(shù)據(jù)庫(kù)
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 里通過(guò) mapper 函數(shù)完成的依賴注入,然后在 UI 里通過(guò) props 調(diào)用搁进。
containers
Containers 文件夾里沒(méi)有 index.js 文件浪感,因?yàn)?container 都是通過(guò) import 在 routes.js 單獨(dú)引入。和普通的非 Mantra Meteor app 一樣饼问,在這里 subscribe 后端數(shù)據(jù)影兽,并將數(shù)據(jù)通過(guò) props 傳遞到 view 的 UI 組件。Mantra 用的 react-komposer 這個(gè) npm 包來(lái)創(chuàng)建 container莱革。和非 Mantra Meteor app 不一樣的是峻堰,actions 是作為依賴注入到 container 的。這樣 UI 部分的顯示就和應(yīng)用的狀態(tài)改變分開(kāi)了驮吱。以 client/modules/core/contianers/newpost.js 為例
...
export const depsMapper = (context, actions) => ({
create: actions.posts.create, // 修改數(shù)據(jù)庫(kù)的 action 作為 props.create 被傳遞進(jìn)了 UI 的 NewPost 組件茧妒。
clearErrors: actions.posts.clearErrors,
context: () => context
});
export default composeAll(
composeWithTracker(composer),
useDeps(depsMapper)
)(NewPost);
components
這里就是 UI 組件了萧吠。也沒(méi)有 index.js, 因?yàn)?container 也是通過(guò) import 引入用到的每個(gè) UI 組件左冬。UI 組件就沒(méi)有什么特別之處了。Layout 和 css 文件也位于這個(gè)文件夾纸型。
其他模塊
這個(gè)博客例子還有一個(gè) comments 模塊拇砰,就是博客的評(píng)論部分梅忌。這個(gè)模塊相當(dāng)于 core 這個(gè)核心模塊就是一個(gè)副模塊了,所以它沒(méi)有 routes.js, 也沒(méi)有 layout 和 css除破,都是通過(guò) core 模塊來(lái)實(shí)現(xiàn)的牧氮。
工作流程舉例
上圖是 Mantra 的數(shù)據(jù)流動(dòng)示意圖,我們下面以它來(lái)說(shuō)明 Mantra 的工作流程瑰枫。假設(shè)你點(diǎn)擊了 http://mantra-sample-blog-app.herokuapp.com 這個(gè)博客的在線例子踱葛,然后接下來(lái)會(huì)發(fā)生
1 client/main.js
首先運(yùn)行的代碼是 client/main.js,在這個(gè)文件里光坝,各個(gè)模塊 module 的 route 和 action 被引入(詳見(jiàn) client/modules/core/index.js 的 export)尸诽,同樣 context 里的 FlowRouter,Collection 和 LocalState 等也被引入盯另。
這里就是圖中紅色虛線框的左邊兩個(gè)框 context 和 states 就緒性含。States 就是 context 里的 Collection 和 LocalState。
2 client/modules/core/route.js
在 client/main.js 里由 mantra-core 包創(chuàng)建的 app.init() 初始化會(huì)調(diào)用各個(gè) module 的 routes.js鸳惯。在 routes.js 里先把前面提到的 context 注入到 layout商蕴,然后根據(jù)用戶輸入或點(diǎn)擊正則匹配到前面列出的 routes.js 的根 url,接著掛載(mount)PostList 這個(gè) container 到注入了依賴的 MainLayoutCtx芝发。
這里就是圖中紅色虛線框的最右邊的框 container 就緒绪商。他們之所以在紅色的虛線里,就是表面他們都是基于 Meteor 的 reactive tracker 機(jī)制工作的后德,就是 Meteor 會(huì)自動(dòng)保證你的 states 的更新部宿。
3 container & UI component
Container 和使用 React-komposer 的非 Mantra app 的沒(méi)有太大區(qū)別,不一樣的是如果包含的 UI 有用戶交互的話瓢湃,那么需要注入 context 和 action理张。可以參看上面的 container 欄列出的 mapper 函數(shù)绵患。actions 就是這樣通過(guò) props 傳遞到 UI 組件的雾叭。因?yàn)槔永锏氖醉?yè)沒(méi)有用戶輸入的交互,所以我們以 newpost.js 這個(gè) container 為例落蝙,它通過(guò)前述方式注入了 action 的 create 和 clearErrors 函數(shù)织狐,然后在 UI 組件里的 newpost.js 通過(guò) props 調(diào)用 create 函數(shù),這就是淺藍(lán)色的 User Action 框筏勒,它執(zhí)行后會(huì)更改應(yīng)用的數(shù)據(jù) states移迫,而這種更改行為是通過(guò)注入 context 里的 Meteor, LocalState 等實(shí)現(xiàn)的。
4 Web Pub Action
上圖中最左邊的 action 是 Meteor 的數(shù)據(jù)訂閱 publication管行,當(dāng)有數(shù)據(jù)更新時(shí)厨埋,Meteor 的 tracker 會(huì)自動(dòng)接收到更新的 action 事件,然后啟動(dòng)相應(yīng) Meteor.subscribe 所在的 container 盡行組件的 re-render捐顷。而這一切也都是通過(guò) context 和瀏覽器里的 minimongo 來(lái)實(shí)現(xiàn)的荡陷。
...
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 的數(shù)據(jù)流動(dòng)方式雨效。
小結(jié)
這就是 Mantra 博客例子的前端代碼解釋。建議多結(jié)合例子代碼還有使用到包的源碼來(lái)理解废赞。和 Redux 類似徽龟,剛開(kāi)始時(shí)可能不太容易理解,因?yàn)椴恢庇^唉地,也不知道為什么非要繞一個(gè)很大的圈子來(lái)完成一件任務(wù)据悔,最好是多和實(shí)際例子聯(lián)系、應(yīng)用耘沼,理解 Mantra 的目的是寫(xiě)出更易于理解和維護(hù)的代碼屠尊,特別是對(duì)復(fù)雜的 app 有幫助。
注意:
- Mantra 官方文檔里有的 JSX 文件例子還是使用 .jsx 后綴「剑現(xiàn)在因?yàn)榻忉屍鬟M(jìn)步讼昆,Meteor 1.3+ 可以使用 .js 支持 JSX 語(yǔ)法,所以建議使用 .js 后綴骚烧。 Atwood's Law 再次發(fā)生作用 - Any application that can be written in JavaScript, will eventually be written in JavaScript
- Mantra 的這個(gè)博客例子里搭建好了 storybook, 大家也可以試試浸赫,它可以分離前后端開(kāi)發(fā),而且讓你對(duì)前端界面的更新立即可見(jiàn)赃绊。所以如果你發(fā)現(xiàn)在開(kāi)發(fā)前端時(shí)等待每次修改結(jié)果顯示時(shí)間太長(zhǎng)既峡,要不換臺(tái)更快的電腦,要不使用 storybook 立即看到你的修改碧查。