12

这更像是“您的意见/我的想法是否正确?” 问题。

在理解 Flux 的同时尽可能严格,我试图找出 XHR 调用的位置、websockets/外部刺激的处理、路由发生等。

从我读到的文章、采访和浏览 facebook 示例中,有几种方法可以处理这些事情。严格遵循 Flux,Action 创建者是执行所有 XHR 调用的人,可能会PENDING/SUCCESS/FAILURE在请求完成之前和之后触发 Action。
另一个是,来自 facebook 的 Ian Obermiller,所有 READ(GETs) 请求都由 Stores 直接处理(不涉及 Action 创建者/调度程序),而 WRITE(POSTs) 请求由 Action Creator 处理整个action>dispatcher>store流程。

我们得出/希望坚持的一些理解/结论:

  1. 理想情况下,任何进出系统的事情都只能通过 Actions 发生。
  2. 离开/进入系统的异步调用将具有PENDING/PROGRESS(think file uploads)/SUCCESS/FAILURE操作。
  3. 跨整个应用程序的单个调度程序。
  4. Action>Dispatcher>Store调用是严格同步的,以坚持调度不能在内部启动另一个调度以避免链接事件/动作。
  5. 商店跨视图持久化(考虑到它是一个单页应用程序,您希望能够重用数据)

我们得出了一些结论,但我并不完全满意:

  1. 如果您采用 Store 进行读取和操作写入的方法,您如何处理多个 Store 可能能够使用来自单个 XHR 调用的数据的情况?
    示例:TeamStore 发出的 API 调用/api/teams/{id}返回如下内容:

        {  
            entities: {  
                teams: [{  
                    name: ...,  
                    description: ...,  
                    members: [1, 2, 4],  
                    version: ...  
                }],  
                users: [{  
                    id: 1  
                    name: ...,  
                    role: ...,  
                    version: ...  
                },  
                {  
                    id: 2  
                    name: ...,  
                    role: ...,  
                    version: ...  
                },  
                {  
                    id: 3  
                    name: ...,  
                    role: ...,  
                    version: ...  
                }]  
            }  
        }  
    

    理想情况下,我还想使用此 API 中返回的信息更新 MemberStore。我们为每个在更新记录时更新的实体维护一个版本号,这是我们在内部使用的拒绝对陈旧数据的调用等。使用它,我可以有一个内部逻辑,如果我作为副作用其他一些 API 调用,我知道我的数据已经过时,我触发了对该记录的刷新。
    看起来,解决方案是您需要商店来触发一个操作(这将有效地更新其他依赖商店)。这会将 Store>View>Action 短路到 Store>Action,我不确定这是否是个好主意。我们已经有一件事情与 Stores 自己的 XHR 调用不同步。像这样的让步最终会开始蔓延到整个系统。
    或了解其他商店并能够与之通信的商店。但这打破了商店没有二传手的规则。

    1. 上述问题的一个简单解决方案是您坚持将动作作为外部传入/传出刺激发生的唯一地方。这简化了多个 Store 更新的逻辑。
      但是现在,您在哪里以及如何处理缓存?我们得出的结论是,缓存将发生在 API Utils/DAO 级别。(如果您查看通量图)。
      但这引入了其他问题。为了更好地理解/解释我的意思:

      • /api/teams返回我显示所有团队列表的所有团队的列表。
      • 单击团队的链接时,我会转到其详细信息视图,/api/teams/{id}如果商店中尚不存在该数据,则该视图需要数据。
        如果 Actions 处理所有 XHR,则 View 会执行类似TeamActions.get([id])which does的操作TeamDAO.get([id])。为了能够立即返回这个调用(因为我们已经缓存了它),DAO 必须进行缓存,但还要维护集合/项目之间的关系。按照设计,这种逻辑已经存在于商店中。
        问题来了:

      • 你是否在 DAO 和 Stores 中重复了这个逻辑?

      • 你是否让 DAO 知道 Stores,他们可以询问 Store 是否已经有一些数据,然后返回 302 说,你有最新的数据很好。
    2. 您如何处理涉及 XHR API 的验证?一些简单的事情,比如重复的团队名称。
      视图直接命中 DAO 并执行类似TeamDAO.validateName([name])返回承诺的操作,或者您是否创建了操作?如果您创建一个动作,考虑到其主要是瞬态数据,通过该动作,有效/无效是否会流回视图?

    3. 你如何处理路由?我查看了 react-router,但我不确定我是否喜欢它。我不一定认为完全需要强制采用 react-ish JSX 方式来提供路由映射/配置。此外,显然,它使用了自己的 RouteDispatcher,它执行单一调度程序规则。
      我更喜欢的解决方案来自一些博客文章/SO 答案,其中您将路由映射存储在 RouteStore 中。
      RouteStore 还维护 CURRENT_VIEW。react AppContainer 组件在 RouteStore 中注册,并在更改时将其子视图替换为 CURRENT_VIEW。当前视图在完全加载时通知 AppContainer,并且 AppContainer 触发 RouteActions.pending/success/failure,可能带有一些上下文,以通知其他组件达到稳定状态,显示/隐藏繁忙/加载指示。

    我无法干净设计的事情是,如果您要设计类似于 Gmail 的路由,您会怎么做?我非常喜欢对 Gmail 的一些观察:

    • 在页面准备好加载之前,URL 不会更改。它在“加载”时停留在当前 URL 上,并在加载完成后移动到新 URL。这使得它......
    • 失败时,您根本不会丢失当前页面。因此,如果您正在撰写,并且“发送”失败,您不会丢失您的邮件(即您不会丢失当前稳定的视图/状态)。(他们不这样做,因为自动保存是 le pwn,但您明白了)您可以选择将邮件复制/粘贴到某个地方以安全保存,直到您可以再次发送。

    一些参考资料:
    https ://github.com/gaearon/flux-react-router-example http://ianobermiller.com/blog/2014/09/15/react-and-flux-interview/ https://github。 com/facebook/flux

