前端架構(gòu)探索與實(shí)踐
前文
從思考、到探索、到腳手架的產(chǎn)生,后面經(jīng)過(guò)一系列的項(xiàng)目開(kāi)發(fā),不斷優(yōu)化和改良。目前已經(jīng)成功應(yīng)用到房產(chǎn)中間頁(yè)(改名天貓房產(chǎn))中。這里,做一下總結(jié)。
?「僅為拋磚,希望看完這個(gè)系列的同學(xué)可以相互探討學(xué)習(xí)一下」
?
為什么使用源碼
目前,我們大多數(shù)頁(yè)面,包括搜索頁(yè)、頻道頁(yè)都是大黃蜂搭建的頁(yè)面。至于搭建的優(yōu)點(diǎn),這里就不多贅述了。而我們使用源碼編寫,主要是基于以下幾點(diǎn)思考:
穩(wěn)定性要求高 頁(yè)面模塊多而不定 快速回滾方案 模塊通信復(fù)雜
源碼架構(gòu)

?架構(gòu)圖需要調(diào)整。此為稿圖,位置放的有些不合理,表述不清
?
底層技術(shù)支撐主要采用 Rax1.0 + TypeScript + Jest 編碼。通過(guò) pmcli生成項(xiàng)目腳手架。腳手架提供基礎(chǔ)的文件代碼組織和組件。包括 Components,commonUtils,document,modules等。當(dāng)然,這些組件最終會(huì)被抽離到 puicom group 下。
再往上,是容器層。容器提供一些可插拔的 hooks 能力。并且根據(jù) component 的配置來(lái)渲染不同的組件到頁(yè)面中,首屏組件和按需加載組件。最后,支撐到每一個(gè)對(duì)應(yīng)的頁(yè)面里面。
分工組織

對(duì)于一個(gè)頁(yè)面,無(wú)論是 react 還是 rax,其實(shí)都是 fn(x)=>UI 的過(guò)程。所以整理流程無(wú)非就是拿到接口屬于渲染到 UI 中。所以對(duì)于中間頁(yè)的架構(gòu)而言也是如此。
首先拿到基本的接口數(shù)據(jù),通過(guò)自定義的狀態(tài)管理,掛載到全局 state 對(duì)應(yīng)的組件名下。容器層通過(guò)組件的配置文件,渲染對(duì)應(yīng)的組件。最終呈現(xiàn)出完成的一個(gè)頁(yè)面。當(dāng)然,其中會(huì)有一些額外的容器附屬功能,比如喚起手淘、監(jiān)聽(tīng)鍵盤彈起等這個(gè)按需插入對(duì)應(yīng) hooks 即可。屬于業(yè)務(wù)層邏輯。
工程目錄
工程結(jié)構(gòu)

頁(yè)面結(jié)構(gòu)

模塊結(jié)構(gòu)



