React Native для веб-решения: react-native-web

исходный код React Native

Оригинальный адрес:Github.com/hu j ia oh j / Первоначально ...

В этой статье мы расскажем о трех аспектах решения React Native to Web: react-native-web.

  • Использование реактивной нативной сети
  • анализ исходного кода react-native-web
  • практика реагирования на нативные веб-сайты

реакция-родной-веб:GitHub.com/Что Колы/Горячие…

использовать

Установить

yarn add react react-dom react-native-web

если используетсяART, необходимо установитьreact-art(Например, react-native-svg используется для решения значков на стороне RN, которое основано на react-art)

yarn add react-art

После установки использование в основном делится на два этапа:

  • конфигурация веб-пакета
  • Новая конфигурация на входе

конфигурация веб-пакета

Конфигурация веб-пакета такая же, как и обычная конфигурация веб-приложения React, а затем добавляется конфигурация псевдонима следующим образом:

// webpack.config.js
module.exports = {
  // ...the rest of your config
  resolve: {
    alias: {
      'react-native$': 'react-native-web'
    }
  }
}

Новая конфигурация на входе

Есть два способа:

  • Использование API реестра приложений
  • использовать метод рендеринга

Использование API реестра приложений

Прежде чем добавлять конфигурацию, сначала посмотрите на входной файл RN:

// index.js
import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('rn_web', () => App);

После добавления конфигурации она выглядит следующим образом:

// index.web.js
import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('rn_web', () => App);

AppRegistry.runApplication('rn_web', {
    rootTag: document.getElementById('react-root')
});

использовать метод рендеринга

Используйте метод рендеринга следующим образом:

import { render } from 'react-native';
import App from './App';

render(<App/>, rootTag: document.getElementById('react-root'));

Видно, что API AppRegistry ближе к методу написания RN, а метод рендеринга такой же, как у ReactDOM.render.

Выше вы можете преобразовать существующую страницу RN в веб-страницу.

Затем возьмите AppRegistry API в качестве записи, чтобы увидеть, что делает react-native-web.

анализ исходного кода react-native-web

Исходный код анализируется из трех частей:

  • Точка входа, API AppRegistry
  • API, который является реализацией RN API.
  • компонент, то есть реализация компонента RN

Запись: API AppRegistry

Код входного файла:

// index.web.js
import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('rn_web', () => App);

AppRegistry.runApplication('rn_web', {
    rootTag: document.getElementById('react-root')
});

Итак, давайте посмотрим, что делают эти два API.

AppRegistry.registerComponent

const runnables = {};
static registerComponent(appKey: string, componentProvider: ComponentProvider): string {
    runnables[appKey] = {
        getApplication: appParameters => getApplication(componentProviderInstrumentationHook(componentProvider), appParameters ? appParameters.initialProps : emptyObject, wrapperComponentProvider && wrapperComponentProvider(appParameters)),
        run: appParameters => renderApplication(componentProviderInstrumentationHook(componentProvider), appParameters.initialProps || emptyObject, appParameters.rootTag, wrapperComponentProvider && wrapperComponentProvider(appParameters), appParameters.callback)
    };
    return appKey;
}

Возьмите пример кода в качестве примера, этот метод определенrunnables['rn_web']Объект, этот объект имеет два метода для getApplication, RUN

AppRegistry.runApplication

static runApplication(appKey: string, appParameters: Object): void {
    runnables[appKey].run(appParameters);
}

Взяв пример кода в качестве примера, этот метод называется

runnables['rn_web'].run({
    rootTag: document.getElementById('react-root')
})

здесьappParametersСтруктура данных следующая:

{
    initialProps, // 初始props
    rootTag, // root DOM节点
    callback, // 回调函数
}

renderApplication

import { render } from 'react-dom';
const renderFn = render;
function renderApplication<Props: Object>(RootComponent: ComponentType<Props>, initialProps: Props, rootTag: any, WrapperComponent?: ?ComponentType<*>, callback?: () => void) {
    renderFn(
        <AppContainer WrapperComponent={WrapperComponent} rootTag={rootTag}>
            <RootComponent {...initialProps} />
        </AppContainer>,
        rootTag,
        callback
    );
}