4

2 回答 2

5

这是我使用 facebook Flux 和 Immutable.js 的实现,我认为它可以根据一些经验法则回应您的许多担忧:

商店

  • Stores 负责通过Immutable.Record维护数据状态,并通过全局Immutable.OrderedMap引用Record实例来维护缓存ids
  • WebAPIUtils存储直接调用读取操作并触发actions写入操作
  • RecordA和之间的关系通过参数FooRecordBRecordA实例解析foo_id并通过调用检索,例如FooStore.get(this.foo_id)
  • 商店只公开getters诸如get(id),getAll()等方法。

APIUTILS

  • 我使用SuperAgent进行 ajax 调用。每个请求都包含在Promise
  • 我使用由 url + params 的哈希索引的读取请求映射Promise
  • Promise当解决或拒绝时,我通过诸如 fooReceived 或 fooError 之类的 ActionCreators 触发操作。
  • fooError动作当然应该包含带有服务器返回的验证错误的有效负载。

成分

  • 控制器视图组件侦听存储中的更改。
  • 我所有的组件,除了控制器视图组件,都是“纯”的,所以我使用ImmutableRenderMixin只重新渲染它真正需要的东西(这意味着如果你打印Perf.printWasted时间,它应该非常低,几毫秒。
  • 由于Relay 和 GraphQL尚未开源,我强制props通过propsType.
  • 父组件应该只传递必要的道具。如果我的父组件包含一个对象,例如(为了简单起见,var fooRecord = { foo:1, bar: 2, baz: 3};我没有在这里使用)并且我的子组件需要显示and ,我不会将整个对象传递给我的子组件,而只是将and作为道具传递给我的子组件因为其他组件可以编辑该值,使子组件重新渲染,而该组件根本不需要这个值!Immutable.RecordfooRecord.foofooRecord.barfoofooRecordFoofooRecordBarfoo.baz

路由 - 我只是使用ReactRouter

执行

这是一个基本示例:

api

apiUtils/Request.js

var request = require('superagent');

//based on http://stackoverflow.com/a/7616484/1836434
var hashUrl = function(url, params) {
    var string = url + JSON.stringify(params);
    var hash = 0, i, chr, len;
    if (string.length == 0) return hash;
    for (i = 0, len = string.length; i < len; i++) {
        chr   = string.charCodeAt(i);
        hash  = ((hash << 5) - hash) + chr;
        hash |= 0; // Convert to 32bit integer
    }
    return hash;
}

var _promises = {};

module.exports = {

    get: function(url, params) {
        var params = params || {};
        var hash = hashUrl(url, params);
        var promise = _promises[hash];
        if (promise == undefined) {
            promise = new Promise(function(resolve, reject) {
                request.get(url).query(params).end( function(err, res) {
                    if (err) {
                        reject(err);
                    } else {
                        resolve(res);
                    }
                });
            });
            _promises[hash] = promise;
        }
        return promise;
    },

    post: function(url, data) {
        return new Promise(function(resolve, reject) {

            var req = request
                .post(url)
                .send(data)
                .end( function(err, res) {
                    if (err) {
                        reject(err);
                    } else {
                        resolve(res);
                    }
                });

        });
    }

};

apiUtils/FooAPI.js

var Request = require('./Request');
var FooActionCreators = require('../actions/FooActionCreators');

var _endpoint = 'http://localhost:8888/api/foos/';

module.exports = {

    getAll: function() {
        FooActionCreators.receiveAllPending();
        Request.get(_endpoint).then( function(res) {
            FooActionCreators.receiveAllSuccess(res.body);
        }).catch( function(err) {
            FooActionCreators.receiveAllError(err);
        });
    },

    get: function(id) {
        FooActionCreators.receivePending();
        Request.get(_endpoint + id+'/').then( function(res) {
            FooActionCreators.receiveSuccess(res.body);
        }).catch( function(err) {
            FooActionCreators.receiveError(err);
        });
    },

    post: function(fooData) {
        FooActionCreators.savePending();
        Request.post(_endpoint, fooData).then (function(res) {
            if (res.badRequest) { //i.e response return code 400 due to validation errors for example
                FooActionCreators.saveInvalidated(res.body);
            }
            FooActionCreators.saved(res.body);
        }).catch( function(err) { //server errors
            FooActionCreators.savedError(err);
        });
    }

    //others foos relative endpoints helper methods...

};

商店

商店/BarStore.js

var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../APIUtils/BarAPI')
var CHANGE_EVENT = 'change';

var _bars = Immutable.OrderedMap();

class Bar extends Immutable.Record({
    'id': undefined,
    'name': undefined,
    'description': undefined,
}) {

    isReady() {
        return this.id != undefined //usefull to know if we can display a spinner when the Bar is loading or the Bar's data if it is ready.
    }

    getBar() {
        return BarStore.get(this.bar_id);
    }
}

function _rehydrate(barId, field, value) {
    //Since _bars is an Immutable, we need to return the new Immutable map. Immutable.js is smart, if we update with the save values, the same reference is returned.
    _bars = _bars.updateIn([barId, field], function() {
        return value;
    });
}


var BarStore = assign({}, EventEmitter.prototype, {

    get: function(id) {
        if (!_bars.has(id)) {
            BarAPI.get(id);
            return new Bar(); //we return an empty Bar record for consistency
        }
        return _bars.get(id)
    },

    getAll: function() {
        return _bars.toList() //we want to get rid of keys and just keep the values
    },

    Bar: Bar,

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },

});