?以上結(jié)構(gòu)在之前文章中都有介紹到
?
補(bǔ)充
?這里補(bǔ)充下動(dòng)態(tài)加載,以及入口 index 的寫法。理論上這部分,在使用這套架構(gòu)的同學(xué),無(wú)需關(guān)心
?
index.tsx
return?(
????<H5PageContainer
??????title={PAGE_TITLE}
??????showPlaceHolder={isLoading}
??????renderPlaceHolder={renderLoadingPage}
??????renderHeader={renderHeader}
??????renderFooter={renderFooter}
??????toTopProps={{
????????threshold:?400,
????????bottom:?203,
??????}}
??????customStyles={{
????????headWrapStyles:?{
??????????zIndex:?6,
????????},
??????}}
????>
??????{renderSyncCom(
????????asyncComConfig,
????????dao,
????????dispatch,
????????reloadPageNotRefresh,
????????reloadTick
??????)}
??????{renderDemandCom(
????????demandComConfig,
????????offsetBottom,
????????dao,
????????dispatch,
????????reloadPageNotRefresh,
????????reloadTick
??????)}
??????<BottomAttention?/>
????H5PageContainer>
??);
模塊動(dòng)態(tài)加載
/**
?*?按需按需加載容器組件
?*
?*?@export
?*?@param?{*}?props?按需加載的組件?props+path
?*?@returns?需按需加載的子組件
?*/
export?default?function(props:?IWrapperProps)?{
??const?placeHolderRef:?any?=?useRef(null);
??const?{?offsetBottom,?...otherProps?}?=?props;
??const?canLoad?=?useDemandLoad(offsetBottom,?placeHolderRef);
??const?[comLoaded,?setComLoaded]?=?useState(false);
??//?加入?hasLoaded?回調(diào)
??const?wrapProps?=?{
????...otherProps,
????hasLoaded:?setComLoaded,
??};
??return?(
????
??????????????x-if={!comLoaded}
????????ref={placeHolderRef}
????????style={{?width:?750,?height:?500,?marginTop:?20?}}
????????source={{?uri:?PLACEHOLDER_PIC?}}
????????resizeMode={"contain"}
??????/>
??????
????
??);
}
/**
?*?動(dòng)態(tài)加載
?*?@param?props
?*/
function?ImportWrap(props:?IWrapperProps)?{
??const?{?path,?...otherProps?}?=?props;
??const?[Com,?error]?=?useImport(path);
??if?(Com)?{
????return? ;
??}?else?if?(error)?{
????console.error(error);
????return?null;
??}?else?{
????return?null;
??}
}
use-demand-load.ts
import?{?useState,?useEffect?}?from?'rax';
import?{?px2rem?}?from?'@ali/puicom-universal-common-unit';
/**
?*?判斷組件按需加載,即將進(jìn)去可視區(qū)
?*/
export?function?useDemandLoad(offsetBottom,?comRef):?boolean?{
????const?[canLoad,?setCanLoad]?=?useState(false);
????const?comOffsetTop?=?px2rem(comRef?.current?.offsetTop?||?0);
????useEffect(()?=>?{
????????if?(canLoad)?return;
????????if?(offsetBottom?>?comOffsetTop?&&?!canLoad)?{
????????????setCanLoad(true);
????????}
????},?[offsetBottom]);
????useEffect(()?=>?{
????????setCanLoad(comRef?.current?.offsetTop?(screen.height?||?screen.availHeight?||?0));
????},?[comRef]);
????return?canLoad;
}
模塊編寫與狀態(tài)分發(fā)
模塊編寫
types
編寫模塊數(shù)據(jù)類型

掛載到 dao(dataAccessObject) 下

統(tǒng)一導(dǎo)出
?避免文件引入過(guò)多過(guò)雜
?
type/index.d.ts

reducers
編寫模塊對(duì)應(yīng)reducer

在 daoReducer 中統(tǒng)一掛載

數(shù)據(jù)分發(fā)

componentConfig

?此處 keyName 是 type/dao.d.ts 下聲明的值。會(huì)進(jìn)行強(qiáng)校驗(yàn)。填錯(cuò)則分發(fā)不到對(duì)應(yīng)的組件中
?

component

數(shù)據(jù)在 props.dataSource 中
狀態(tài)分發(fā)
模塊聲明需要掛載到 type/dao.d.ts中reducer?需要combine?到dao.reduer.ts中在 useDataInit中dispatch對(duì)應(yīng)Action在 config?中配置 (才會(huì)被渲染到 UI)
Demo 演示
?以彈層為例
?

將所有彈層看做為一個(gè)模塊,只是內(nèi)容不同而已。而內(nèi)容,即為我們之前說(shuō)的組件目錄結(jié)構(gòu)中的 components 內(nèi)容
定義模塊 Models
定義模塊類型
編寫模塊屬于類型
掛載到 dao 中

reducer
編寫組件所需的 reducer

?actions 的注釋非常有必要
?

combine 到 dao 中

編寫組件



組件編寫

通信
導(dǎo)入對(duì)應(yīng) action
import?{?actions?as?modelActions?}?from?"../../../reducers/models.reducer";
dispatch
?dispatch([modelActions.setModelVisible(true),modelActions.setModelType("setSubscribeType")])
?觸發(fā) ts 校驗(yàn)
?


效果

