UNPKG

react-native-visitor

Version:

基于React开发方式,jsx对象一旦生成将无法修改。但如果你有对jsx生成对象二次修改的需求时,ReactDOM可以通过ref获取到Element引用进行修改,但ReactNative则无能为力……ReactNativeVisitor便是提供你对jsx产生的对象进行二次修改的能力,想改啥就改啥!

529 lines (459 loc) 18.1 kB
# [ReactNativeVisitor](https://github.com/Raykid/ReactNativeVisitor) &middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Raykid/ReactNativeVisitor/blob/master/LICENSE) [![npm version](https://img.shields.io/npm/v/react-native-visitor.svg?style=flat)](https://www.npmjs.com/package/react-native-visitor) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Raykid/ReactNativeVisitor/pulls) ReactNativeVisitor提供给你对jsx语法产生的、无法修改的对象进行二次修改的能力。 * **破解jsx对象:** 通过React jsx语法产生的对象其实并不是实际的显示对象,而是一个为下次渲染提供参考的配置对象。而且React在底层通过Object.freeze方法冻结了该对象,使得用户在拿到该对象后便没有任何机会对其进行修改。ReactNativeVisitor提供了用户在实际渲染之前对生成对象进行修改的机会,即某种程度上实现了对React jsx机制的破解。 * **下次渲染前你将可以:** * 修改节点传入参数; * 修改节点样式Style; * 增删移动节点; * 通过key获取某节点内部任何一个后代节点(无需关心嵌套层级)。 * **此外还友情提供:** * 对样式Style的嵌套功能支持,类似sass/less的嵌套样式语法,以及样式的递归混合功能; * 对Typescript的支持。 ## Installation ReactNativeVisitor支持npm安装 ``` npm install react-native-visitor ``` ## Tutorial * [生成Visitor](#生成Visitor) * [获取节点类型](#获取节点类型) * [获取后代Visitor](#获取后代Visitor) * [获取ReactNode](#获取ReactNode) * [获取父节点](#获取父节点) * [获取子节点列表](#获取子节点列表) * [Text组件内文本](#Text组件内文本) * [组件参数](#组件参数) * [组件样式](#组件样式) * [子节点API](#子节点API) 你可以发给我一个[Pull Request](https://github.com/Raykid/ReactNativeVisitor/pulls),和我一起完善这个工具。 ## 生成Visitor 使用wrapVisitor方法可以很轻松地生成一个访问器对象,仅需在jsx代码外部包裹一层方法调用即可,如下。 ```jsx import { wrapVisitor } from 'react-native-visitor'; const visitor = wrapVisitor()( <View> <Text>Hello ReactNativeVisitor!</Text> </View> ); ``` [[返回目录]](#Tutorial) ## 获取节点类型 可以通过Visitor提供的type属性获取到该Visitor对应的节点类型,type是个字符串。 ```jsx const visitor = wrapVisitor()( <View> <Text key="text">Hello ReactNativeVisitor!</Text> </View> ); console.log(visitor.type);// View console.log(visitor.text.type)// Text ``` [[返回目录]](#Tutorial) ## 获取后代Visitor 你可以通过给后代节点起名字(key)来为Visitor提供访问依据,该功能忽略层级嵌套,如下。 ```jsx 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! ``` [[返回目录]](#Tutorial) ## 获取ReactNode 当你需要获取ReactNode对象时,例如在React组件的render方法中需要return时,使用visitor的node属性即可。 ```jsx function render() { const visitor = wrapVisitor()( <View> <Text>Hello ReactNativeVisitor!</Text> </View> ); return visitor.node; } ``` [[返回目录]](#Tutorial) ## 获取父节点 调用Visitor的parent属性即可获取父级Visitor。 ```jsx const visitor = wrapVisitor()( <View key="parentContainer"> <View key="subContainer"/> </View> ); console.log(visitor.subContainer.parent === visitor.parentContainer);// true ``` [[返回目录]](#Tutorial) ## 获取子节点列表 调用Visitor的children属性可以获取容器节点Visitor的所有子节点Visitor数组。 ```jsx const visitor = wrapVisitor()( <View> <Text>第一行文本</Text> <Text>第二行文本</Text> <Text>第三行文本</Text> </View> ); console.log(visitor.children);// 返回由三个Text类型Visitor组成的数组 ``` [[返回目录]](#Tutorial) ## Text组件内文本 和获取子节点数组一样,调用Text组件Visitor的children属性,对于Text组件来说,children返回的是字符串而不是数组。如果想修改文本,只需设置它即可。 ```jsx function render() { const visitor = wrapVisitor()(<Text>我是文本</Text>); console.log(visitor.children);// 我是文本 visitor.children = "我是新的文本"; return visitor.node;// 界面上将渲染“我是新的文本”这行字 } ``` [[返回目录]](#Tutorial) ## 组件参数 Visitor提供了props属性,以允许你访问和修改组件的传入参数。 ```jsx 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 } ``` [[返回目录]](#Tutorial) ## 组件样式 Visitor提供了style属性,以允许你更加便捷地访问和修改组件的样式。 ```jsx 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方法,用于支持样式多层嵌套功能。 ```jsx 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方法允许你将多个样式对象进行合并,得出一个样式对象,所有内嵌的同名样式都会***递归***地被合并。如下: ```jsx 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是如何给一个样式赋上状态的。 ```jsx 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增加新的样式。 ```jsx 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,则用这个方法可以给组件内部容器增加样式。 [[返回目录]](#Tutorial) ## 子节点API 通过Visitor提供的子节点API,你可以方便地添加新节点或者移除、移动已有节点。 ```jsx 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> */ } ``` ### 全部子节点API包括: ```ts /** * 查看是否含有某个子节点 * * @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; ``` [[返回目录]](#Tutorial) ### Issues 如果你有任何问题、意见或建议,欢迎给我提[issues](https://github.com/Raykid/ReactNativeVisitor/issues),和我一起完善这个小工具。 ### License ReactNativeVisitor is [MIT licensed](./LICENSE).