var _setBar = function(barData) {
    _bars = _bars.set(barData.id, new Bar(barData));
};

var _setBars = function(barList) {
    barList.forEach(function (barData) {
        _setbar(barData);
    });
};

BarStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {   
        case ActionTypes.BAR_LIST_RECEIVED_SUCESS:
            _setBars(action.barList);
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_RECEIVED_SUCCESS:
            _setBar(action.bar);
            BarStore.emitChange();
            break;

        case ActionTypes.BAR_REHYDRATED:
            _rehydrate(
                action.barId,
                action.field,
                action.value
            );
            BarStore.emitChange();
            break;
    }
});

module.exports = BarStore;

商店/FooStore.js

var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/FooConstants').ActionTypes;
var BarStore = require('./BarStore');
var FooAPI = require('../APIUtils/FooAPI')
var CHANGE_EVENT = 'change';

var _foos = Immutable.OrderedMap();

class Foo extends Immutable.Record({
    'id': undefined,
    'bar_id': undefined, //relation to Bar record
    'baz': undefined,
}) {

    isReady() {
        return this.id != undefined;
    }

    getBar() {
        // The whole point to store an id reference to Bar
        // is to delegate the Bar retrieval to the BarStore,
        // if the BarStore does not have this Bar object in
        // its cache, the BarStore will trigger a GET request
        return BarStore.get(this.bar_id); 
    }
}