Фактический вызов:

ReactDOM.render(
    <AppContainer WrapperComponent={WrapperComponent} rootTag={rootTag}>
        <App {...initialProps} />
    </AppContainer>,
    rootTag,
    callback
);

AppContainer

export default class AppContainer extends Component<Props, State> {
  state = { mainKey: 1 };

  static childContextTypes = {
    rootTag: any
  };

  static propTypes = {
    WrapperComponent: any,
    children: node,
    rootTag: any.isRequired
  };

  getChildContext(): Context {
    return {
      rootTag: this.props.rootTag
    };
  }

  render() {
    const { children, WrapperComponent } = this.props;
    let innerView = (
      <View
        children={children}
        key={this.state.mainKey}
        pointerEvents="box-none"
        style={styles.appContainer}
      />
    );

    if (WrapperComponent) {
      innerView = <WrapperComponent>{innerView}</WrapperComponent>;
    }
    return (
      <View pointerEvents="box-none" style={styles.appContainer}>
        {innerView}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  appContainer: {
    flex: 1
  }
});

API

Возьмите StyleSheet в качестве примера для анализа исходного кода react-native-web API.

Мы все знаем, что таблицы стилей, используемые в RN, являются подмножеством CSS, давайте посмотрим на обработку таблиц стилей в react-native-web.

StyleSheet

const StyleSheet = {
  absoluteFill,
  absoluteFillObject,
  compose(style1, style2) {
    ...
  },
  create(styles) {
    ...
  },
  flatten: flattenStyle,
  hairlineWidth: 1
};

Модуль StyleSheet RN имеет следующие методы и константы:

1. Метод:

  • setStyleAttributePreprocessor (этот метод является рискованным)
  • create
  • flatten

2. Константы:

  • hairlineWidth
  • absoluteFill
  • absoluteFillObject

Можно обнаружить, что StyleSheet в react-native-web определяет все методы и константы, кроме метода setStyleAttributePreprocessor (этот метод является рискованным). Кроме того, добавлен метод compose, который используется в компонентах react-native-web.

Сначала взгляните на метод StyleSheet.create.

StyleSheet.create
create(styles) {
  const result = {};
  Object.keys(styles).forEach(key => {
    const id = styles[key] && ReactNativePropRegistry.register(styles[key]);
    result[key] = id;
  });
  return result;
}

Код относительно прост, главное пройтись по стилям и вызвать все стилиReactNativePropRegistry.registerПолучите соответствующий идентификатор и верните объект, соответствующий идентификатору ключа. Сначала рассмотрим пример:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
  ellipsis: {
    width: 200,
  }
});

console.log(styles);

Давайте посмотрим, что такое печатные стили?

{container: 78, welcome: 79, instructions: 80, ellipsis: 81}

Давайте взглянемReactNativePropRegistry.registerЧто вы наделали

ReactNativePropRegistry
const emptyObject = {};
const objects = {};
const prefix = 'r';
let uniqueID = 1;

const createKey = id => `${prefix}-${id}`;

export default class ReactNativePropRegistry {
  static register(object: Object): number {
    const id = uniqueID++;
    if (process.env.NODE_ENV !== 'production') {
      Object.freeze(object);
    }
    const key = createKey(id);
    objects[key] = object;
    return id;
  }

  static getByID(id: number): Object {
    if (!id) {
      return emptyObject;
    }
    const key = createKey(id);
    const object = objects[key];
    if (!object) {
      return emptyObject;
    }
    return object;
  }
}

Этот модуль определяет два метода: register, getByID, register сохраняет объект стиля в объекте objects и возвращает соответствующий идентификатор; getByID получает соответствующий объект стиля через идентификатор

Во всем процессе конвертации стилей react-native-web помимо StyleSheet.create нужно еще обратить внимание на метод StyleSheet.flatten, а именно flattenStyle

