構建高可伸縮性的WEB交互式系統(下)
本文是《構建高可伸縮性的WEB交互式系統》系列文章的第三篇,以網易的NEJ框架為例,對模塊的可伸縮性進行分析介紹。
實例分析
NEJ框架根據前兩篇的描述對此套架構模式做了實現,下面我們用具體實例講解如何使用NEJ中的模塊調度系統來拆分一個復雜系統、開發測試模塊、整合系統等。
系統分解
繪制層級關系圖
當我們拿到一個復雜系統時,根據交互稿可以繪制出組成系統的模塊的層級關系圖,并確定系統對外可訪問的模塊。

抽象依賴關系樹
從模塊的層級關系圖中,我們可以非常方便的抽象出模塊的依賴關系樹:

然后,我們將抽象出來的依賴關系樹根據UMI規則進行格式化。格式化的主要操作包括:
- 增加一個名稱為“/”的根結點(也可將“m”結點改為“/”)
- 每個結點增加“/”的子節點作為默認節點

至此輸出的依賴關系樹,具有以下特性:
- 任何一個結點(除根結點外)到根結點路徑上的結點名稱用“/”分隔組合起來即為結點的UMI值,如list結點的UMI值為/m/blog/list
- 任何結點上的模塊都依賴于他祖先結點(注冊有模塊)上的模塊存在,如blog結點和list結點均注冊有模塊,則list結點上的模塊顯示必須以blog結點上的模塊的顯示為先決條件
確定對外模塊注冊節點
五個對外可訪問的模塊:日志、標簽、基本資料、個人經歷、權限設置,在依賴關系樹中找到合適的結點(葉子結點,層級關系樹在依賴關系樹中對應的結點或“/”結點)來注冊對外可訪問的模塊:

確定布局模塊注冊節點
從可訪問模塊注冊的結點往根結點遍歷,凡碰到兩模塊交叉的結點即為布局模塊注冊結點,系統所需的組件相關的模塊可注冊到根結點,這樣任何模塊使用的時候都可以保證這些組件已經被載入。

映射模塊功能
原則:結點的公共父結點實現結點上注冊的模塊的公共功能。
舉例:blog結點和setting結點的公共父結點為m結點,則我們可以通過切換blog模塊和setting模塊識別不變的功能即為m模塊實現的功能,同理其他模塊。

分解復雜模塊
進一步分解復雜模塊,一般需要分解的模塊包括:
- 可共用模塊,比如日志列表,可以在日志管理頁面呈現,也可以在彈層中顯示
- 邏輯上無必然聯系的模塊,如日志模塊中日志列表與右側的按標簽查看的標簽列表之間沒有必然的聯系,任何一個模塊的移除或添加都不會影響到另外一個模塊的業務邏輯
至此我們可以得到兩棵系統分解后的依賴關系樹——對外模塊依賴關系樹:

以及私有模塊依賴關系樹:

繪制模塊功能規范表
本例中為了說明分解過程,將所有可分解的模塊都做了分解。實際項目看具體情況,比如這里的/m模塊組合的/?/tab/模塊的功能可以直接在/m模塊中實現,而不需要新建一個/?/tab/模塊來實現這個功能。
規范表范例如下所示:

構建目錄
項目目錄
項目目錄的構建如下圖所示:

各目錄說明
webroot 項目前端開發相關目錄
|- res 靜態資源文件目錄,打包時可配置使用到該目錄下的靜態資源帶版本信息
|- src 前端源碼目錄,最終發布時該目錄不會部署到線上
|- html
|- module 單頁面模塊目錄,系統所有模塊的實現均在此目錄下
|- app.html 單頁面入口文件 模塊單元目錄
根據模塊封裝規則一個模塊單元由以下幾部分組成:
- 模塊測試:模塊實現的功能可以通過模塊測試頁面獨立進行測試
- 模塊結構:模塊所涉及的結構分解出來的若干模板集合
- 模塊邏輯:根據模塊規范實現的模塊業務邏輯,從模塊基類繼承
- 模塊樣式:模塊特有的樣式,一般情況下這部分樣式可以直接在css目錄下實現
結構范例如下所示:

至此我們可以得到所有模塊的目錄結構,如下所示:

