1.0.5 • Published 4 years ago

react-native-visitor v1.0.5

Weekly downloads
2
License
MIT
Repository
github
Last release
4 years ago

ReactNativeVisitor · GitHub license npm version PRs Welcome

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

你可以发给我一个Pull Request,和我一起完善这个工具。

生成Visitor

使用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

[返回目录]

获取后代Visitor

你可以通过给后代节点起名字(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

当你需要获取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组件内文本

和获取子节点数组一样,调用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,则用这个方法可以给组件内部容器增加样式。

[返回目录]

子节点API

通过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>
    */
}

全部子节点API包括:

/**
 * 查看是否含有某个子节点
 *
 * @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

如果你有任何问题、意见或建议,欢迎给我提issues,和我一起完善这个小工具。

License

ReactNativeVisitor is MIT licensed.

1.0.6

4 years ago

1.0.5

4 years ago

1.0.4

4 years ago

1.0.3

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago