GithubHelp home page GithubHelp logo

binnerlee / react-redux-dynamic-injection Goto Github PK

View Code? Open in Web Editor NEW

This project forked from tongchengqiu/react-redux-dynamic-injection

0.0 2.0 0.0 120 KB

🎯 This is a demo project for redux's dynamic injection in React.

Home Page: https://qiutc.me/post/react-redux-dynamic-injection.html

License: MIT License

HTML 14.62% JavaScript 85.38%

react-redux-dynamic-injection's Introduction

Redux store 的动态注入


前言

在 React + Redux + React-Router 的单页应用架构中,�我们将 UI 层( React 组件)和数据层( Redux store )分离开来,以做到更好地管理应用的。 Redux store 既是存储整个应用的数据状态,它的 state 是一个树的数据结构,可以看到如图的例子:

state tree

而随着应用和业务逻辑的增大,项目中的业务组件和数据状态也会越来越多;在�� Router 层面�可以使用 React-Router 结合 webpack 做按需加载 以减少单个 js 包的大小。 而在 store 层面,随着应用增大,整个结构可能会变的非常的大,应用加载初始化的时候就会去初始化定义整个应用的 store state 和 actions ,这对与内存和资源的大小都是一个比较大的占用和消耗。

因此如何做到像 �Router 一样地在需要某一块业务组件的时候再去添加这部分的 Redux 相关的数据呢?

Redux store 动态注入 的方案则是用以解决以上的问题。

在阅读本文的时候建议了解以下一些�概念:


方案实践

原理

在 Redux 中,对于 store state 的定义是通过组合 reducer 函数来得到的,也就是说 reducer 决定了最后的整个状态的数据结构�。在生成的 store 中有一个 replaceReducer(nextReducer) 方法,它是 Redux 中的一个高阶 API ,该函数接收一个 nextReducer 参数,�用于替换 store 中原原有的 reducer ,以此可以改变 store 中原有的状态的数据结构。

因此,在初始化 store 的时候,我们可以只定义一些默认公用 reducer(登录状态、全局信息等等),也就是在 createStore 函数中只传入这部分相关的 reducer ,这时候其状态的数据结构如下:

state tree

当我们加载到某一个业务逻辑对应的页面时,比如 /home,这部分的业务代码经过 Router 中的处理是�按需加载的,在其初始化该部分的组件之前,我们可以在 store 中注入该模块对应的 reducer ,这时候其整体状态的数据结构应该如下:

state tree

在这里需要做的就是将新增的 reducer 与原有的 reducer 组合,然后通过 store.replaceReducer 函数更新其 reducer 来做到在 store 中的动态注入。

代码

话不多说,直接上代码 https://github.com/TongchengQiu/react-redux-dynamic-injection ,示例项目的目录结构如下:

.
├── src
|   ├── pages
|   |   ├── Detail
|   |   |   ├── index.js
|   |   |   ├── index.jsx
|   |   |   └── reducer.jsx
|   |   ├── Home
|   |   |   ├── index.js
|   |   |   ├── index.jsx
|   |   |   └── reducer.jsx
|   |   ├── List
|   |   |   ├── index.js
|   |   |   ├── index.jsx
|   |   |   └── reducer.jsx
|   |   ├── Root.js
|   |   └── rootReducer.js
|   ├── store
|   |   ├── createStore.js
|   |   ├── location.js
|   |   └── reducerUtil.js
|   └── index.js
└── package.json

入口

首先来看整个应用的入口文件 ./src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Root from './pages/Root';

ReactDOM.render(<Root />, document.getElementById('root'));

这里所做的就是在 #root DOM 元素上挂载渲染 Root 组件;

Root 根组件

./src/pages/Root.jsx 中:

import React, { Component} from 'react';
import { Provider } from 'react-redux';
import { Link, Switch, Route, Router as BrowserRouter } from 'react-router';