function _rehydrate(fooId, field, value) {
    _foos = _foos.updateIn([voucherId, field], function() {
        return value;
    });
}

var _setFoo = function(fooData) {
    _foos = _foos.set(fooData.id, new Foo(fooData));
};

var _setFoos = function(fooList) {
    fooList.forEach(function (foo) {
        _setFoo(foo);
    });
};

var FooStore = assign({}, EventEmitter.prototype, {

    get: function(id) {
        if (!_foos.has(id)) {
            FooAPI.get(id);
            return new Foo();
        }
        return _foos.get(id)
    },

    getAll: function() {
        if (_foos.size == 0) {
            FooAPI.getAll();
        }
        return _foos.toList()
    },

    Foo: Foo,

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    },

});

FooStore.dispatchToken = AppDispatcher.register(function(action) {
    switch (action.type)
    {
        case ActionTypes.FOO_LIST_RECEIVED_SUCCESS:
            _setFoos(action.fooList);
            FooStore.emitChange();
            break;

        case ActionTypes.FOO_RECEIVED_SUCCESS:
            _setFoo(action.foo);
            FooStore.emitChange();
            break;

        case ActionTypes.FOO_REHYDRATED:
            _rehydrate(
                action.fooId,
                action.field,
                action.value
            );
            FooStore.emitChange();
            break;
    }
});

module.exports = FooStore;

成分

components/BarList.react.js(控制器视图组件)

var React = require('react/addons');
var Immutable = require('immutable');

var BarListItem = require('./BarListItem.react');
var BarStore = require('../stores/BarStore');

function getStateFromStore() {
    return {
        barList: BarStore.getAll(),
    };
}

module.exports = React.createClass({

    getInitialState: function() {
        return getStateFromStore();
    },

    componentDidMount: function() {
        BarStore.addChangeListener(this._onChange);
    },

    componentWillUnmount: function() {
        BarStore.removeChangeListener(this._onChange);
    },

    render: function() {
        var barItems = this.state.barList.toJS().map(function (bar) {
            // We could pass the entire Bar object here
            // but I tend to keep the component not tightly coupled
            // with store data, the BarItem can be seen as a standalone
            // component that only need specific data
            return <BarItem
                        key={bar.get('id')}
                        id={bar.get('id')}
                        name={bar.get('name')}
                        description={bar.get('description')}/>
        });

        if (barItems.length == 0) {
            return (
                <p>Loading...</p>
            )
        }

        return (
            <div>
                {barItems}
            </div>
        )

    },

    _onChange: function() {
        this.setState(getStateFromStore();
    }

});

组件/BarListItem.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    // I use propTypes to explicitly telling
    // what data this component need. This 
    // component is a standalone component
    // and we could have passed an entire
    // object such as {id: ..., name, ..., description, ...}
    // since we use all the datas (and when we use all the data it's
    // a better approach since we don't want to write dozens of propTypes)
    // but let's do that for the example's sake 
    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    }

    render: function() {

        return (
            <li>
                <p>{this.props.id}</p>
                <p>{this.props.name}</p>
                <p>{this.props.description}</p>
            </li>
        )

    }

});

