React入门
一.构建项目
– 使用 create-react-app 创建一个React应用
npm install -g create-react-app
create-react-app --version
*v1.4.1
create-react-app hackernews
cd hackernews
// 在 http://localhost:3000 启动应用
npm start
// 运行所有测试
npm test
// 构建项目的产品文件
npm run build
– JSX 混合使用了 HTML 和 JavaScript 在 React 组件的方法中定义它的输出(html + js混编)
ReactDOM.render(
<h1>Hello React World</h1>,
document.getElementById('root')
);
– React 中,组件、示例和元素是不同的概念
import React, { Component } from 'react';
class App extends Component { //App为组件
– ReactDOM.render() 是 React 应用连接 DOM 的入口方法
render() {
...
}
– JavaScript 内建函数可以在 JSX 中使用
略
– map 遍历方法,可以被用来把列表成员渲染成 HTML 的元素
const list = [
{
title: '百度',
url: 'https://www.baidu.com',
author: '李彦宏',
objectID: 0,
},
{
title: 'QQ',
url: 'https://www.qq.com',
author: '马化腾',
objectID: 1,
},
];
使用map方法遍历以上数据
list.map( (item,key) =>
– ES6箭头函数简单写法
原:
{list.map(function(item, key) {
return (
<div key={item.objectID}>...</div>
);
})
改写为:
{list.map( (item, key)=>
<div key={item.objectID}></div>
)}
二.React基础
组件内部状态
组件内部状态也被称为局部状态,允许你保存、修改和删除存储在组件内部的属性。使用 ES6 类组件可以在构造函数中初始化组件的状态。构造函数只会在组件初始化时调用一次。
class App extends Component {
constructor(props) {
super(props);
}
this.state = {
list: list,
};
}
当你使用 ES6 编写的组件有一个构造函数时,它需要强制地调用 super(); 方法,因为这个App 组件是 Component 的子类。
你也可以调用 super(props); ,它会在你的构造函数中设置 this.props 以供在构造函数中访问它们。否则当在构造函数中访问 this.props ,会得到 undefined 。
state 通过使用 this 绑定在类上。因此,你可以在整个组件中访问到 state。例如它可以用在 render() 方法中。
例:list.map(function(item, key) ===》 thislist.map(function(item, key)
ES6 对象初始化
// ES5
var userService = {
getUserName: function (user) {
return user.firstname + ' ' + user.lastname;
},
};
// ES6
const userService = {
getUserName(user) {
return user.firstname + ' ' + user.lastname;
},
};
//值得一提的是你可以在 ES6 中使用计算属性名。
// ES5
var user = {
name: 'Robin',
};
// ES6
const key = 'name';
const user = {
[key]: 'Robin',
}
单向数据流
通过 JavaScript 内置的 filter 方法来删除列表中的一项。fitler 方法以一个函数作为输入。这个函数可以访问列表中的每一项,因为它会遍历整个列表。通过这种方式,你可以基于过滤条件来判断列表的每一项。如果该项判断结果为 true,则该项保留在列表中。否则将从列表中过滤掉。另外,好的一点是这个方法会返回一个新的列表而不是改变旧列表。它遵循了 React 中不可变数据的约定。
class App extends Component {
constructor(props) {
super(props);
this.state = {
list: list,
};
this.clickFunc = this.clickFunc.bind(this);
}
clickFunc(id) {
const isNotId = item => item.objectID !== id;
const updatedList = this.state.list.filter(isNotId);
this.setState({list: updatedList});
}
render() {
return (
<div className="App">
{this.state.list.map( (item,key) =>
<div key={item.objectID}>
<span>
<a href={item.url}>{item.title}</a>
</span>
<span>{item.author}</span>
<span>{item.num_comments}</span>
<span>{item.points}</span>
<span>
<button
onClick={() => this.clickFunc(item.objectID)}
type="button"
>
delete
</button>
</span>
<span>{key}</span>
</div>
)}
</div>
);
}
}
绑定
当使用 ES6 编写的 React 组件时,了解在 JavaScript 类的绑定会非常重要
如上文的:this.onDismiss = this.onDismiss.bind(this);
为什么一开始就需要这么做呢?绑定的步骤是非常重要的,因为类方法不会自动绑定this到实例上。如果没有进行绑定,则this会指向undefined。这也是React的主要BUG来源!
类方法的绑定也可以写起其他地方,比如写在 render() 函数中。
render() {
return (
<button
onClick={this.onClickMe.bind(this)}
type="button"
>
Click Me
</button>
);
}
但是你应该避免这样做,因为它会在每次 render() 方法执行时绑定类方法。总结来说组件每次运行更新时都会导致性能消耗。当在构造函数中绑定时,绑定只会在组件实例化时运行一次,这样做是一个更好的方式。
还有别的方法,在构造函数内部改写。但是,构造函数目的只是实例化你的类以及所有的属性。这就是为什么我们应该把业务逻辑应该定义在构造函数之外。
class ExplainBindingsComponent extends Component {
constructor() {
super();
this.doSomething = this.doSomething.bind(this);
this.doSomethingElse = this.doSomethingElse.bind(this);
}
doSomething() {
// do something
}
doSomethingElse() {
// do something else
}
...
}
提一下,类方法可以使用ES6 的箭头函数做到自动地绑定。
class ExplainBindingsComponent extends Component {
onClickMe = () => {
console.log(this);
}
render() {
return (
<button
onClick={this.onClickMe}
type="button"
>
Click Me
</button>
);
}
}
如果在构造函数中的重复绑定对你有所困扰,你可以使用这种方式代替。React 的官方文档中坚持在构造函数中绑定类方法。
事件处理
传给元素事件处理器的内容必须是函数。作为一个示例,请尝试以下代码:
class App extends Component {
...
render() {
return (
<div className="App">
{this.state.list.map(item =>
<div key={item.objectID}>
<span>
<button onClick={console.log(item.objectID)} type="button" > Dismiss </button> </span> </div> )} </div> ); }}它会在浏览器加载该程序时执行,但点击按钮时并不会。而下面的代码只会在点击按钮时
执行。它是一个在触发事件时才会执行的函数。
和表单交互
第一步,你需要在 JSX 中定义一个带有输入框的表单。
class App extends Component {
...
render() {
return (
<div className="App">
<form>
<input type="text" />
</form>
{this.state.list.map(item =>
...
)}
</div>
);
}
}
在下面的场景中,将会使用在输入框中的内容作为搜索字段来临时过滤列表。为了能根据输入框的值过滤列表,你需要将输入的值储存在你的本地状态中,但是如何访问这个值呢?你可以使用 React 的合成事件来访问事件返回值。让我们为输入框定义一个 onChange 处理程序。
class App extends Component {
...
render() {
return (
<div className="App">
<form>
<input
type="text"
onChange={this.onSearchChange}
/>
</form>
...
</div>
);
}
}
又是一个类方法。初始搜索栏为空。
class App extends Component {
constructor(props) {
super(props);
this.state = {
list,
searchTerm: '',
};
this.onSearchChange = this.onSearchChange.bind(this);
this.onDismiss = this.onDismiss.bind(this);
}
...
onSearchChange(event) {
this.setState({ searchTerm: event.target.value });
}
}
现在列表还没有根据储存在本地状态中的输入字段进行过滤。在 map 映射列表之前,插入一个过滤的方法。
{this.state.list.filter(isSearched(this.state.searchTerm)).map(item =>
在函数外部定义一个方
法
function isSearched(searchTerm) {
return function(item) {
return item.title.toLowerCase().includes(searchTerm.toLowerCase());
}
}
ES6 解构
在 JavaScript ES6 中有一种更方便的方法来访问对象和数组的属性,叫做解构。
比较一下
const user = {
firstname: 'Robin',
lastname: 'Wieruch',
};
// ES5
var firstname = user.firstname;
var lastname = user.lastname;
console.log(firstname + ' ' + lastname);
// output: Robin Wieruch
// ES6
const { firstname, lastname } = user;
console.log(firstname + ' ' + lastname);
//又或者
const users = ['Robin', 'Andrew', 'Dan'];
const [
userOne,
userTwo,
userThree
] = users;
console.log(userOne, userTwo, userThree);
解构,可以让 map 和filter 部分的代码更简短。
render() {
const { searchTerm, list } = this.state;
return (
<div className="App">
...
{list.filter(isSearched(searchTerm)).map(item =>
...
)}
</div>
);
贴一个ES5、ES6对比
// ES5
var searchTerm = this.state.searchTerm;
var list = this.state.list;
// ES6
const { searchTerm, list } = this.state;
受控组件
表单元素比如 <input> , <textarea> 和 <select> 会以原生 HTML 的形式保存他们自己的状态。一旦有人从外部做了一些修改,它们就会修改内部的值,在 React 中这被称为不受控组件,因为它们自己处理状态。在 React 中,你应该确保这些元素变为受控组件。
<form>
<input
type="text"
value={searchTerm}
onChange={this.onSearchChange}
/>
</form>
拆分组件
现在,你有一个大型的 App 组件。它在不停地扩展,最终可能会变得混乱。你可以开始将它拆分成若干个更小的组件。
让我们开始使用一个用于搜索的输入组件和一个用于展示的列表组件。
class App extends Component {
...
render() {
const { searchTerm, list } = this.state;
return (
<div className="App">
<Search />
<Table />
</div>
);
}
}
继续拆分
Search组件
class Search extends Component {
render() {
const { value, onChange } = this.props;
return (
<form>
<input
type="text"
value={value}
onChange={onChange}
/>
</form>
);
}
}
Table组件
class Table extends Component {
render() {
const { list, pattern, onDismiss } = this.props;
return (
<div>
{list.filter(isSearched(pattern)).map(item =>
<div key={item.objectID}>
<span>
<a href={item.url}>{item.title}</a>
</span>
<span>{item.author}</span>
<span>{item.num_comments}</span>
<span>{item.points}</span>
<span>
<button
onClick={() => onDismiss(item.objectID)}
type="button"
>
Dismiss
</button>
</span>
</div>
)}
</div>
);
}
}
就这样,算是提取出来了
因为组件是通过props 对象来获取它们的值,所以当你在别的地方重用它时,你可以每一次都传递不同的props,这些组件就变得可复用了。
可组合组件
{children}--它不仅可以把文本作为子元素传递,还可以将一个元素或者元素树(它还可以再次封装成组件)作为子元素传递。children属性让组件相互组合到一起成为可能。
可复用组件
给组件声明样式
可以复用 src/App.css 和 src/index.css 文件。因为你是用 create-react-app 来创建的,所以这些文件应该已经在你的项目中了。它们应该也被引入到你的 src/App.js 和 src/index.js 文件中了。我准备了一些 CSS,你可以直接复制粘贴到这些文件中,你也可以随意使用你自己的样式。
index.css
body {
color: #222;
background: #f4f4f4;
font: 400 14px CoreSans, Arial,sans-serif;
}
a {
color: #222;
}
a:hover {
text-decoration: underline;
}
ul, li {
list-style: none;
padding: 0;
margin: 0;
}
input {
padding: 10px;
border-radius: 5px;
outline: none;
margin-right: 10px;
border: 1px solid #dddddd;
}
button {
padding: 10px;
border-radius: 5px;
border: 1px solid #dddddd;
background: transparent;
color: #808080;
cursor: pointer;
}
button:hover {
color: #222;
}
*:focus {
outline: none;
}
app.css
.page {
margin: 20px;
}
.interactions {
text-align: center;
}
.table {
margin: 20px 0;
}
.table-header {
display: flex;
line-height: 24px;
font-size: 16px;
padding: 0 10px;
justify-content: space-between;
}
.table-empty {
margin: 200px;
text-align: center;
font-size: 16px;
}
.table-row {
display: flex;
line-height: 24px;
white-space: nowrap;
margin: 10px 0;
padding: 10px;
background: #ffffff;
border: 1px solid #e3e3e3;
}
.table-header > span {
overflow: hidden;
text-overflow: ellipsis;
padding: 0 5px;
}
.table-row > span {
overflow: hidden;
text-overflow: ellipsis;
padding: 0 5px;
}
.button-inline {
border-width: 0;
background: transparent;
color: inherit;
text-align: inherit;
-webkit-font-smoothing: inherit;
padding: 0;
font-size: inherit;
cursor: pointer;
}
.button-active {
border-radius: 0;
border-bottom: 1px solid #38BB6C;
}
内联样式
<span style={{ width: '40%' }}>
也可使用style包起来
const largeColumn = {
width: '40%',
};
const midColumn = {
width: '30%',
};
const smallColumn = {
width: '10%',
};
//随后你可以将它们用于你的 columns : <span style={smallColumn}> 。
回顾基础
• React
– 使用 this.state 和 setState() 来管理你的内部组件状态– 将函数或者类方法传递到你的元素处理器
– 在 React 中使用表单或者事件来添加交互
– 在 React 中单向数据流是一个非常重要的概念
– 拥抱 controlled components
– 通过 children 和可复用组件来组合组件
– ES6 类组件和函数式无状态组件的使用方法和实现
– 给你的组件声明样式的方法
• ES6
– 绑定到一个类的函数叫作类方法– 解构对象和数组
– 默认参数
• General
– 高阶函数三.生命周期
组件的生命周期分成三个状态:
Mounting:已插入真实 DOM
Updating:正在被重新渲染
Unmounting:已移出真实 DOM
React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。
componentWillMount()
componentDidMount()
//第一次加载完成的事件,一般在此获取网络数据
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
//更新之后触发事件,用于清空或更新数据
componentWillUnmount()
//组件销毁时触发的事件,一般用于存储信息,清楚定时器
componentWillReceiveProps(object nextProps):
//已加载组件收到新的参数时调用
shouldComponentUpdate(object nextProps, object nextState):
//组件判断是否重新渲染时调用,一般用于性能优化
这些方法的详细说明,可以参考官方文档。下面是一个例子
var Hello = React.createClass({
getInitialState: function () {
return {
opacity: 1.0
};
},
componentDidMount: function () {
this.timer = setInterval(function () {
var opacity = this.state.opacity;
opacity -= .05;
if (opacity < 0.1) {
opacity = 1.0;
}
this.setState({
opacity: opacity
});
}.bind(this), 100);
},
render: function () {
return (
<div style={{opacity: this.state.opacity}}>
Hello {this.props.name}
</div>
);
}});
ReactDOM.render(
<Hello name="world"/>,
document.body);
上面代码在hello组件加载以后,通过 componentDidMount 方法设置一个定时器,每隔100毫秒,就重新设置组件的透明度,从而引发重新渲染。
另外,组件的style属性的设置方式也值得注意,不能写成
style="opacity:{this.state.opacity};"
而要写成
style={{opacity: this.state.opacity}}
这是因为 React 组件样式是一个对象,所以第一重大括号表示这是 JavaScript 语法,第二重大括号表示样式对象。
getInitialState
初始化组件 state 数据,但是在 es6 的语法中,我们可以使用以下书写方式代替
class Hello extends React.Component {
constructor(props, context) {
super(props, context);
// 初始化组件 state 数据
this.state = { now: Date.now()
}
}
}
Render
返回要渲染的模板
举个栗子,下面是React与Ajax的配合
组件的数据来源,通常是通过Ajax请求从服务器获取,可以使用componentdidmount方法设置Ajax请求,等到请求成功,再用this.setstate方法重新渲染UI
var UserGist = React.createClass({
getInitialState: function() {
return {
username: '',
lastGistUrl: ''
};
},
componentDidMount: function() {
$.get(this.props.source, function(result) {
var lastGist = result[0];
if (this.isMounted()) {
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}
}.bind(this));
},
render: function() {
return (
<div>
{this.state.username}'s last gist is
<a href={this.state.lastGistUrl}>here</a>.
</div>
);
}});
ReactDOM.render(
<UserGist source="https://api.github.com/users/octocat/gists" />,
document.body);
上面代码使用jQuery完成Ajax请求,这是为了便于说明。React本身没有任何依赖,完全可以不用jQuery,而使用其他库。
我们甚至可以把一个Promise对象传入组件
ReactDOM.render(
<RepoList
promise={$.getJSON('https://api.github.com/search/repositories?q=javascript&sort=stars')}
/>,
document.body);
上面代码从Github的API抓取数据,然后将Promise对象作为属性,传给RepoList组件。
如果Promise对象正在抓取数据(pending状态),组件显示“正在加载”;如果Promise对象报错(rejected状态),组件显示报错信息;如果Promise对象抓取数据成功(fulfilled状态),组件显示获取的数据。
var RepoList = React.createClass({
getInitialState: function() {
return { loading: true, error: null, data: null};
},
componentDidMount() {
this.props.promise.then(
value => this.setState({loading: false, data: value}),
error => this.setState({loading: false, error: error}));
},
render: function() {
if (this.state.loading) {
return <span>Loading...</span>;
}
else if (this.state.error !== null) {
return <span>Error: {this.state.error.message}</span>;
}
else {
var repos = this.state.data.items;
var repoList = repos.map(function (repo) {
return (
<li>
<a href={repo.html_url}>{repo.name}</a> ({repo.stargazers_count} stars) <br/> {repo.description}
</li>
);
});
return (
<main>
<h1>Most Popular JavaScript Projects in Github</h1>
<ol>{repoList}</ol>
</main>
);
}
}});
四.数据交互
数据请求
上面介绍了一下React-Ajax数据请求,现在来一个正式的。
设置好 URL 常量和默认参数,来将 API 请求分解成几步。
import React, { Component } from 'react';
import './App.css';
const DEFAULT_QUERY = 'redux';
const PATH_BASE = 'https://hn.algolia.com/api/v1';
const PATH_SEARCH = '/search';
const PARAM_SEARCH = 'query=';
ES6 中,你可以用模板字符串(template strings)去连接字符串。你将用它来拼接最终的 API 访问地址。
// ES6
const url = `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${DEFAULT_QUERY}`;
// ES5
var url = PATH_BASE + PATH_SEARCH + '?' + PARAM_SEARCH + DEFAULT_QUERY;
console.log(url);
// output: https://hn.algolia.com/api/v1/search?query=redux
开始请求吧
先给出全部代码,首先组件通过构造函数得到初始化,之后它将初始化的状态渲染出来。
因为此时本地状态中的结果为空,便阻止了组件的显示。React 允许组件通过返回 null 来不渲染任何东西。
接着 componentDidMount() 生命周期函数执行。在这个方法中你从 Hacker News API 中异步地拿到了数据。
一旦数据到达,组件就通过 setSearchTopStories() 函数改变组件内部的状态。
因为状态的更新,更新生命周期开始运行。
组件再次执行 render() 方法,但这次组件的内部状态中的结果已经填充,因此将重新渲染 Table 组件的内容。
class App extends Component {
constructor(props) {
super(props);
this.state = {
result: null,
searchTerm: DEFAULT_QUERY,
};
this.setSearchTopStories = this.setSearchTopStories.bind(this);
this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
this.onDismiss = this.onDismiss.bind(this);
}
setSearchTopStories(result) {
this.setState({ result });
}
fetchSearchTopStories(searchTerm) {
fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTerm}`)
.then(response => response.json())
.then(result => this.setSearchTopStories(result))
.catch(e => e);
}
componentDidMount() {
const { searchTerm } = this.state;
this.fetchSearchTopStories(searchTerm);
}
...
}
上面使用的fetch被主流浏览器支持,也可以使用axios或者superagent等第三方库。
扩展操作符
接着上文的请求获得了数据,但存在一些BUG。这个时候的数据获得了,但是按钮不工作,这是因为这个点击方法不能处理复杂的result对象了。现在,我们去操作这个result对象而不是去操作list列表。
看看这个方法
onDismiss(id) {
const isNotId = item => item.objectID !== id;
const updatedHits = this.state.result.hits.filter(isNotId);
this.setState({
...
});
}
发现result中的结构不一,解决方法之一是直接改变 result 对象中的 hits 字段,但是React 拥护不可变的数据结构,所以不应该去改变一个对象(状态),而是应该去新建一个新的对象。这样做的好处是原有对象的状态也存在,数据结构不改变。错误用法:
// don`t do this
this.state.result.hits = updatedHits;
用 JavaScript ES6 中的 Object.assign() 函数。它把接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。只要把目标对象设置成一个空对象,我们就得到了一个新的对象。这种做法是拥抱不变性的,因为没有任何源对象被改变。以下是代码实现:
const updatedHits = { hits: updatedHits };
const updatedResult = Object.assign({}, this.state.result, updatedHits);
现在来改写一下点击方法
onDismiss(id) {
const isNotId = item => item.objectID !== id;
const updatedHits = this.state.result.hits.filter(isNotId);
this.setState({
result: Object.assign({}, this.state.result, { hits: updatedHits })
});
}
在 JavaScript ES6 以及之后的 JavaScript 版本中还有一个更简单的方法--扩展操作符。它只由三个点组成: ... 当使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象。
最终,它可以用来代替 Object.assign()
onDismiss(id) {
const isNotId = item => item.objectID !== id;
const updatedHits = this.state.result.hits.filter(isNotId);
this.setState({
result: { ...this.state.result, hits: updatedHits }
});
}
条件渲染
数据驱动视图,根据数据来选择是否渲染视图
举个栗子
render() {
return (
<div className="page">
<div className="interactions">
<Search
value={searchTerm}
onChange={this.onSearchChange}
>
Search
</Search>
</div>
{ result
? <Table
list={result.hits}
pattern={searchTerm}
onDismiss={this.onDismiss}
/>
: null
}
</div>
);
}
当 API 的结果还没返回时,此时的 App 组件没有返回任何元素。这已经是一个条件渲染了,因为在某个特定条件下,render() 方法提前返回了。根据条件,App 组件渲染它的元素或者什么都不渲染。还有一种,运用 && 逻辑运算符。在 JavaScript 中, true&& 'Hello World' 的值永远是“Hello World”。而 false && 'Hello World' 的值则永远是false。
const result = true && 'Hello World';
console.log(result);
// output: Hello World
const result = false && 'Hello World';
console.log(result);
// output: false
在 React 中你也可以利用这个运算符。如果条件判断为 true, && 操作符后面的表达式的值
将会被输出。如果条件判断为 false,React 将会忽略并跳过后面的表达式。这个操作符可
以用来实现 Table 组件的条件渲染,因为它返回一个 Table 组件或者什么都不返回。
{ result &&
<Table
list={result.hits}
pattern={searchTerm}
onDismiss={this.onDismiss}
/>
}
客户端处理-服务端搜索
使用 Search 组件的输入栏时,在请求之前,应先把数据过滤 。在 App 组件中定义一个 onSearchSubmit() 方法。当 Search 组件执行搜索时,可以用API 获取结果。这与 componentDidMount() 生命周期方法中的获取数据的方式相同。
class App extends Component {
constructor(props) {
super(props);
this.state = {
result: null,
searchTerm: DEFAULT_QUERY,
};
this.setSearchTopStories = this.setSearchTopStories.bind(this);
this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
this.onSearchSubmit = this.onSearchSubmit.bind(this);
this.onDismiss = this.onDismiss.bind(this);
}
...
onSearchSubmit() {
const { searchTerm } = this.state;
this.fetchSearchTopStories(searchTerm);
}
...
}