ReactNativeVisitor提供给你对jsx语法产生的、无法修改的对象进行二次修改的能力。
- 破解jsx对象: 通过React jsx语法产生的对象其实并不是实际的显示对象,而是一个为下次渲染提供参考的配置对象。而且React在底层通过Object.freeze方法冻结了该对象,使得用户在拿到该对象后便没有任何机会对其进行修改。ReactNativeVisitor提供了用户在实际渲染之前对生成对象进行修改的机会,即某种程度上实现了对React jsx机制的破解。
- 下次渲染前你将可以:
- 修改节点传入参数;
- 修改节点样式Style;
- 增删移动节点;
- 通过key获取某节点内部任何一个后代节点(无需关心嵌套层级)。
- 此外还友情提供:
- 对样式Style的嵌套功能支持,类似sass/less的嵌套样式语法,以及样式的递归混合功能;
- 对Typescript的支持。
ReactNativeVisitor支持npm安装
npm install react-native-visitor
你可以发给我一个Pull Request,和我一起完善这个工具。
使用wrapVisitor方法可以很轻松地生成一个访问器对象,仅需在jsx代码外部包裹一层方法调用即可,如下。
import { wrapVisitor } from 'react-native-visitor';
const visitor = wrapVisitor()(
<View>
<Text>Hello ReactNativeVisitor!</Text>
</View>
);
可以通过Visitor提供的type属性获取到该Visitor对应的节点类型,type是个字符串。
const visitor = wrapVisitor()(
<View>
<Text key="text">Hello ReactNativeVisitor!</Text>
</View>
);
console.log(visitor.type);// View
console.log(visitor.text.type)// Text
你可以通过给后代节点起名字(key)来为Visitor提供访问依据,该功能忽略层级嵌套,如下。
const visitor = wrapVisitor()(
<View key="depth_0">
<View key="depth_1">
<Text key="target_text">Hello ReactNativeVisitor!</Text>
</View>
</View>
);
console.log(visitor.keyDict.depth_0);// 根级别View组件的Visitor
console.log(visitor.depth_1);// 直接访问key的效果与通过keyDict访问相同
console.log(visitor.target_text.children);// Hello ReactNativeVisitor!
当你需要获取ReactNode对象时,例如在React组件的render方法中需要return时,使用visitor的node属性即可。
function render()
{
const visitor = wrapVisitor()(
<View>
<Text>Hello ReactNativeVisitor!</Text>
</View>
);
return visitor.node;
}
调用Visitor的parent属性即可获取父级Visitor。
const visitor = wrapVisitor()(
<View key="parentContainer">
<View key="subContainer"/>
</View>
);
console.log(visitor.subContainer.parent === visitor.parentContainer);// true
调用Visitor的children属性可以获取容器节点Visitor的所有子节点Visitor数组。
const visitor = wrapVisitor()(
<View>
<Text>第一行文本</Text>
<Text>第二行文本</Text>
<Text>第三行文本</Text>
</View>
);
console.log(visitor.children);// 返回由三个Text类型Visitor组成的数组
和获取子节点数组一样,调用Text组件Visitor的children属性,对于Text组件来说,children返回的是字符串而不是数组。如果想修改文本,只需设置它即可。
function render()
{
const visitor = wrapVisitor()(<Text>我是文本</Text>);
console.log(visitor.children);// 我是文本
visitor.children = "我是新的文本";
return visitor.node;// 界面上将渲染“我是新的文本”这行字
}
Visitor提供了props属性,以允许你访问和修改组件的传入参数。
function render()
{
const visitor = wrapVisitor()(<MyComp prop1="这是参数1"></MyComp>);
console.log(visitor.props.prop1);// 这是参数1
visitor.props.prop2 = "这里新增了参数2";
return visitor.node;// MyComp组件将接收到值为"这里新增了参数2"的参数prop2
}
Visitor提供了style属性,以允许你更加便捷地访问和修改组件的样式。
function render()
{
const visitor = wrapVisitor()(
<Text style={{color: "black"}}>我是一段文字</Text>
);
visitor.style.color = "red";
return visitor.node;// 最终“我是一段文字”会被渲染成红色
}
在样式方面,ReactNodeVisitor还额外提供了类似于sass/less的完整解决方案,其中包括:
ReactNodeVisitor提供一个createStyleSheet方法,可以替换掉React Native原始的StyleSheet.create方法,用于支持样式多层嵌套功能。
import { createStyleSheet } from 'react-native-visitor';
function render()
{
// 声明一个含有任意层级结构的样式表对象
const styles = createStyleSheet({
root: {
width: "100%",
height: "100%",
// 这就是个嵌套的样式结构
text1: {
color: "black",
fontWeight: "500"
},
// 这是第二个嵌套的样式结构
second: {
display: "flex",
justifyContent: "center",
alignItems: "center",
// 它里面还有一层嵌套的样式结构
text2: {
color: "red"
}
}
}
});
// 在书写结构时按照层级结构赋值样式,让结构更清晰
return <View style={styles.root}>
<Text style={styles.root.text1}>文本1</Text>
<View style={styles.root.second}>
<Text style={styles.root.second.text2}>文本2</Text>
</View>
</View>;
}
mergeStyles方法允许你将多个样式对象进行合并,得出一个样式对象,所有内嵌的同名样式都会递归地被合并。如下:
import { createStyleSheet, mergeStyles } from 'react-native-visitor';
function render()
{
// 这是第一个样式表
const style1 = createStyleSheet({
root: {
width: "100%",
text: {
fontSize: 30
}
}
});
// 这是第二个样式表
const style2 = createStyleSheet({
root: {
height: 200,
text: {
color: "red"
}
}
});
// 这是合并后的样式表
const mergeRoot = mergeStyles(style1.root, style2.root);
// 使用合并后的样式表书写结构
return <View style={mergeRoot}>
<Text style={mergeRoot.text}>文本</Text>
</View>;
// 最后得到的样式表形如
/*
{
root: {
width: "100%",
height: 200,
text: {
fontSize: 30,
color: "red"
}
}
}
*/
}
有这样一个需求,整个页面有正常、正确和错误3个状态,页面中有一个按钮,正常是黄色的。当页面状态为正确时,按钮背景要变成绿色,反之当页面状态为错误时,按钮背景要变成红色。
交叉合并功能就是为了解决这样的问题而诞生的,它模仿的是sass/less中的并列样式功能。
交叉合并用到的方法为crossMergeStyles。下面的例子就说明了crossMergeStyles是如何给一个样式赋上状态的。
import { createStyleSheet, crossMergeStyles } from 'react-native-visitor';
function render()
{
const styles = createStyleSheet({
root: {
width: "100%",
height: "100%",
button: {
position: "absolute",
bottom: 100,
width: 200,
height: 100,
backgroundColor: "yellow",
display: "flex",
justifyContent: "center",
alignItems: "center",
text: {
fontSize: 30,
color: "black",
}
},
// 这个是root为正确状态下的样式结构
_right: {
button: {
backgroundColor: "green",
text: {
color: "white"
}
}
},
// 这个是root为错误状态下的样式结构
_wrong: {
button: {
backgroundColor: "red",
text: {
color: "white"
}
}
}
}
});
// 假设有一个isRight变量可以获取到页面状态
// crossMergeStyles方法第二个参数接收一个字符串数组,字符串都是某个样式节点的状态名
// 样式的状态名必须是该层样式的第一级子样式。一般都用下划线作为前缀,标明它是个状态,而不是一个嵌套结构,但这不是必须的,你完全可以自己决定前缀或者完全没有前缀(这将不太好管理)。
const mergeRoot = crossMergeStyles(styles.root, [
isRight && "_right",
!isRight && "_wrong"
]);
return <View style={mergeRoot}>
<View style={mergeRoot.button}>
<Text style={mergeRoot.button.text}>点我</Text>
</View>
</View>;
// 这样当isRight为true时,你会看到按钮变成了绿底白字,而当isRight为false时,按钮则变成了红底白字。
}
交叉合并样式几乎是开发界面时最常用的方法,因为通常页面或者组件都需要维护多个不同的状态,如果为每个状态都写一个完整的样式结构将会非常麻烦,特别是当样式不支持嵌套时……
上面几个方法都可以脱离Visitor体系使用。但如果你使用Visitor体系开发,则appendStyles可以让你快速给一个已经有样式的Visitor增加新的样式。
import { createStyleSheet, crossMergeStyles, appendStyles } from 'react-native-visitor';
function render()
{
// 假设这是我从其他地方获取到的Visitor
const visitor = wrapVisitor()(
<View>
<Text key="text" style={{color: "red"}}>文本</Text>
</View>
);
// 我希望让Text组件的字体加粗,我可以这么干
// 第一个参数给要附加样式的Visitor,这里是visitor.text
// 第二个参数就是要附加的样式结构
appendStyles(visitor.text, {
fontWeight: "bold"
});
// 当然你也可以使用上面提到的任何方法修改你需要的样式,然后再附加上去
const styles = createStyleSheet({
fontWeight: "bold",
_right: {
color: "green"
},
_wrong: {
color: "red"
}
});
appendStyles(visitor.text, crossMergeStyles(styles, [
isRight && "_right",
!isRight && "_wrong"
]));
// 返回修改后的结构
return visitor.node;
}
这个和附加样式差不多,只不过ScrollView系列组件有两个样式(style和contentContainerStyle),appendContentContainerStyles就是针对后者的附加样式。如果你拿到的Visitor是ScrollView、ListView、FlatList,则用这个方法可以给组件内部容器增加样式。
通过Visitor提供的子节点API,你可以方便地添加新节点或者移除、移动已有节点。
function render()
{
const visitor = wrapVisitor()(
<View>
<Text key="text1">文本1</Text>
<Text key="text2">文本2</Text>
<Text key="text3">文本3</Text>
</View>
);
// 添加节点,addChild可以直接接收ReactNode,后面它会被转换为子Visitor
visitor.addChild(<Text key="text5">我是新增的文本5</Text>);
// 按索引添加节点,你也可以传递一个Visitor,和直接传递ReactNode是一样的
const insertVisitor = wrapVisitor()(
<Text key="text4">我是插入的文本4</Text>
);
visitor.addChildAt(insertVisitor, 3);
// 移除节点
visitor.removeChild(visitor.text3);
// 替换节点
visitor.replace(<Text>我是新的文本2</Text>, visitor.text2);
// 返回node
return visitor.node;
// 这样下来实际的显示会和以下代码生成的一样
/*
<View>
<Text key="text1">文本1</Text>
<Text>我是新的文本2</Text>
<Text key="text4">我是插入的文本4</Text>
<Text key="text5">我是新增的文本5</Text>
</View>
*/
}
/**
* 查看是否含有某个子节点
*
* @param {ReactNodeVisitor} child 要测试的子节点Visitor
* @returns {boolean} 是否含有该节点
*/
hasChild(child:ReactNodeVisitor):boolean;
/**
* 获取子节点索引
*
* @param {ReactNodeVisitor} child 子节点Visitor
* @returns {number} 子节点索引
*/
getChildIndex(child:ReactNodeVisitor):number;
/**
* 获取指定索引处的子节点Visitor
*
* @param {number} index 指定索引
* @returns {ReactNodeVisitor} 获取到的子节点Visitor
*/
getChildAt(index:number):ReactNodeVisitor;
/**
* 添加显示节点
*
* @param {(ReactNodeVisitor|React.ReactNode)} child 要添加的显示节点,支持ReactNode或Visitor
* @returns {ReactNodeVisitor} 添加的节点Visitor
*/
addChild(child:ReactNodeVisitor|React.ReactNode):ReactNodeVisitor;
/**
* 在指定索引处添加显示节点
*
* @param {(ReactNodeVisitor|React.ReactNode)} child 要添加的显示节点,支持ReactNode或Visitor
* @param {number} index 要添加到的索引
* @returns {ReactNodeVisitor} 添加的节点Visitor
*/
addChildAt(child:ReactNodeVisitor|React.ReactNode, index:number):ReactNodeVisitor;
/**
* 在某个已有子节点前面插入显示节点
*
* @param {(ReactNodeVisitor|React.ReactNode)} child 要添加的显示节点,支持ReactNode或Visitor
* @param {ReactNodeVisitor} refChild 已有节点Visitor
* @returns {ReactNodeVisitor} 添加的节点Visitor
*/
addChildBefore(child:ReactNodeVisitor|React.ReactNode, refChild:ReactNodeVisitor):ReactNodeVisitor;
/**
* 在某个已有子节点后面插入显示节点
*
* @param {(ReactNodeVisitor|React.ReactNode)} child 要添加的显示节点,支持ReactNode或Visitor
* @param {ReactNodeVisitor} refChild 已有节点Visitor
* @returns {ReactNodeVisitor} 添加的节点Visitor
*/
addChildAfter(child:ReactNodeVisitor|React.ReactNode, refChild:ReactNodeVisitor):ReactNodeVisitor;
/**
* 将自身从父容器中移除
*
* @returns {ReactNodeVisitor} 返回被移除的节点Visitor
*/
remove():ReactNodeVisitor;
/**
* 移除一个子节点
*
* @param {ReactNodeVisitor} child 要移除的子节点Visitor
* @returns {ReactNodeVisitor} 被移除的节点Visitor
*/
removeChild(child:ReactNodeVisitor):ReactNodeVisitor;
/**
* 移除指定索引处的子节点
*
* @param {number} index 要移除的子节点索引
* @returns {ReactNodeVisitor} 被移除的节点Visitor
*/
removeChildAt(index:number):ReactNodeVisitor;
/**
* 清空子节点
*
* @returns {ReactNodeVisitor[]} 被移除的字节点Visitor列表
*/
removeChildren():ReactNodeVisitor[];
/**
* 替换子节点
*
* @param {(ReactNodeVisitor|React.ReactNode)} child 要替换成的子节点,支持ReactNode或Visitor
* @param {ReactNodeVisitor} refChild 要被替换的子节点Visitor
* @returns {ReactNodeVisitor} 返回被添加的节点Visitor
*/
replace(child:ReactNodeVisitor|React.ReactNode, refChild:ReactNodeVisitor):ReactNodeVisitor;
如果你有任何问题、意见或建议,欢迎给我提issues,和我一起完善这个小工具。
ReactNativeVisitor is MIT licensed.