Meteor Mantra 系列文章:
Meteor Mantra 介紹(一)- 基本概念
Meteor Mantra 介紹(二)- 前端架構(gòu)詳解
Meteor Mantra 介紹(三)- 后端架構(gòu)解釋
Meteor Mantra 介紹(四)- 博客例子前端代碼解讀
Meteor Mantra 介紹(五)- 博客例子后端代碼解讀
Meteor Mantra 介紹(六)- 使用 mantra-cli 命令行生成源碼
這篇文章由兩部分組成
基本介紹。對每部分源代碼作用的介紹
工作流程舉例。數(shù)據(jù)如何在各部分流動
基本介紹
這篇文章是對 Meteor Mantra 的官方博客例子的詳細解讀,相當于前面幾篇文章的一個應用例子。
前端入口
Mantra app 的前端入口是 client/main.js,這是 Meteor 框架的約定,會首先被執(zhí)行。它不應該有任何其他邏輯,只是初始化配置和加載必要模塊。
例子利用 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();
// 創(chuàng)建整個 app,加載模塊并初始化
const app = createApp(context);
app.loadModule(coreModule);
app.loadModule(commentsModule);
app.init();
這里有必要解釋下初始化 context,就是 client/configs/context.js 這個文件。Context 的意思是上下文,這里是把全體環(huán)境使用到的第三方包和變量等引入,相當于全局變量,統(tǒng)一引入可以避免再在每個文件去重復 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 環(huán)境,數(shù)據(jù)庫的集合,路由,還有本地的響應式變量等都引入了
export default function () {
return {
Meteor,
FlowRouter,
Collections,
LocalState: new ReactiveDict(),
Tracker
};
}
在 main.js 初始化 context 后,需要創(chuàng)建整個 app,加載各個模塊并初始化。參考 mantra-core 的源代碼 可以看到這里主要是通過依賴注入方式,把context,actions 和 UI 連接起來的地方,這樣你寫代碼的時候可以把它們分開寫,達到 store、action 和 UI 解耦的目的,并讓數(shù)據(jù)單向流動。
Modules
Mantra 使用的是模塊化結(jié)構(gòu)。這里的模塊不是 ES2015 的模塊,而是指結(jié)構(gòu)上的模塊,形式上就是一組 ES2015 exports 構(gòu)成的一個文件夾,完成一個具體的功能。
我們這里以每個 Mantra app 都必須有的 core 模塊為例。
index.js
如果是 import 一個文件夾的話,Node.js 的約定是從 index.js 開始,Manta 里大量使用到了這個約定,所以基本每個文件里都會發(fā)現(xiàn)一個 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 組件會根據(jù)用戶的交互和 URL 變化,所以很自然的就是根據(jù) client/core/routes.js 和 url 決定 mount 那些組件。
這里注意兩點,第一是 Mantra 在 routes.js 使用了 injectDeps 對 Layout 注入了依賴。從 mantra-core 的源代碼 可以看到注入了 context 和 actions。第二是 mount 的 content 是一個函數(shù),而不是通常的 React 組件。因為使用了 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
這里是模塊級的配置。入口 index.js 文件輸出一個缺省函數(shù),這個函數(shù)的第一個參數(shù)通常就是 Application Context。這里通常是 Meteor 的 method stubs 代碼,目的是獲得 optimistic UI 特性。如果有的 method 有自己特別的邏輯不想公開,可以在這里實現(xiàn)和服務端不一樣的代碼,只要能預測用戶交互的結(jié)果就行。
actions
和前面的文件夾一樣,也是通過 index.jx export。下面的代碼就是一個完整的 action。可以看到這個 action 修改了 LocalState 這個客戶端的全局變量,還有通過 Meteor.call 更新了數(shù)據(jù)庫,最后跳轉(zhuǎn)到新的博客頁面。
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 更新數(shù)據(jù)庫
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 函數(shù)完成的依賴注入,然后在 UI 里通過 props 調(diào)用。
containers
Containers 文件夾里沒有 index.js 文件,因為 container 都是通過 import 在 routes.js 單獨引入。和普通的非 Mantra Meteor app 一樣,在這里 subscribe 后端數(shù)據(jù),并將數(shù)據(jù)通過 props 傳遞到 view 的 UI 組件。Mantra 用的 react-komposer 這個 npm 包來創(chuàng)建 container。和非 Mantra Meteor app 不一樣的是,actions 是作為依賴注入到 container 的。這樣 UI 部分的顯示就和應用的狀態(tài)改變分開了。以 client/modules/core/contianers/newpost.js 為例
...
export const depsMapper = (context, actions) => ({
create: actions.posts.create, // 修改數(shù)據(jù)庫的 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 模塊來實現(xiàn)的。
工作流程舉例
上圖是 Mantra 的數(shù)據(jù)流動示意圖,我們下面以它來說明 Mantra 的工作流程。假設(shè)你點擊了 http://mantra-sample-blog-app.herokuapp.com 這個博客的在線例子,然后接下來會發(fā)生
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 包創(chuàng)建的 app.init() 初始化會調(diào)用各個 module 的 routes.js。在 routes.js 里先把前面提到的 context 注入到 layout,然后根據(jù)用戶輸入或點擊正則匹配到前面列出的 routes.js 的根 url,接著掛載(mount)PostList 這個 container 到注入了依賴的 MainLayoutCtx。
這里就是圖中紅色虛線框的最右邊的框 container 就緒。他們之所以在紅色的虛線里,就是表面他們都是基于 Meteor 的 reactive tracker 機制工作的,就是 Meteor 會自動保證你的 states 的更新。
3 container & UI component
Container 和使用 React-komposer 的非 Mantra app 的沒有太大區(qū)別,不一樣的是如果包含的 UI 有用戶交互的話,那么需要注入 context 和 action。可以參看上面的 container 欄列出的 mapper 函數(shù)。actions 就是這樣通過 props 傳遞到 UI 組件的。因為例子里的首頁沒有用戶輸入的交互,所以我們以 newpost.js 這個 container 為例,它通過前述方式注入了 action 的 create 和 clearErrors 函數(shù),然后在 UI 組件里的 newpost.js 通過 props 調(diào)用 create 函數(shù),這就是淺藍色的 User Action 框,它執(zhí)行后會更改應用的數(shù)據(jù) states,而這種更改行為是通過注入 context 里的 Meteor, LocalState 等實現(xiàn)的。
4 Web Pub Action
上圖中最左邊的 action 是 Meteor 的數(shù)據(jù)訂閱 publication,當有數(shù)據(jù)更新時,Meteor 的 tracker 會自動接收到更新的 action 事件,然后啟動相應 Meteor.subscribe 所在的 container 盡行組件的 re-render。而這一切也都是通過 context 和瀏覽器里的 minimongo 來實現(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ù)流動方式。
小結(jié)
這就是 Mantra 博客例子的前端代碼解釋。建議多結(jié)合例子代碼還有使用到包的源碼來理解。和 Redux 類似,剛開始時可能不太容易理解,因為不直觀,也不知道為什么非要繞一個很大的圈子來完成一件任務,最好是多和實際例子聯(lián)系、應用,理解 Mantra 的目的是寫出更易于理解和維護的代碼,特別是對復雜的 app 有幫助。
注意:
- Mantra 官方文檔里有的 JSX 文件例子還是使用 .jsx 后綴。現(xiàn)在因為解釋器進步,Meteor 1.3+ 可以使用 .js 支持 JSX 語法,所以建議使用 .js 后綴。 Atwood's Law 再次發(fā)生作用 - Any application that can be written in JavaScript, will eventually be written in JavaScript
- Mantra 的這個博客例子里搭建好了 storybook, 大家也可以試試,它可以分離前后端開發(fā),而且讓你對前端界面的更新立即可見。所以如果你發(fā)現(xiàn)在開發(fā)前端時等待每次修改結(jié)果顯示時間太長,要不換臺更快的電腦,要不使用 storybook 立即看到你的修改。