专栏名称: wooline
目录
相关文章推荐
中国能建  ·  中国能建版“云南咖啡”,来一杯吗? ·  昨天  
中国能建  ·  全名单!“十四五”科技创新表彰 ·  昨天  
中国电信  ·  我的新同事可真“狗”🤖 ·  昨天  
中国能建  ·  划时代!中国能建科技创新大会胜利召开 ·  2 天前  
51好读  ›  专栏  ›  wooline

React干货(二):提取公共代码、建立路由Store、Check路由参数类型

wooline  · 掘金  ·  · 2019-02-25 03:38

正文

阅读 40

React干货(二):提取公共代码、建立路由Store、Check路由参数类型

你可能觉得本Demo中对路由封装过于重度,以及不喜欢使用Class继承的方式来组织Model。没关系,本Demo只是演示众多解决方案中的一种,并不是唯一。

本Demo中使用Class继承的方式来组织Model,并不要求对React组件使用Class Component风格,并不违反React FP 编程思想和趋势。随着React Hooks的正式发布,本框架将保持API不变的前提下,使用其替换掉Redux及相关库。

安装

git clone https://github.com/wooline/react-coat-spa-demo.git
npm install
复制代码

运行

  • npm start 以开发模式运行
  • npm run build 以产品模式编译生成文件
  • npm run prod-express-demo 以产品模式编译生成文件并启用一个 express 做 demo
  • npm run gen-icon 自动生成 iconfont 文件及 ts 类型

查看在线 Demo

项目背景

项目要求及说明请看: 第一站 Helloworld ,在第一站点,我们总结了某些 “美中不足” ,主要是 3 点:

  • 路由控制需要更细粒度
  • 路由参数需要 TS 类型检查
  • 公共代码需要提取

路由控制需要更细粒度

某市场人员说:评论内容很吸引眼球,希望分享链接的时候,能指定某些评论

某脑洞大开的产品经理说,我希望你能在 android 手机上模拟原生 APP,点击手机工具栏的 “返回” 键,能撤销上一步操作,比如:点返回键能关闭弹窗。

所以每一步操作,都要用一条 URL 来驱动?比如:旅行路线详情+弹出评论弹窗+评论列表(按最新排序、第 2 页)

/photos/1/comments?comments-search={"articleId":"1","isNewest":true,"page":2}&photos-showComment=true

看到这个长长的 URL,我们不禁想想路由的本质是什么?

  • 路由是程序状态的切片。路由中包含的信息越多越细,程序的切片就能越多越细。
  • 路由是程序的状态机。跟 ReduxStore 一样,路由也是一种 Store,我们可以称其为 RouterStore,它记录了程序运行的某些状态,只不过 ReduxStore 存在内存中,而 RouterStore 存在地址栏。

Router Store 概念

如果接受 RouterStore 这个概念,那我们程序中就不是单例 Store,而是两个 Store 了,可是 Redux 不是推荐单例 Store 么?两个 Store 那会不会把维护变复杂呢?

所以,我们要特殊一点看待 RouterStore,仅把它当作是 ReduxStore 的一种 冗余设计 。也就是说,“你大爷还是你大爷”,程序的运行还是围绕 ReduxStore 来展开,除了 router 信息的流入流出源头之外,你就当 RouterStore 不存在,RouterStore 中有的信息,ReduxStore 中全都有。

  • RouterStore 是瞬时局部的,而 ReduxStore 是完整的。
  • 程序中不要直接依赖 RouterStore 中的状态,而是依赖 ReduxStore 状态。
  • 控制住 RouterStore 流入流出的源头,第一时间将其消化转换为 ReduxStore。
  • RouterStore 是只读的。RouterStore 对程序本身是透明的,所以也不存在修改它。

比如在 photos 模块中,详情页面需要控制评论的显示与隐藏,所以我们必须在 Store 中定义 showComment: boolean,而我们还想通过 url 来控制它,所以在 url 中也有&photos-showComment=true,这时就出现 RouterStore 和 ReduxStore 中都有 showComment 这个状态。你可能会想,那能不能把 ReduxStore 中的这个 showComment 去掉,直接使用 RouterStore 中的 showComment 就好?答案是不行的,不仅不能省,而且在 photos.Details 这个 view 中依赖的状态还必须是 ReduxStore 中的这个 showComment。

SearchData: {showComment: boolean}; // Router Store中的 showComment 不要直接在view中依赖
State: {showComment?: boolean}; // Redux Store中的 showComment
复制代码

Router Store 结构