flattenStyle
function getStyle(style) {
  if (typeof style === 'number') {
    return ReactNativePropRegistry.getByID(style);
  }
  return style;
}

function flattenStyle(style: ?StyleObj): ?Object {
  if (!style) {
    return undefined;
  }

  if (!Array.isArray(style)) {
    return getStyle(style);
  }

  const result = {};
  for (let i = 0, styleLength = style.length; i < styleLength; ++i) {
    const computedStyle = flattenStyle(style[i]);
    if (computedStyle) {
      for (const key in computedStyle) {
        const value = computedStyle[key];
        result[key] = value;
      }
    }
  }
  return result;
}

Параметр стилей, принимаемый методом flattenStyle, представляет собой массив или переменную, содержащую идентификатор таблицы стилей.ReactNativePropRegistry.getByIDметод, получить соответствующий объект стиля по идентификатору и вернуть его.

Выше мы взяли StyleSheet в качестве примера для анализа исходного кода react-native-web для реализации RN API.

компоненты

На примере компонента View проанализируйте исходный код компонента react-native-web.

const calculateHitSlopStyle = hitSlop => {
  const hitStyle = {};
  for (const prop in hitSlop) {
    if (hitSlop.hasOwnProperty(prop)) {
      const value = hitSlop[prop];
      hitStyle[prop] = value > 0 ? -1 * value : 0;
    }
  }
  return hitStyle;
};

class View extends Component<ViewProps> {
  static displayName = 'View';

  static contextTypes = {
    isInAParentText: bool
  };

  static propTypes = ViewPropTypes;

  render() {
    const hitSlop = this.props.hitSlop;
    const supportedProps = filterSupportedProps(this.props);

    const { isInAParentText } = this.context;

    supportedProps.style = StyleSheet.compose(
      styles.initial,
      StyleSheet.compose(isInAParentText && styles.inline, this.props.style)
    );

    if (hitSlop) {
      const hitSlopStyle = calculateHitSlopStyle(hitSlop);
      const hitSlopChild = createElement('span', { style: [styles.hitSlop, hitSlopStyle] });
      supportedProps.children = React.Children.toArray([hitSlopChild, supportedProps.children]);
    }

    return createElement('div', supportedProps);
  }
}

const styles = StyleSheet.create({
  // https://github.com/facebook/css-layout#default-values
  initial: {
    alignItems: 'stretch',
    borderWidth: 0,
    borderStyle: 'solid',
    boxSizing: 'border-box',
    display: 'flex',
    flexDirection: 'column',
    margin: 0,
    padding: 0,
    position: 'relative',
    zIndex: 0,
    // fix flexbox bugs
    minHeight: 0,
    minWidth: 0
  },
  inline: {
    display: 'inline-flex'
  },
  // this zIndex-ordering positions the hitSlop above the View but behind
  // its children
  hitSlop: {
    ...StyleSheet.absoluteFillObject,
    zIndex: -1
  }
});

export default applyLayout(applyNativeMethods(View));

Компонент View — это простой компонент React, давайте сначала сосредоточимся на нем:

export default applyLayout(applyNativeMethods(View));

в,applyNativeMethodsМетод заключается в преобразовании собственного метода в соответствующий метод DOM;applyLayoutМетод заключается в переопределении функции жизненного цикла компонента. Эта часть заинтересованных друзей разберется сама~

Затем обратите внимание на метод рендеринга компонента View, в основном для выполнения некоторой обработки реквизитов компонента, включая проверку того, поддерживаются ли реквизиты, обработку стилей и, наконец, вызовcreateElementметод

createElement

const createElement = (component, props, ...children) => {
  // use equivalent platform elements where possible
  let accessibilityComponent;
  if (component && component.constructor === String) {
    accessibilityComponent = AccessibilityUtil.propsToAccessibilityComponent(props);
  }
  const Component = accessibilityComponent || component;
  const domProps = createDOMProps(Component, props);
  adjustProps(domProps);
  return React.createElement(Component, domProps, ...children);
};

