├── .gitignore ├── README.md ├── docs ├── .nojekyll ├── README.md ├── _coverpage.md ├── _navbar.md ├── _sidebar.md ├── house │ ├── index.md │ ├── longgang.md │ ├── src │ │ ├── lg_2_2.jpg │ │ ├── lg_3_1.jpg │ │ ├── lg_3_2.jpg │ │ └── lg_3_3.jpg │ └── target.md ├── index.html ├── readmyself.md └── src │ ├── index.md │ ├── resources │ ├── no-animation.gif │ ├── present-animation.gif │ └── push-animation.gif │ ├── router.md │ └── router_how.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── RouterConfig.js ├── index.css ├── index.js ├── pages │ ├── Detail.js │ ├── Home.js │ ├── HybridPage.js │ ├── PopPage.js │ ├── ReplacePage.js │ ├── components │ │ ├── Button.js │ │ ├── Title.js │ │ ├── Toast.js │ │ └── index.js │ └── index.js ├── router │ ├── AnimatedSwitch.css │ ├── AnimatedSwitch.js │ ├── Router.js │ ├── StackSwitch.css │ ├── StackSwitch.js │ └── WrapedComp.js └── serviceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## react-router-virgo 使用手册 2 | 3 | One line of code and one route configuration file, realize the function of `react-router`, and match the transition animation effect of native route. 4 | 5 | 一行代码 + 一个路由配置文件,就可以实现 `react-router` 的功能,并让你的 Web 页面切换媲美 Native 路由的转场动画体验 6 | 7 | > 这里主要介绍怎么使用react-router-virgo,对实现原理感兴趣的小伙伴,可以去看我的这篇[技术博客](https://juejin.im/post/5eec73a5518825658d034976) 8 | 9 | ---- 10 | 11 | ### 一、Brief Introduction 概述 12 | 13 | > [react-router-virgo](https://github.com/JackXJR/react-router-virgo) is a router with secondary encapsulation based on `react-router`, which makes the integrated routing function extremely simple. In addition, the extended functions such as route transition animation are added: `No transition animation`, `push transition animation`, `present transition animation` 14 | > 15 | > [react-router-virgo](https://github.com/JackXJR/react-router-virgo)是在 react-router 的基础上进行二次封装的 Router,使集成路由功能变得极其简单。此外,还增加了路由转场动画等扩展功能:`无转场动画`, `Push 转场动画`, `Present 转场动画`,我们先来看看效果吧 16 | 17 | - 无转场动画(现有的 web 页面切换体验) 18 | 19 | 20 | 21 | - push 动画切换(右侧淡入,右侧淡出) 22 | 23 | 24 | 25 | - present 动画切换(下方淡入,下方淡出的模态切换) 26 | 27 | 28 | 29 | > 总体上,基本能达到 native 路由的切换体验,当然,毕竟是 web 页面,相对于 iOS 的原生的 native 路由体验还是有点差距,感兴趣的可以自己运行完整 demo 体验下。。。 30 | 31 | 32 | ### 二、Installation 安装 33 | 34 | ``` 35 | ### use npm 36 | ### 使用npm 37 | npm install --save react-router-virgo 38 | 39 | ### use yarn 40 | ### 使用yarn 41 | yarn add react-router-virgo 42 | ``` 43 | 44 | ### 三、RouterConfig 设置路由配置文件 45 | 46 | > Set route profile `RouterConfig.js` 47 | > 48 | > 设置路由配置文件 `RouterConfig.js` 49 | 50 | #### 3.1 Example 代码演示 51 | 52 | ```javascript 53 | import { Home, Detail } from './pages/index' 54 | 55 | // Three scenes are exemplified: no animation, push transition animation and present transition animation 56 | // 例举了无动画、push转场动画、present转场动画三种场景 57 | export const RouterConfig = [ 58 | { path: '/', component: Home }, 59 | // No transition animation (the new page directly covers the current page) 60 | // 无转场动画(新页面直接覆盖当前页面) 61 | { 62 | path: '/detail', 63 | component: Detail, 64 | sceneConfig: { 65 | enter: 'no-animation', 66 | exit: 'no-animation', 67 | }, 68 | }, 69 | // Push transition animation (when opening the page, overwrite from left to right; when closing the page, withdraw from right to left) 70 | // Push 转场动画(打开页面时,从左往右覆盖;关闭页面时,从右往左收回) 71 | { 72 | path: '/push/detail', 73 | component: Detail, 74 | sceneConfig: { 75 | enter: 'from-right', 76 | exit: 'to-right', 77 | }, 78 | }, 79 | // Present transition animation (when opening the page, it will pop up from the bottom; when closing the page, it will pop up from the top) 80 | // Present 转场动画(打开页面时,从下往上弹起;关闭页面时,从上往下收起) 81 | { 82 | path: '/present/detail', 83 | component: Detail, 84 | sceneConfig: { 85 | enter: 'from-bottom', 86 | exit: 'to-bottom', 87 | }, 88 | }, 89 | ] 90 | ``` 91 | 92 | #### 3.2 RouterConfig Description 配置项说明 93 | 94 | | key | description | type | default | | 95 | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------- | ---------- | 96 | | `path` | The routing path can take parameters. The one after `/:` is a parameter, such as `/ detail /: id`. The parameter is id. 路由路径,可以带参数,在`/:`后的为参数,如 `/detail/:id`, 参数为 id | string | | `Required` | 97 | | `component` | Page components of route path mapping. 路由路径映射的页面组件 | class | | `Required` | 98 | | `sceneConfig` | The route transition animation configuration supports three scenes: `no animation`, `push animation` and `present animation`. The default is push animation. 路由转场动画配置,支持`无动画`、`Push 动画`、`Present 动画`三种场景,默认使用 Push 动画 | object | `{enter: 'from-right', exit: 'to-right'}` | `Optional` | 99 | | `exact` | Use exact match or not. 是否使用精准匹配 | bool | true | `Optional` | 100 | 101 | - Routing transition animation parameter `sceneConfig` configuration supports the following three scenarios 102 | - 路由转场动画参数`sceneConfig`配置,支持以下三种场景 103 | 104 | ``` 105 | ### No animation configuration 106 | ### 无动画配置 107 | { 108 | enter: 'no-animation', 109 | exit: 'no-animation', 110 | } 111 | 112 | ### Push animation configuration 113 | ### Push动画配置 114 | { 115 | enter: 'from-right', 116 | exit: 'to-right', 117 | } 118 | 119 | ### Present animation configuration 120 | ### Present动画配置 121 | { 122 | enter: 'from-bottom', 123 | exit: 'to-bottom', 124 | } 125 | ``` 126 | 127 | ### 四、Use Router 使用路由 128 | 129 | > Add `Router` to entry file `App.js` 130 | > 131 | > 在入口文件`App.js`中添加`Router` 132 | 133 | #### 4.1 Example 代码演示 134 | 135 | ```javascript 136 | import React from 'react' 137 | import Router from './router/Router' 138 | import { RouterConfig } from './RouterConfig' 139 | import './index.css' 140 | 141 | function App() { 142 | return 143 | } 144 | 145 | export default App 146 | ``` 147 | 148 | #### 4.2 API 149 | 150 | | props | description | type | default | | 151 | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----- | ------- | ---------- | 152 | | `routerConfig` | Route configuration data. 路由配置数据 | array | [] | `Required` | 153 | | `useBrowserRouter` | Routing type `BrowserRouter/HashRouter`. By default, HashRouter is used. 路由类型 `BrowserRouter/HashRouter`,默认使用 `HashRouter` | bool | false | `Optional` | 154 | | `useAnimatedSwitch` | Use transition animation or not. 是否使用转场过渡动画 | bool | true | `Optional` | 155 | 156 | ### 五. FAQ 常见问题 157 | 158 | > Q: Which routing types are supported? 159 | > 支持哪些路由类型? 160 | 161 | - Two types of `BrowserRouter` and `HashRouter` are supported. They can be set by the property `useBrowserRouter`. The default is HashRouter 162 | - 目前支持 BrowserRouter 和 HashRouter 两种类型,可通过属性`useBrowserRouter`来设置,默认使用 HashRouter 163 | 164 | > Q: What transition animations are supported? 165 | > 支持哪些转场动画? 166 | 167 | - It supports three kinds of scenes: no animation, pop-up presentation animation, and right to left push animation. You can configure `sceneConfig` according to rules in the routing configuration file. If the sceneConfig field is not configured, push animation is used by default 168 | - 目前支持无动画、从下往上弹起的 Present 动画,从右往左打开的 Push 动画三种场景。可以在路由配置文件中按规则配置`sceneConfig`即可,如果未配置 sceneConfig 字段,则默认使用 Push 动画 169 | 170 | > Q: After opening a new page, will the previous page be destroyed? 171 | > 打开新页面后,上一级页面是否会被销毁? 172 | 173 | - It will be destroyed. When returning to the previous page, the page will be re rendered, and the subsequent version iterations will support the stack routing function. 174 | - 会被销毁,返回上一级页面时,页面会重新渲染,后续版本迭代会支持 Stack 路由功能。 175 | 176 | > Q:Why does compilation fail after installation? 177 | > 安装后,编译失败的原因? 178 | 179 | - Confirm whether there are two dependencies `react-router-dom` and `react-transition-group` in your project. If not, please import the dependency through yarn or NPM. 180 | - 确认下项目里是否有 `react-router-dom`、`react-transition-group` 这 2 个依赖,如果没有,请通过 yarn 或者 npm 引入依赖. 181 | 182 | ``` 183 | yarn add react-router-dom react-transition-group 184 | ``` 185 | 186 | > Q: Whether the project developed by TS is supported 187 | > 是否支持 ts 开发的项目 188 | 189 | - Subsequent iterations will support 190 | - 后续迭代会支持 191 | 192 | ### 六. More 其它 193 | 194 | If you are interested in more specific information, go to the code. If you find a bug, please mention an [issue](https://github.com/JackXJR/react-router-virgo/issues), I will repair and optimize it as soon as possible... 195 | 196 | 更具体的信息大家感兴趣的话去看代码吧,如果发现 bug,请提一个[issue](https://github.com/JackXJR/react-router-virgo/issues),我会第一时间进行修复和优化... 197 | 198 | > Welcome to use, if you think it's good, please give a little `star` encouragement~ 199 | > 200 | > 欢迎使用,觉得不错请给一个小小的 `star` 鼓励一下~ 201 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### 路由 Router 2 | 3 | 项目中的路由使用[react-router-virgo](https://github.com/JackXJR/react-router-virgo)中的 Router,可以实现 native 路由的转场动画体验。 4 | 5 | - [react-router-virgo 使用手册](/src/router.md) 6 | 7 | - [react-router-virgo 原理解析](/src/router_how.md) 8 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | # 谷底飞龙 2 | 3 | 本文档主要针对基于 React-Router 封装含转场过渡动画的 Router 的使用及原理进行详细介绍!希望能给你带来帮助! 4 | 5 | > 如果觉得对你有所帮助,请帮忙给个 [Star](https://github.com/JackXJR/react-router-virgo)! 6 | 7 | [GitHub](https://github.com/JackXJR/react-router-virgo) 8 | [Get Started](README.md) 9 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - [自述](/readmyself.md) 2 | 3 | - [react-router-virgo](/src/index.md) 4 | - [react-route-virgo 使用手册](/src/router.md) 5 | - [react-route-virgo 原理解析](/src/router_how.md) 6 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [自述](/readmyself.md) 2 | 3 | - [react-router-virgo](/src/index.md) 4 | 5 | - [react-route-virgo 使用手册](/src/router.md) 6 | - [react-route-virgo 原理解析](/src/router_how.md) 7 | 8 | - [深房投资攻略](/house/index.md) 9 | - [预期目标](/house/target.md) 10 | - [龙岗区](house/longgang.md) 11 | -------------------------------------------------------------------------------- /docs/house/index.md: -------------------------------------------------------------------------------- 1 | # 深圳房产投资攻略 2 | -------------------------------------------------------------------------------- /docs/house/longgang.md: -------------------------------------------------------------------------------- 1 | # 龙岗区 2 | 3 | ## 两室 4 | 5 | > 1.区域:[宝荷 中环至外环](https://sz.lianjia.com/ershoufang/105104494035.html) 6 | 7 | | 总价 | 单价 | 面积 | 朝向 | 楼龄 | 8 | | ------ | ----- | ---- | --------------------- | --------- | 9 | | 259 万 | 43312 | 59.8 | 东南/精装/电梯/高(33) | 2014/板塔 | 10 | 11 | 缺点:`无地铁/无学校`、`满两年` 12 | 13 | > 2.[大运新城 THETOWN 乐城](https://sz.lianjia.com/ershoufang/105103790167.html),两室一厅 14 | 15 | | 总价 | 单价 | 面积 | 朝向 | 楼龄 | 16 | | ------ | ----- | ----- | --------------------- | ------- | 17 | | 245 万 | 47426 | 51.66 | 东南/简装/电梯/中(33) | 2014/板 | 18 | 19 | ![大运新城 THETOWN 乐城](./src/lg_2_2.jpg) 20 | 21 | 优点:`地铁3号线荷坳站374米、大运站1215米`、`学校配套完善` 22 | 23 | 缺点:`满两年` 24 | 25 | ## 三室 26 | 27 | > 1.区域:[龙岗中心城 汇龙天下](https://sz.lianjia.com/ershoufang/105104136111.html),三室一厅 28 | 29 | | 总价 | 单价 | 面积 | 朝向 | 楼龄 | 30 | | ------ | ----- | ----- | ------------------- | --------- | 31 | | 290 万 | 32251 | 89.92 | 南/简装/电梯/高(26) | 2011/板塔 | 32 | 33 | ![龙岗中心城 汇龙天下](./src/lg_3_1.jpg) 34 | 35 | 优点:`地铁3号线龙城广场站1.3公里`、`满五唯一`、`学校配套完善` 36 | 37 | > 2.区域:[双龙 金汐府](https://sz.lianjia.com/ershoufang/105104341680.html),三室两厅 38 | 39 | | 总价 | 单价 | 面积 | 朝向 | 楼龄 | 40 | | ------ | ----- | ----- | --------------------- | --------- | 41 | | 279 万 | 35917 | 77.68 | 西南/精装/电梯/中(30) | 2015/板塔 | 42 | 43 | ![双龙 金汐府](./src/lg_3_2.jpg) 44 | 45 | 优点:`地铁3号线双龙站1.52公里`、`学校配套完善` 46 | 47 | > 3.区域:[坪地 中骏四季阳光二期](https://sz.lianjia.com/ershoufang/105104573589.html),三室两厅 48 | 49 | | 总价 | 单价 | 面积 | 朝向 | 楼龄 | 50 | | ------ | ----- | ----- | ------------------- | --------- | 51 | | 290 万 | 32251 | 84.26 | 南/毛胚/电梯/高(31) | 2016/板塔 | 52 | 53 | ![坪地 中骏四季阳光二期](./src/lg_3_3.jpg) 54 | 55 | 缺点:`无地铁` 56 | -------------------------------------------------------------------------------- /docs/house/src/lg_2_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/house/src/lg_2_2.jpg -------------------------------------------------------------------------------- /docs/house/src/lg_3_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/house/src/lg_3_1.jpg -------------------------------------------------------------------------------- /docs/house/src/lg_3_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/house/src/lg_3_2.jpg -------------------------------------------------------------------------------- /docs/house/src/lg_3_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/house/src/lg_3_3.jpg -------------------------------------------------------------------------------- /docs/house/target.md: -------------------------------------------------------------------------------- 1 | # 预期目标 2 | 3 | > 投资预期 4 | 5 | | 总价/元 | 面积/m2 | 首付/元 | 月供/元 | 租金/元 | 6 | | ---------- | ------- | --------- | ---------- | --------- | 7 | | 200~300 万 | 50~90 | 65~100 万 | 0.7~1.1 万 | 3000~5000 | 8 | 9 | > 房屋要求 10 | 11 | | 房型 | 楼龄 | 楼层 | 朝向 | 位置 | 其它 | 12 | | ------- | ------ | ------ | ---- | -------------- | ---------------- | 13 | | 两/三室 | <15 年 | 中高层 | 朝南 | 地铁/学校/生活 | 满二/电梯/商品房 | 14 | 15 | > 费用计算 16 | 17 | | 中介费 | 契税 | 个税 | 增值税 | 18 | | ------ | ---- | ---- | ------ | 19 | | 1~2% | 1% | 0~1% | 0 | 20 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 谷底飞龙 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/readmyself.md: -------------------------------------------------------------------------------- 1 | ### 作者简述 2 | 3 | > 笔名`谷底飞龙`! 投资生涯始于 2013 年,技术生涯始于 2014 年,致力于成为媲美巴菲特的价值投资者,业界顶尖的高水平大前端架构师!愿景是`成为投资界里的技术大牛,技术界里的投资大鳄!` 4 | 5 | ### 技术栈 6 | 7 | | iOS | 前端 | 小程序 | 跨平台 | 其它 | 8 | | --- | ----- | ------------- | ------------------- | ---------- | 9 | | OC | React | 微信/字节跳动 | ReactNative/Flutter | nodejs ... | 10 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | > [react-router-virgo](https://github.com/JackXJR/react-router-virgo)是基于 `react-router + react-transition-group` 封装的 Router,可以实现 native 路由的转场动画体验。 2 | 3 | - [react-router-virgo 使用手册](/src/router.md) 4 | 5 | - [react-router-virgo 原理解析](/src/router_how.md) 6 | -------------------------------------------------------------------------------- /docs/src/resources/no-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/src/resources/no-animation.gif -------------------------------------------------------------------------------- /docs/src/resources/present-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/src/resources/present-animation.gif -------------------------------------------------------------------------------- /docs/src/resources/push-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/docs/src/resources/push-animation.gif -------------------------------------------------------------------------------- /docs/src/router.md: -------------------------------------------------------------------------------- 1 | One line of code and one route configuration file, realize the function of `react-router`, and match the transition animation effect of native route. 2 | 3 | 一行代码 + 一个路由配置文件,就可以实现 `react-router` 的功能,并让你的 Web 页面切换媲美 Native 路由的转场动画体验 4 | 5 | ### 一、Brief Introduction 概述 6 | 7 | > [react-router-virgo](https://github.com/JackXJR/react-router-virgo) is a router with secondary encapsulation based on `react-router`, which makes the integrated routing function extremely simple. In addition, the extended functions such as route transition animation are added: `No transition animation`, `push transition animation`, `present transition animation` 8 | > 9 | > [react-router-virgo](https://github.com/JackXJR/react-router-virgo)是在 react-router 的基础上进行二次封装的 Router,使集成路由功能变得极其简单。此外,还增加了路由转场动画等扩展功能:`无转场动画`, `Push 转场动画`, `Present 转场动画` 10 | 11 | - 无转场动画 12 | 13 | ![无转场动画](./resources/no_animation.gif ':size=320x570') 14 | 15 | - Push 转场动画 16 | 17 | ![Push 转场动画](./resources/push_animation.gif ':size=320x570') 18 | 19 | - Present 转场动画 20 | 21 | ![Present 转场动画](./resources/present_animation.gif ':size=320x570') 22 | 23 | ### 二、Installation 安装 24 | 25 | ``` 26 | ### use npm 27 | ### 使用npm 28 | npm install --save react-router-virgo 29 | 30 | ### use yarn 31 | ### 使用yarn 32 | yarn add react-router-virgo 33 | ``` 34 | 35 | ### 三、RouterConfig 设置路由配置文件 36 | 37 | > Set route profile `RouterConfig.js` 38 | > 39 | > 设置路由配置文件 `RouterConfig.js` 40 | 41 | #### 3.1 Example 代码演示 42 | 43 | ```javascript 44 | import { Home, Detail } from './pages/index' 45 | 46 | // Three scenes are exemplified: no animation, push transition animation and present transition animation 47 | // 例举了无动画、push转场动画、present转场动画三种场景 48 | export const RouterConfig = [ 49 | { path: '/', component: Home }, 50 | // No transition animation (the new page directly covers the current page) 51 | // 无转场动画(新页面直接覆盖当前页面) 52 | { 53 | path: '/detail', 54 | component: Detail, 55 | sceneConfig: { 56 | enter: 'no-animation', 57 | exit: 'no-animation', 58 | }, 59 | }, 60 | // Push transition animation (when opening the page, overwrite from left to right; when closing the page, withdraw from right to left) 61 | // Push 转场动画(打开页面时,从左往右覆盖;关闭页面时,从右往左收回) 62 | { 63 | path: '/push/detail', 64 | component: Detail, 65 | sceneConfig: { 66 | enter: 'from-right', 67 | exit: 'to-right', 68 | }, 69 | }, 70 | // Present transition animation (when opening the page, it will pop up from the bottom; when closing the page, it will pop up from the top) 71 | // Present 转场动画(打开页面时,从下往上弹起;关闭页面时,从上往下收起) 72 | { 73 | path: '/present/detail', 74 | component: Detail, 75 | sceneConfig: { 76 | enter: 'from-bottom', 77 | exit: 'to-bottom', 78 | }, 79 | }, 80 | ] 81 | ``` 82 | 83 | #### 3.2 RouterConfig Description 配置项说明 84 | 85 | | key | description | type | default | | 86 | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------------------- | ---------- | 87 | | `path` | The routing path can take parameters. The one after `/:` is a parameter, such as `/ detail /: id`. The parameter is id. 路由路径,可以带参数,在`/:`后的为参数,如 `/detail/:id`, 参数为 id | string | | `Required` | 88 | | `component` | Page components of route path mapping. 路由路径映射的页面组件 | class | | `Required` | 89 | | `sceneConfig` | The route transition animation configuration supports three scenes: `no animation`, `push animation` and `present animation`. The default is push animation. 路由转场动画配置,支持`无动画`、`Push 动画`、`Present 动画`三种场景,默认使用 Push 动画 | object | `{enter: 'from-right', exit: 'to-right'}` | `Optional` | 90 | | `exact` | Use exact match or not. 是否使用精准匹配 | bool | true | `Optional` | 91 | 92 | - Routing transition animation parameter `sceneConfig` configuration supports the following three scenarios 93 | - 路由转场动画参数`sceneConfig`配置,支持以下三种场景 94 | 95 | ``` 96 | ### No animation configuration 97 | ### 无动画配置 98 | { 99 | enter: 'no-animation', 100 | exit: 'no-animation', 101 | } 102 | 103 | ### Push animation configuration 104 | ### Push动画配置 105 | { 106 | enter: 'from-right', 107 | exit: 'to-right', 108 | } 109 | 110 | ### Present animation configuration 111 | ### Present动画配置 112 | { 113 | enter: 'from-bottom', 114 | exit: 'to-bottom', 115 | } 116 | ``` 117 | 118 | ### 四、Use Router 使用路由 119 | 120 | > Add `Router` to entry file `App.js` 121 | > 122 | > 在入口文件`App.js`中添加`Router` 123 | 124 | #### 4.1 Example 代码演示 125 | 126 | ```javascript 127 | import React from 'react' 128 | import Router from './router/Router' 129 | import { RouterConfig } from './RouterConfig' 130 | import './index.css' 131 | 132 | function App() { 133 | return 134 | } 135 | 136 | export default App 137 | ``` 138 | 139 | #### 4.2 API 140 | 141 | | props | description | type | default | | 142 | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----- | ------- | ---------- | 143 | | `routerConfig` | Route configuration data. 路由配置数据 | array | [] | `Required` | 144 | | `useBrowserRouter` | Routing type `BrowserRouter/HashRouter`. By default, HashRouter is used. 路由类型 `BrowserRouter/HashRouter`,默认使用 `HashRouter` | bool | false | `Optional` | 145 | | `useAnimatedSwitch` | Use transition animation or not. 是否使用转场过渡动画 | bool | true | `Optional` | 146 | 147 | ### 五. FAQ 常见问题 148 | 149 | > Q: Which routing types are supported? 150 | > 支持哪些路由类型? 151 | 152 | - Two types of `BrowserRouter` and `HashRouter` are supported. They can be set by the property `useBrowserRouter`. The default is HashRouter 153 | - 目前支持 BrowserRouter 和 HashRouter 两种类型,可通过属性`useBrowserRouter`来设置,默认使用 HashRouter 154 | 155 | > Q: What transition animations are supported? 156 | > 支持哪些转场动画? 157 | 158 | - It supports three kinds of scenes: no animation, pop-up presentation animation, and right to left push animation. You can configure `sceneConfig` according to rules in the routing configuration file. If the sceneConfig field is not configured, push animation is used by default 159 | - 目前支持无动画、从下往上弹起的 Present 动画,从右往左打开的 Push 动画三种场景。可以在路由配置文件中按规则配置`sceneConfig`即可,如果未配置 sceneConfig 字段,则默认使用 Push 动画 160 | 161 | > Q: After opening a new page, will the previous page be destroyed? 162 | > 打开新页面后,上一级页面是否会被销毁? 163 | 164 | - It will be destroyed. When returning to the previous page, the page will be re rendered, and the subsequent version iterations will support the stack routing function. 165 | - 会被销毁,返回上一级页面时,页面会重新渲染,后续版本迭代会支持 Stack 路由功能。 166 | 167 | > Q:Why does compilation fail after installation? 168 | > 安装后,编译失败的原因? 169 | 170 | - Confirm whether there are two dependencies `react-router-dom` and `react-transition-group` in your project. If not, please import the dependency through yarn or NPM. 171 | - 确认下项目里是否有 `react-router-dom`、`react-transition-group` 这 2 个依赖,如果没有,请通过 yarn 或者 npm 引入依赖. 172 | 173 | ``` 174 | yarn add react-router-dom react-transition-group 175 | ``` 176 | 177 | > Q: Whether the project developed by TS is supported 178 | > 是否支持 ts 开发的项目 179 | 180 | - Subsequent iterations will support 181 | - 后续迭代会支持 182 | 183 | ### 六. More 其它 184 | 185 | If you are interested in more specific information, go to the code. If you find a bug, please mention an [issue](https://github.com/JackXJR/react-router-virgo/issues), I will repair and optimize it as soon as possible... 186 | 187 | 更具体的信息大家感兴趣的话去看代码吧,如果发现 bug,请提一个[issue](https://github.com/JackXJR/react-router-virgo/issues),我会第一时间进行修复和优化... 188 | 189 | > Welcome to use, if you think it's good, please give a little `star` encouragement~ 190 | > 191 | > 欢迎使用,觉得不错请给一个小小的 `star` 鼓励一下~ 192 | -------------------------------------------------------------------------------- /docs/src/router_how.md: -------------------------------------------------------------------------------- 1 | 在使用 React 开发 web 页面的时候,一般都会使用`react-router`来实现路由功能,相较于 native 路由流畅丝滑的体验,web 页面切换起来会很生硬。作为从 iOS 转前端的我来说,尤其不能接受,因此,在 react-router 的基础上,封装了一个 Router 库 [react-router-virgo](https://github.com/JackXJR/react-router-virgo),让使用者只需要`一行代码 + 一个路由配置文件`就可以使 Web 页面切换能达到 native 路由切换的流畅体验。 2 | 3 | ### 一、react-router-virgo 简介 4 | 5 | 这个路由组件还没完全达到我的预期,后面有时间会继续迭代优化。目前这个路由组件支持以下功能: 6 | 7 | - 无转场动画切换(现有的 web 页面切换体验) 8 | - push 动画切换(右侧淡入,右侧淡出的 Native 路由切换体验) 9 | - present 动画切换(下方淡入,下方淡出的 Native 路由模态切换体验) 10 | - 支持 HashRouter,BrowserRouter 两种路由 11 | 12 | > 总体上,基本能达到 native 路由的切换体验,当然,毕竟是 web 页面,相对于 iOS 的原生的 native 路由体验还是有点差距,感兴趣的也可以 [运行完整 demo](https://github.com/JackXJR/react-router-virgo)体验下。。。 13 | 14 | 下面我们来简要看一下实现原理吧 15 | 16 | 简单来说,`react-router-virgo`是在 [react-router](https://reacttraining.com/react-router/web/guides/quick-start) 和 [react-transition-group](https://reactcommunity.org/react-transition-group/#Transition-prop-mountOnEnter) 的基础上进行二次封装的 Router。这里也会分这两部分进行递进分析 17 | 18 | ### 二、react-router 19 | 20 | 首先,我们先简要介绍下 react-router 的基本用法(详细看[官网](https://reacttraining.com/react-router/web/guides/quick-start)介绍)。 21 | 22 | 我们主要使用到 react-router 提供的 HashRouter/BrowserRouter,Switch,Route 三个组件。 23 | 24 | #### 2.1 HashRouter/BrowserRouter 25 | 26 | react-router 主要提供了两种 Router 27 | 28 | - HashRouter: hash 形式实现的路由,使用 `createHashHistory` 创建的 history 29 | 30 | - BrowserRouter:以 html5 提供的 history api 形式实现的路由,使用 `createBrowserHistory` 创建的 history 31 | 32 | #### 2.2 Route 33 | 34 | 路由组件,`path` 指定匹配的路由,`component` 指定路由匹配时展示的组件(Route 也可以通过 children 和 render 的形式创建展示的组件)。 35 | 36 | ```javascript 37 | 38 | ``` 39 | 40 | #### 2.3 Switch 41 | 42 | 多个 Route 组件同时匹配时,默认都会显示,但是被 Switch 包裹起来的 Route 组件只会显示第一个被匹配上的路由。路由的转场动画,其实就是基于 Switch 进行封装,使页面切换时,具有动画效果。 43 | 44 | #### 2.4 代码演示 45 | 46 | > 使用`react-router-dom`中的 `HashRouter/BrowserRouter` 、 `Route`、`Switch` 一个简单的无动画路由切换功能,具体如下: 47 | 48 | ```javascript 49 | import React from 'react'; 50 | import { HashRouter, Route, Switch } from 'react-router-dom'; 51 | import { RouterConfig } from './routerConfig'; 52 | 53 | const Router = () => ( 54 | // 这里使用 HashRouter , 也可以使用 BrowserRouter 55 | 56 | 57 | {RouterConfig.map((config, index) => { 58 | return ; 59 | })} 60 | 61 | 62 | ); 63 | 64 | export default Router; 65 | ``` 66 | 67 | 那怎么让页面切换的时候,带有转场动画效果呢,这就需要结合 react-transition-group 了 68 | 69 | ### 三、react-transition-group 70 | 71 | 在介绍实现转场动画之前,我们得先学习如何使用 [react-transition-group](https://reactcommunity.org/react-transition-group/#Transition-prop-mountOnEnter)。基于此,接下来就将对其提供的 `CSSTransition` 和 `TransitionGroup` 这两个组件展开简要介绍。 72 | 73 | #### 3.1 CSSTransition 74 | 75 | CSSTransition 会自动给包裹的标签添加样式,是一个可以通过设置 css 来实现的过渡动画组件。我们先来看下 CSSTransition 的属性: 76 | 77 | | 属性 | 详情 | 78 | | --------- | ------------------------------------------------------------- | 79 | | `in` | 取值 `true/false`,决定包裹的元素是要进行出场动画还是入场动画 | 80 | | `timeout` | 设置动画时间 | 81 | 82 | 实际上,CSSTransition 并没有给组件任何动画效果,只是在一段时间内,给包裹的组件加上三个类,这个三类中,我们可以写动画效果。 83 | 84 | - in 属性的值从 false 变为 true,就会元素加上下面三个类(这里前缀`xxx` 代指设置的 css 类型,默认是`fade`),即执行入场动画: 85 | 86 | | css 类 | 详情 | 87 | | ------------------ | ---------------------------------------------------------------------------------- | 88 | | `xxx-enter` | 入场动画的第一个瞬间(帧)加入的,动画即将结束前消失 | 89 | | `xxx-enter-active` | `xxx-enter` 加入后,第二个瞬间加入,持续到入场动画即将执行完成,动画即将结束前消失 | 90 | | `xxx-enter-done` | 入场动画结束瞬间,加入之后一直存在 | 91 | 92 | - 相反地,当 in 属性置为 false 时,CSSTransition 会给子组件加上 `xxx-exit`、`xxx-exit-active`、`xxx-exit-done` 的 class。(更多详细介绍可以戳[官网](http://reactcommunity.org/react-transition-group/css-transition)查看) 93 | 94 | 基于以上两点,我们给打开页面时的 class 设置为`forward-from-right`,则对应的 css 样式: 95 | 96 | ```javascript 97 | /** 98 | * 打开页面:右侧淡入,右侧淡出 99 | */ 100 | .forward-from-right-enter { 101 | z-index: 2; 102 | transform: translate3d(100%, 0, 0); 103 | } 104 | 105 | .forward-from-right-enter-active { 106 | z-index: 2; 107 | transform: translate3d(0, 0, 0); 108 | transition: all 300ms; 109 | } 110 | 111 | .forward-from-right-enter-done { 112 | z-index: 2; 113 | } 114 | 115 | .forward-from-right-exit { 116 | z-index: 1; 117 | transform: translate3d(0, 0, 0); 118 | } 119 | 120 | .forward-from-right-exit-active { 121 | z-index: 1; 122 | transform: translate3d(-30%, 0, 0); 123 | transition: all 300ms; 124 | } 125 | 126 | .forward-from-right-exit-done { 127 | z-index: 1; 128 | } 129 | ``` 130 | 131 | #### 3.2 TransitionGroup 132 | 133 | 用 CSSTransition 可以实现单个页面的动画,而路由的转场动画需要管理新旧两个页面的联动切换。如何让新旧页面切换的过程中,同时存在两个 DOM 节点,切换结束后,再移除旧节点呢? 134 | 135 | 为此我们再来介绍 react-transition-group 提供的[TransitionGroup](https://reactcommunity.org/react-transition-group/transition-group)这个组件。 136 | 137 | > 如[官网](https://reactcommunity.org/react-transition-group/transition-group)介绍,TransitionGroup 组件就是用来管理一堆节点 mounting 和 unmounting 过程的组件,非常适合处理路由切换的情况。TransitionGroup 在感知到其 children 变化时,会先保存住即将要被移除的节点,而在其动画结束时才会真正移除该节点。 138 | 139 | #### 3.2 代码演示 140 | 141 | > 使用 `react-transition-group`中的`TransitionGroup`和`CSSTransition` 对 Switch 进行封装,实现路由转场动画组件`AnimatedSwitch` 142 | 143 | ```javascript 144 | import React from 'react'; 145 | import { Switch, withRouter } from 'react-router-dom'; 146 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 147 | import './AnimatedSwitch.css'; // 动画样式 148 | 149 | let oldLocation = null; 150 | class AnimatedSwitch extends React.Component { 151 | static propsTypes = { 152 | routerConfig: PropsTypes.array.isRequired, 153 | }; 154 | render() { 155 | const { location, history, children, routerConfig } = this.props; 156 | let classNames = ''; 157 | if (history.action === 'PUSH') { 158 | // 打开页面的转场动画 159 | classNames = 'forward-from-right'; 160 | } else if (history.action === 'POP' && oldLocation) { 161 | // 关闭页面的转场动画 162 | classNames = 'back-to-right'; 163 | } 164 | oldLocation = location; 165 | // 使用 TransitionGroup 和 CSSTransition 包裹 Switch,实现转场动画 166 | return ( 167 | React.cloneElement(child, { classNames })} 170 | > 171 | 172 | {children} 173 | 174 | 175 | ); 176 | } 177 | } 178 | // 通过 withRouter 包裹,可以从props中获取location,history等对象。 179 | export default withRouter(AnimatedSwitch); 180 | ``` 181 | 182 | 至此,路由转场动画算是基本实现了。 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.13.1", 7 | "react-dom": "^16.13.1", 8 | "react-router-dom": "^5.2.0", 9 | "react-router-virgo": "^0.1.10", 10 | "react-transition-group": "^4.4.1" 11 | }, 12 | "devDependencies": { 13 | "react-scripts": "3.4.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "start-doc": "npx docsify serve ./docs" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | React App 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackXJR/react-router-virgo/95a9b62a1d8de9dd67187f40a37a5c4829442854/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // import { Router } from 'react-router-virgo' 3 | import Router from './router/Router' 4 | import { RouterConfig } from './RouterConfig' 5 | import './index.css' 6 | 7 | function App() { 8 | return 9 | } 10 | 11 | export default App 12 | -------------------------------------------------------------------------------- /src/RouterConfig.js: -------------------------------------------------------------------------------- 1 | // import { SceneConfig } from 'react-router-virgo' 2 | import { SceneConfig } from './router/Router' 3 | import { Home, Detail, PopPage, ReplacePage, HybridPage } from './pages/index' 4 | 5 | // 无转场动画(新页面直接覆盖当前页面) 6 | // 属性 useAnimatedSwitch 设置为false时,sceneConfig 可不配置 7 | const NoAnimateConfig = [ 8 | { 9 | path: '/detail/:type', 10 | component: Detail, 11 | sceneConfig: SceneConfig.NONE, 12 | }, 13 | { 14 | path: '/pop-page', 15 | component: PopPage, 16 | sceneConfig: SceneConfig.NONE, 17 | }, 18 | ] 19 | 20 | // Push 转场动画(打开时,从左往右覆盖;关闭时,从右往左收回) 21 | // 属性 useAnimatedSwitch 未设置,或设置为true时,sceneConfig 可不配置 22 | const PushConfig = [ 23 | { 24 | path: '/push/detail/:type', 25 | component: Detail, 26 | sceneConfig: SceneConfig.PUSH, 27 | }, 28 | { 29 | path: '/push/pop-page', 30 | component: PopPage, 31 | sceneConfig: SceneConfig.PUSH, 32 | }, 33 | { 34 | path: '/push/replace-page', 35 | component: ReplacePage, 36 | sceneConfig: SceneConfig.PUSH, 37 | }, 38 | { 39 | path: '/push/hybrid-page', 40 | component: HybridPage, 41 | sceneConfig: SceneConfig.PUSH, 42 | }, 43 | ] 44 | 45 | // Present 转场动画(打开时,从下往上弹起;关闭时,从上往下收起) 46 | // sceneConfig 必须设置 47 | const PresentConfig = [ 48 | { 49 | path: '/present/detail/:type', 50 | component: Detail, 51 | sceneConfig: SceneConfig.PRESENT, 52 | }, 53 | { 54 | path: '/present/pop-page', 55 | component: PopPage, 56 | sceneConfig: SceneConfig.PRESENT, 57 | }, 58 | ] 59 | 60 | /** 61 | * 路由配置文件(NoAnimateConfig、PushConfig、PresentConfig) 62 | * 63 | * path 路由路径,路径可以带参数,在:后的即为参数,如 '/detail/:id', 参数为id。必填 64 | * component 路径对应的组件页面。必填 65 | * sceneConfig 转场动画配置,支持无动画、push动画、present动画三种场景。选填 66 | * exact 是否使用精准匹配,默认为true。选填 67 | */ 68 | export const RouterConfig = [{ path: '/', component: Home }] 69 | .concat(NoAnimateConfig) 70 | .concat(PushConfig) 71 | .concat(PresentConfig) 72 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import * as serviceWorker from './serviceWorker' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | 8 | serviceWorker.unregister() 9 | -------------------------------------------------------------------------------- /src/pages/Detail.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Title } from './components' 3 | 4 | const PageType = { 5 | PUSH: 'PUSH', 6 | PRESENT: 'PRESENT', 7 | NONE: 'NONE', 8 | } 9 | 10 | class Detail extends React.Component { 11 | constructor(props) { 12 | super(props) 13 | 14 | let curPath = 15 | (this.props.history.location && this.props.history.location.pathname) || 16 | '' 17 | // 默认是无动画页面 18 | this.pageType = PageType.NONE 19 | if (curPath.indexOf('/push/') === 0) { 20 | // Push 动画页面 21 | this.pageType = PageType.PUSH 22 | } else if (curPath.indexOf('/present/') === 0) { 23 | // Present 动画页面 24 | this.pageType = PageType.PRESENT 25 | } 26 | } 27 | 28 | goBack() { 29 | this.props.history.goBack() 30 | } 31 | 32 | // 根据不同页面类型展示标题 33 | getTitle() { 34 | if (this.pageType === PageType.PUSH) { 35 | return '测试Push动画场景' 36 | } else if (this.pageType === PageType.PRESENT) { 37 | return '测试Present动画场景' 38 | } else { 39 | return '测试无动画场景' 40 | } 41 | } 42 | 43 | // 打开新页面 44 | pushPage(path) { 45 | if (this.pageType === PageType.PUSH) { 46 | // Push 动画 47 | this.props.history.push('/push' + path) 48 | } else if (this.pageType === PageType.PRESENT) { 49 | // Present 动画 50 | this.props.history.push('/present' + path) 51 | } else { 52 | // 无动画 53 | this.props.history.push(path) 54 | } 55 | } 56 | 57 | render() { 58 | return ( 59 |
60 | 61 | <Button title="回到Home首页" onClick={() => this.goBack()} /> 62 | <Button 63 | title="props.history.push" 64 | onClick={() => this.pushPage('/pop-page')} 65 | /> 66 | <Button 67 | title="window.history.pushState" 68 | onClick={() => window.history.pushState({}, '', '/#/pop-page')} 69 | /> 70 | </div> 71 | ) 72 | } 73 | } 74 | 75 | export default Detail 76 | 77 | const styles = { 78 | container: { 79 | display: 'flex', 80 | backgroundColor: 'rgb(97,218,251)', 81 | height: '100vh', 82 | width: '100vw', 83 | overflow: 'hidden', 84 | flexDirection: 'column', 85 | }, 86 | button: { marginTop: 10 }, 87 | } 88 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Title } from './components' 3 | 4 | class Home extends React.Component { 5 | gotoPage(pathname, query) { 6 | this.props.history.push({ pathname, query }) 7 | } 8 | 9 | render() { 10 | return ( 11 | <div style={styles.container}> 12 | <Title title="Home首页" /> 13 | <Button 14 | title="无动画" 15 | onClick={() => this.gotoPage('/detail/无动画')} 16 | /> 17 | <Button 18 | title="Push动画" 19 | onClick={() => { 20 | this.gotoPage('/push/detail/Push动画') 21 | }} 22 | /> 23 | <Button 24 | title="Present动画" 25 | onClick={() => { 26 | this.gotoPage('/present/detail/Present动画') 27 | }} 28 | /> 29 | <Button 30 | title="混合场景" 31 | onClick={() => this.gotoPage('/push/hybrid-page')} 32 | /> 33 | </div> 34 | ) 35 | } 36 | } 37 | 38 | export default Home 39 | 40 | const styles = { 41 | container: { 42 | display: 'flex', 43 | height: '100vh', 44 | width: '100vw', 45 | overflow: 'hidden', 46 | flexDirection: 'column', 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/HybridPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Title } from './components' 3 | 4 | class HybridPage extends React.Component { 5 | goBack() { 6 | this.props.history.goBack() 7 | } 8 | /** 9 | * 可以给某一个页面同时配置多种动画,以 PopPage 为例 10 | * 11 | * 1.打开配置路径 "/pop-page" 时,无动画 12 | * 2.打开配置路径 "/push/pop-page" 时,Push动画 13 | * 3.打开配置路径 "/present/pop-page" 时,Present动画 14 | */ 15 | render() { 16 | return ( 17 | <div style={styles.container}> 18 | <Title title="混合场景页面" /> 19 | <Button title="回到Home首页" onClick={() => this.goBack()} /> 20 | <Button 21 | title="无动画打开页面" 22 | onClick={() => this.props.history.push('/pop-page')} 23 | /> 24 | <Button 25 | title="Push动画打开页面" 26 | onClick={() => this.props.history.push('/push/pop-page')} 27 | /> 28 | <Button 29 | title="Present动画打开页面" 30 | onClick={() => this.props.history.push('/present/pop-page')} 31 | /> 32 | </div> 33 | ) 34 | } 35 | } 36 | 37 | export default HybridPage 38 | 39 | const styles = { 40 | container: { 41 | display: 'flex', 42 | backgroundColor: 'orange', 43 | height: '100vh', 44 | width: '100vw', 45 | overflow: 'hidden', 46 | flexDirection: 'column', 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/PopPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Title } from './components' 3 | 4 | class PopPage extends React.Component { 5 | pushPage(pathname, query) { 6 | this.props.history.push({ pathname, query }) 7 | } 8 | 9 | replacePage(pathname, query) { 10 | this.props.history.replace({ pathname, query }) 11 | } 12 | 13 | goBack() { 14 | this.props.history.goBack() 15 | } 16 | 17 | go(num) { 18 | this.props.history.go(num) 19 | } 20 | 21 | href(pathname) { 22 | window.location.href = pathname 23 | } 24 | 25 | render() { 26 | return ( 27 | <div style={styles.container}> 28 | <Title title="测试ACTION-POP页面" /> 29 | <Button title="props.history.goBack" onClick={() => this.goBack()} /> 30 | <Button title="props.history.go(-1)" onClick={() => this.go(-1)} /> 31 | <Button title="props.history.go(-2)" onClick={() => this.go(-2)} /> 32 | <Button title="window.location.href" onClick={() => this.href('/')} /> 33 | </div> 34 | ) 35 | } 36 | } 37 | 38 | export default PopPage 39 | 40 | const styles = { 41 | container: { 42 | display: 'flex', 43 | backgroundColor: 'yellow', 44 | height: '100vh', 45 | width: '100vw', 46 | overflow: 'hidden', 47 | flexDirection: 'column', 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/ReplacePage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Title, Toast } from './components'; 3 | 4 | export default class ReplacePage extends React.Component { 5 | goBack() { 6 | this.props.history.goBack(); 7 | } 8 | 9 | render() { 10 | return ( 11 | <div style={styles.container}> 12 | <Title title="REPLACE测试页面" /> 13 | <Button title="回到Home首页" onClick={() => this.goBack()} /> 14 | <Button 15 | title="props.history.replace" 16 | onClick={() => this.props.history.replace('/push/pop-page')} 17 | /> 18 | <Button 19 | title="window.location.replace" 20 | onClick={() => window.location.replace('/push/pop-page')} 21 | /> 22 | <Button 23 | title="window.history.replaceState" 24 | onClick={() => 25 | window.history.replaceState({}, '', '/#/push/pop-page') 26 | } 27 | /> 28 | <Toast title={'页面正在加载中...'} /> 29 | </div> 30 | ); 31 | } 32 | } 33 | 34 | const styles = { 35 | container: { 36 | display: 'flex', 37 | backgroundColor: 'rgb(97,218,251)', 38 | height: '100vh', 39 | width: '100vw', 40 | overflow: 'hidden', 41 | flexDirection: 'column', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/pages/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Button extends React.Component { 4 | render() { 5 | const { style = {}, title, onClick } = this.props 6 | let btnStyle = Object.assign({}, styles.button, style) 7 | return ( 8 | <div 9 | style={btnStyle} 10 | onClick={() => { 11 | onClick && onClick() 12 | }} 13 | > 14 | {title} 15 | </div> 16 | ) 17 | } 18 | } 19 | 20 | const styles = { 21 | button: { 22 | margin: 15, 23 | backgroundColor: 'rgb(20,120,227)', 24 | padding: 15, 25 | color: 'white', 26 | borderRadius: 5, 27 | fontWeight: '500', 28 | fontSize: 18, 29 | textAlign: 'center', 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/components/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class Title extends React.Component { 4 | render() { 5 | const { title, style } = this.props 6 | const titleStyle = Object.assign({}, styles.title, style) 7 | return <div style={titleStyle}>{title}</div> 8 | } 9 | } 10 | 11 | const styles = { 12 | title: { 13 | fontSize: 25, 14 | fontWeight: 'bold', 15 | padding: 20, 16 | textAlign: 'center', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/components/Toast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Toast extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.state = { 8 | showToast: true, 9 | }; 10 | } 11 | 12 | componentDidMount() { 13 | setTimeout(() => { 14 | this.setState({ showToast: false }); 15 | }, 1000); 16 | } 17 | 18 | render() { 19 | const { title = '' } = this.props; 20 | if (!this.state.showToast) return null; 21 | return ( 22 | <div style={styles.container}> 23 | <div style={styles.title}>{title}</div> 24 | </div> 25 | ); 26 | } 27 | } 28 | 29 | const styles = { 30 | container: { 31 | position: 'absolute', 32 | width: '100vw', 33 | height: '100vh', 34 | display: 'flex', 35 | alignItems: 'center', 36 | justifyContent: 'center', 37 | }, 38 | title: { 39 | display: 'flex', 40 | color: '#FFFFFF', 41 | fontSize: 14, 42 | maxWidth: '80vw', 43 | backgroundColor: '#414141', 44 | borderRadius: 4, 45 | padding: 10, 46 | marginTop: -60, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/pages/components/index.js: -------------------------------------------------------------------------------- 1 | import Button from './Button' 2 | import Title from './Title' 3 | import Toast from './Toast' 4 | 5 | export { Button, Title, Toast } 6 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | import Detail from './Detail'; 3 | import PopPage from './PopPage'; 4 | import ReplacePage from './ReplacePage'; 5 | import HybridPage from './HybridPage'; 6 | 7 | export { Home, Detail, ReplacePage, HybridPage, PopPage }; 8 | -------------------------------------------------------------------------------- /src/router/AnimatedSwitch.css: -------------------------------------------------------------------------------- 1 | .router-wrapper { 2 | overflow: hidden; 3 | } 4 | 5 | /** 6 | * No animation transition 7 | * 无动画转场切换 8 | */ 9 | .forward-no-animation-enter { 10 | opacity: 1; 11 | z-index: 2; 12 | } 13 | 14 | .forward-no-animation-enter-active { 15 | opacity: 1; 16 | z-index: 2; 17 | } 18 | 19 | .forward-no-animation-enter-done { 20 | z-index: 2; 21 | } 22 | 23 | .forward-no-animation-exit { 24 | opacity: 0; 25 | z-index: 1; 26 | } 27 | 28 | .forward-no-animation-exit-active { 29 | opacity: 0; 30 | z-index: 1; 31 | } 32 | 33 | .forward-no-animation-exit-done { 34 | z-index: 1; 35 | } 36 | 37 | .back-no-animation-enter { 38 | opacity: 1; 39 | z-index: 2; 40 | } 41 | 42 | .back-no-animation-enter-active { 43 | opacity: 1; 44 | z-index: 2; 45 | } 46 | 47 | .back-no-animation-exit { 48 | opacity: 0; 49 | z-index: 1; 50 | } 51 | 52 | .back-no-animation-exit-active { 53 | opacity: 0; 54 | z-index: 1; 55 | } 56 | 57 | /** 58 | * Fade in right, fade out right 59 | * 右侧淡入,右侧淡出 60 | */ 61 | .forward-from-right-enter { 62 | z-index: 2; 63 | transform: translate3d(100%, 0, 0); 64 | } 65 | 66 | .forward-from-right-enter-active { 67 | z-index: 2; 68 | transform: translate3d(0, 0, 0); 69 | transition: all 300ms; 70 | } 71 | 72 | .forward-from-right-enter-done { 73 | z-index: 2; 74 | } 75 | 76 | .forward-from-right-exit { 77 | z-index: 1; 78 | transform: translate3d(0, 0, 0); 79 | } 80 | 81 | .forward-from-right-exit-active { 82 | z-index: 1; 83 | transform: translate3d(-30%, 0, 0); 84 | transition: all 300ms; 85 | } 86 | 87 | .forward-from-right-exit-done { 88 | z-index: 1; 89 | } 90 | 91 | .back-to-right-enter { 92 | z-index: 1; 93 | transform: translate3d(-30%, 0, 0); 94 | } 95 | 96 | .back-to-right-enter-active { 97 | z-index: 1; 98 | transform: translate3d(0%, 0, 0); 99 | transition: all 300ms; 100 | } 101 | 102 | .back-to-right-enter-done { 103 | z-index: 1; 104 | } 105 | 106 | .back-to-right-exit { 107 | z-index: 2; 108 | transform: translate3d(0%, 0, 0); 109 | } 110 | 111 | .back-to-right-exit-active { 112 | z-index: 2; 113 | transform: translate3d(100%, 0, 0); 114 | transition: all 300ms; 115 | } 116 | 117 | .back-to-right-exit-done { 118 | z-index: 2; 119 | } 120 | 121 | /** 122 | * Fade in below, fade out below 123 | * 下方淡入,下方淡出 124 | */ 125 | .forward-from-bottom-enter { 126 | z-index: 2; 127 | opacity: 0; 128 | transform: translateY(100%); 129 | } 130 | 131 | .forward-from-bottom-enter-active { 132 | z-index: 2; 133 | opacity: 1; 134 | transform: translateY(0); 135 | transition: all 300ms; 136 | } 137 | 138 | .forward-from-bottom-exit { 139 | z-index: 1; 140 | opacity: 1; 141 | } 142 | 143 | .forward-from-bottom-exit-active { 144 | z-index: 1; 145 | opacity: 0.3; 146 | transition: all 300ms; 147 | } 148 | 149 | .back-to-bottom-enter { 150 | z-index: 1; 151 | opacity: 0.3; 152 | } 153 | 154 | .back-to-bottom-enter-active { 155 | z-index: 1; 156 | opacity: 1; 157 | transition: all 300ms; 158 | } 159 | 160 | .back-to-bottom-exit { 161 | z-index: 2; 162 | opacity: 1; 163 | transform: translateY(0); 164 | } 165 | 166 | .back-to-bottom-exit-active { 167 | z-index: 2; 168 | opacity: 0; 169 | transform: translateY(100%); 170 | transition: all 300ms; 171 | } 172 | -------------------------------------------------------------------------------- /src/router/AnimatedSwitch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, withRouter } from 'react-router-dom'; 3 | import { CSSTransition, TransitionGroup } from 'react-transition-group'; 4 | import './AnimatedSwitch.css'; 5 | 6 | const PropsTypes = require('prop-types'); 7 | 8 | const DEFAULT_SCENE_CONFIG = { 9 | enter: 'from-right', 10 | exit: 'to-right', 11 | }; 12 | 13 | // Get the configuration of transition animation 14 | // 获取转场动画配置 15 | const getSceneConfig = (location, routerConfig = []) => { 16 | const matchedRoute = routerConfig.find((config) => { 17 | let paramIndex = config.path.indexOf('/:'); 18 | if (paramIndex === -1) { 19 | return config.path === location.pathname; 20 | } else { 21 | let configPath = config.path.slice(paramIndex).split('/:'), 22 | locationPath = location.pathname.slice(paramIndex).split('/'); 23 | return ( 24 | location.pathname.indexOf(config.path.slice(0, paramIndex)) === 0 && 25 | configPath.length === locationPath.length 26 | ); 27 | } 28 | }); 29 | return (matchedRoute && matchedRoute.sceneConfig) || DEFAULT_SCENE_CONFIG; 30 | }; 31 | 32 | let oldLocation = null; 33 | class AnimatedSwitch extends React.Component { 34 | static propsTypes = { 35 | routerConfig: PropsTypes.array.isRequired, 36 | }; 37 | 38 | render() { 39 | const { location, history, children, routerConfig } = this.props; 40 | let classNames = ''; 41 | if (history.action === 'PUSH') { 42 | // When opening the page, the animation configuration is obtained from the configuration. By default, the push animation opened from right to left is used 43 | // 打开页面时的动画,从配置项中获取动画配置,默认使用从右至左打开的PUSH动画 44 | classNames = 'forward-' + getSceneConfig(location, routerConfig).enter; 45 | } else if (history.action === 'POP' && oldLocation) { 46 | // The animation when closing the page, get the animation configuration from the configuration, and use the left to right closed push animation by default 47 | // 关闭页面时的动画,从配置项中获取动画配置,默认使用从左至右关闭的push动画 48 | classNames = 'back-' + getSceneConfig(oldLocation, routerConfig).exit; 49 | } 50 | 51 | // Update old location 52 | // 更新旧location 53 | oldLocation = location; 54 | 55 | // Set transition animation duration 56 | // 设置转场动画时长 57 | let useAnimation = classNames.indexOf('no-animation') === -1; 58 | return ( 59 | <TransitionGroup 60 | className={'router-wrapper'} 61 | childFactory={(child) => React.cloneElement(child, { classNames })} 62 | > 63 | <CSSTransition timeout={useAnimation ? 300 : 0} key={location.pathname}> 64 | <Switch location={location}>{children}</Switch> 65 | </CSSTransition> 66 | </TransitionGroup> 67 | ); 68 | } 69 | } 70 | 71 | export default withRouter(AnimatedSwitch); 72 | -------------------------------------------------------------------------------- /src/router/Router.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HashRouter, BrowserRouter, Route, Switch } from 'react-router-dom' 3 | import AnimatedSwitch from './AnimatedSwitch' 4 | import StackSwitch from './StackSwitch' 5 | import { WrappedAnimatedRoute } from './WrapedComp' 6 | 7 | const PropTypes = require('prop-types') 8 | 9 | // Transition animation type configuration 10 | // 转场动画类型配置 11 | export const SceneConfig = { 12 | // No animation configuration 13 | // 无动画配置 14 | NONE: { 15 | enter: 'no-animation', 16 | exit: 'no-animation', 17 | }, 18 | 19 | // Push animation configuration 20 | // Push动画配置 21 | PUSH: { 22 | enter: 'from-right', 23 | exit: 'to-right', 24 | }, 25 | 26 | // Present animation configuration 27 | // Present动画配置 28 | PRESENT: { 29 | enter: 'from-bottom', 30 | exit: 'to-bottom', 31 | }, 32 | } 33 | 34 | class Router extends React.Component { 35 | static propTypes = { 36 | // By default, HashRouter is used. If 'usebrowserrouter' is set externally, BrowserRouter is used 37 | // 默认使用 HashRouter, 如果外部设置useBrowserRouter,则使用 BrowserRouter 38 | useBrowserRouter: PropTypes.bool, 39 | // Transition transition animation is used by default. If the external setting is false, no animation is used 40 | // 默认使用转场过渡动画,如果外部设置为false,则不使用动画 41 | useAnimatedSwitch: PropTypes.bool, 42 | // Similar to the native route effect, When you open or close a page, the previous page will not be destroyed 43 | // 完全类似native路由效果,打开和关闭页面时,均不销毁上一级页面 44 | useStackSwitch: PropTypes.bool, 45 | // Routing configuration table 46 | // 路由配置表 47 | routerConfig: PropTypes.array.isRequired, 48 | } 49 | 50 | render() { 51 | const { 52 | useBrowserRouter = false, 53 | useAnimatedSwitch = true, 54 | useStackSwitch = false, 55 | routerConfig = [], 56 | } = this.props 57 | 58 | // By default, HashRouter is used. If 'useBrowserRouter' is set externally, BrowserRouter is used 59 | // 默认使用 HashRouter, 如果外部设置useBrowserRouter,则使用 BrowserRouter 60 | const CommonRouter = useBrowserRouter ? BrowserRouter : HashRouter 61 | 62 | /** 63 | * If `useStackSwitch` is set, the native routing effect of the page is not destroyed 64 | * If `useStackSwitch` is not set and `useAnimatedSwitch` is set to false, the non animated routing effect is used 65 | * If `useStackSwitch` and `useAnimatedSwitch` are not set, the routing effect of the destroy page is used by default 66 | 67 | * 如果设置了useStackSwitch,则使用不销毁页面的native路由效果 68 | * 如果未设置useStackSwitch,且useAnimatedSwitch设置为false,则使用无动画的路由效果 69 | * 如果未设置useStackSwitch和useAnimatedSwitch,则默认使用销毁页面的路由效果 70 | * */ 71 | const CommonSwitch = useStackSwitch 72 | ? StackSwitch 73 | : useAnimatedSwitch 74 | ? AnimatedSwitch 75 | : Switch 76 | 77 | return ( 78 | <CommonRouter> 79 | <CommonSwitch {...this.props}> 80 | {useStackSwitch 81 | ? null 82 | : routerConfig.map((config, index) => { 83 | return ( 84 | <Route 85 | exact 86 | key={index} 87 | {...config} 88 | component={WrappedAnimatedRoute(config.component)} 89 | /> 90 | ) 91 | })} 92 | </CommonSwitch> 93 | </CommonRouter> 94 | ) 95 | } 96 | } 97 | 98 | export default Router 99 | -------------------------------------------------------------------------------- /src/router/StackSwitch.css: -------------------------------------------------------------------------------- 1 | .forward-no-animation-hide { 2 | display: none; 3 | transform: translate(0%, 0%) translateZ(0px); 4 | } 5 | 6 | .forward-no-animation { 7 | display: block; 8 | transform: translate(0%, 0%) translateZ(0px); 9 | } 10 | 11 | .forward-no-animation-active { 12 | transform: translate(0%, 0%) translateZ(0px); 13 | } 14 | 15 | .back-no-animation-hide { 16 | display: none; 17 | transform: translate(0%, 0%) translateZ(0px); 18 | } 19 | 20 | .back-no-animation { 21 | display: block; 22 | transform: translate(0%, 0%) translateZ(0px); 23 | } 24 | 25 | .back-no-animation-active { 26 | transform: translate(100%, 0%) translateZ(0px); 27 | } 28 | 29 | /** 30 | * 无动画切换 31 | */ 32 | 33 | /** 34 | * 右侧淡入,右侧淡出 35 | */ 36 | .forward-from-right-hide { 37 | display: none; 38 | transform: translate(-50%, 0%) translateZ(0px); 39 | } 40 | 41 | @keyframes push-animate { 42 | from { 43 | transform: translate(0%, 0%) translateZ(0px); 44 | } 45 | to { 46 | transform: translate(-50%, 0%) translateZ(0px); 47 | } 48 | } 49 | 50 | .forward-from-right { 51 | display: block; 52 | transform: translate(-50%, 0%) translateZ(0px); 53 | animation: push-animate 0.4s; 54 | } 55 | 56 | @keyframes push-active-animate { 57 | from { 58 | transform: translate(100%, 0%) translateZ(0px); 59 | } 60 | to { 61 | transform: translate(0%, 0%) translateZ(0px); 62 | } 63 | } 64 | 65 | .forward-from-right-active { 66 | transform: translate(0%, 0%) translateZ(0px); 67 | animation: push-active-animate 0.4s; 68 | } 69 | 70 | .back-to-right-hide { 71 | display: none; 72 | transform: translate(-50%, 0%) translateZ(0px); 73 | } 74 | 75 | @keyframes pop-animate { 76 | from { 77 | transform: translate(-50%, 0%) translateZ(0px); 78 | } 79 | to { 80 | transform: translate(0%, 0%) translateZ(0px); 81 | } 82 | } 83 | 84 | .back-to-right { 85 | display: block; 86 | transform: translate(0%, 0%) translateZ(0px); 87 | animation: pop-animate 0.4s; 88 | } 89 | 90 | @keyframes pop-active-animate { 91 | from { 92 | transform: translate(0%, 0%) translateZ(0px); 93 | } 94 | to { 95 | transform: translate(100%, 0%) translateZ(0px); 96 | } 97 | } 98 | .back-to-right-active { 99 | transform: translate(100%, 0%) translateZ(0px); 100 | animation: pop-active-animate 0.4s; 101 | } 102 | 103 | /** 104 | * 下方淡入,下方淡出 105 | */ 106 | 107 | .forward-from-bottom-hide { 108 | display: none; 109 | transform: translate(0%, -50%) translateZ(0px); 110 | } 111 | 112 | .forward-from-bottom { 113 | display: block; 114 | transform: translate(0%, 0%) translateZ(0px); 115 | } 116 | 117 | @keyframes push-bottom-active-animate { 118 | from { 119 | transform: translate(0%, 100%) translateZ(0px); 120 | } 121 | to { 122 | transform: translate(0%, 0%) translateZ(0px); 123 | } 124 | } 125 | 126 | .forward-from-bottom-active { 127 | transform: translate(0%, 0%) translateZ(0px); 128 | animation: push-bottom-active-animate 0.4s; 129 | } 130 | 131 | .back-to-bottom-hide { 132 | display: none; 133 | transform: translate(0%, -50%) translateZ(0px); 134 | } 135 | 136 | .back-to-bottom { 137 | display: block; 138 | transform: translate(0%, 0%) translateZ(0px); 139 | } 140 | 141 | @keyframes pop-bottom-active-animate { 142 | from { 143 | transform: translate(0%, 0%) translateZ(0px); 144 | } 145 | to { 146 | transform: translate(0%, 100%) translateZ(0px); 147 | } 148 | } 149 | .back-to-bottom-active { 150 | transform: translate(0%, 100%) translateZ(0px); 151 | animation: pop-bottom-active-animate 0.4s; 152 | } 153 | -------------------------------------------------------------------------------- /src/router/StackSwitch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { WrappedStackRoute } from './WrapedComp'; 4 | import './StackSwitch.css'; 5 | 6 | const PropsTypes = require('prop-types'); 7 | 8 | const DEFAULT_SCENE_CONFIG = { 9 | enter: 'from-right', 10 | exit: 'to-right', 11 | }; 12 | 13 | // 获取转场动画配置项 14 | const getSceneConfig = (location, routerConfig = []) => { 15 | const matchedRoute = routerConfig.find((config) => { 16 | let paramIndex = config.path.indexOf('/:'); 17 | if (paramIndex === -1) { 18 | return config.path === location.pathname; 19 | } else { 20 | let configPath = config.path.slice(paramIndex).split('/:'), 21 | locationPath = location.pathname.slice(paramIndex).split('/'); 22 | return ( 23 | location.pathname.indexOf(config.path.slice(0, paramIndex)) === 0 && 24 | configPath.length === locationPath.length 25 | ); 26 | } 27 | }); 28 | return (matchedRoute && matchedRoute.sceneConfig) || DEFAULT_SCENE_CONFIG; 29 | }; 30 | 31 | // 获取location匹配的组件 32 | const getMatchedComponent = (location, routerConfig = []) => { 33 | const matchedRoute = routerConfig.find((config) => { 34 | let paramIndex = config.path.indexOf('/:'); 35 | if (paramIndex === -1) { 36 | return config.path === location.pathname; 37 | } else { 38 | let configPath = config.path.slice(paramIndex).split('/:'), 39 | locationPath = location.pathname.slice(paramIndex).split('/'); 40 | return ( 41 | location.pathname.indexOf(config.path.slice(0, paramIndex)) === 0 && 42 | configPath.length === locationPath.length 43 | ); 44 | } 45 | }); 46 | return matchedRoute && matchedRoute.component; 47 | }; 48 | 49 | let routerPages = []; 50 | let oldRouterPages = []; 51 | let Pages = []; 52 | let oldLocation = null; 53 | 54 | class StackSwitch extends React.Component { 55 | static propsTypes = { 56 | routerConfig: PropsTypes.array.isRequired, //路由配置信息 57 | }; 58 | 59 | constructor(props) { 60 | super(props); 61 | 62 | this.state = { 63 | hidePage: false, 64 | classNames: '', 65 | Pages: [], 66 | isClosePage: false, 67 | }; 68 | 69 | this._isMounted = false; 70 | 71 | const { location, history } = this.props; 72 | let _this = this; 73 | _this._pendingLocation = location; 74 | 75 | history.listen(function (location) { 76 | if (_this._isMounted) { 77 | _this.refreshPage(location); 78 | } else { 79 | _this._pendingLocation = location; 80 | } 81 | }); 82 | } 83 | 84 | componentDidMount() { 85 | let _this = this; 86 | _this._isMounted = true; 87 | if (_this._pendingLocation) { 88 | _this.refreshPage(_this._pendingLocation); 89 | this._pendingLocation = null; 90 | } 91 | } 92 | 93 | // 监听路由的history的变化,刷新页面 94 | refreshPage(location) { 95 | const { history, routerConfig } = this.props; 96 | 97 | let classNames = ''; 98 | let isClosePage = false; 99 | let matchedComponent = getMatchedComponent(location, routerConfig); 100 | if (history.action === 'PUSH') { 101 | // 打开页面的动画,从配置项中获取,默认从右至左 102 | classNames = 'forward-' + getSceneConfig(location, routerConfig).enter; 103 | matchedComponent && routerPages.push(matchedComponent); 104 | Pages = routerPages; 105 | isClosePage = false; 106 | } else if (history.action === 'POP' && oldLocation) { 107 | // 关闭页面的动画,从配置项中获取,默认从左至右 108 | classNames = 'back-' + getSceneConfig(oldLocation, routerConfig).exit; 109 | routerPages.pop(); 110 | Pages = oldRouterPages; 111 | isClosePage = true; 112 | } else { 113 | matchedComponent && routerPages.push(matchedComponent); 114 | Pages = routerPages; 115 | isClosePage = false; 116 | } 117 | 118 | // 更新旧location 119 | oldLocation = location; 120 | oldRouterPages = [].concat(routerPages); 121 | 122 | this.setState({ 123 | classNames, 124 | Pages, 125 | routerPages, 126 | isClosePage, 127 | }); 128 | } 129 | 130 | render() { 131 | const { classNames, Pages } = this.state; 132 | if (!Pages.length) return null; 133 | return ( 134 | <React.Fragment> 135 | {Pages.map((Page, index) => { 136 | if (Page) { 137 | const className = 138 | index === Pages.length - 1 139 | ? classNames + '-active' 140 | : index === Pages.length - 2 141 | ? classNames 142 | : classNames + '-hide'; 143 | return React.createElement( 144 | WrappedStackRoute(Page, className, index), 145 | Object.assign({}, { key: index }, { ...this.props }), 146 | null 147 | ); 148 | } 149 | return null; 150 | })} 151 | </React.Fragment> 152 | ); 153 | } 154 | } 155 | 156 | export default withRouter(StackSwitch); 157 | -------------------------------------------------------------------------------- /src/router/WrapedComp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './StackSwitch.css'; 3 | 4 | function WrappedAnimatedRoute(WrappedComponent) { 5 | return class WrappedAnimatedRoute extends React.Component { 6 | render() { 7 | return ( 8 | <div style={styles.container}> 9 | <WrappedComponent {...this.props} /> 10 | </div> 11 | ); 12 | } 13 | }; 14 | } 15 | 16 | function WrappedStackRoute(WrappedComponent, className, index) { 17 | return class WrappedStackRoute extends React.Component { 18 | render() { 19 | return ( 20 | <div 21 | className={className} 22 | style={Object.assign({}, styles.container, { 23 | zIndex: index + 1, 24 | })} 25 | onAnimationEnd={() => {}} 26 | > 27 | <WrappedComponent {...this.props} /> 28 | </div> 29 | ); 30 | } 31 | }; 32 | } 33 | 34 | export { WrappedAnimatedRoute, WrappedStackRoute }; 35 | 36 | const styles = { 37 | container: { 38 | position: 'absolute', 39 | left: 0, 40 | top: 0, 41 | width: '100vw', 42 | height: '100vh', 43 | opacity: 1, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------