import createStore from '../store/createStore';
import { injectReducer } from '../store/reducerUtils';
import reducer, { key } from './rootReducer';

export const store  = createStore({} , {
  [key]: reducer
});

const lazyLoader = (importComponent) => (
  class AsyncComponent extends Component {
    state = { C: null }

    async componentDidMount () {
      const { default: C } = await importComponent();
      this.setState({ C });
    }

    render () {
      const { C } = this.state;
      return C ? <C {...this.props} /> : null;
    }
  }
);

export default class Root extends Component {
  render () {
    return (
      <div className='root__container'>
        <Provider store={store}>
          <Router>
            <div className='root__content'>
              <Link to='/'>Home</Link>
              <br />
              <Link to='/list'>List</Link>
              <br />
              <Link to='/detail'>Detail</Link>
              <Switch>
                <Route exact path='/'
                  component={lazyLoader(() => import('./Home'))}
                />
                <Route path='/list'
                  component={lazyLoader(() => import('./List'))}
                />
                <Route path='/detail'
                  component={lazyLoader(() => import('./Detail'))}
                />
              </Switch>
            </div>
          </Router>
        </Provider>
      </div>
    );
  }
}

首先是创建了一个 Redux 的 store ,这里的 createStore 函数并并没有用 Redux 中原生提供的,而是重新封装了一层来改造它; 它接收两个参数,第一个是初始化的状态数据,第二个是初始化的 reducer,这里传入的是一个名称为 key 的 reducer ,这里的 keyreducer 是在 ./src/pages/rootReducer.js 中定义的,它用来存储一些通用和全局的状态数据和处理函数的; lazyLoader 函数是用来异步加载组件的,也就是通过不同的 route 来分割代码做按需加载,具体可参考 code-splitting ; 他的用法就是在 Route 组件中传入的 component 使用 lazyLoader(() => import('./List')) 的方式来导入; 接下来就是定义了一个 Root 组件并暴露,其中 Provider 是用来连接 Redux store 和 React 组件,这里需要传入 store 对象。

创建 STORE

前面提到,创建 store 的函数是重新封装 Redux 提供的 createStore 函数,那么这里面做了什么处理的? 看 ./src/store/createStore.js 文件:

import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';

import { makeAllReducer } from './reducerUtils';

export default (initialState = {}, initialReducer = {}) => {
  const middlewares = [thunk];

  const enhancers = [];

  if (process.env.NODE_ENV === 'development') {
    const devToolsExtension = window.devToolsExtension;
    if (typeof devToolsExtension === 'function') {
      enhancers.push(devToolsExtension());
    }
  }

  const store = createStore(
    makeAllReducer(initialReducer),
    initialState,
    compose(
      applyMiddleware(...middlewares),
      ...enhancers
    )
  );

  store.asyncReducers = {
    ...initialReducer
  };

  return store;
}

首先在暴露出的 createStore 函数中,先是定义了 Redux 中我们需要的一些 middlewaresenhancers

  • redux-thunk 是用来在 Redux 中更好的处理异步操作的;
  • devToolsExtension 是在开发环境下可以在 chrome 的 redux devtool 中观察数据变化;

之后就是生成了 store ,其中传入的 reducer 是由 makeAllReducer 函数生成的; 最后返回 store ,在这之前给 store 增加了一个 asyncReducers 的属性对象,它的作用就是用来缓存旧的 reducers 然后与新的 reducer 合并,其具体的操作是在 injectReducer 中;

生成 REDUCER

./src/store/reducerUtils.js 中:

import { combineReducers } from 'redux';

export const makeAllReducer = (asyncReducers) => combineReducers({
  ...asyncReducers
});

export const injectReducer = (store, { key, reducer }) => {
  if (Object.hasOwnProperty.call(store.asyncReducers, key)) return;

  store.asyncReducers[key] = reducer;
  store.replaceReducer(makeAllReducer(store.asyncReducers));
}

