在閱讀本篇文章之前,你要有React和React-native相關(guān)經(jīng)驗(yàn)少孝。react-native-web主要將你的react-native應(yīng)用H5化继低,以讓我們能達(dá)到“Write one, run everywhere”。
主要從三個(gè)方面分享 react native 轉(zhuǎn) web 方案:react-native-web
- react-native-web 的使用
- react-native-web 源碼分析
- react-native-web 實(shí)踐
react-naitive: https://github.com/facebook/react-native
react-native-web:https://github.com/necolas/react-native-web
React-native項(xiàng)目引入React-native-web
安裝
npm install react react-dom react-native-web --save
如果使用了 ART
稍走,需要安裝 react-art
(比如袁翁,使用了 react-native-svg 來(lái)做RN端icon方案,這就是基于 react-art)
npm i react-art --save
安裝好之后婿脸,使用主要分一下兩步:
- 入口處新增配置
- webpack配置
入口處新增配置
有兩種方式:
- 使用 AppRegistry API
- 使用 render 方法
使用 AppRegistry 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')
});
使用 render 方法
使用 render 方法如下:
import { render } from 'react-native';
import App from './App';
render(<App/>, rootTag: document.getElementById('react-root'));
可以看到狐树,AppRegistry API 更貼近RN的寫法焙压,render 方法跟 ReactDOM.render 是一個(gè)意思。
以上抑钟,就能夠?qū)F(xiàn)有RN頁(yè)面轉(zhuǎn)成web頁(yè)面了
接下來(lái)涯曲,以 AppRegistry API 為入口,看看 react-native-web 做了什么
react-native-web 源碼分析
從三部分來(lái)對(duì)源碼進(jìn)行分析:
- 入口在塔,即 AppRegistry API
- API幻件,即對(duì) RN API 實(shí)現(xiàn)
- 組件,即對(duì) RN 組件實(shí)現(xiàn)
入口:AppRegistry API
入口文件代碼:
// 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')
});
webpack配置
webpack配置就跟普通 React web 應(yīng)用配置一致即可蛔溃,然后新增alias配置绰沥,如下:
// webpack.config.js
module.exports = {
// ...the rest of your config
resolve: {
alias: {
'react-native$': 'react-native-web'
}
}
}
那我們來(lái)來(lái)看看這兩個(gè) 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']
對(duì)象,此對(duì)象有 getApplication揪利、run 兩個(gè)方法
AppRegistry.runApplication
static runApplication(appKey: string, appParameters: Object): void {
runnables[appKey].run(appParameters);
}
以例子代碼為例态兴,此方法就是調(diào)用了
runnables['rn_web'].run({
rootTag: document.getElementById('react-root')
})
這里的 appParameters 數(shù)據(jù)結(jié)構(gòu)如下:
{
initialProps, // 初始props
rootTag, // root DOM節(jié)點(diǎn)
callback, // 回調(diào)函數(shù)
}
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
);
}
實(shí)際調(diào)用的是:
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的子集,我們來(lái)看看 react-native-web 對(duì)樣式表的處理
StyleSheet
const StyleSheet = {
absoluteFill,
absoluteFillObject,
compose(style1, style2) {
...
},
create(styles) {
...
},
flatten: flattenStyle,
hairlineWidth: 1
};
RN的StyleSheet模塊有以下幾個(gè)方法和常量:
1喘垂、方法:
- setStyleAttributePreprocessor(此方法存在風(fēng)險(xiǎn))
- create
- flatten
2甜刻、常量:
- hairlineWidth
- absoluteFill
- absoluteFillObject
可以發(fā)現(xiàn)忌堂,react-native-web 中 StyleSheet 定義了除 setStyleAttributePreprocessor(此方法存在風(fēng)險(xiǎn))方法之外的所有方法和常量犀忱。此外,還新增了 compose 方法柿扣,此方法在 react-native-web 的組件中使用
首先來(lái)看看 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;
}
代碼比較簡(jiǎn)單章贞,主要就是遍歷styles祥绞,對(duì)所有styles調(diào)用 ReactNativePropRegistry.register
獲取對(duì)應(yīng)的id,返回對(duì)應(yīng) key-id 的對(duì)象鸭限。我們先看個(gè)例子:
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);
我們來(lái)看看打印出來(lái)的styles是什么蜕径?
{container: 78, welcome: 79, instructions: 80, ellipsis: 81}
接著來(lái)看看 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;
}
}
這個(gè)模塊,定義了兩個(gè)方法:register败京、getByID兜喻,register 是將樣式對(duì)象存入 objects 對(duì)象中,并返回對(duì)應(yīng)的 id赡麦;getByID 則是通過(guò) id 獲取對(duì)應(yīng)的樣式對(duì)象
在react-native-web整個(gè)樣式轉(zhuǎn)換過(guò)程中朴皆,除了StyleSheet.create,還需要關(guān)注一下 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 方法接受的 styles 參數(shù)是存有樣式表id的數(shù)組或變量遂铡,通過(guò)遞歸遍歷 styles,調(diào)用上一部分提到的 ReactNativePropRegistry.getByID
方法晶姊,通過(guò)id獲取對(duì)應(yīng)的樣式對(duì)象扒接,并返回。
以上帽借,我們以 StyleSheet 為例分析了 react-native-web 實(shí)現(xiàn) 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 組件就是一個(gè)簡(jiǎn)單的React組件砍艾,首先關(guān)注一下:
export default applyLayout(applyNativeMethods(View));
其中蒂教,applyNativeMethods
方法是將native的方法轉(zhuǎn)換為對(duì)應(yīng)的DOM方法;applyLayout
方法是對(duì)組件的生命周期函數(shù)進(jìn)行重寫脆荷。這部分感興趣的小伙伴自行了解~
接下來(lái)關(guān)注一下 View 組件的 render 方法凝垛,主要是對(duì)組件的 props 做些處理懊悯,包括校驗(yàn) props 是否支持、style 處理梦皮,最后調(diào)用 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);
};
最終是調(diào)用了 React.createElement
方法創(chuàng)建 React Element炭分,在此之前,主要做的事情就是調(diào)用 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 方法代碼較長(zhǎng)捧毛,這里就不全部粘貼,從幾個(gè)注釋可以知道让网,此方法主要是將各 props 轉(zhuǎn)換成對(duì)應(yīng)的 web 端的props呀忧,這里我們以 style 為例,看看是如何做轉(zhuǎn)換的溃睹。
樣式轉(zhuǎn)換工作量主要在 styleResolver
方法而账,即調(diào)用 ReactNativeStyleResolver
實(shí)例的 resolve
方法。此方法最后會(huì)返回 className 和 style因篇,最后會(huì)賦值到 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: 樣式對(duì)象
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: 存儲(chǔ)id的數(shù)組
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);
}
接下來(lái)看看 _injectRegisteredStyle
和 _resolveStyleIfNeeded
_injectRegisteredStyle
_injectRegisteredStyle(id) {
const { doLeftAndRightSwapInRTL, isRTL } = I18nManager;
const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr';
if (!this.injectedCache[dir][id]) {
// 根據(jù)id獲取對(duì)應(yīng)的樣式對(duì)象
const style = flattenStyle(id);
// 對(duì)樣式對(duì)象格式化:各樣式屬性排序泞辐;添加長(zhǎng)度單位;顏色值處理竞滓;特定屬性處理咐吼;返回格式化之后的樣式對(duì)象
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);
}
});
// 將此樣式標(biāo)記為已插入
this.injectedCache[dir][id] = true;
}
}
其中,styleSheetManager.injectDeclaration
是基于 domStyleElement.sheet
對(duì)頁(yè)面樣式進(jìn)行插入操作虽界,我們可以看看轉(zhuǎn)出來(lái)的web頁(yè)面的樣式:
[圖片上傳失敗...(image-5cd020-1551534493771)]
_resolveStyleIfNeeded
_resolveStyleIfNeeded 方法即是調(diào)用 _resolveStyle 方法汽烦,源碼如下:
_resolveStyle(style) {
// 獲取對(duì)應(yīng)id的樣式對(duì)象
const flatStyle = flattenStyle(style);
// 對(duì)樣式對(duì)象格式化:各樣式屬性排序;添加長(zhǎng)度單位莉御;顏色值處理撇吞;特定屬性處理;返回格式化之后的樣式對(duì)象
const domStyle = createReactDOMStyle(i18nStyle(flatStyle));
const props = Object.keys(domStyle).reduce(
(props, styleProp) => {
const value = domStyle[styleProp];
if (value != null) {
// 獲取 WebStyleSheet 中特定樣式屬性及值對(duì)應(yīng)的className
// 通過(guò) StyleSheet.create 創(chuàng)建的樣式礁叔,會(huì)插入到 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.
// 單條樣式屬性牍颈,如果不是特殊屬性,則直接放進(jìn)props.style中
// 單條樣式屬性是指未通過(guò) StyleSheet.create 創(chuàng)建的樣式
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;
}
此方法主要是獲取所有樣式對(duì)應(yīng)的 className 或者 style琅关,并存入props中返回
以上煮岁,我們以 View 組件為例分析了 react-native-web 實(shí)現(xiàn) RN 組件的源碼。
我們做完源碼分析之后涣易,我們看看如何基于 react-native-web 做一些修改
實(shí)踐
以 Text 組件為例画机,RN Text組件可以設(shè)置 numberOfLines
,來(lái)實(shí)現(xiàn)單行或多行省略新症,但是react-native-web只實(shí)現(xiàn)了單行省略步氏,所以我們要把多行省略的功能加上,代碼如下:
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);
}
...
}
舉的這個(gè)例子比較簡(jiǎn)單徒爹,想表達(dá)的是我們通過(guò)看react-native-web源碼荚醒,在開(kāi)發(fā)過(guò)程中芋类,遇到了轉(zhuǎn)換web的問(wèn)題,我們可以通過(guò)修改源碼界阁、或者使用它提供的API來(lái)解決侯繁。