本文涵盖如下受控组件:css
同时也包含:html
在学习 React.js 时我遇到了一个问题,那就是很难找到受控组件的真实示例。受控文本输入框的例子却是很丰富,但复选框、单选框、下拉选择框的例子却不尽人意。node
本文列举了真实的受控表单组件示例,要是我在学习 React 的时候早点发现这些示例就行了。除了日期和时间输入框须要另开篇幅详细讨论,文中列举了全部的表单元素。react
有时候,为了减小开发时间,有时候人们很容易为了一些东西(譬如表单元素)引入一个库。而对于表单,我发现当须要添加自定义行为或表单校验时,使用库会让事情变得更复杂。不过一旦掌握合适的 React 模式,你会发现构建表单组件并不是难事,而且有些东西彻底能够本身动手,丰衣足食。请把本文的示例代码看成你建立表单组件的起点或灵感之源。ios
除了提供单独的组件代码,我还将这些组件放进表单中,方便你理解子组件如何更新父组件 state ,以及接下来父组件如何经过 props(单向数据流)更新子组件。git
注意:本表单示例由很赞的 create-react-app 构建配置生成,若是你尚未安装该构建配置,我强烈推荐你安装一下(npm install -g create-react-app
)。目前这是搭建 React 应用最简单的方式。github
受控组件有两个特色:npm
onChange
事件发生时控制它们的数据,而不是一次性地获取表单数据(例如用户点提交按钮时)。“被控制“ 的表单数据保存在 state 中(在本文示例中,是父组件或容器组件的 state)。这个单向循环 —— (数据)从(1)子组件输入到(2)父组件的 state,接着(3)经过 props 回到子组件,就是 React.js 应用架构中单向数据流的含义。json
咱们的顶级组件叫作 App
,这是它的代码:
import React, { Component } from 'react'; import '../node_modules/spectre.css/dist/spectre.min.css'; import './styles.css'; import FormContainer from './containers/FormContainer'; class App extends Component { render() { return ( <div className="container"> <div className="columns"> <div className="col-md-9 centered"> <h3>React.js Controlled Form Components</h3> <FormContainer /> </div> </div> </div> ); } } export default App;
App
只负责渲染 index.html
页面。整个 App
组件最有趣的部分是 13 行,FormContainer
组件。
是时候说起一下容器(智能)组件和普通(木偶)组件了。容器组件包含业务逻辑,它会发起数据请求或进行其余业务操做。普通组件则从它的父(容器)组件接收数据。木偶组件有可能触发更新 state (译注:容器组件的 state)这类逻辑行为,但它仅经过从父(容器)组件传入的方法来达到该目的。
注意: 虽然在咱们的表单应用里父组件就是容器组件,但我要强调,并不是全部的父组件都是容器组件。木偶组件嵌套木偶组件也是能够的。
FormContainer
组件包含了表单元素组件,它在生命周期钩子方法 componentDidMount
里请求数据,此外还包含更新表单应用 state 的逻辑行为。在下面的预览代码里,我移除了表单元素的 props 和 change 事件处理方法,这样看起来更简洁清晰(拉到文章底部,能够看到完整代码)。
import React, {Component} from 'react'; import CheckboxOrRadioGroup from '../components/CheckboxOrRadioGroup'; import SingleInput from '../components/SingleInput'; import TextArea from '../components/TextArea'; import Select from '../components/Select'; class FormContainer extends Component { constructor(props) { super(props); this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleClearForm = this.handleClearForm.bind(this); } componentDidMount() { fetch('./fake_db.json') .then(res => res.json()) .then(data => { this.setState({ ownerName: data.ownerName, petSelections: data.petSelections, selectedPets: data.selectedPets, ageOptions: data.ageOptions, ownerAgeRangeSelection: data.ownerAgeRangeSelection, siblingOptions: data.siblingOptions, siblingSelection: data.siblingSelection, currentPetCount: data.currentPetCount, description: data.description }); }); } handleFormSubmit() { // 提交逻辑写在这 } handleClearForm() { // 清除表单逻辑写在这 } render() { return ( <form className="container" onSubmit={this.handleFormSubmit}> <h5>Pet Adoption Form</h5> <SingleInput /> {/* Full name text input */} <Select /> {/* Owner age range select */} <CheckboxOrRadioGroup /> {/* Pet type checkboxes */} <CheckboxOrRadioGroup /> {/* Will you adopt siblings? radios */} <SingleInput /> {/* Number of current pets number input */} <TextArea /> {/* Descriptions of current pets textarea */} <input type="submit" className="btn btn-primary float-right" value="Submit"/> <button className="btn btn-link float-left" onClick={this.handleClearForm}>Clear form</button> </form> ); }
咱们勾勒出了应用基础结构,接下来咱们一块儿浏览下每一个子组件的细节。
<SingleInput />
组件该组件能够是 text
或 number
输入框,这取决于传入的 props。经过 React 的 PropTypes,咱们能够很是好地记录组件拿到的 props。若是漏传 props 或传入错误的数据类型, 浏览器的控制台中会出现警告信息。
下面列举 <SingleInput />
组件的 PropTypes:
SingleInput.propTypes = { inputType: React.PropTypes.oneOf(['text', 'number']).isRequired, title: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired, controlFunc: React.PropTypes.func.isRequired, content: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, ]).isRequired, placeholder: React.PropTypes.string, };
PropTypes 声明了 prop 的类型(string、 number、 array、 object 等等),其中包括了必需(isRequired
)和非必需的 prop,固然它还有更多的用途(欲知更多细节,请查看 React 文档)。
下面咱们逐个讨论这些 PropType:
inputType
:接收两个字符串:'text'
或 'number'
。该设置指定渲染 <input type="text" />
组件或 <input type="number" />
组件。title
:接收一个字符串,咱们将它渲染到输入框的 label 元素中。name
:输入框的 name 属性。controlFunc
:它是从父组件或容器组件传下来的方法。由于该方法挂载在 React 的 onChange 处理方法上,因此每当输入框的输入值改变时,该方法都会被执行,从而更新父组件或容器组件的 state。content
:输入框内容。受控输入框只会显示经过 props 传入的数据。placeholder
:输入框的占位符文本,是一个字符串。既然该组件不须要任何逻辑行为和内部 state,那咱们能够将它写成纯函数组件(pure functional component)。咱们将纯函数组件赋值给一个 const
常量上。下面是 <SingleInput />
组件的全部代码。本文列举的全部表单元素组件都是纯函数组件。
import React from 'react'; const SingleInput = (props) => ( <div className="form-group"> <label className="form-label">{props.title}</label> <input className="form-input" name={props.name} type={props.inputType} value={props.content} onChange={props.controlFunc} placeholder={props.placeholder} /> </div> ); SingleInput.propTypes = { inputType: React.PropTypes.oneOf(['text', 'number']).isRequired, title: React.PropTypes.string.isRequired, name: React.PropTypes.string.isRequired, controlFunc: React.PropTypes.func.isRequired, content: React.PropTypes.oneOfType([ React.PropTypes.string, React.PropTypes.number, ]).isRequired, placeholder: React.PropTypes.string, }; export default SingleInput;
接着,咱们用 handleFullNameChange
方法(它被传入到 controlFunc
prop 属性)来更新 <FormContainer />
容器组件的 state。
// FormContainer.js handleFullNameChange(e) { this.setState({ ownerName: e.target.value }); } // constructor 方法里别漏掉了这行: // this.handleFullNameChange = this.handleFullNameChange.bind(this);
随后咱们将容器组件更新后的 state (译注:这里指 state 上挂载的 ownerName 属性)经过 content
prop 传回 <SingleInput />
组件。
<Select />
组件选择组件(就是下拉选择组件),接收如下 props:
Select.propTypes = { name: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOption: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired, placeholder: React.PropTypes.string };
name
:填充表单元素上 name
属性的字符串变量。options
:是一个数组(本例是字符串数组)。经过在组件的 render 方法中使用 props.options.map()
, 该数组中的每一项都会被渲染成一个选择项。selectedOption
:用以显示表单填充的默认选项,或用户已选择的选项(例如当用户编辑以前已提交过的表单数据时,可使用这个 prop)。controlFunc
:它是从父组件或容器组件传下来的方法。由于该方法挂载在 React 的 onChange 处理方法上,因此每当改变选择框组件的值时,该方法都会被执行,从而更新父组件或容器组件的 state。placeholder
:做为占位文本的字符串,用来填充第一个 <option>
标签。本组件中,咱们将第一个选项的值设置成空字符串(参看下面代码的第 10 行)。import React from 'react'; const Select = (props) => ( <div className="form-group"> <select name={props.name} value={props.selectedOption} onChange={props.controlFunc} className="form-select"> <option value="">{props.placeholder}</option> {props.options.map(opt => { return ( <option key={opt} value={opt}>{opt}</option> ); })} </select> </div> ); Select.propTypes = { name: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOption: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired, placeholder: React.PropTypes.string }; export default Select;
请注意 option 标签中的 key
属性(第 14 行)。React 要求被重复操做渲染的每一个元素必须拥有独一无二的 key
值,咱们这里的 .map()
方法就是所谓的重复操做。既然选择项数组中的每一个元素是独有的,咱们就把它们当成 key
prop。该 key
值协助 React 追踪 DOM 变化。虽然在循环操做或 mapping 时忘加 key
属性不会中断应用,可是浏览器的控制台里会出现警告,而且渲染性能将受到影响。
如下是控制选择框组件(记住,该组件存在于 <FormContainer />
组件中)的处理方法(该方法从 <FormContainer />
组件传入到子组件的 controlFun
prop 中)
// FormContainer.js handleAgeRangeSelect(e) { this.setState({ ownerAgeRangeSelection: e.target.value }); } // constructor 方法里别漏掉了这行: // this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this);
<CheckboxOrRadioGroup />
组件<CheckboxOrRadioGroup />
不同凡响, 它从 props 拿到传入的数组(像此前 <Select />
组件的选项数组同样),经过遍历数组来渲染一组表单元素的集合 —— 能够是复选框集合或单选框集合。
让咱们深刻 PropTypes 来更好地理解 <CheckboxOrRadioGroup />
组件。
CheckboxGroup.propTypes = { title: React.PropTypes.string.isRequired, type: React.PropTypes.oneOf(['checkbox', 'radio']).isRequired, setName: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOptions: React.PropTypes.array, controlFunc: React.PropTypes.func.isRequired };
title
:一个字符串,用以填充单选或复选框集合的 label 标签内容。type
:接收 'checkbox'
或 'radio'
两种配置的一种,并用指定的配置渲染输入框(译注:这里指复选输入框或单选输入框)。setName
:一个字符串,用以填充每一个单选或复选框的 name
属性值。options
:一个由字符串元素组成的数组,数组元素用以渲染每一个单选框或复选框的值和 label 的内容。例如,['dog', 'cat', 'pony']
数组中的元素将会渲染三个单选框或复选框。selectedOptions
:一个由字符串元素组成的数组,用来表示预选项。在示例 4 中,若是 selectedOptions
数组包含 'dog'
和 'pony'
元素,那么相应的两个选项会被渲染成选中状态,而 'cat'
选项则被渲染成未选中状态。当用户提交表单时,该数组将会是用户的选择数据。controlFunc
:一个方法,用来处理从 selectedOptions
数组 prop 中添加或删除字符串的操做。这是本表单应用中最有趣的组件,让咱们来看一下:
import React from 'react'; const CheckboxOrRadioGroup = (props) => ( <div> <label className="form-label">{props.title}</label> <div className="checkbox-group"> {props.options.map(opt => { return ( <label key={opt} className="form-label capitalize"> <input className="form-checkbox" name={props.setName} onChange={props.controlFunc} value={opt} checked={ props.selectedOptions.indexOf(opt) > -1 } type={props.type} /> {opt} </label> ); })} </div> </div> ); CheckboxOrRadioGroup.propTypes = { title: React.PropTypes.string.isRequired, type: React.PropTypes.oneOf(['checkbox', 'radio']).isRequired, setName: React.PropTypes.string.isRequired, options: React.PropTypes.array.isRequired, selectedOptions: React.PropTypes.array, controlFunc: React.PropTypes.func.isRequired }; export default CheckboxOrRadioGroup;
checked={ props.selectedOptions.indexOf(option) > -1 }
这一行代码表示单选框或复选框是否被选中的逻辑。
属性 checked
接收一个布尔值,用来表示 input 组件是否应该被渲染成选中状态。咱们在检查到 input 的值是不是 props.selectedOptions
数组的元素之一时生成该布尔值。myArray.indexOf(item)
方法返回 item 在数组中的索引值。若是 item 不在数组中,返回 -1
,所以,咱们写了 > -1
。
注意,0
是一个合法的索引值,因此咱们须要 > -1
,不然代码会有 bug。若是没有 > -1
,selectedOptions
数组中的第一个 item —— 其索引为 0 —— 将永远不会被渲染成选中状态,由于 0
是一个类 false
的值(译注:在 checked
属性中,0
会被当成 false
处理)。
本组件的处理方法一样比其余的有趣。
handlePetSelection(e) { const newSelection = e.target.value; let newSelectionArray; if(this.state.selectedPets.indexOf(newSelection) > -1) { newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection) } else { newSelectionArray = [...this.state.selectedPets, newSelection]; } this.setState({ selectedPets: newSelectionArray }); }
如同全部处理方法同样,事件对象被传入方法,这样一来咱们就能拿到事件对象的值(译注:准确来讲,应该是事件目标元素的值)。咱们将该值赋给newSelection
常量。接着咱们在函数顶部附近定义 newSelectionArray
变量。由于咱们将在一个 if/else
代码块里对该变量进行赋值,因此用 let
而非 const
来定义它。咱们在代码块外部进行定义,这样一来被定义变量的做用域就是函数内部的最外沿,而且函数内的代码块都能访问到外部定义的变量。
该方法须要处理两种可能的状况。
若是 input 组件的值不在 selectedOptions
数组中,咱们要将值添加进该数组。
若是 input 组件的值在 selectedOptions
数组中,咱们要从数组中删除该值。
添加(第 8 - 10 行):
为了将新值添加进选项数组,咱们经过解构旧数组(数组前的三点...
表示解构)建立一个新数组,而且将新值添加到数组的尾部 newSelectionArray = [...this.state.selectedPets, newSelection];
。
注意,咱们建立了一个新数组,而不是经过相似 .push()
的方法来改变原数组。不改变已存在的对象和数组,而是建立新的对象和数组,这在 React 中是又一个最佳实践。开发者这样作能够更容易地跟踪 state 的变化,而第三方 state 管理库,如 Redux 则能够作高性能的浅比较,而不是阻塞性能的深比较。
删除(第 6 - 8 行):if
代码块借助此前用到的 .indexOf()
小技巧,检查选项是否在数组中。若是选项已经在数组中,经过.filter()
方法,该选项将被移除。 该方法返回一个包含全部知足 filter 条件的元素的新数组(记住要避免在 React 直接修改数组或对象!)。
newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection)
在这种状况下,除了传入到方法中的选项以外,其余选项都会被返回。
<TextArea />
组件<TextArea />
和咱们已提到的那些组件很是类似,除了 resize
和 rows
,目前你应该对它的 props 很熟悉了。
TextArea.propTypes = { title: React.PropTypes.string.isRequired, rows: React.PropTypes.number.isRequired, name: React.PropTypes.string.isRequired, content: React.PropTypes.string.isRequired, resize: React.PropTypes.bool, placeholder: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired };
title
:接收一个字符串,用以渲染文本域的 label 标签内容。rows
:接收一个整数,用来指定文本域的行数。name
:文本域的 name 属性。content
:文本域的内容。受控组件只会显示经过 props 传入的数据。resize
: 接受一个布尔值,用来指定文本域可否调整大小。placeholder
:充当文本域占位文本的字符串。controlFunc
: 它是从父组件或容器组件传下来的方法。由于该方法挂载在 React 的 onChange 处理方法上,因此每当改变选择框组件的值时,该方法都会被执行,从而更新父组件或容器组件的 state。<TextArea />
组件的完整代码:
import React from 'react'; const TextArea = (props) => ( <div className="form-group"> <label className="form-label">{props.title}</label> <textarea className="form-input" style={props.resize ? null : {resize: 'none'}} name={props.name} rows={props.rows} value={props.content} onChange={props.controlFunc} placeholder={props.placeholder} /> </div> ); TextArea.propTypes = { title: React.PropTypes.string.isRequired, rows: React.PropTypes.number.isRequired, name: React.PropTypes.string.isRequired, content: React.PropTypes.string.isRequired, resize: React.PropTypes.bool, placeholder: React.PropTypes.string, controlFunc: React.PropTypes.func.isRequired }; export default TextArea;
<TextAreas />
组件的控制方法和 <SingleInput />
一模一样。细节部分请参考 <SingleInput />
组件。
handleClearForm
和 handleFormSubmit
方法操做整个表单。
既然咱们在表单的各处都使用了单向数据流,那么清除表单数据对咱们来讲也是小菜一碟。<FormContainer />
组件的 state 控制了每一个表单元素的值。该容器的 state 经过 props 传入子组件。只有当 <FormContainer />
组件的 state 改变时,表单组件显示的值才会改变。
清除表单子组件中显示的数据很简单,只要把容器的 state (译注:这里是指 state 对象上挂载的各个变量)设置成空数组和空字符串就能够了(若是有数字输入框的话则是将值设置成 0
)。
handleClearForm(e) { e.preventDefault(); this.setState({ ownerName: '', selectedPets: [], ownerAgeRangeSelection: '', siblingSelection: [], currentPetCount: 0, description: '' }); }
注意,e.preventDefault()
阻止了页面从新加载,接着 setState()
方法用来清除表单数据。
为了提交表单数据,咱们从 state 中抽取须要提交的属性值,建立了一个对象。接着使用 AJAX 库或技术将这些数据发送给 API(本文不包含此类内容)。
handleFormSubmit(e) { e.preventDefault(); const formPayload = { ownerName: this.state.ownerName, selectedPets: this.state.selectedPets, ownerAgeRangeSelection: this.state.ownerAgeRangeSelection, siblingSelection: this.state.siblingSelection, currentPetCount: this.state.currentPetCount, description: this.state.description }; console.log('Send this in a POST request:', formPayload); this.handleClearForm(e); }
请注意咱们在提交数据后执行 this.handleClearForm(e)
清除了表单。
受控表单组件很是适合自定义表单校验。假设要从 <TextArea />
组件中排除字母 "e",能够这样作:
handleDescriptionChange(e) { const textArray = e.target.value.split('').filter(x => x !== 'e'); console.log('string split into array of letters',textArray); const filteredText = textArray.join(''); this.setState({ description: filteredText }); }
把 e.target.value
字符串分割成字母数组,就生成了上述的 textArray
。这样字母 “e” (或其余设法排除的字母)就被过滤掉了。再把剩余的字母组成的数组拼成字符串,最后用该新字符串去设置组件 state。还不错吧?
以上代码放在本文的仓库中,但我将它们注释掉了,你能够按本身的需求自由地调整。
<FormContainer />
组件下面是我承诺给大家的 <FormContainer />
组件完整代码,
import React, {Component} from 'react'; import CheckboxOrRadioGroup from '../components/CheckboxOrRadioGroup'; import SingleInput from '../components/SingleInput'; import TextArea from '../components/TextArea'; import Select from '../components/Select'; class FormContainer extends Component { constructor(props) { super(props); this.state = { ownerName: '', petSelections: [], selectedPets: [], ageOptions: [], ownerAgeRangeSelection: '', siblingOptions: [], siblingSelection: [], currentPetCount: 0, description: '' }; this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleClearForm = this.handleClearForm.bind(this); this.handleFullNameChange = this.handleFullNameChange.bind(this); this.handleCurrentPetCountChange = this.handleCurrentPetCountChange.bind(this); this.handleAgeRangeSelect = this.handleAgeRangeSelect.bind(this); this.handlePetSelection = this.handlePetSelection.bind(this); this.handleSiblingsSelection = this.handleSiblingsSelection.bind(this); this.handleDescriptionChange = this.handleDescriptionChange.bind(this); } componentDidMount() { // 模拟请求用户数据 //(create-react-app 构建配置里包含了 fetch 的 polyfill) fetch('./fake_db.json') .then(res => res.json()) .then(data => { this.setState({ ownerName: data.ownerName, petSelections: data.petSelections, selectedPets: data.selectedPets, ageOptions: data.ageOptions, ownerAgeRangeSelection: data.ownerAgeRangeSelection, siblingOptions: data.siblingOptions, siblingSelection: data.siblingSelection, currentPetCount: data.currentPetCount, description: data.description }); }); } handleFullNameChange(e) { this.setState({ ownerName: e.target.value }); } handleCurrentPetCountChange(e) { this.setState({ currentPetCount: e.target.value }); } handleAgeRangeSelect(e) { this.setState({ ownerAgeRangeSelection: e.target.value }); } handlePetSelection(e) { const newSelection = e.target.value; let newSelectionArray; if(this.state.selectedPets.indexOf(newSelection) > -1) { newSelectionArray = this.state.selectedPets.filter(s => s !== newSelection) } else { newSelectionArray = [...this.state.selectedPets, newSelection]; } this.setState({ selectedPets: newSelectionArray }); } handleSiblingsSelection(e) { this.setState({ siblingSelection: [e.target.value] }); } handleDescriptionChange(e) { this.setState({ description: e.target.value }); } handleClearForm(e) { e.preventDefault(); this.setState({ ownerName: '', selectedPets: [], ownerAgeRangeSelection: '', siblingSelection: [], currentPetCount: 0, description: '' }); } handleFormSubmit(e) { e.preventDefault(); const formPayload = { ownerName: this.state.ownerName, selectedPets: this.state.selectedPets, ownerAgeRangeSelection: this.state.ownerAgeRangeSelection, siblingSelection: this.state.siblingSelection, currentPetCount: this.state.currentPetCount, description: this.state.description }; console.log('Send this in a POST request:', formPayload) this.handleClearForm(e); } render() { return ( <form className="container" onSubmit={this.handleFormSubmit}> <h5>Pet Adoption Form</h5> <SingleInput inputType={'text'} title={'Full name'} name={'name'} controlFunc={this.handleFullNameChange} content={this.state.ownerName} placeholder={'Type first and last name here'} /> <Select name={'ageRange'} placeholder={'Choose your age range'} controlFunc={this.handleAgeRangeSelect} options={this.state.ageOptions} selectedOption={this.state.ownerAgeRangeSelection} /> <CheckboxOrRadioGroup title={'Which kinds of pets would you like to adopt?'} setName={'pets'} type={'checkbox'} controlFunc={this.handlePetSelection} options={this.state.petSelections} selectedOptions={this.state.selectedPets} /> <CheckboxOrRadioGroup title={'Are you willing to adopt more than one pet if we have siblings for adoption?'} setName={'siblings'} controlFunc={this.handleSiblingsSelection} type={'radio'} options={this.state.siblingOptions} selectedOptions={this.state.siblingSelection} /> <SingleInput inputType={'number'} title={'How many pets do you currently own?'} name={'currentPetCount'} controlFunc={this.handleCurrentPetCountChange} content={this.state.currentPetCount} placeholder={'Enter number of current pets'} /> <TextArea title={'If you currently own pets, please write their names, breeds, and an outline of their personalities.'} rows={5} resize={false} content={this.state.description} name={'currentPetInfo'} controlFunc={this.handleDescriptionChange} placeholder={'Please be thorough in your descriptions'} /> <input type="submit" className="btn btn-primary float-right" value="Submit"/> <button className="btn btn-link float-left" onClick={this.handleClearForm}>Clear form</button> </form> ); } } export default FormContainer;
我认可用 React 构建受控表单组件要作一些重复劳动(好比容器组件中的处理方法),但就你对应用的掌控度和 state 变动的透明度来讲,预先投入精力是超值的。你的代码会变得可维护而且很高效。
若是想在我发布新文章时接到通知,你能够在博客的导航栏部分注册个人邮件发送清单。
iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。