Py学习  »  Git

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

Kirk-Wang • 5 年前 • 413 次点击  

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
 
413 次点击