组件/BarDetail.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');

var BarActionCreators = require('../actions/BarActionCreators');

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        description: React.PropTypes.string.isRequired
    },

    handleSubmit: function(event) {
        //Since we keep the Bar data up to date with user input
        //we can simply save the actual object in Store.
        //If the user goes back without saving, we could display a 
        //"Warning : item not saved" 
        BarActionCreators.save(this.props.id);
    },

    handleChange: function(event) {
        BarActionCreators.rehydrate(
            this.props.id,
            event.target.name, //the field we want to rehydrate
            event.target.value //the updated value
        );
    },

    render: function() {

        return (
            <form onSubmit={this.handleSumit}>
                <input
                    type="text"
                    name="name"
                    value={this.props.name}
                    onChange={this.handleChange}/>
                <textarea
                    name="description"
                    value={this.props.description}
                    onChange={this.handleChange}/>
                <input
                    type="submit"
                    defaultValue="Submit"/>
            </form>
        )

    },

});

components/FooList.react.js(控制器视图组件)

var React = require('react/addons');

var FooStore = require('../stores/FooStore');
var BarStore = require('../stores/BarStore');

function getStateFromStore() {
    return {
        fooList: FooStore.getAll(),
    };
}


module.exports = React.createClass({

    getInitialState: function() {
        return getStateFromStore();
    },

    componentDidMount: function() {
        FooStore.addChangeListener(this._onChange);
        BarStore.addChangeListener(this._onChange);
    },

    componentWillUnmount: function() {
        FooStore.removeChangeListener(this._onChange);
        BarStore.removeChangeListener(this._onChange);
    },

    render: function() {

        if (this.state.fooList.size == 0) {
            return <p>Loading...</p>
        }

        return this.state.fooList.toJS().map(function (foo) {
            <FooListItem 
                fooId={foo.get('id')}
                fooBar={foo.getBar()}
                fooBaz={foo.get('baz')}/>
        });

    },

    _onChange: function() {
        this.setState(getStateFromStore();
    }

});

组件/FooListItem.react.js

var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')

var Bar = require('../stores/BarStore').Bar;

module.exports = React.createClass({

    mixins: [ImmutableRenderMixin],

    propTypes: {
        fooId: React.PropTypes.number.isRequired,
        fooBar: React.PropTypes.instanceOf(Bar).isRequired,
        fooBaz: React.PropTypes.string.isRequired
    }

    render: function() {

        //we could (should) use a component here but this answer is already too long...
        var bar = <p>Loading...</p>;

        if (bar.isReady()) {
            bar = (
                <div>
                    <p>{bar.get('name')}</p>
                    <p>{bar.get('description')}</p>
                </div>
            );
        }

        return (
            <div>
                <p>{this.props.fooId}</p>
                <p>{this.props.fooBaz}</p>
                {bar}
            </div>
        )

    },

});

让我们通过一个完整的循环 for FooList

状态 1:

  • FooList用户通过控制器视图组件点击页面 /foos/ 列出 Foos
  • FooList控制器视图组件调用FooStore.getAll()
  • _foos地图为空,FooStore因此FooStore通过FooAPI.getAll()
  • 控制器视图组件将FooList自身呈现为加载状态,因为它的state.fooList.size == 0.

这是我们列表的实际外观:

++++++++++++++++++++++++
+                      +
+     "loading..."     +
+                      +
++++++++++++++++++++++++
  • FooAPI.getAll()请求解析并触发FooActionCreators.receiveAllSuccess动作
  • FooStore接收此操作,更新其内部状态,并发出更改。

状态 2:

  • FooList控制器视图组件接收更改事件并更新其状态以从FooStore
  • this.state.fooList.size不再是== 0,因此列表实际上可以呈现自己(请注意,我们使用toJS()显式获取原始 javascript 对象,因为React尚未正确处理非原始对象上的映射)。
  • 我们将所需的道具传递给FooListItem组件。
  • 通过打电话foo.getBar(),我们告诉FooStore他们我们想要Bar回记录。
  • getBar()记录方法Foo检索Bar记录通过BarStore
  • BarStore在它的缓存中没有这条Bar记录_bars,所以它触发一个请求BarAPI来检索它。
  • 所有Foo在控制器视图组件this.sate.fooList中都会发生同样的情况FooList
  • 该页面现在看起来像这样:
++++++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 酒吧:+
+ "加载中..." +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 酒吧:+
+ "加载中..." +
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 酒吧:+
+ "加载中..." +
+ +
++++++++++++++++++++++++++++

- 现在假设BarAPI.get(2)(由 Foo2请求)在BarAPI.get(1)(由 Foo1 请求)之前解析。由于它是异步的,因此完全合理。- BarActionCreators BarStore 的BarAPI触发器通过更新其内部存储来响应此操作并发出更改。这就是现在有趣的部分......BAR_RECEIVED_SUCCESS' action via the. - The

状态 3:

  • 控制器视图组件通过更新其状态来FooList响应更改。BarStore
  • render方法被称为
  • foo.getBar()调用现在Bar从 中检索真实记录BarStore。由于该Bar记录已被有效检索,因此ImmutablePureRenderMixin会将旧道具与当前道具进行比较并确定Bar对象已更改!宾果游戏,我们可以重新渲染FooListItem组件(这里更好的方法是创建一个单独的 FooListBarDetail 组件,只让这个组件重新渲染,这里我们还重新渲染了 Foo 的细节没有改变但是为了简单让我们这样做)。
  • 页面现在看起来像这样:
++++++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 酒吧:+
+ "加载中..." +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 酒吧:+
+ "酒吧名称" +
+“酒吧描述”+
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 酒吧:+
+ "加载中..." +
+ +
++++++++++++++++++++++++++++

如果您希望我从非详细部分添加更多详细信息(例如动作创建者、常量、路由等、使用BarListDetail表单组件、POST 等),请在评论中告诉我 :)。