頁(yè)面容器
?基于拍賣通用容器組件改造
?
改造點(diǎn):「基于 body 滾動(dòng)」。
因?yàn)槲覀兡壳绊?yè)面都是 h5 頁(yè)面了,之前則是 weex 的。所以對(duì)于容器的底層,之前使用的 RecycleView :固定 div 高度,基于 overflow 來(lái)實(shí)現(xiàn)滾動(dòng)的。
雖然,在 h5 里面這種滾動(dòng)機(jī)制有些”難受“,但是罪不至”換“。但是尷尬至于在于,iOS 的橡皮筋想過(guò),在頁(yè)面滾動(dòng)到頂部以后,如果頁(yè)面有頻繁的動(dòng)畫或者 setState 的時(shí)候,會(huì)導(dǎo)致頁(yè)面重繪,重新回到頂部。與手動(dòng)下拉頁(yè)面容器的橡皮筋效果沖突,而「倒是頁(yè)面瘋狂抖動(dòng)」。所以。。。。重構(gòu)。
舊版容器功能點(diǎn)
?源碼頁(yè)面中使用的部分
?

重構(gòu)后的使用
?基本沒(méi)有太大改變
?

簡(jiǎn)單拆解實(shí)現(xiàn)
type
import?{?FunctionComponent,?RaxChild,?RaxChildren,?RaxNode,?CSSProperties?}?from?'rax';
export?interface?IHeadFootWrapperProps?{
????/**
?????*?需要渲染的子組件
?????*/
????comFunc?:?()?=>?FunctionComponent?|?JSX.Element;
????/**
?????*?組件類型
?????*/
????type:?"head"?|?"foot",
????/**
?????*?容器樣式
?????*/
????wrapStyles?:?CSSProperties;
}
/**
?*?滾動(dòng)到頂部組件屬性
?*/
export?interface?IScrollToTopProps?{
????/**
?????*?距離底部距離
?????*/
????bottom?:?number;
????/**
?????*?zIndex
?????*/
????zIndex?:?number;
????/**
?????*?icon?圖片地址
?????*/
????icon?:?string;
????/**
?????*?暗黑模式的?icon?圖片地址
?????*/
????darkModeIcon?:?string;
????/**
?????*?icon寬度
?????*/
????iconWidth?:?number;
????/**
?????*?icon?高度
?????*/
????iconHeight?:?number;
????/**
?????*?滾動(dòng)距離(滾動(dòng)多少觸發(fā))
?????*/
????threshold?:?number;
????/**
?????*?點(diǎn)擊回滾到頂部是否有動(dòng)畫
?????*/
????animated?:?boolean;
????/**
?????*?距離容器右側(cè)距離
?????*/
????right?:?number;
????/**
?????*?展示回調(diào)
?????*/
????onShow?:?(...args)?=>?void;
????/**
?????*?消失回調(diào)
?????*/
????onHide?:?(...args)?=>?void;
}
/**
?*?內(nèi)容容器
?*/
export?interface?IContentWrapProps{
????/**
?????*?children
?????*/
????children:RaxNode;
????/**
?????*?隱藏滾動(dòng)到頂部
?????*/
????hiddenScrollToTop?:boolean;
?????/**
?????*?返回頂部組件?Props
?????*/
????toTopProps?:?IScrollToTopProps;
????/**
?????*?渲染頭部
?????*/
????renderHeader?:?()?=>?FunctionComponent?|?JSX.Element;
????/**
?????*?渲染底部
?????*/
????renderFooter?:?()?=>?FunctionComponent?|?JSX.Element;
????/**
?????*?自定義容器樣式
?????*/
????customStyles?:?{
????????/**
?????????*?body?容器樣式
?????????*/
????????contentWrapStyles?:?CSSProperties;
????????/**
?????????*?頭部容器樣式
?????????*/
????????headWrapStyles?:?CSSProperties;
????????/**
?????????*?底部容器樣式
?????????*/
????????bottomWrapStyle?:?CSSProperties;
????};
????/**
?????*?距離底部多少距離開(kāi)始觸發(fā)?endReached
?????*/
????onEndReachedThreshold?:?number;
}
export?interface?IContainerProps?extends?IContentWrapProps?{
????/**
?????*?頁(yè)面標(biāo)題
?????*/
????title:?string;
????/**
?????*?頁(yè)面?placeHolder
?????*/
????renderPlaceHolder?:?()?=>?FunctionComponent?|?JSX.Element;
????/**
?????*?是否展示?placeH
?????*/
????showPlaceHolder?:?boolean;
}
index.tsx
const isDebug = isTrue(getUrlParam('pm-debug'));
export default function({
children,
renderFooter,
renderHeader,
title,
onEndReachedThreshold = 0,
customStyles = {},
toTopProps = {},
showPlaceHolder,
renderPlaceHolder,
hiddenScrollToTop=false
}: IContainerProps) {
if (!isWeb) return null;
// 監(jiān)聽(tīng)滾動(dòng)
useListenScroll();
// 設(shè)置標(biāo)題
useSetTitle(title);
// 監(jiān)聽(tīng) error 界面觸發(fā)
const { errorType } = useListenError();
return (
x-if={errorType === "" && !showPlaceHolder}
renderFooter={renderFooter}
customStyles={customStyles}
renderHeader={renderHeader}
onEndReachedThreshold={onEndReachedThreshold}
toTopProps={toTopProps}
hiddenScrollToTop={hiddenScrollToTop}
>
{children}
{renderPlaceHolder && showPlaceHolder && renderPlaceHolder()}
);
}
export { APP_CONTAINER_EVENTS };
通過(guò) Fragment 包裹,主題是 ContentWrap,ErrorPage、VConsole、Holder放置主體以外。

