测试react-router v4以及redux并实现store持久化

胡家华 7815bd316d 增加异步action扩展参数 лет назад: 5
config 204764af6e '配置less,redux' лет назад: 6
public 77039b48fa '初始仓库' лет назад: 6
scripts 204764af6e '配置less,redux' лет назад: 6
src 7815bd316d 增加异步action扩展参数 лет назад: 5
.gitignore 77039b48fa '初始仓库' лет назад: 6
README.md f5e582cc1a 'redux持久化' лет назад: 6
images.d.ts 77039b48fa '初始仓库' лет назад: 6
index.d.ts 9e401f47f7 重新修改redux写法 лет назад: 5
package.json 9e401f47f7 重新修改redux写法 лет назад: 5
tsconfig.json 77039b48fa '初始仓库' лет назад: 6
tsconfig.prod.json 77039b48fa '初始仓库' лет назад: 6
tsconfig.test.json 77039b48fa '初始仓库' лет назад: 6
tslint.json 9e401f47f7 重新修改redux写法 лет назад: 5
yarn.lock 9e401f47f7 重新修改redux写法 лет назад: 5

README.md

下载依赖文件

yarn add react-router-dom --save

运行

yarn start

打包

yarn build

一些笔记

基本组件

React Router中有三种类型的组件:路由器组件,路由匹配组件和导航组件

路由器组件

对于Web项目,react-router-dom提供<BrowserRouter><HashRouter>路由器。

两者区别:

  • 地址栏: BrowserRouter地址栏显示方式:http://localhost:8080/abc/def,HashRouter地址栏显示方式:http://localhost:8080/#/abc/def
  • <BrowserRouter>需要server端的配合,每一次跳转页面都向服务器发送一次请求,服务器需要进行处理
  • <HashRouter>不会向服务器发起请求,会找到相对于的模块然后进行渲染
  • 个人觉得如果是静态页面,使用<HashRouter>就好

<BrowserRouter>使用: 需要在服务器接受路由,然后返回文件(以node后台为例)

app.get('*',(request,response)=>{

  response.sendFile(path.resolve(__dirname,'../index.html'))

})

<HashRouter>使用:

import { HashRouter as Router, Route } from 'react-router-dom'
import Home from './views/home'

<Router>
  <Route path='/' component={Home}/>
</Router>

路由匹配组件<Route><Switch>

<Switch>可对子元素<Route>进行分组,并仅渲染与当前位置匹配的第一个元素

<Route>如果匹配到对应的path将会渲染对应组件,如果没有匹配到,则返回null,如果不设置path属性,将会一直渲染 使用:

import About from './views/about'
import Home from './views/home'
import Services from './views/services'

public render():JSX.Element {
  return (
    <Router>
      <Switch>
        {/* 当路由为 " / " 时 */}
        {/* Home组件将会被渲染 */}
        <Route path='/' component={Home}/>
        {/* About不会渲染,返回null */}
        <Route path='/about' component={About}/>
        {/*上面都不匹配将会渲染Services组件 */}
        <Route component={Services}/>
      </Switch>
    </Router>
  )
}

渲染方式:componentrender,和children

使用:

import About from './views/about'
import Home from './views/home'
import Services from './views/services'

const AboutCom = (props:any) => {
  return (
    <About />
  )
}

class App extends React.Component {
  public render():JSX.Element {
    return (
      <Router>
        <Switch>
          {/* 使用component渲染 */}
          <Route path='/home' component={Home}/>
          {/* 使用render渲染 */}
          <Route path='/about' 
          render={AboutCom}/>
          {/* 使用children渲染 */}
          <Route children={AboutCom}/>
        </Switch>
      </Router>
    )
  }
}

<Link>导航

这个组件主要用于导航,它将会找到相匹配的路由,然后进行渲染,以下为测试结果:

App.tsx

<Router>
  <Switch>
    <Route path='/' exact={true} component={Home} />
    <Route path='/linkTest' component={LinkTest} />>
  </Switch>
</Router>

Home.tsx

<div>
  <p>这是首页</p>
  <Link to='/linkTest'>点击这个Link跳转到Link组件</Link>
</div>

默认页面是Home,点击Home页面中Link跳转到LinkTest页面,因为这是在App.tsx中定义的,所以将会重新打开一个新的页面