наконец позвонилReact.createElementметод создания React Element, перед этим главное сделать вызовcreateDOMPropsметод, получить domProps

createDOMProps

const createDOMProps = (component, props, styleResolver) => {
  ...
  const {
    ...
    ...domProps
  } = props;

  // GENERAL ACCESSIBILITY
  ...

  // DISABLED
  ...

  // FOCUS
  // Assume that 'link' is focusable by default (uses <a>).
  // Assume that 'button' is not (uses <div role='button'>) but must be treated as such.
  ...

  // STYLE
  // Resolve React Native styles to optimized browser equivalent
  const reactNativeStyle = [
    component === 'a' && resetStyles.link,
    component === 'button' && resetStyles.button,
    role === 'heading' && resetStyles.heading,
    component === 'ul' && resetStyles.list,
    role === 'button' && !disabled && resetStyles.ariaButton,
    pointerEvents && pointerEventsStyles[pointerEvents],
    providedStyle,
    placeholderTextColor && { placeholderTextColor }
  ];
  const { className, style } = styleResolver(reactNativeStyle);
  if (className && className.constructor === String) {
    domProps.className = props.className ? `${props.className} ${className}` : className;
  }
  if (style) {
    domProps.style = style;
  }

  // OTHER
  // Link security and automation test ids
  ...
  return domProps;
};

Код метода createDOMProps длинный, поэтому я не буду вставлять его сюда целиком. Из нескольких комментариев мы можем узнать, что этот метод в основном преобразует каждый реквизит в соответствующий веб-сайт. Здесь мы берем стиль в качестве примера для посмотреть, как происходит преобразование.

Рабочая нагрузка в стиле преобразования в основном вstyleResolverметод, который вызываетReactNativeStyleResolverпримерresolveметод.此方法最后会返回 className 和 style,最后会赋值到 domProps 中

styleResolver

resolve(style) {
  // fast and cachable
  // style: id
  if (typeof style === 'number') {
    this._injectRegisteredStyle(style);
    const key = createCacheKey(style);
    return this._resolveStyleIfNeeded(style, key);
  }
  // resolve a plain RN style object
  // style: 样式对象
  if (!Array.isArray(style)) {
    return this._resolveStyleIfNeeded(style);
  }
  // flatten the style array
  // cache resolved props when all styles are registered
  // otherwise fallback to resolving
  // style: 存储id的数组
  const flatArray = flattenArray(style);
  let isArrayOfNumbers = true;
  for (let i = 0; i < flatArray.length; i++) {
    const id = flatArray[i];
    if (typeof id !== 'number') {
      isArrayOfNumbers = false;
    } else {
      this._injectRegisteredStyle(id);
    }
  }
  const key = isArrayOfNumbers ? createCacheKey(flatArray.join('-')) : null;
  return this._resolveStyleIfNeeded(flatArray, key);
}

см. далее_injectRegisteredStyleа также_resolveStyleIfNeeded

_injectRegisteredStyle

_injectRegisteredStyle(id) {
  const { doLeftAndRightSwapInRTL, isRTL } = I18nManager;
  const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr';
  if (!this.injectedCache[dir][id]) {
    // 根据id获取对应的样式对象
    const style = flattenStyle(id);
    // 对样式对象格式化:各样式属性排序;添加长度单位;颜色值处理;特定属性处理;返回格式化之后的样式对象
    const domStyle = createReactDOMStyle(i18nStyle(style));
    Object.keys(domStyle).forEach(styleProp => {
      const value = domStyle[styleProp];
      if (value != null) {
        // 将样式插入 WebStyleSheet(domStyleElement.sheet)中
        this.styleSheetManager.injectDeclaration(styleProp, value);
      }
    });
    // 将此样式标记为已插入
    this.injectedCache[dir][id] = true;
  }
}

в,styleSheetManager.injectDeclarationосновывается наdomStyleElement.sheetВставляем стиль страницы, можем посмотреть стиль переданной веб-страницы:

