Facebook React.js:如何在服务器上呈现有状态组件?

29

我认为在使用React.js进行服务器端渲染方面,我概念上缺少了一些东西。

假设我想创建一个页面来显示来自服务器端数据库的项目,并带有一个输入字段以进行筛选。

我想要一个页面:

  • 响应类似于 /items?name=foobar 的URL
  • 带有用于按名称筛选项目的React输入字段
  • 带有用于显示经过筛选的项目列表的React组件

假设我有一个公共REST API来查询客户端上的项目。

从概念上讲,在第一次请求 (GET /items?name=foobar) 时,我想要做的是:

  • 我希望我的输入字段显示用户传递的参数,因此我需要将查询参数('foobar')作为“prop”(例如initialName)传递给React组件。

所以我尝试了这个:

 // A stateful component, maintaining the value of a query field
 var ItemForm = React.createClass({
    getInitialState : function () {
        return {
            name : this.props.initialName
        };
    },
    handleInputValueChanged : function() {
        var enteredName = this.refs.query.getDOMNode().value.trim();
        this.props.onNameChanged(enteredName);
    },
    render : function () {

        return React.DOM.form({
            children : [
                React.DOM.label({
                    children : "System name"
                }),
                React.DOM.input({
                    ref : "query",
                    value : this.state.name,
                    onChange : this.handleInputValueChanged
                })
            ]
        });
    }
});
  • 我还需要在服务器上执行数据库查询,获取项目列表,并将此项目列表作为ItemList的“prop”(如“initialItems”)传递

据我所知,我需要一个简单的组件来显示列表,将其作为“prop”接收:

 // A stateless component, displaying a list of item
 var ItemList = return React.createClass({

    propTypes : {
        items : React.PropTypes.array
    },

    render : function () {
        return React.DOM.ul({
            children : _.map(this.props.items, function (item) {
                return React.DOM.li({
                    children : [
                        React.DOM.span({
                            children : ["Name : ", item.name].join(" ")
                        })]
                });
            })
        });
    }
});
  • 现在,我需要一个组件来显示整个页面;该组件将需要维护整个页面的状态,即查看字段中输入的名称,并进行API查询以更新列表项。 然而,我不明白如何让这个组件拥有一个“initialState”,既可以在服务器端工作,也可以在客户端后续渲染时使用。

我尝试了这个:

 // A stateful react component, maintaining the list of items
 var ItemPage = React.createClass({

    getInitialState : function () {
        // ?????
        // This is where I'm sure the problem lies. 
        // How can this be known both on server and client side ? 
        return {
            items : this.props.initialItems || []
        };
    },
    queryItems : function (enteredName) {

        var self = this;

        // The field was emptied, we must clear everything
        if (!enteredName) {
            this.setState({
                items : []
            });

        } else {

            // The field was changed, we want to do a query
            // then change the state to trigger a UI update. 
            // The query code is irrelevant, I think.
            doQuery(enteredName).then(function (items) {
                self.setState({
                   items : items
                });
            });
        }
    },
    render : function () {

        // I don't want to display the same view
        // if there is no results. 
        // This uses a 'state' property of the page
        var results = null;
        if (_.isEmpty(this.state.items)) {
            results = React.DOM.div({
                ref : "results",
                children : "No results"
            });
        } else {
            results = ItemListView({
                // Here items is a 'prop', the ItemList is technically
                // stateless
                items : this.state.items
            });
        }

        return React.DOM.div({
            children : [
                ItemForm({
                    initialName : this.props.initialName,
                    onNameChanged : this.queryItems
                }),
                results
            ]
        });
    }
});

我现在遇到的问题是:我可以使用类似下面的代码在服务器端渲染内容:

var name = // The name from the query parameters
var items = doQueryOnServerSide(name);
React.renderComponentAsString(ItemPage({
  initialName : name,
  initialItems : items
});

但是当我尝试编写客户端JavaScript时,我该怎么做?我知道我想要渲染dom的位置,但是我应该向React组件传递哪些初始属性呢?

React.renderComponent(ItemPage({
  initialName : ??? // Read the name parameter from URL ? 
  initialItems : ??? // I don't know yet, and I don't want to make an API call until the user entered something in the input box 
});

我尝试的大多数方法最终导致DOM在客户端被“擦除”,并显示以下消息:
React尝试在容器中重复使用标记,但校验和无效。这通常意味着您正在使用服务器渲染,但在服务器上生成的标记与客户端预期的不同。React注入了新的标记来进行补偿,这可以工作,但您失去了许多服务器渲染的好处。相反,请查明为什么在客户端或服务器上生成的标记不同。
请注意,只有我的ItemList组件的标记被擦除,因此我认为这是我的组件实现问题,但我真的不知道从哪里开始。
我走对路了吗?还是我完全错过了什么?
1个回答

15
使用服务器渲染时,应该始终传递与在服务器上呈现组件所使用的相同的props。在这种情况下,您需要传递相同的initialItems属性,以便React.renderComponent可以获取服务器呈现的标记(通过简单地将props转换为JSON并将其放入对renderComponent的调用中)。
当指定从initialItems读取数据的一般结构对我来说是有意义的。这样做允许您创建预加载数据的组件或没有数据的组件。您需要将初始状态设置为取决于在从头开始呈现全新组件的情况下要显示的内容。(如果您总是获取服务器呈现的标记,则可以始终传递initialName和initialItems props。)
希望这能让您理解。

1
这是否意味着我还必须始终在客户端执行请求以获取initialItems列表,以便我可以将它们作为initialItems传递到客户端?我正在考虑实际上将json嵌入到服务器端生成的页面中,以便客户端可以从某些东西开始...因为我不希望我的客户端执行相同的API请求,在我的情况下这将违背服务器端渲染的目的... - phtrivier
7
抱歉如果我表达不清——将JSON嵌入到服务器生成的页面中是我的建议。 - Sophie Alpert
2
好的,我明白了...所以我需要在服务器端生成标记,并将页面数据嵌入到脚本标记中(使用一个众所周知的ID来解析它,并将其注入到客户端渲染中),假设React应该意识到标记是相同的? - phtrivier
是的,如果您在服务器和客户端上传递完全相同的props,则标记将匹配,并且React将看到校验和匹配。 - Sophie Alpert
我试过了,它运行得很好(在页面中嵌入json起初有些困扰,但是嘿...) - phtrivier
6
将JSON嵌入网页源代码是大多数热门网站使用的普遍技术,所以不用担心。但要小心这种XSS攻击的可能性:http://benalpert.com/2012/08/03/preventing-xss-json.html。 - Sophie Alpert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接