`重定向

将导航到新的位置,且新位置会覆盖历史堆栈中的位置

<Router getUserConfirmation={getConfirmation}>
  <Switch>
    <Route path='/' exact={true} component={Home}/>
    <Route path='/home' component={Home}/>
    <Route path='/about' 
    render={AboutCom}/>
    {/* 当地址为:ip::3000/#/anywhere都将跳转到Home页面 */}
    <Redirect to='/'/>
  </Switch>
</Router>

路径(path)

<Route path='/roster'/>
// 当路径名为'/'时, path不匹配
// 当路径名为'/roster'或'/roster/2'时, path匹配
// 当你只想匹配'/roster'时,你需要使用"exact"参数, 路由仅匹配'/roster'而不会匹配'/roster/2'
<Route exact={true} path='/roster'/>

嵌套路由

在以前的版本中可以使用以下:

<Route component={App}>
    <Route path="/" components={Home} />
    <Route path="users" components={Users}>
      <Route path="users/:userId" component={Profile} />
    </Route>
</Route>

但是在4.0版本之后就和之前的版本不一样了,详见;react-router官网

路由传参

  • 配置路由 <Route path='/user/:id' component={User} />
  • 跳转传参
    • 1.传入string:<Link to={'/user/2'}>跳转传递字符串</Link>
    • 2.传入Object:<Link to={{ pathname:'/user', search: '?sort=name',hash: '#the-hash',state: { fromDashboard: true }}}>跳转传递对象</Link>
  • User页面获取
    • 1.传入string时获取方式:this.props.match.params.id
    • 2.传入Object时获取方式: this.props.location

1.这里的id和路由中的/user/:id一样,如果路由为 /user/:userId,则获取 this.props.match.params.userId;2.如果参数是对象,则存在this.props.location

异步加载方案

安装Loadable

yarn add react-loadable

简单封装一次

router/index.tsx引入使用:

import * as React from 'react'
import * as Loadable from 'react-loadable'

interface IRoute {
    component: Promise<React.ComponentClass<any> | React.StatelessComponent<any> | { default: React.ComponentType<any> }>,
    path: string
}

export const route:IRoute[] = [
    {
        component: import('../views/Support'),
        path: '/support'
    },
    {
        component: import('../views/About'),
        path: '/about'
    },
    {
        component: import('../views/Services'),
        path: '/services'
    }
]

function Loading(props: any) {
    console.log(props)
    if (props.error) {
        return <div>Error!</div>
    } else if (props.timedOut) {
        return <div>Taking a long time...</div>
    } else if (props.pastDelay) {
        return <div>loading</div>
    } else {
        return <div>loading</div>
    }
}

export const loadable = (component: Promise<React.ComponentClass<any> | React.StatelessComponent<any> | { default: React.ComponentType<any> }>) => {
     return Loadable({
        delay: 200,
        loader: () => component,
        loading: Loading,
        timeout: 10000,
    })
}
调用

在定义路由的地方调用:

import * as React from 'react'
import { Route, Switch } from 'react-router-dom'
import Nav from '../../component/nav'
import { loadable, route } from '../../routes'

interface IRouteItem {
    component: Promise<React.ComponentClass<any> | React.StatelessComponent<any> | { default: React.ComponentType<any> }>,
    path: string
}

export default class Home extends React.Component {
    public render ():JSX.Element {
        return (
            <div>
                <Nav />
                <Switch>
                    {route.map((item:IRouteItem, index:number) => {
                        return (
                            <Route path={item.path} component={loadable(item.component)} key={index}/>
                        )
                    })}
                </Switch>
            </div>
        )
    }
}

Webpack配置Less css加载器

1.安装less相关依赖

这是不可逆的操作,它将create-react-app的配置反编译到当前项目,从而完全取得 webpack 文件的控制权,无法回到原来的项目版本!

自己在使用的时候,它提示让我提交了代码再使用这命令,应该是想让我保存当前版本吧。-.- yarn eject 这个命令详见:这里

2.安装less相关依赖

npm install less less-loader --save-dev

3.配置webpack.config.dev.jswebpack.config.prod.js文件(做相同配置)
  • 找到 css-loader所在位置
  • 修改csstest正则 为/\.(css|less)$/,
  • 在use中添加less-loader
{
    loader: require.resolve('less-loader'),
    options: {
      importLoaders: 1
    }
}
  • 找到exclude,在末尾添加/\.(css|less)$/

什么是Redux

  • ReduxJavaScript 状态容器,它提供对状态的统一管理
  • Redux 除了和 React 一起用外,还支持其它界面库,如果需要在React中使用,需要单独安装react-redux
  • react-redux是一个能过够让ReduxReact项目中使用的第三方库

Redux需要关注的有:Action,Reducer,Store

Action

Action主要用来管理一些分发出去的动作。 Action 本质上是一个普通对象,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作,当应用规模越来越大时,建议使用单独的模块或文件来存放 action。如下:

redux/action/index.tsx

// 为了后期项目的可维护,把所有的action都写在单独文件
const ACTION = {
    CHANGEVALUE: (obj:any) => {
        return {
            type: 'CHANGEVALUE',
            value: obj.value
        }
    },
    DECREASE: {
        type: 'DECREASE'
    },
    INCREASE: {
        type: 'INCREASE'
    }
}

export default ACTION
Reducer

Reducer主要指定了一些如何改变store中状态的方法:如下

redux/reducers/index.tsx


interface IInitState {
    count: number
}
interface IActionType {
    type: string,
    value: string
}

export default function reducers (state: IInitState , action: IActionType) {
    // 这里是根据传入动作的type,来对Redux中的state进行状态值修改 
    switch (action.type) {
        case 'INCREASE':
            return { count: ++state.count }
            break
        case 'DECREASE':
            return { count: --state.count }
            break
        case 'CHANGEVALUE':
            return {count: action.value}
            break
        default:
            return { count: state.count }
    }
}

Store

Store就是将ActionReducer联系起来,使得可以通过Action来修改State
主要API:

  • getState(),返回应用当前的 state 树。
  • dispatch(action),分发 action,这是触发 state 状态值改变
  • subscribe(listener),添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 getState() 来拿到当前 state。如果需要解绑这个变化监听器,执行 subscribe 返回的函数即可。
  • replaceReducer(nextReducer),替换 store 当前用来计算 state 的 reducer。

redux/store/index.tsx

import { createStore } from 'redux'
import reducer from '../reducers'

export const initState = {
    count: 0
}
// createStore 方法详见Redux API文档:http://www.redux.org.cn/docs/api/createStore.html
const store = createStore(reducer, initState)

export default store

现在redux的工作已经完成了。我们可以在react文件中直接引入:import store from '../redux/store/index.tsx',然后使用Storeapi进行操作,但是这样并不会触发react组件的更新,因为他们之间缺少了映射关系,这时候就需要用到react-redux这个第三方库

react-redux的使用

Reduxreact-redux是不同的库,如果我们想要在react中使用,我们需要用到react-redux这个库,主要作用是将我们项目的状态集成到Redux中去管理

需要关注的有:connect, Provider

connect
  • connect方法,顾名思义,主要将容器组件和redux进行连接 javascript // 这里是把component这个组件进行连接,连接后可在props上查看映射关系 const appContainer = connect(mapStateToProps, mapDispatchToProps)(component)
  • 他有两个参数:mapStateToPropsmapDispatchToProps,这两个参数的类型都是函数,具体作用如下:
  • mapStateToProps:将Reduxstate映射到组件的props
  • mapDispatchToProps:将Reduxaction映射到组件的props上,如下:

redux/index.tsx:

import { connect } from 'react-redux'
import appContainer from '../views/Appcontainer'
import ACTION from './action'

function mapStateToProps(state: any) {
    // 这里将会把 strore 中的initState映射到组件的props上
    return {
        count: state.count
    }
}

function mapDispatchToProps(dispatch: (obj: any) => void) {
    return {
        // CHANGEVALUE, DECREASE, INCREASE这些函数名会映射到被连接的组件props上
        CHANGEVALUE: (obj:any) => {
            // 当连接redux的组件调用props上的CHANGEVALUE方法时,此处将分发对应的Action,Action在redux/action/index.tsx中维护
            // 找到reducers中相匹配的action,然后执行定义的方法
            dispatch(ACTION.CHANGEVALUE(obj))
        },
        DECREASE: () => {
            dispatch(ACTION.DECREASE)
        },
        INCREASE: () => {
            dispatch(ACTION.INCREASE)
        }
    }
}
// 此处将连接好的组件导出
export const AppContainer = connect(mapStateToProps, mapDispatchToProps)(appContainer)

如果需要现在使用导出的appContainer组件,还需要用到react-redux提供的Provider组件

Provider

它主要的功能有:

  • 在原应用组件上包裹一层,使原来整个应用成为Provider的子组件
  • 接收Redux的store作为props,并通过context传递给子组件

App.tsx

import * as React from 'react'
import { Provider } from 'react-redux'
import './App.css'
import { AppContainer } from './redux'
import store from './redux/store'

class App extends React.Component {
    public render(): JSX.Element {
        return (
            <div>
                <Provider store={store}>
                    <AppContainer/>
                </Provider>
            </div>
        )
    }
}

export default App

AppContainer.tsx



import * as React from 'react'
import { HashRouter as Router, Route } from 'react-router-dom'
import Nav from '../../component/nav'
import { loadable, route } from '../../routes'
import Home from '../Home'

interface IRouteItem {
    component: Promise<React.ComponentClass<any> | React.StatelessComponent<any> | { default: React.ComponentType<any> }>,
    path: string
}

export default class Appcontainer extends React.Component {
    public render ():JSX.Element {
        return (
            <Router>
                <div>
                    <Nav />
                    <Route path='/' exact={true} component={Home}/>
                    {
                        route.map((item:IRouteItem, index:number) => {
                            return (
                                <Route path={item.path} component={loadable(item.component)} key={index}/>
                            )
                        })
                    }
                </div>            
            </Router>
        )
    }
}

验证react-redux已经将store映射到Appcontainer组件,如下:

image

可以看到Appcontainer组件的props已经有了之前在redux/index.tsx中定义的方法,现在就可以在Appcontainer中直接使用这些方法来修改store

子孙组件调用

在编写组件的时候我们为了可维护性,不会把组件都写在一个文件,所以肯定会有很多子组件或者孙子组件。一般来说,我们最外层的父组件用于逻辑处理,而子组件等用来做UI显示。
但是在子孙组件中,获取store不能如同在Appcontainer组件中一样通过this.props.xx获取,现在了解的有三种方法:

  • 1.Appcontainer作为父组件,可以通过props一层一层的把方法和值传递给子组件或者孙子组件
  • 2.假设现在有子组件A,在redux/index.tsx使用connect方法将A组件与redux进行连接,再导出调用,这样子组件也有了和Appcontainer组件一样的操作store的方法和值
  • 3.在Appcontainer组件中定义context上下文,子组件通过this.context.xx.来获取

这里我使用的是第三种方法:context

Appcontainer.tsx中:


import * as PropTypes from 'prop-types'
import * as React from 'react'

interface IRouteItem {
    component: Promise<React.ComponentClass<any> | React.StatelessComponent<any> | { default: React.ComponentType<any> }>,
    path: string
}

export default class Appcontainer extends React.Component<any, any>{
    // childContextTypes属性,声明给子孙组件提供的属性
    public static childContextTypes = {
        store: PropTypes.object
      }
    constructor (props:any) {
        super(props)
    }
    // 这个方法给子组件设置context的值,值为当前组件的props
    public getChildContext () {
        return {
            store: this.props
        }
    }
    public render ():JSX.Element {
        return (
           ...
        )
    }
}

子组件Counter中:

import * as PropTypes from 'prop-types'

export default class Counter extends React.Component<any, any> {
    // 声明静态属性 contextTypes 才能访问顶层组件定义的context,属性名字也需要和父组件一样,否则访问不到值
    public static contextTypes = {
        store: PropTypes.object
    }
    public handleDecreaseCB () {
        this.context.store.DECREASE()
    }
    public render () {
        return (
            <div className="container">
                <div className="container_count">
                    <DecreaseBtn handleDecrease={this.handleDecreaseCB}/>
                    {this.props.children}
                </div>
            </div>
        )    
    }
}

demo效果如下:

image

redux持久化

效果图中有一个问题,就是刷新后,原来的store变为了0,变成之前初始化state的值,如果需要让数据持久化,则需要用到一个第三方包:redux-persist,配置也很简单:

安装redux-persist

yarn add redux-persist --save

redux/store/index.tsx
import { createStore } from 'redux'
import * as persist from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import reducer from '../reducers'

const { persistStore, persistReducer } = persist

const persistConfig = {
    key: 'root',
    storage,
  }
const initState = {
    count: 0
}
const persistedReducer = persistReducer(persistConfig, reducer)
const store = createStore(persistedReducer, initState)
const persistor  = persistStore(store)

export default {
    persistor,
    store
}
App.tsx
import * as React from 'react'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
import './App.css'
import { AppContainer } from './redux'
import { persistor, store } from './redux/store'

export default class App extends React.Component {
    public render(): JSX.Element {
        return (
            <div>
                <Provider store={store}>
                    <PersistGate loading='加载中...' persistor={persistor}>
                        <AppContainer/>
                    </PersistGate>
                </Provider>
            </div>
        )
    }
}

以上方法都是参照官方的例子实现,详见:gitHub,效果如下:

image