于 2015-08-26T10:27:09.223 回答
0

我的实现中的一些差异:

  1. 我喜欢采用蝇量级模式的商店。也就是说,除非被迫,否则所有操作都是“getOrRetrieveOrCreate”

  2. 我不得不放弃承诺大力发展以支持事件/状态。异步通信仍应使用 Promise,也就是说,动作中的事物使用它们,否则使用事件进行通信。如果视图总是呈现当前状态,那么您需要像“isLoading”这样的状态来呈现微调器。或者您需要触发一个事件,然后更新视图的状态。我认为用承诺响应行动可能是一种反模式(不完全确定)。

  3. URL 更改会触发相应的操作。GET 应该可以工作并且是幂等的,因此 URL 更改通常不会导致失败。但是,它可能会导致重定向。对于某些操作,我有一个“authRequired”装饰器。如果您未通过身份验证,我们会将您重定向到登录页面,并将目标 URL 列为重定向路径。

  4. 为了验证,我们正在考虑从一个动作开始,在我们开始之前触发一个“xyzModel:willSaveData”;然后触发“xyzModel:didSaveData”或“xyzModel:failedSaveData”事件。监听这些事件的存储将向关心的视图指示“保存”。它还可能向关心的视图指示“hasValidationError”。如果你想消除一个错误。您可以从指示错误“wasReceived”的视图中触发操作,这会删除“hasValidationError”标志,或者可以选择执行其他操作,例如清除所有验证错误。验证很有趣,因为验证方式不同。理想情况下,由于输入元素的限制,您可以创建一个可以接受大多数输入的应用程序。再说一次,

于 2015-05-20T16:36:05.497 回答