模塊實現
結構
這里我們假設系統的靜態頁面已經做完,這里的模塊實現只是在原有結構的基礎上進行結構分解和業務邏輯的實現,結構部分內容主要將模塊相關的靜態結構拆分成若干NEJ的模板。注意:
- 模板中的外聯資源如css,js文件地址如果使用的是相對路徑則均相對于模塊的html文件路徑
- 模板集合中的外聯資源必須使用@TEMPLATE標記標識,這個在后面打包發布章節會詳細介紹
NEJ模板說明

模塊結構舉例
<meta charset="utf-8"/>
<textarea name="txt" id="m-ifrm-module">
<div class="n-login">
<div class="iner j-flag">
<span class="cls j-flag">×</span>
<span class="min j-flag">-</span>
</div>
<div class="cnt j-cnt"></div>
</div>
</textarea>
<!-- @TEMPLATE -->
<textarea name="js" data-src="./index.css"></textarea>
<textarea name="js" data-src="./index.js"></textarea>
<!-- /@TEMPLATE --> 邏輯
依賴util/dispatcher/module模塊,我們從_$$ModuleAbstract擴展一個項目的模塊基類,完成項目中模塊特有屬性、行為的抽象。
/*
* ------------------------------------------
* 項目模塊基類實現文件
* @version 1.0
* @author genify(caijf@corp.netease.com)
* ------------------------------------------
*/
NEJ.define([
'base/klass',
'util/dispatcher/module'
],function(_k,_t,_p){
// variable declaration
var _pro;
/**
* 項目模塊基類對象
* @class {_$$Module}
* @extends {_$$ModuleAbstract}
* @param {Object} 可選配置參數,已處理參數列表如下所示
*/
_p._$$Module = _k._$klass();
_pro = _p._$$Module._$extend(_t._$$ModuleAbstract);
/**
* 操作
* @param {Object}
* @return {Void}
*/
_pro.__doSomething = function(_args){
// TODO
};
// TODO
return _p;
}); 根據模塊狀態的劃分,我們在實現一個模塊時需要實現以下幾個接口:

各階段對應的接口:
- 構建 - __doBuild:構建模塊結構,緩存模塊需要使用的節點,初始化組合控件的配置參數
- 顯示 - __onShow:將模塊放置到指定的容器中,分配組合控件,添加相關事件,執行__onRefresh的業務邏輯
- 刷新 - __onRefresh:根據外界輸入的參數信息獲取數據并展示(這里主要做數據處理)
- 隱藏 - __onHide:模塊放至內存中,回收在__onShow中分配的組合控件和添加的事件,回收__onRefresh中產生的視圖(這里盡量保證執行完成后恢復到__doBuild后的狀態)
具體模塊實現舉例
/*
* ------------------------------------------
* 項目模塊實現文件
* @version 1.0
* @author genify(caijf@corp.netease.com)
* ------------------------------------------
*/
NEJ.define([
'base/klass',
'util/dispatcher/module',
'/path/to/project/module.js'
],function(_k,_e,_t,_p){
// variable declaration
var _pro;
/**
* 項目模塊對象
* @class {_$$ModuleDemo}
* @extends {_$$Module}
* @param {Object} 可選配置參數
*/
_p._$$ModuleDemo = _k._$klass();
_pro = _p._$$ModuleDemo._$extend(_t._$$Module);
/**
* 構建模塊,主要處理以下業務邏輯
* - 構建模塊結構
* - 緩存后續需要使用的節點
* - 初始化需要使用的組件的配置信息
* @return {Void}
*/
_pro.__doBuild = function(){
this.__super();
// TODO
};
/**
* 顯示模塊,主要處理以下業務邏輯
* - 添加事件
* - 分配組件
* - 處理輸入信息
* @param {Object} 輸入參數
* @return {Void}
*/
_pro.__onShow = function(_options){
this.__super(_options);
// TODO
};
/**
* 刷新模塊,主要處理以下業務邏輯
* - 分配組件,分配之前需驗證
* - 處理輸入信息
* - 同步狀態
* - 載入數據
* @return {Void}
*/
_pro.__onRefresh = function(_options){
this.__super(_options);
// TODO
};
/**
* 隱藏模塊,主要處理以下業務邏輯
* - 回收事件
* - 回收組件
* - 盡量保證恢復到構建時的狀態
* @return {Void}
*/
_pro.__onHide = function(){
this.__super();
// TODO
};
// notify dispatcher
_e._$regist(
'umi_or_alias',
_p._$$ModuleDemo
);
return _p;
}); 消息
點對點消息
模塊可以通過__doSendMessage接口向指定UMI的模塊發送消息,也可以通過實現__onMessage接口來接收其他模塊發給他的消息。
發送消息
_pro.__doSomething = function(){
// TODO
this.__doSendMessage(
'/m/setting/account/',{
a:'aaaaaa',
b:'bbbbbbbbb'
}
);
}; 接收消息
_pro.__onMessage = function(_event){
// _event.from 消息來源
// _event.data 消息數據,這里可能是 {a:'aaaaaa',b:'bbbbbbbbb'}
// TODO
}; 發布訂閱消息
發布消息
_pro.__doSomething = function(){
// TODO
this.__doPublishMessage(
'onok',{
a:'aaaaaa',
b:'bbbbbbbb'
}
);
}; 訂閱消息
_pro.__doBuild = function(){
// TODO
this.__doSubscribeMessage(
'/m/message/account/','onok',
this.__onMessageReceive._$bind(this)
);
}; 自測
創建html頁面,使用模板引入模塊實現文件
<!-- template box --> <div id="template-box" style="display:none;"> <textarea name="html" data-src="../index.html"></textarea> </div>
模塊放至document.mbody指定的容器中
NEJ.define([
'util/dispatcher/test'
],function(_e){
document.mbody = 'module-id-0';
// test module
_e._$testByTemplate('template-box');
}); 系統整合
映射依賴關系樹
系統整合時,我們只需要將依賴關系樹中需要注冊模塊的節點同模塊實現文件進行映射即可。
對外模塊整合