export const createReducer = (initialState, ACTION_HANDLES) => (
  (state = initialState, action) => {
    const handler = ACTION_HANDLES[action.type];
    return handler ? handler(state, action) : state;
  }
);

在初始化创建 store 的时候,其中的 reducer 是由 makeAllReducer 函数来生成的,这里接收一个 asyncReducers 参数,它是一个包含 keyreducer 函数的对象;

injectReducer 函数是用来在 store 中动态注入 reducer 的,首先判断当前 store 中的 asyncReducers 是否存在该 reducer ,如果存在则不需要做处理,而这里的 asyncReducers 则是存储当前已有的 reducers ; 如果需要新增 reducer ,则在 asyncReducers 对象中加入新增的 reducer ,然后通过 makeAllReducer 函数返回原有的 reducer 和新的 reducer 的合并,并通过 store.replaceReducer 函数替换 store 中的 reducer。

createReducer 函数则是用来生成一个新的 reducer 。

定义 ACTION 与 REDUCER

关于如何定义一个 action 与 reducer 这里以 rootReducer 的定义来示例 ./src/pages/rootReducer.js

import { createReducer } from '../store/reducerUtils';

export const key = 'root';

export const ROOT_AUTH = `${key}/ROOT_AUTH`;

export const auth = () => (
  (dispatch, getState) => (
    new Promise((resolve) => {
      setTimeout(() => {
        dispatch({
          type: ROOT_AUTH,
          payload: true
        });
        resolve();
      }, 300);
    })
  )
);

export const actions = {
  auth
};

const ACTION_HANLDERS = {
  [ROOT_AUTH]: (state, action) => ({
    ...state,
    auth: action.payload
  })
};

const initalState = {
  auth: false
};

export default createReducer(initalState, ACTION_HANLDERS);

这一步其实比较简单,主要是结合 redux-thunk 的异步操作做了一个模拟 auth 验证的函数;

首先是定义了这个 reducer 对应的 state 在根节点中的 key ; 然后定义了 actions ; 之后定义了操作函数 auth ,其实就是触发一个 ROOT_AUTH 的 action; 之后定义 actions 对应的处理函数,存储在 ACTION_HANLDERS 对象中; 最后通过 createReducer 函数生成一个 reducer 并暴露出去;

对于在业务组件中需要动态注入的 reducer 的定义也是按照这套模式,具体可以观察每个业务组件中�的 reducer.js 文件;

动态注入 REDUCER

在前面,我们生成了一个 store 并赋予其初始化的 state 和 reducer ,当我们加载到某一块业务组件的时候,则需要动态注入该组件对应的一些 state 和 reducer。

以 Home 组件为示例,当加载到该组件的时候,首先执行 index.js 文件:

import { injectReducer } from '../../store/reducerUtils';
import { store } from '../Root';
import Home from './index.jsx';
import reducer, { key } from './reducer';

injectReducer(store, { key, reducer });

export default Home;

首先是在 store 中插入其业务模块对于的 reducer: injectReducer(store, { key, reducer }) ,之后直接暴露该组件; 因此在该组件初始化之前,在 store 中就注入了其对应的 state 和 reducer;

而在 index.jsx 中对于 Redux 的使用和其标准的用法并无区别;感兴趣可以阅读该部分的代码。

运行示例

clone 仓库:

git clone https://github.com/TongchengQiu/react-redux-dynamic-injection.git

初始化:

npm i -d

运行:

npm start

可以看到启动了项目 http://localhost:3000/

通过 Redux Devtool ,可以看到这里的初始状态为:

state tree

点击 List 到 List 对应的页面,可以看到原来的状态变为了:

state tree

也就是说在加载到 List 组件的时候,动态插入了这部分对应的 state 和 reducer。

react-redux-dynamic-injection's People

Contributors

tongchengqiu avatar

Watchers

James Cloos avatar binner avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.