摘要:所以,我們使用來序列化第二級參數(shù),比如張家界這樣做也有個不好的地方,就是需要,然后特殊字符會變得比較丑。模塊規(guī)劃模塊與無關(guān)劃分模塊可以很好的拆解功能,化繁為簡,并且對內(nèi)隱藏細節(jié),對外暴露少量接口。
本項目地址:react-coat-helloworld
react-coat 同時支持瀏覽器渲染(SPA)和服務(wù)器渲染(SSR),本 Demo 僅演示瀏覽器渲染,請先了解一下:react-coat
第一站:Helloworld 安裝git clone https://github.com/wooline/react-coat-helloworld.git npm install運行
npm start 以開發(fā)模式運行
npm run build 以產(chǎn)品模式編譯生成文件
npm run prod-express-demo 以產(chǎn)品模式編譯生成文件并啟用一個 express 做 demo
npm run gen-icon 自動生成 iconfont 文件及 ts 類型
查看在線 Demo點擊查看在線 Demo
關(guān)于腳手架采用 webpack 4.0 為核心搭建,無二次封裝,干凈透明
采用 typescript 作開發(fā)語言,使用 Postcss 及 less 構(gòu)建 css
不使用 css module,用模塊化命名空間保證 css 不沖突
采用 editorconfig > prettier 作統(tǒng)一的風格配置,建議使用 vscode 作為 IDE,并安裝 prettier 插件以自動格式化
采用 tslint、eslint、stylelint 作代碼檢查
PeerDependencies開發(fā)環(huán)境需要很多的 dependencies,你可以自行安裝特定版本,如果特殊要求,建議本站提供的 react-coat-pkg 以及 react-coat-dev-pkg,它們已經(jīng)包含了絕大部分 dependencies。
TS 類型的定義使用 Typescript 意味著使用強類型,我們把業(yè)務(wù)實體中 TS 類型定義分兩大類:API類型和Entity類型。
API 類型:指的是來自于后臺 API 輸入的類型,它們可能直接由 swagger 生成,或是機器生成。
Entity 類型:指的是本系統(tǒng)為業(yè)務(wù)實體建模而定義的類型,每個業(yè)務(wù)實體(resource)都會有定義。
理想狀況下,API 類型和 Entity 類型會保持一致,因為業(yè)務(wù)邏輯是同一套,但實際開發(fā)中,可能因為前后端并行開發(fā)、或者前后端視角不同而出現(xiàn)兩者各表。
為了充分的解耦,我們允許這種不一致,我們把 API 類型在源頭就轉(zhuǎn)化為 Entity 類型,而在本系統(tǒng)的代碼邏輯中,不直接使用 API 類型,應(yīng)當使用自已定義的 Entity 類型,以減少其它系統(tǒng)對本系統(tǒng)的影響。假定項目:旅途 web app 主要頁面:
旅游路線展示
旅途小視頻展示
站內(nèi)信展示(需登錄)
評論展示 (訪客可查看評論,發(fā)表則需登錄)
項目要求web SPA 單頁應(yīng)用
主要用于 mobile 瀏覽器,也可以適應(yīng)于桌面瀏覽器
無 SEO 要求,但需要能將當前頁面分享給他人
初次進入本站時,顯示 welcome 廣告,并倒計時
路由規(guī)劃SPA 單頁不就一個頁面么?為什么還需要規(guī)劃路由呢?
其一,為了用戶刷新時盡可能的保持當前展示
其二,為了用戶能將當前展示通過 url 分享給他人
其三,為了后續(xù)的 SEO
path 規(guī)劃根據(jù)項目需求及 UI 圖,我們初步規(guī)劃主要路由 path 如下:
旅行路線列表 photosList:/photos
旅行路線詳情 photosItem:/photos/:photoId
分享小視頻列表 videosList:/videos
分享小視頻詳情 videosItem:/videos/:videoId
站內(nèi)信列表 messagesList:/messages
參數(shù)規(guī)劃因為列表頁是有分頁、有搜索的,所以列表類型的路由是有參數(shù)的,比如:
/photos?title=張家界&page=3&pageSize=20
我們估且將這部分查詢列表條件叫"ListSearch",但除了ListSearch之外,也可能會出現(xiàn)別的路由參數(shù),用來控制其它條件(本 demo 暫未涉及),比如:
/photos?title=張家界&page=3&pageSize=20&showComment=true
所以,如果參數(shù)一多,用扁平的一維結(jié)構(gòu)就變得不好表達。而且,利用 URL 參數(shù)存數(shù)據(jù),數(shù)據(jù)將全變成為字符串。比如id=2,你無法知道 2 是數(shù)字型還是字符型,這樣會讓后續(xù)接收處理變得繁重。所以,我們使用 JSON 來序列化第二級參數(shù),比如:
/photos?search={title:"張家界",page:3,pageSize:20}&showComment=true
這樣做也有個不好的地方,就是需要 encodeURI,然后特殊字符會變得比較丑。
路由參數(shù)默認值為了縮短 URL 長度,本框架設(shè)計了參數(shù)默認值,如果某參數(shù)和默認值相同,可以省去。我們需要做兩項工作:
生成 Url 查詢條件時,對比默認值,如果相同,則省去
原值:{title:"張家界",page:1,pageSize:20} 默認值: {title:"",page:1,pageSize:20},省去后為:{title:"張家界"}原值:{title:"",page:1,pageSize:20} 默認值: {title:"",page:1,pageSize:20},省去后為:空
收到 Url 查詢條件時,將查詢條件和默認值 merge
/photos?search={page:2} === photos?search={title:"",page:2,pageSize:20}/photos === photos?search={title:"",page:1,pageSize:20}
處理 null、undefined
由于接收 Url 參數(shù)時,如果某 key 為 undefined,我們會用相應(yīng)的默值將其填充,所以不能將 undefined 作為路由參數(shù)值定義,改為使用 null。也就是說,路由參數(shù)中的每一項,都是必填的,比如:
// 路由參數(shù)定義時,每一項都必填,以下為錯誤示例 interface ListSearch{ title?:string, age?:number } // 改為如下正確定義: interface ListSearch{ title:string | null, age:number | null }
區(qū)分:原始路由參數(shù)(SearchData) 默認路由參數(shù)(SearchData) 和 完整路由參數(shù)(WholeSearchData)。完整路由參數(shù)(WholeSearchData) = merage(默認路由參數(shù)(SearchData), 原始路由參數(shù)(SearchData))
原始路由參數(shù)(SearchData)每一項都是可選的,用 TS 類型表示為:Partial
完整路由參數(shù)(WholeSearchData)每一項都是必填的,用 TS 類型表示為:Required
默認路由參數(shù)(SearchData)和完整路由參數(shù)(WholeSearchData)類型一致
不直接使用路由狀態(tài)路由及其參數(shù)本質(zhì)上也是一種 Store,與 Redux Store 一樣,反映當前程序的某些狀態(tài)。但它是片面的,是瞬時的,是不穩(wěn)定的,我們把它看作是 Redux Store 的一種冗余。所以最好不要在程序中直接依賴和使用它,而是控制住它的入口和出口,第一時間在其源頭進行消化轉(zhuǎn)換,讓其成為整個 Redux Store 的一部分,后續(xù)的運行中,我們直接依賴 Redux Store。這樣,我們就將程序與路由設(shè)計解耦了,程序有更大的靈活度甚至可以遷移到無 URL 概念的其它運行環(huán)境中。
模塊規(guī)劃 模塊與 Page 無關(guān)劃分模塊可以很好的拆解功能,化繁為簡,并且對內(nèi)隱藏細節(jié),對外暴露少量接口。劃分模塊的標準是高內(nèi)聚,低耦合,而不是以 Page 或是 View,一個模塊包含某些完整的業(yè)務(wù)功能,這些功能可能涉及到多個 Page 或多個 View。
所以回過頭,看我們的項目需求和 UI 圖,大體上可以分為三個模塊:
photos //旅游線路展示
videos //分享視頻展示
messages //站內(nèi)消息展示
這三個模塊顯而易見,但是我們注意到:“圖片詳情”和“視頻詳情”都包含“評論展示”,而“評論展示”本身又具有分頁、排序、詳情展示、創(chuàng)建回復(fù)等功能,它具有自已獨立的邏輯,只不過在 view 上被 photoDetail 和 videoDetail 嵌套了,所以將“評論展示”獨立劃分成一個模塊是合適的。
另個,整個程序應(yīng)當有個啟動模塊,它是“上帝視角模塊”,它可以做一些公共事業(yè),必要的時候也可以用來做多個模塊之間的協(xié)調(diào)和調(diào)度,我們叫把它叫做 applicatioin 模塊。
所以最終,本 Demo 被劃分為 5 個模塊:
app // 啟動模塊
photos //旅游線路展示
videos //分享視頻展示
messages //站內(nèi)消息展示
comments //評論展示
為模塊劃分 View每個模塊可能包含一組 View,View 反映某些特定的業(yè)務(wù)邏輯。View 就是 React 中的 Component,那反過來 Component 就是 View 么?非也,它們之間還是有些區(qū)別的:
view 展現(xiàn)的是 Store 數(shù)據(jù),更偏重于表現(xiàn)特定的具體的業(yè)務(wù)邏輯,所以它的 props 一般是直接用 mapStateToProps connect 到 store。
component 體現(xiàn)的是一個沒有業(yè)務(wù)邏輯上下文的純組件,它的 props 一般來源于父級傳遞。
component 通常是公共的,而 view 通常非公用
回過頭,看我們的項目需求和 UI 圖,大體上劃分以下 view:
app views:Main、TopNav、BottomNav、LoginPop、Welcome、Loading
photos views:Main、List、Details
videos views:Main、List、Details
messages views:Main、List
comments views:Main、List、Details、Editor
目錄結(jié)構(gòu)經(jīng)過上面的分析,我們有了項目大至的骨架,由于模塊比較少,所以我們就不再用二級目錄分類了:
src ├── asset // 存放公共靜態(tài)資源 │ ├── css │ ├── imgs │ └── font ├── entity // 存放業(yè)務(wù)實體TS類型定義 ├── common // 存放公共代碼 ├── components // 存放React公共組件 ├── modules │ ├── app │ │ ├── views │ │ │ ├── TopNav │ │ │ ├── BottomNav │ │ │ ├── ... │ │ │ └── index.ts //導(dǎo)出給其它模塊使用的view │ │ ├── model.ts //定義ModuleState和ModuleActions │ │ ├── api //將本模塊需要的后臺api封裝一下 │ │ ├── facade.ts //導(dǎo)出本模塊對外的邏輯接口(類型、Actions、路由默認參數(shù)) │ │ └── index.ts //導(dǎo)出本模塊實體(view和model) │ ├── photos │ │ ├── views │ │ ├── model.ts │ │ ├── api │ │ ├── facade.ts │ │ └── index.ts │ ├── videos │ ├── messages │ ├── comments │ ├── names.ts //定義模塊名,使用枚舉類型來保證不重復(fù) │ └── index.ts //導(dǎo)出模塊的全局設(shè)置,如RootState類型、模塊載入方式等 └──index.tsx 啟動入口facade.ts
其它目錄都好理解,注意到每個 module 目錄中,有一個 facade.ts 的文件,冒似它與 index.ts 一樣都是導(dǎo)出本模塊,那為什么不合并成一個呢?
index.ts 導(dǎo)出的是整個模塊的物理代碼,因為模塊是較為獨立的,所以我們一般希望將整個模塊的代碼打包成一個獨立的 chunk 文件。
facade.ts 僅導(dǎo)出本模塊的一些類型和邏輯接口,我們知道 TS 類型在編譯之后是會被徹底抹去的,而接口僅僅是一個空的句柄。假如在 ModuleA 中需要 dispatch ModuleB 的 action,我們僅需要 import ModuleB 的 facade.ts,它只是一個空的句柄而以,并不會引起兩個模塊代碼的物理依賴。
配置模塊問:在 react-coat 中怎么配置一個模塊?包括打包、加載、注冊、管理其生命周期等?
答:./src/modules 根目錄下的 index.ts 文件為模塊總的配置文件,增加一個模塊,只需要在此配置一下
// ./src/modules/index.ts // 一個驗證器,利用TS類型來確保增加一個module時,相關(guān)的配置都同時增加了 type ModulesDefined路由和加載= T; // 定義模塊的加載方案,同步或者異步均可 export const moduleGetter = { [ModuleNames.app]: () => { return import(/* webpackChunkName: "app" */ "modules/app"); }, [ModuleNames.photos]: () => { return import(/* webpackChunkName: "photos" */ "modules/photos"); }, [ModuleNames.videos]: () => { return import(/* webpackChunkName: "videos" */ "modules/videos"); }, [ModuleNames.messages]: () => { return import(/* webpackChunkName: "messages" */ "modules/messages"); }, [ModuleNames.comments]: () => { return import(/* webpackChunkName: "comments" */ "modules/comments"); }, }; export type ModuleGetter = ModulesDefined ; // 驗證一下是否有模塊忘了配置 // 定義整站Module States interface States { [ModuleNames.app]: AppState; [ModuleNames.photos]: PhotosState; [ModuleNames.videos]: VideosState; [ModuleNames.messages]: MessagesState; [ModuleNames.comments]: CommentsState; } // 定義整站的Root State export type RootState = BaseState & ModulesDefined ; // 驗證一下是否有模塊忘了配置
本 Demo 直接使用 react-router V4,路由即組件,所以并不需要什么特別的路由配置,直接在./app/views/Main.tsx 中:
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main"); const VideosView = loadView(moduleGetter, ModuleNames.videos, "Main"); const MessagesView = loadView(moduleGetter, ModuleNames.messages, "Main");
使用 loadView()表示異步按需加載一個 View,如果你不想按需加載,完全可以直接 import:
import {Main as PhotosView} from "modules/photos/views"
載入 View 時自動載入其相關(guān)的模塊并初始化 Model。沒有 Model,view 是沒有“靈魂”的,所以在載入 View 時,框架會自動載入其 Model 并完成初始化,這個過程包含 3 步:
1.載入模塊對應(yīng)的 JS Chunk 包
2.初始化模塊 Model,派發(fā) module/INIT Action
3.模塊可以監(jiān)聽自已的 module/INIT Action,作出初始化行為,如獲取遠程數(shù)據(jù)等
Redux Store 結(jié)構(gòu)module 的劃分不僅體現(xiàn)在工程目錄上,而體現(xiàn)在 Redux Store 中:
router: { // 由 connected-react-router 生成 location: { pathname: "/photos", search: "", hash: "#refresh=true", key: "gb9ick" }, action: "PUSH" }, app: {...}, // app ModuleState photos: { // photos ModuleState isModule: true, // 框架自動生成,標明該節(jié)點為一個ModuleState listSearch: { // 列表搜索條件 title: "", page: 1, pageSize: 10 }, listItems: [ // 列表數(shù)據(jù) { id: "1", title: "新加坡+吉隆坡+馬六甲6或7日跟團游", departure: "無錫", type: "跟團游", price: 2499, hot: 265, coverUrl: "/imgs/1.jpg" }, ... ], listSummary: { page: 1, pageSize: 5, totalItems: 10, totalPages: 2 } }, messages: {...}, // messages ModuleState comments: {...}, // comments ModuleState }具體實現(xiàn)
見 Demo 源碼,有注釋
美中不足 路由規(guī)劃的不足到目前為止,本 Demo 完成了項目要求中的內(nèi)容,接下來,業(yè)務(wù)看了之后提出了幾個問題:
無法分享指定的“評論”,評論是很重要的吸引眼球的內(nèi)容,我們希望分享鏈接時,可以指定評論。
目前可以分享的路由只有 5 種:
- /photos - /photos/1 - /videos - /videos/1 - /messages
看樣子,我們得增加:
/photos/1/comments/3 //展示id為3的評論
評論內(nèi)容對以后的 SEO 很重要,我們希望路由能控制評論列表翻頁和排序:
/photos/1?comments-search={page:2,sort:"createDate"}
目前我們的項目主要用于移動瀏覽器訪問,很多 android 用戶習慣用手機下面的返回鍵,來撤消操作,如關(guān)閉彈窗等,能否模擬一下原生 APP?
思考:android 用戶點擊手機下面的返回鍵會引起瀏覽器的后退,后退關(guān)閉彈窗,那就需要在彈出彈窗時增加一條 URL 記錄
結(jié)論:Url 路由不只用來記錄展示哪個 Page、哪個 View,還得標識一些交互操作,完全顛覆了傳統(tǒng)的路由觀念了。
看樣子,路由會越來越復(fù)雜,到目前為止,我們還沒有在 TS 中很好的管理路由參數(shù),拼接 URL 時沒有做 TS 類型的校驗。對于 pathname 我們都是直接用字符串寫死在程序中,比如:
if(pathname === "/photos"){ .... } const arr = pathname.match(/^/photos/(d+)$/);
這樣直接 hardcode 似利不是很好,如果后其產(chǎn)品想換一下名稱怎么搞。
Model 中重復(fù)寫同樣的代碼注意到,photos/model.ts、videos/model.ts 中,90%的代碼是一樣的,為什么?因為它們兩個模塊基本上功能都是差不多的:列表展示、搜索、獲取詳情...
其實不只是 photos 和 videos,套用 RestFul 的理念,我們用網(wǎng)頁交互的過程就是在對“資源 Resource”進行維護,無外乎“增刪改查”這些基本操作,大部分情況下,它們的邏輯是相似的。由其是在后臺系統(tǒng)中,基本上連 UI 界面也可以標準化,如果將這部分“增刪改查”的邏輯提取出來,模塊可以省去不少重復(fù)的代碼。
下一個 Demo既然有這么多美中不足,那我們就期待在下一個 Demo 中一步步解決它吧
進階:SPA(單頁應(yīng)用)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/102211.html
摘要:下面也是以模塊的模塊集為例,可以發(fā)現(xiàn)和路由有一些不同就是這里為了防止模塊跟全局耦合,運用函數(shù)式編程思想類似于依賴注入,將全局的實例作為函數(shù)參數(shù)傳入,再返回出一個包含的對象,這個導(dǎo)出的對象將會被以模塊名命名,合并到全局的集中。 前言 web前端發(fā)展到現(xiàn)代,已經(jīng)不再是嚴格意義上的后端MVC的V層,它越來越向類似客戶端開發(fā)的方向發(fā)展,已獨立擁有了自己的MVVM設(shè)計模型。前后端的分離也使前端人...
摘要:下面也是以模塊的模塊集為例,可以發(fā)現(xiàn)和路由有一些不同就是這里為了防止模塊跟全局耦合,運用函數(shù)式編程思想類似于依賴注入,將全局的實例作為函數(shù)參數(shù)傳入,再返回出一個包含的對象,這個導(dǎo)出的對象將會被以模塊名命名,合并到全局的集中。 前言 web前端發(fā)展到現(xiàn)代,已經(jīng)不再是嚴格意義上的后端MVC的V層,它越來越向類似客戶端開發(fā)的方向發(fā)展,已獨立擁有了自己的MVVM設(shè)計模型。前后端的分離也使前端人...
摘要:系統(tǒng)架構(gòu)介紹本項目開發(fā)基于框架,利用進行模塊化構(gòu)建,前端編寫語言是,利用進行轉(zhuǎn)換。單頁是為單頁應(yīng)用量身定做的你可以把拆成很多,這些由路由來加載。前者用來獲取的狀態(tài),后者用來修改的狀態(tài)。 系統(tǒng)架構(gòu)介紹 本項目開發(fā)基于 React + Redux + React-Route 框架,利用 webpack 進行模塊化構(gòu)建,前端編寫語言是 JavaScript ES6,利用 babel進行轉(zhuǎn)換。...
摘要:之前分享過幾篇關(guān)于技術(shù)棧的原創(chuàng)文章解析前端架構(gòu)學習復(fù)雜場景數(shù)據(jù)設(shè)計干貨總結(jié)打造單頁應(yīng)用一個項目理解最前沿技術(shù)棧真諦一個工程實例今天進一步剖析一個實際案例移動網(wǎng)頁版。目前面臨的問題在于提高產(chǎn)品的各方面性能體驗。 之前分享過幾篇關(guān)于React技術(shù)棧的原創(chuàng)文章: 解析Twitter前端架構(gòu) 學習復(fù)雜場景數(shù)據(jù)設(shè)計 React Conf 2017 干貨總結(jié)1: React + ES next ...
摘要:我的入門到放棄之路最近看到很多相關(guān)的問題跟討論,越來越多的小伙伴喜歡這個框架了,同時也在看到了有些入門的小伙伴遇到了各種各樣的問題,本人也是框架使用都一枚,公司是騰訊阿里平安三巨頭合資的一家公司,分別上海深圳杭州北京廣州等多個分部,前端人員 showImg(https://segmentfault.com/img/bVbhonB?w=1278&h=722); 我的react入門到放棄之...
閱讀 3387·2022-01-04 14:20
閱讀 3117·2021-09-22 15:08
閱讀 2203·2021-09-03 10:44
閱讀 2321·2019-08-30 15:44
閱讀 1500·2019-08-29 18:40
閱讀 2665·2019-08-29 17:09
閱讀 2993·2019-08-26 13:53
閱讀 3226·2019-08-26 13:37