react-intl中injectIntl/intlProvider方法的源码简析

谭晓博
2023-12-01

问题:react-intl用法部分

function Home(props) {
  return (
    <DocumentTitle title={`Ant Design - ${props.intl.formatMessage({ id: 'app.home.slogan' })}`}>
      <div className="main-wrapper">
        <Link />
        <Banner {...props} />
        <Page1 {...props} />
        <Page2 {...props} />
        <Page3 {...props} />
        <Page4 {...props} />
        <style dangerouslySetInnerHTML={{ __html: getStyle() }} />
      </div>
    </DocumentTitle>
  );
}

export default injectIntl(Home);//传入一个包裹组件WrappedComponent,返回一个InjectIntl组件实例

问题:源码分析部分

/*
 * Copyright 2015, Yahoo Inc.
 * Copyrights licensed under the New BSD License.
 * See the accompanying LICENSE file for terms.
 */

// Inspired by react-redux's `connect()` HOC factory function implementation:
// https://github.com/rackt/react-redux

import React, {Component} from 'react';
import invariant from 'invariant';
import {intlShape} from './types';
import {invariantIntlContext} from './utils';

function getDisplayName(Component) {
    return Component.displayName || Component.name || 'Component';
}

export default function injectIntl(WrappedComponent, options = {}) {
    const {
        intlPropName = 'intl',
        withRef      = false,//为WrappedComponent添加一个ref属性
    } = options;

    class InjectIntl extends Component {
        static displayName = `InjectIntl(${getDisplayName(WrappedComponent)})`;

        static contextTypes = {//可以接受父组件存放在context中的intl属性
            intl: intlShape,
        };

        static WrappedComponent = WrappedComponent;

        constructor(props, context) {
            super(props, context);
            invariantIntlContext(context);
        }
         //getWrappedInstance调用时候欧返回我们的ref="wrappedInstance"
        getWrappedInstance() {
            invariant(withRef,
                '[React Intl] To access the wrapped instance, ' +
                'the `{withRef: true}` option must be set when calling: ' +
                '`injectIntl()`'
            );

            return this.refs.wrappedInstance;
        }

        render() {
            return (
                <WrappedComponent //我们的WrappedComponent会被存放一个intl属性作为props,这是为什么上面的例子可以通过props直接获取到
                    {...this.props}
                    {...{[intlPropName]: this.context.intl}}//如果有ref属性,那么返回我们的WrappedComponent实例
                    ref={withRef ? 'wrappedInstance' : null}
                />
            );
        }
    }

    return InjectIntl;
}

问题:为什么要用这个InjectIntl而不是直接放在Context中,就像下面的例子就是直接放在context中的

import React, {PropTypes} from 'react';
import {intlShape, FormattedRelative} from 'react-intl';

const Component = ({date}, context) => ( //如果有ContextTypes,那么第二个参数就是我们的context
    <span title={context.intl.formatDate(date)}> //change here, use context directly
        <FormattedRelative value={date}/>
    </span>
);

Component.propTypes = {
    date: PropTypes.any.isRequired,
};

Component.contextTypes = {
    intl: intlShape.isRequired,
}
export default Component;

而下面的例子就是通过injectIntl来完成的

import React, {PropTypes} from 'react';
import {injectIntl, intlShape, FormattedRelative} from 'react-intl';

const Component = ({date, intl}) => (
    <span title={intl.formatDate(date)}>
        <FormattedRelative value={date}/>
    </span>
);

Component.propTypes = {
    date: PropTypes.any.isRequired,
    intl: intlShape.isRequired,
};
export default injectIntl(Component);

injectIntl提供了一个非直接层,他的作用是解耦,使用React的intl来替代我们的React的context。如果以后React的context的API发生变化,代码的改变只要在injectIntl中进行就可以了,而不是在整个应用中


问题:方才说了第二个参数是context,我们看看都有哪些方法会传入第二个参数context


问题:最后我们看看IntlProvider做了哪些内容

/*
 * Copyright 2015, Yahoo Inc.
 * Copyrights licensed under the New BSD License.
 * See the accompanying LICENSE file for terms.
 */

import {Component, Children, PropTypes} from 'react';
import IntlMessageFormat from 'intl-messageformat';
import IntlRelativeFormat from 'intl-relativeformat';
import IntlPluralFormat from '../plural';
import memoizeIntlConstructor from 'intl-format-cache';
import invariant from 'invariant';
import {shouldIntlComponentUpdate, filterProps} from '../utils';
import {intlConfigPropTypes, intlFormatPropTypes, intlShape} from '../types';
import * as format from '../format';
import {hasLocaleData} from '../locale-data-registry';

const intlConfigPropNames = Object.keys(intlConfigPropTypes);
const intlFormatPropNames = Object.keys(intlFormatPropTypes);

// These are not a static property on the `IntlProvider` class so the intl
// config values can be inherited from an <IntlProvider> ancestor.
const defaultProps = {
    formats : {},
    messages: {},
    textComponent: 'span',

    defaultLocale : 'en',
    defaultFormats: {},
};

