社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Git

React-Admin 之 Admin 组件源码分析(github 持续更新中)

Kirk-Wang • 6 年前 • 437 次点击  

Admin 组件源码分析

import React, { createElement } from 'react';
import PropTypes from 'prop-types';
import { createStore, compose, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import createHistory from 'history/createHashHistory';
import { Switch, Route } from 'react-router-dom';
import { ConnectedRouter, routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import { all, fork } from 'redux-saga/effects';
import withContext from 'recompose/withContext';

import { USER_LOGOUT } from './actions/authActions';

import createAppReducer from './reducer';
import { adminSaga } from './sideEffect';
import { TranslationProvider, defaultI18nProvider } from './i18n';
import CoreAdminRouter from './CoreAdminRouter';

const CoreAdmin = ({
    appLayout,
    authProvider,
    children,
    customReducers = {},
    customSagas = [],
    customRoutes = [],
    dashboard,
    history,
    menu, // deprecated, use a custom layout instead
    catchAll,
    dataProvider,
    i18nProvider = defaultI18nProvider,
    theme,
    title = 'React Admin',
    loading,
    loginPage,
    logoutButton,
    initialState,
    locale = 'en',
}) => {
    const messages = i18nProvider(locale);
    const appReducer = createAppReducer(customReducers, locale, messages);

    const resettableAppReducer = (state, action) =>
        appReducer(action.type !== USER_LOGOUT ? state : undefined, action);
    const saga = function* rootSaga() {
        yield all(
            [
                adminSaga(dataProvider, authProvider, i18nProvider),
                ...customSagas,
            ].map(fork)
        );
    };
    const sagaMiddleware = createSagaMiddleware();
    const routerHistory = history || createHistory();
    const store = createStore(
        resettableAppReducer,
        initialState,
        compose(
            applyMiddleware(sagaMiddleware, routerMiddleware(routerHistory)),
            typeof window !== 'undefined' && window.devToolsExtension
                ? window.devToolsExtension()
                : f => f
        )
    );
    sagaMiddleware.run(saga);

    const logout = authProvider ? createElement(logoutButton) : null;

    return (
        <Provider store={store}>
            <TranslationProvider>
                <ConnectedRouter history={routerHistory}>
                    <Switch>
                        <Route
                            exact
                            path="/login"
                            render={props =>
                                createElement(loginPage, {
                                    ...props,
                                    title,
                                })
                            }
                        />
                        <Route
                            path="/"
                            render={props => (
                                <CoreAdminRouter
                                    appLayout={appLayout}
                                    catchAll={catchAll}
                                    customRoutes={customRoutes}
                                    dashboard={dashboard}
                                    loading={loading}
                                    loginPage={loginPage}
                                    logout={logout}
                                    menu={menu}
                                    theme={theme}
                                    title={title}
                                    {...props}
                                >
                                    {children}
                                </CoreAdminRouter>
                            )}
                        />
                    </Switch>
                </ConnectedRouter>
            </TranslationProvider>
        </Provider>
    );
};

const componentPropType = PropTypes.oneOfType([
    PropTypes.func,
    PropTypes.string,
]);

CoreAdmin.propTypes = {
    appLayout: componentPropType,
    authProvider: PropTypes.func,
    children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
    catchAll: componentPropType,
    customSagas: PropTypes.array,
    customReducers: PropTypes.object,
    customRoutes: PropTypes.array,
    dashboard: componentPropType,
    dataProvider: PropTypes.func.isRequired,
    history: PropTypes.object,
    i18nProvider: PropTypes.func,
    initialState: PropTypes.object,
    loading: componentPropType,
    locale: PropTypes.string,
    loginPage: componentPropType,
    logoutButton: componentPropType,
    menu: componentPropType,
    theme: PropTypes.object,
    title: PropTypes.node,
};

export default withContext(
    {
        authProvider: PropTypes.func,
    },
    ({ authProvider }) => ({ authProvider })
)(CoreAdmin);

通过上面代码,我们知道这是一个函数式组件(Functional Components) ,他接受如下属性:

const CoreAdmin = ({
    // 自定义布局
    appLayout,
    // 自定义身份验证策略
    authProvider,
    // 子组件
    children,
    // 自定义 Redux Reducer
    customReducers = {},
    // 自定义 Redux Saga
    customSagas = [],
    // 自定义路由
    customRoutes = [],
    // 仪表盘
    dashboard,
    // 历史记录
    history,
    // 目前已废弃,自定义菜单
    menu, // deprecated, use a custom layout instead
    // 可以用来自定义 Not Found
    catchAll,
    // 唯一必需的属性,它必须是一个返回一个promise的函数
    dataProvider,
    // 国际化,用来做多语言切换
    i18nProvider = defaultI18nProvider,
    // 自定义主题
    theme,
    // 自定义标题,默认是 React Admin
    title = 'React Admin',
    // 资源加载 loading
    loading,
    // 登录页
    loginPage,
    // 注销按钮
    logoutButton,
    // 初始 Redux State
    initialState,
    // 本地化,默认是英文
    locale = 'en',
}) => {
    ...
}

相关文档,可以查看 Admin

本地化处理

const messages = i18nProvider(locale);
  1. 分析下这个默认的 i18nProvider(defaultI18nProvider):
import defaultMessages from 'ra-language-english';

export default () => defaultMessages;

我们发现它是直接返回一个箭头函数,调用函数直接返回 react-admin 所支持的英文语言包 ra-language-english。具体内容大家自行点开查看。

创建 App Reducer

const appReducer = createAppReducer(customReducers, locale, messages);

它是一个如下函数:

export default (customReducers, locale, messages) =>
    combineReducers({
        admin,
        i18n: i18nReducer(locale, messages),
        form: formReducer,
        routing: routerReducer,
        ...customReducers,
    });

在这里,我们首先来聊一下这个 combineReducers。它是由 Redux 提供的一个辅助函数。作用是把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。看一个 Redux 官方的测试用例,来秒懂一下:

 it('returns a composite reducer that maps the state keys to given reducers', () => {
      const reducer = combineReducers({
        counter: (state = 0, action) =>
          action.type === 'increment' ? state + 1 : state,
        stack: (state = [], action) =>
          action.type === 'push' ? [...state, action.value] : state
      })

      const s1 = reducer({}, { type: 'increment' })
      expect(s1).toEqual({ counter: 1, stack: [] })
      const s2 = reducer(s1, { type: 'push', value: 'a' })
      expect(s2).toEqual({ counter: 1, stack: ['a'] })
    })
/**
 * 通俗点说,就是有一堆这样的函数 --> (state, action) => nextState
 * 把它们合并起来变成一个具有它们所有改变 state 能力的函数 --> (state, action) => nextState
 */

执行 reducer({}, { type: 'increment' }),实际上就是执行这段代码




    
    // 一眼看出它是一个标准的 Redux Reducer ---> (state, action) => nextState
    return function combination(state = {}, action) {
        // ....省略一些错误处理代码

        // 状态有无改变标志位
        let hasChanged = false
        // 创建一个空的 state, 如果 hasChanged 为真,则返回它
        const nextState = {}
        // 遍历合并过的 reducer
        for (let i = 0; i < finalReducerKeys.length; i++) {
            // 拿到 reducer key
            const key = finalReducerKeys[i]
            // 得到这个 key 所对应的 reducer,key 与实际 reducer 的函数名不一定相同
            const reducer = finalReducers[key]
            // 得到这个 key 所对应的当前 state
            const previousStateForKey = state[key]
            // 通过 reducer 处理,得到当前 key 的下一次 state
            const nextStateForKey = reducer(previousStateForKey, action)
            // 错误处理
            if (typeof nextStateForKey === 'undefined') {
                const errorMessage = getUndefinedStateErrorMessage(key, action)
                throw new Error(errorMessage)
            }
            // 给当前 key 赋值 state
            nextState[key] = nextStateForKey
            // 判断是否有 reducer 改变了 state
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        // 有改变 state, 返回 nextState,没有就返回 state
        return hasChanged ? nextState : state
    }

处理注销动作时,所有状态 reset,这个操作比较风骚

    const resettableAppReducer = (state, action) =>
        appReducer(action.type !== USER_LOGOUT ? state : undefined, action);

Redux-Saga--->> 中文文档

const saga = function* rootSaga() {
    yield all(
        [
            adminSaga(dataProvider, authProvider, i18nProvider),
            ...customSagas,
        ].map(fork)
    );
};

从这里入手我们将知道,整个 React-Admin 的所有 Saga,用它们来处理一切的副作用:

const sagaMiddleware = createSagaMiddleware();

这个,就是创造一个 Redux 中间件函数。类似于这样的一个函数,当然里面的逻辑是比较复杂的:

store => next => action => {}

通过它去劫持 action 并处理。

这里推荐一篇好文,Redux-Saga 漫谈。附一张里面的图(清晰的描述了执行流程):

History

import createHistory from 'history/createHashHistory';

const routerHistory = history || createHistory();

这里用的默认是 createHashHistory

store

这里引用 redux 中文文档 对它的解释(详情自行参考文档):

createStore(reducer, [preloadedState], enhancer)

创建一个 Redux store 来以存放应用中所有的 state。
应用中应有且仅有一个 store。

参数
  1. reducer (Function): 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。

  2. [preloadedState] (any): 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 reducer 可理解的内容。

  3. enhancer (Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。

const store = createStore(
    resettableAppReducer,
    initialState,
    compose(
        applyMiddleware(sagaMiddleware, routerMiddleware(routerHistory)),
        typeof window !== 'undefined' && window.devToolsExtension
            ? window.devToolsExtension()
            : f => f
    )
);

我们现在看看这段代码就很清晰了,这里我们主要来解释一下这段代码:

compose(
        applyMiddleware(sagaMiddleware, routerMiddleware(routerHistory)),
        typeof window !== 'undefined' && window.devToolsExtension
            ? window.devToolsExtension()
            : f => f
    )
/*
 * componse 我们知道它是将多个具有不同功能的函数通过 reduce 组合为一个函数,方便我们使用
 * 由于在 redux 中的参数 enhancer 必须长成这样(可以翻看源码得知):
 * createStore => (...args) => { ... }
 * 所以执行 compose 一定返回这样式的函数。
 * 所以 applyMiddleware(sagaMiddleware, routerMiddleware(routerHistory)) , window.devToolsExtension() 应该也是返回上面的一样的函数
 * middleware 前面已经说了是这样子:store => next => action => {}
 */

在这里已经做了 devToolsExtension() 的处理,所以我们可以直接安装一个 Chrome 插件( Redux DevTools ) 来查看我们的每一次 dispatch。

runSaga

启动 Saga,因为 sagas 是一些个 generator function,所以不会自己调用 .next(),也就是自动执行。所以需要一个函数来帮忙。

sagaMiddleware.run(saga);

logoutButton

有提供 authProvider,就会显示注销按钮。

const logout = authProvider ? createElement(logoutButton) : null;

Provider 组件

react-redux 到底干了啥,我们先不管,我们现在只看 Provider 组件,以下是它源码:

/**
 * 在生命周期函数中引用Context
 * 如果在一个组件中定义了contextTypes,那么下面这些生命周期函数中将会接收到额外的参数,即context对象:
 * constructor(props, context)
 * componentWillReceiveProps(nextProps, nextContext)
 * shouldComponentUpdate(nextProps, nextState, nextContext)
 * componentWillUpdate(nextProps, nextState, nextContext)
 * componentDidUpdate(prevProps, prevState, prevContext)
 * 当state或者props更新时getChildContext方法会被调用。
*/

export function createProvider(storeKey = 'store') {
    const subscriptionKey = `${storeKey}Subscription`
    class Provider extends Component {
        getChildContext() {
          // return { store: this.store, storeSubscription: null }
          // 被包裹的子组件,随时随地从 context 中拿到 store
          return { [storeKey]: this[storeKey], [subscriptionKey]: null }
        }

        constructor(props, context) {
          super(props, context)
          // 直接将传入的 store,挂载当前 store 上
          this[storeKey] = props.store;
        }

        render() {
          // 返回children中仅有的子级。否则抛出异常。
          return Children.only(this.props.children)
        }
    }
    ...
    Provider.childContextTypes = {
        [storeKey]: storeShape.isRequired,
        [subscriptionKey]: subscriptionShape,
    }
    return Provider
}
export default createProvider()

这个组件使用了 React 中的 Context;在有些场景中,你不想要向下每层都手动地传递你需要的 props。这就需要强大的 context API了。

通过在 Provider(context提供者)中添加 childContextTypes 和 getChildContext ,React 会向下自动传递参数,任何组件只要在它的子组件中,就能通过定义 contextTypes 来获取参数。如果 contextTypes 没有定义,那么 context 将会是个空对象。具体请查看legacy-context

TranslationProvider

创建一个可用于其子元素的翻译上下文,必须在Redux应用程序中调用。这个组件在 context 中提供 translate 和 locale 属性。方便子组件访问。

const MyApp = () => (
    <Provider store={store}>
        <TranslationProvider locale="fr" messages={messages}>
          <!-- Child components go here -->
         </TranslationProvider>
      </Provider>
);

这个组件会单独分析

ConnectedRouter,Switch,Route

ConnectedRouter 看下面代码,就秒懂了(路由改变时,同步状态到 store):




    
handleLocationChange = (location, action) => {
    this.store.dispatch({
      type: LOCATION_CHANGE,
      payload: {
        location,
        action
      }
    });
  };

componentWillMount() {
    if (!isSSR)
      // 利用 history.listen 方法
      this.unsubscribeFromHistory = history.listen(this.handleLocationChange);
    this.handleLocationChange(history.location);
  }

这三个组件,大家可参看react-router 文档,查看详细用法。

CoreAdminRouter

这个组件单独分析


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/dCNN3okm50
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/23568
 
437 次点击