私有模塊整合

提取系統配置信息
規則配置舉例
rules:{
rewrite:{
'404':'/m/blog/list/',
'/m/blog/list/':'/m/blog/',
'/m/setting/account/':'/m/setting/'
},
title:{
'/m/blog/tag/':'日志標簽',
'/m/blog/list/':'日志列表',
'/m/setting/permission/':'權限管理',
'/m/setting/account/':'基本資料',
'/m/setting/account/edu/':'教育經歷'
},
alias:{
'system-tab':'/?/tab/',
'blog-tab':'/?/blog/tab/',
'blog-list-box':'/?/blog/box/',
'blog-list-tag':'/?/blog/tag/',
'blog-list-class':'/?/blog/class/',
'blog-list':'/?/blog/list/',
'setting-tab':'/?/setting/tab/',
'setting-account-tab':'/?/setting/account/tab/',
'layout-system':'/m',
'layout-blog':'/m/blog',
'layout-blog-list':'/m/blog/list/',
'layout-setting':'/m/setting',
'layout-setting-account':'/m/setting/account',
'blog-tag':'/m/blog/tag/',
'setting-edu':'/m/setting/account/edu/',
'setting-profile':'/m/setting/account/',
'setting-permission':'/m/setting/permission/'
}
} 模塊配置舉例
modules:{
'/?/tab/':'module/tab/index.html',
'/?/blog/tab/':'module/blog/tab/index.html',
'/?/blog/box/':'module/blog/list.box/index.html',
'/?/blog/tag/':'module/blog/list.tag/index.html',
'/?/blog/class/':'module/blog/list.class/index.html',
'/?/blog/list/':'module/blog/list/index.html',
'/?/setting/tab/':'module/setting/tab/index.html',
'/?/setting/account/tab/':'module/setting/account.tab/index.html',
'/m':{
module:'module/layout/system/index.html',
composite:{
tab:'/?/tab/'
}
},
'/m/blog':{
module:'module/layout/blog/index.html',
composite:{
tab:'/?/blog/tab/'
}
},
'/m/blog/list/':{
module:'module/layout/blog.list/index.html',
composite:{
box:'/?/blog/box/',
tag:'/?/blog/tag/',
list:'/?/blog/list/',
clazz:'/?/blog/class/'
}
},
'/m/blog/tag/':'module/blog/tag/index.html',
'/m/setting':{
module:'module/layout/setting/index.html',
composite:{
tab:'/?/setting/tab/'
}
},
'/m/setting/account':{
module:'module/layout/setting.account/index.html',
composite:{
tab:'/?/setting/account/tab/'
}
},
'/m/setting/account/':'module/setting/profile/index.html',
'/m/setting/account/edu/':'module/setting/edu/index.html',
'/m/setting/permission/':'module/setting/permission/index.html'
} 模塊組合
模塊通過__export屬性開放組合模塊的容器,__export中的parent為子模塊的容器節點,頂層模塊(如 “/m”)可以通過重寫__doParseParent來明確指定應用所在容器。
_pro.__doBuild = function(){
this.__body = _e._$html2node(
_e._$getTextTemplate('module-id-l2')
);
// 0 - box select
// 1 - class list box
// 2 - tag list box
// 3 - sub module box
var _list = _e._$getByClassName(this.__body,'j-flag');
this.__export = {
box:_list[0],
clazz:_list[1],
tag:_list[2],
list:_list[3],
parent:_list[3]
};
}; 通過composite配置模塊組合
'/m/blog/list/':{
module:'module/layout/blog.list/index.html',
composite:{
box:'/?/blog/box/',
tag:'/?/blog/tag/',
list:'/?/blog/list/',
clazz:'/?/blog/class/'
}
} 模塊組合時可以指定組合模塊的處理狀態:
- onshow - 這里配置的組合模塊僅在模塊顯示時組合,后續的模塊refresh操作不會導致組合模塊的refresh,適合于模塊在顯示后不會隨外界輸入變化而變化的模塊
- onrefresh - 這里配置的模塊在模塊顯示時組合,后續如果模塊refresh時也會跟隨做refresh操作,適用于組合的模塊需要與外部輸入同步的模塊
- 不指定onshow或者onrefresh的模塊等價于onrefresh配置的模塊
composite:{
onshow:{
// 模塊onshow時組合
// 組合的模塊在模塊onrefresh時不會刷新
},
onrefresh{
// 模塊onshow時組合
// 組合的模塊在模塊onrefresh時也同時會刷新
}
// 這里配置的組合模塊等價于onrefresh中配置的模塊
} 啟動應用
根據配置啟動應用
NEJ.define([
'util/dispatcher/dispatcher'
],function(_e){
_e._$startup({
// 規則配置
rules:{
rewrite:{
// 重寫規則配置
},
title:{
// 標題配置
},
alias:{
// 別名配置
// 建議模塊實現文件中的注冊采用這里配置的別名
}
},
// 模塊配置
modules:{
// 模塊UMI對應實現文件的映射表
// 同時完成模塊的組合
}
});
}); 打包發布
打包發布內容詳見NEJ工具集相關文檔
系統變更
當系統需求變化而進行模塊變更我們只需要開發新的模塊或刪除模塊配置即可
新增模塊
如果增加一個全新的模塊,則只需按照上面的邏輯實現步驟開發一個模塊即可。如果新增的模塊功能在系統中已經實現,則只需修改配置即可。如上例中我們需要在將日志管理下的標簽模塊在博客設置中也加一份,訪問路徑為/m/setting/tag/