_resolveStyleIfNeeded

Метод _resolveStyleIfNeeded вызывает метод _resolveStyle.Исходный код выглядит следующим образом:

_resolveStyle(style) {
  // 获取对应id的样式对象
  const flatStyle = flattenStyle(style);
  // 对样式对象格式化:各样式属性排序;添加长度单位;颜色值处理;特定属性处理;返回格式化之后的样式对象
  const domStyle = createReactDOMStyle(i18nStyle(flatStyle));

  const props = Object.keys(domStyle).reduce(
    (props, styleProp) => {
      const value = domStyle[styleProp];
      if (value != null) {
        // 获取 WebStyleSheet 中特定样式属性及值对应的className
        // 通过 StyleSheet.create 创建的样式,会插入到 WebStyleSheet
        const className = this.styleSheetManager.getClassName(styleProp, value);
        if (className) {
          // 将此className放入props.classList中
          props.classList.push(className);
        } else {
          // Certain properties and values are not transformed by 'createReactDOMStyle' as they
          // require more complex transforms into multiple CSS rules. Here we assume that StyleManager
          // can bind these styles to a className, and prevent them becoming invalid inline-styles.
          // 单条样式属性,如果不是特殊属性,则直接放进props.style中
          // 单条样式属性是指未通过 StyleSheet.create 创建的样式
          if (
            styleProp === 'pointerEvents' ||
            styleProp === 'placeholderTextColor' ||
            styleProp === 'animationName'
          ) {
            const className = this.styleSheetManager.injectDeclaration(styleProp, value);
            if (className) {
              props.classList.push(className);
            }
          } else {
            if (!props.style) {
              props.style = {};
            }
            // 4x slower render
            props.style[styleProp] = value;
          }
        }
      }
      return props;
    },
    { classList: [] }
  );

  props.className = classListToString(props.classList);
  if (props.style) {
    props.style = prefixInlineStyles(props.style);
  }
  return props;
}

Этот метод в основном предназначен для получения имени класса или стиля, соответствующего всем стилям, и сохранения их в свойствах для возврата.

Выше мы взяли компонент View в качестве примера для анализа исходного кода react-native-web для реализации компонента RN.

После того, как мы закончим анализ исходного кода, давайте посмотрим, как внести некоторые изменения на основе react-native-web.

упражняться

Взяв в качестве примера компонент Text, компонент RN Text может установитьnumberOfLines, чтобы добиться однострочного или многострочного пропуска, но react-native-web реализует только однострочный пропуск, поэтому нам нужно добавить функцию многострочного пропуска, код выглядит следующим образом:

class Text extends Component<*> {
  ...
  render() {
    ...
    // allow browsers to automatically infer the language writing direction
    otherProps.dir = dir !== undefined ? dir : 'auto';
    otherProps.style = [
      styles.initial,
      this.context.isInAParentText === true && styles.isInAParentText,
      style,
      selectable === false && styles.notSelectable,
      numberOfLines === 1 && styles.singleLineStyle,
      onPress && styles.pressable
    ];
    // 支持多行省略
    if (numberOfLines > 1) {
      otherProps.style.push({
        display: '-webkit-box',
        WebkitBoxOrient: 'vertical',
        WebkitLineClamp: numberOfLines,
        overflow: 'hidden',
        textOverflow: 'ellipsis',
      });
    }
    const component = isInAParentText ? 'span' : 'div';
    return createElement(component, otherProps);
  }
  ...
}

Этот пример относительно прост. Мы хотим показать, что, глядя на исходный код react-native-web в процессе разработки, мы сталкиваемся с проблемой преобразования сети. Мы можем решить эту проблему, изменив исходный код или используя предоставляемый им API.

Конкретный код см. в примере проекта:rn_web, включая комментарии к исходному коду и пример кода

напиши в конце

Выше я делюсь исходным кодом react-native-web, надеюсь, он будет полезен тем, кому он нужен~~~

Если вам понравилась моя статья, вы можете перейтимой личный блогнажмите звездочку ⭐️