?相關(guān) hooks 功能點(diǎn)完全區(qū)分開(kāi)來(lái)
?
廣播事件
/**
?*?Events?以頁(yè)面為單位
?*/
export?const?APP_CONTAINER_EVENTS?=?{
????SCROLL:?'H5_PAGE_CONTAINER:SCROLL',
????TRIGGER_ERROR:?'H5_PAGE_CONTAINER:TRIGGER_ERROR',
????END_REACHED:?'H5_PAGE_CONTAINER:END_REACHED',
????HIDE_TO_TOP:?'H5_PAGE_CONTAINER:HIDE_TO_TOP',
????RESET_SCROLL:?'H5_PAGE_CONTAINER:RESET_SCROLL',
????ENABLE_SCROLL:"H5_PAGE_CONTAINER:H5_PAGE_CONTAINER"
}
pm-cli
詳見(jiàn):pm-cli腳手架,統(tǒng)一阿里拍賣源碼架構(gòu)
安裝:tnpm install -g @ali/pmcli

這里在介紹下命令:
基本使用
pmc init
在空目錄中調(diào)用,則分兩步工作: 首先調(diào)用 tnpm init rax初始化出來(lái) rax 官方腳手架目錄修改 package.json中name為當(dāng)前所在文件夾的文件夾名稱升級(jí)為拍賣源碼架構(gòu),下載對(duì)應(yīng)腳手架模板:init-project 在已 init rax后的項(xiàng)目中調(diào)用升級(jí)為拍賣源碼架構(gòu),下載對(duì)應(yīng)腳手架模板:init-project
?注意:經(jīng)過(guò) pmc 初始化的項(xiàng)目,在項(xiàng)目根目錄下回存有
?.pm-cli.config.json配置文件
pmc add-page
在當(dāng)前 項(xiàng)目中新增頁(yè)面,選擇三種頁(yè)面類型

推薦使用 simpleSource、customStateManage
頁(yè)面模板地址:add-page
pmc add-mod
根據(jù)所選擇頁(yè)面,初始化不同類型的模塊
模塊模板地址為:add-mod
pmc init-mod
調(diào)用def init tbe-mod,并且將倉(cāng)庫(kù)升級(jí)為支持 ts 開(kāi)發(fā)模式
pmc publish-init
發(fā)布端架構(gòu)初始化,基于 react 應(yīng)用
發(fā)布端架構(gòu)模板地址:publish-project
pmc publish-add
添加發(fā)布端模塊
模塊模板地址:publish-mod
pmc init-mod
調(diào)用 def init tbe-mod 命令,并同時(shí)升級(jí)為 ts 編碼環(huán)境。

?配置環(huán)境、安裝依賴、直接運(yùn)行
?
相關(guān)體驗(yàn)地址(部分無(wú)法訪問(wèn))
阿里房產(chǎn) 底層容器 (單獨(dú)抽離組件ing) pmCli ts tbeMod