RouterStore 有着与 ReduxStore 类似的结构。

  • 首先,它是一个所有模块共用的 Store,每个模块都可以往这个 Store 中存取状态。
  • 既然是公共 Store,就存在命名空间的管理。与 ReduxStore 一样,每个模块在 RouterStore 下面分配一个节点,模块可以在此节点中定义和维护自已的路由状态。
  • 根据 URL 的结构,我们进一步将 RouterStore 细分为:pathData、searchData、hashData。比如:
/photos/1/comments?comments-search={"articleId":"1","isNewest":true,"page":2}&photos-showComment=true#app-forceRefresh=true

将这个 URL 反序列化为 RouterStore:

{
  pathData: {
    photos: {itemId: "1"},
    comments: {type: "photos", typeId: "1"},
  },
  searchData: {
    comments: {search: {articleId: "1", isNewest: true, page: 2}},
    photos: {showComment: true},
  },
  hashData: {
    app: {forceRefresh: true},
  },
}

复制代码

从以上示例看出,为了解决命名空间的问题,在序列化为 URL 时,为每笔 key 增加了一个 moduleName- 作为前缀,而反序列化时,将该前缀去掉转换成 JS 的数据结构,当然这个过程是可以由某函数统一自动处理的。其它都好明白,就是 pathData 是怎么得来的?

/photos/1/comments

怎么得出:

pathData: {
    photos: {itemId: "1"},
    comments: {type: "photos", typeId: "1"},
}
复制代码

pathname 和 view 的映射

对于同一个 pathname,photos 模块分析得出的 pathData 是 {itemId: "1"},而 comments 模块分析得出的 pathData 是 {type: "photos", typeId: "1"},这是因为我们配置了 pathname 与 view 的映射规则 viewToPath

// ./src/modules/index.ts

// 定义整站路由与 view 的匹配模式
export const viewToPath: {[K in keyof ModuleGetter]: {[V in keyof ReturnModule<ModuleGetter[K]>["views"]]+?: string}} = {
  app: {Main: "/"},
  photos: {Main: "/photos", Details: "/photos/:itemId"},
  videos: {Main: "/videos", Details: "/videos/:itemId"},
  messages: {Main: "/messages"},
  comments: {Main: "/:type/:typeId/comments", Details: "/:type/:typeId/comments/:itemId"},
};
复制代码

反过来也可以反向推导出 pathToView

// ./src/common/routers.ts

// 根据 modules/index.ts中定义的 viewToPath 反推导出来,形如:
{
  "/": ["app", "Main"],
  "/photos": ["photos", "Main"],
  "/photos/:itemId": ["photos", "Details"],
  "/videos": ["videos", "Main"],
  "/videos/:itemId": ["videos", "Details"],
  "/messages": ["messages", "Main"],
  "/:type/:typeId/comments": ["comments", "Main"],
  "/:type/:typeId/comments/:itemId": ["comments", "Details"],
}
复制代码

比如当前 pathname 为:/photos/1/comments,匹配这条的有:

  • "/": ["app", "Main"]
  • "/photos": ["photos", "Main"]
  • "/photos/:itemId": ["photos", "Details"],
  • "/:type/:typeId/comments": ["comments", "Main"],
// 原来模块自已去写正则解析pathname
const arr = pathname.match(/^\/photos\/(\d+)$/);

// 变成集中统一处理,在使用时只需要引入强类型 PathData
pathData: {
    photos: {itemId: "1"},
    comments: {type: "photos", typeId: "1"},
}
复制代码

结论

  • 这样一来,我们既用 TS 强类型来规范了 pathname 中的参数,还可以让每个模块自由定义参数名称

定义 searchData 和 hashData

RouterStore 是一个公共的 Store,每个模块都可以往里面存取信息,在反序列化时,由于会统一自动加上 moduleName 作前缀,所以也不用担心命名冲突。

每个 Module 自已定义自已的 router 结构,将所有 Module 的 router 结构合并起来,就组成了整个 RouterState。

我们在上文: 第一站 Helloworld 中,提到过“ 参数默认值 ”的概念:

  • 区分:原始路由参数(SearchData) 默认路由参数(SearchData) 和 完整路由参数(WholeSearchData)
  • 完整路由参数(WholeSearchData) = merage(默认路由参数(SearchData), 原始路由参数(SearchData))
    • 原始路由参数(SearchData)每一项都是可选的,用 TS 类型表示为: Partial<WholeSearchData>
    • 完整路由参数(WholeSearchData)每一项都是必填的,用 TS 类型表示为: Required<SearchData>
    • 默认路由参数(SearchData)和完整路由参数(WholeSearchData)类型一致

所以,在模块中对于路由部分有两个工作要做:1. 定义路由结构 ,2. 定义默认参数 ,如:

// ./src/modules/photos/facade.ts

export const defRouteData: ModuleRoute<PathData, SearchData, HashData> = {
  pathData: {},
  searchData: {
    search






请到「今天看啥」查看全文