React单页怎样计划路由、设想Store、分别模块、按需加载

  • 本项目地点: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

关于脚手架

  • 采纳 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

主要页面:

  • 旅游线路展示
  • 旅途小视频展示
  • 站内信展示(需登录)
  • 批评展示 (访客可检察批评,宣布则需登录)

《React单页怎样计划路由、设想Store、分别模块、按需加载》

项目请求

  • 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>
    • 完整路由参数(WholeSearchData)每一项都是必填的,用 TS 范例示意为:Required<SearchData>
    • 默许路由参数(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 extends {[key in ModuleNames]: any}> = 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<typeof moduleGetter>; // 考证一下是不是有模块忘了设置

// 定义整站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<States>; // 考证一下是不是有模块忘了设置

路由和加载

本 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");

<Switch>
  <Redirect exact={true} path="/" to="/photos" />
  <Route exact={false} path="/photos" component={PhotosView} />
  <Route exact={false} path="/videos" component={VideosView} />
  <Route exact={false} path="/messages" component={MessagesView} />
  <Route component={NotFound} />
</Switch>

运用 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(单页运用)

    原文作者:wooline
    原文地址: https://segmentfault.com/a/1190000018340372
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