export default class IntlProvider extends Component {
    static displayName = 'IntlProvider';

    static contextTypes = {
        intl: intlShape,
    };
    //获取父组件放在context中的intl属性

    static childContextTypes = {
        intl: intlShape.isRequired,
    };
    //同时把intl放在context中,那么所有的子组件都是可以使用的,其实是给injectIntl(Home)处理过的组件使用的
    //injectIntl(Home)处理过的组件(也就是此处的Home)会获取父组件的context中保存的intl,并作为Props传递给Home

    static propTypes = {
        ...intlConfigPropTypes,
        children  : PropTypes.element.isRequired,
        initialNow: PropTypes.any,
    };
   //实例化的时候会采用如下调用方式:<IntlProvider locale={appLocale.locale} messages={appLocale.messages}/>
   //这的第二个参数是context要注意
    constructor(props, context = {}) {
        super(props, context);

        invariant(typeof Intl !== 'undefined',
            '[React Intl] The `Intl` APIs must be available in the runtime, ' +
            'and do not appear to be built-in. An `Intl` polyfill should be loaded.\n' +
            'See: http://formatjs.io/guides/runtime-environments/'
        );

        const {intl: intlContext} = context;
        // Used to stabilize time when performing an initial rendering so that
        // all relative times use the same reference "now" time.
        let initialNow;
        if (isFinite(props.initialNow)) {
            initialNow = Number(props.initialNow);
        } else {
            // When an `initialNow` isn't provided via `props`, look to see an
            // <IntlProvider> exists in the ancestry and call its `now()`
            // function to propagate its value for "now".
            initialNow = intlContext ? intlContext.now() : Date.now();
        }

        // Creating `Intl*` formatters is expensive. If there's a parent
        // `<IntlProvider>`, then its formatters will be used. Otherwise, this
        // memoize the `Intl*` constructors and cache them for the lifecycle of
        // this IntlProvider instance.
        const {formatters = {
            getDateTimeFormat: memoizeIntlConstructor(Intl.DateTimeFormat),
            getNumberFormat  : memoizeIntlConstructor(Intl.NumberFormat),
            getMessageFormat : memoizeIntlConstructor(IntlMessageFormat),
            getRelativeFormat: memoizeIntlConstructor(IntlRelativeFormat),
            getPluralFormat  : memoizeIntlConstructor(IntlPluralFormat),
        }} = (intlContext || {});

        this.state = {
            ...formatters,

            // Wrapper to provide stable "now" time for initial render.
            now: () => {
                return this._didDisplay ? Date.now() : initialNow;
            },
        };
    }

    getConfig() {
        const {intl: intlContext} = this.context;

        // Build a whitelisted config object from `props`, defaults, and
        // `context.intl`, if an <IntlProvider> exists in the ancestry.
        let config = filterProps(this.props, intlConfigPropNames, intlContext);

        // Apply default props. This must be applied last after the props have
        // been resolved and inherited from any <IntlProvider> in the ancestry.
        // This matches how React resolves `defaultProps`.
        for (let propName in defaultProps) {
            if (config[propName] === undefined) {
                config[propName] = defaultProps[propName];
            }
        }

        if (!hasLocaleData(config.locale)) {
            const {
                locale,
                defaultLocale,
                defaultFormats,
            } = config;

            if (process.env.NODE_ENV !== 'production') {
                console.error(
                    `[React Intl] Missing locale data for locale: "${locale}". ` +
                    `Using default locale: "${defaultLocale}" as fallback.`
                );
            }

            // Since there's no registered locale data for `locale`, this will
            // fallback to the `defaultLocale` to make sure things can render.
            // The `messages` are overridden to the `defaultProps` empty object
            // to maintain referential equality across re-renders. It's assumed
            // each <FormattedMessage> contains a `defaultMessage` prop.
            config = {
                ...config,
                locale  : defaultLocale,
                formats : defaultFormats,
                messages: defaultProps.messages,
            };
        }

        return config;
    }

    getBoundFormatFns(config, state) {
        return intlFormatPropNames.reduce((boundFormatFns, name) => {
            boundFormatFns[name] = format[name].bind(null, config, state);
            return boundFormatFns;
        }, {});
    }
    
    getChildContext() {
        const config = this.getConfig();

        // Bind intl factories and current config to the format functions.
        const boundFormatFns = this.getBoundFormatFns(config, this.state);

        const {now, ...formatters} = this.state;

        return {
            intl: {
                ...config,
                ...boundFormatFns,
                formatters,
                now,
            },
        };
    }

    shouldComponentUpdate(...next) {
        return shouldIntlComponentUpdate(this, ...next);
    }

    componentDidMount() {
        this._didDisplay = true;
    }

    render() {
        return Children.only(this.props.children);
    }
}

参考资料:

runtime environment

在React项目中使用React-intl实现多语言支持

react-intl

 类似资料: