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