修改規則配置
rules:{
// ...
alias:{
// ...
'blog-tag':['/m/blog/tag/','/m/setting/tag/']
}
} 修改模塊配置
modules:{
// ...
'/m/setting/tag/':'module/blog/tag/index.html'
} 如果要在/?/setting/tab模塊的結構模板中增加一個標簽即可
<textarea name="txt" id="module-id-8">
<div class="ma-t w-tab f-cb">
<a class="itm fl" href="#/setting/account/" data-id="/setting/account/">賬號管理</a>
<a class="itm fl" href="#/setting/permission/" data-id="/setting/permission/">權限設置</a>
<a class="itm fl" href="#/setting/tag/" data-id="/setting/tag/">日志標簽</a>
</div>
</textarea> 刪除模塊
將退化的模塊從系統中刪除只需要將模塊對應的UMI配置從模塊配置中刪除即可,而無需修改具體業務邏輯。
總結
隨著WEB技術的快速發展,單頁面系統(SPA)的應用變得越來越廣泛,隨著此類系統復雜度的增加,其對平臺及模塊的伸縮性方面需求變得越來越重要。對于這兩方面,業界給出了不少解決方案,本文我們主要探討了網易NEJ框架在這些方面給出的解決方案。網易在單頁面系統方面也做了多年的實踐和技術積累,如近幾年的網易云音樂PC版、易信WebIM、網易郵箱助手等,早些年的網易相冊、網易郵箱等,移動端的網易云相冊IPad版、Lofter Android版等產品,均是此類單頁面系統的應用實踐。實踐過程中對這方面有興趣的同學可進一步做交流。
作者: 蔡劍飛 來源: infoq