前端组件化
一个简单的点赞功能
<!--用火狐浏览器打开-->
<div class="wrapper">
<button class="btn">
<span class="btn-text">点赞</span>
<span>👍</span>
</button>
</div>
<script>
const btn=document.querySelector('.btn');
const btnText=btn.querySelector('.btn-text');
let isLiked=false;
btn.onclick=()=>{
isLiked=!isLiked;
if(isLiked) {
btnText.innerHTML="取消";
}else {
btnText.innerHTML="点赞";
}
}
</script>
结构复用
<div class="wrapper"></div>
<script>
class LikeButton {
render() {
return `
<button class="btn">
<span class="btn-text">点赞</span>
<span>👍</span>
</button>
`
}
}
const wrapper=document.querySelector('.wrapper');
const likeButton1=new LikeButton();
wrapper.innerHTML=likeButton1.render();
const likeButton2=new LikeButton();
wrapper.innerHTML+=likeButton2.render();
</script>
实现简单的组件化
<div class="wrapper"></div>
<script>
const createDOMFromString=(domString)=>{
const div=document.createElement('div');
div.innerHTML=domString;
return div;
}
class LikeButton {
constructor() {
this.state={
isLiked:false
};
}
changeText() {
const btnText=this.el.querySelector('.btn-text');
this.state.isLiked=!this.state.isLiked;
btnText.innerHTML=this.state.isLiked?'取消':'赞成';
}
render() {
this.el=createDOMFromString(`
<button class="btn">
<span class="btn-text">点赞</span>
<span>👍</span>
</button>
`);
this.el.addEventListener('click', this.changeText.bind(this), false);
return this.el;
}
}
const wrapper=document.querySelector('.wrapper');
const likeButton1=new LikeButton();
wrapper.appendChild(likeButton1.render());
const likeButton2=new LikeButton();
wrapper.appendChild(likeButton2.render());
</script>
状态改变->构建新的DOM元素更新页面->重新插入新的DOM元素
<div class="wrapper"></div>
<script>
const createDOMFromString=(domString)=>{
const div=document.createElement('div');
div.innerHTML=domString;
return div;
}
class LikeButton {
constructor() {
this.state={
isLiked:false
};
}
setState(state) {
const oldEl=this.el;
this.state=state;
this.el=this.render();
if (this.onStateChange) this.onStateChange(oldEl, this.el);
}
changeText() {
this.setState({
isLiked:!this.state.isLiked
});
}
render() {
this.el=createDOMFromString(`
<button class="btn">
<span class="btn-text">${this.state.isLiked?'取消':'点赞'}</span>
<span>👍</span>
</button>
`);
this.el.addEventListener('click', this.changeText.bind(this), false);
return this.el;
}
}
const wrapper=document.querySelector('.wrapper');
const likeButton1=new LikeButton();
likeButton1.onStateChange = (oldEl, newEl) => {
wrapper.insertBefore(newEl, oldEl) // 插入新的元素
wrapper.removeChild(oldEl) // 删除旧的元素
}
wrapper.appendChild(likeButton1.render());
const likeButton2=new LikeButton();
likeButton2.onStateChange = (oldEl, newEl) => {
wrapper.insertBefore(newEl, oldEl) // 插入新的元素
wrapper.removeChild(oldEl) // 删除旧的元素
}
wrapper.appendChild(likeButton2.render());
</script>
搭建开发环境
// 安装create-react-app工具(一键生成所需要的工程目录,并做好各种配置和依赖)
// 工具地址:https://github.com/facebookincubator/create-react-app
npm install -g create-react-app
// 把npm的源改成taobao的源
npm config set registry https://registry.npm.taobao.org
// 通过create-react-app命令创建一个名为hello-react的工程
create-react-app hello-react
// 进入工程目录然后通过npm启动工程
cd hello-react
npm start
// http://localhost:3000/
// http://192.168.1.11:3000/
使用JSX描述UI信息
JSX原理
要点:
1.每个DOM元素的结构都可以用JS对象来表示。
2.编译的过程会把类似HTML的JSX结构转换成JS的对象结构。
3.JSX是JS语言的一种语法扩展。
组件的render方法
表达式插入
要点:
1.{}内可以放任何JS的代码。
2.表达式插入不但可以用在标签内部,也可以用在标签的属性上。
3.如果插入的表达式返回null,那么React会什么都不显示,相当于忽略了该表达式的插入。结合条件返回,可以做到显示或隐藏某些元素。
条件返回
{}内也可以放置JSX。
JSX元素变量(即JS对象)
可赋值给变量、作为函数参数传递、作为函数的返回值。
组件的组合、嵌套和组件树
事件监听
要点:
1.React封装了一系列的on*的属性(若没有经过处理,这些on*的事件监听只能用在普通的HTML的标签上,而不能用在组件标签上),地址:https://facebook.github.io/react/docs/events.html#supported-events。
2.事件属性名都必须要用驼峰命名法。
event对象
和普通浏览器一样,事件监听函数会被自动传入一个event对象(event对象并不是由浏览器所提供,而是由react内部所创建)。
关于事件中的this
要点:
1.React调用你所传给它的方法的时候,并不是通过对象方法的方式调用(this.handleClick),而是直接通过函数调用(handleClick),所以事件监听函数内并不能通过this获取到实例。
2.如果你想在事件函数当中使用当前的实例,你需要手动地将实例方法bind到当前实例上再传入给React的onClick事件监听,也可以在bind的时候给事件监听函数传入一些参数。
组件的state和setState
state
一个组件的显示形态是可以由它数据状态和配置参数决定的。
setState接收对象参数
要点:
1.setState方法由父类所提供。当我们调用这个函数的时候,React会更新组件的状态state,并且重新调用render方法,然后再把render方法所渲染的最新的内容显示到页面上。
2.接收一个对象或者函数作为参数。
3.传入一个对象的时候,这个对象表示该组件的新状态。但你只需要传入需要给更新的部分就可以了,而不需要传入整个对象。
setState接收函数参数
当你调用setState的时候,React并不会马上修改state。而是把这个对象放到一个更新队列里面,稍后才会从队列当中把新的状态提取出来合并到state当中,然后再触发组件更新。
setState合并
React内部会把JS事件循环中的消息队列的同一个消息中的setState都进行合并以后再重新渲染组件,因此不用担心多次进行setState会带来性能问题。
配置组件的props
要点:
1.每个组件都接收一个props参数,它是一个对象,包含了所有你对这个组件的配置。
2.组件内部通过this.props的方式获取到组件的参数。
3.在使用一个组件的时候,可以把参数放在标签的属性当中,所有的属性都会作为props对象的键值。
4.可以把任何类型的数据作为组件的参数,包括字符串、数字、对象、数组、甚至是函数等等。
默认配置defaultProps
里面是对props中各个属性的默认配置。
props不可变
props一旦传入,你就不可以在组件内部对它进行修改。但是你可以通过父组件主动重新渲染的方式来传入新的props,从而达到更新的效果。
state vs props
要点:
1.state的主要作用是用于组件保存、控制、修改自己的可变状态。(state是让组件控制自己的状态)
2.props的主要作用是让使用该组件的父组件可以传入参数来配置该组件。(props是让外部对组件自己进行配置)
3.尽量少用state,尽量多用props。
4.没有state的组件叫无状态组件,设置了state的叫做有状态组件。
渲染列表数据
渲染存放JSX元素的数组
<div id="box"></div>
<!--如果往{}中放入一个数组,React会帮你把数组里面的元素一个个的罗列并渲染出来-->
<script type="text/babel">
class Index extends React.Component {
render() {
return (
<div>
{
[
<span>i </span>,
<span>love </span>,
<span>react</span>
]
}
</div>
)
}
}
ReactDOM.render(
<Index />,
document.getElementById('box')
)
</script>
使用map渲染列表数据
<div id="box"></div>
<script type="text/babel">
const users=[
{
username:'osoLife',
age:18,
sex:'male'
},
{
username:'michael',
age:28,
sex:'female'
},
{
username:'moli',
age:38,
sex:'male'
}
]
class Index extends React.Component {
render() {
const arr=[];
// 循环每个用户,构建JSX,push到数组中
for(let user of users) {
arr.push(
<div>
<div>姓名:{user.username}</div>
<div>年龄:{user.age}</div>
<div>性别:{user.sex}</div>
<hr/>
</div>
);
}
return (
<div>{arr}</div>
)
}
}
ReactDOM.render(
<Index />,
document.getElementById('box')
)
</script>
<div id="box"></div>
<script type="text/babel">
const users=[
{
username:'osoLife',
age:18,
sex:'male'
},
{
username:'michael',
age:28,
sex:'female'
},
{
username:'moli',
age:38,
sex:'male'
}
]
class Index extends React.Component {
render() {
return (
<div>
{
users.map(
(user)=>{
return (
<div>
<div>姓名:{user.username}</div>
<div>年龄:{user.age}</div>
<div>性别:{user.sex}</div>
<hr/>
</div>
)
}
)
}
</div>
)
}
}
ReactDOM.render(
<Index />,
document.getElementById('box')
)
</script>
<div id="box"></div>
<script type="text/babel">
const users=[
{
username:'osoLife',
age:18,
sex:'male'
},
{
username:'michael',
age:28,
sex:'female'
},
{
username:'moli',
age:38,
sex:'male'
}
]
class User extends React.Component {
render() {
const {user}=this.props;
return (
<div>
<div>姓名:{user.username}</div>
<div>年龄:{user.age}</div>
<div>性别:{user.sex}</div>
<hr/>
</div>
);
}
}
class Index extends React.Component {
render() {
return (
<div>
{
users.map(
(user)=><User user={user} />
)
}
</div>
);
}
}
ReactDOM.render(
<Index />,
document.getElementById('box')
)
</script>
<!--对于用表达式套数组罗列到页面上的元素,都要为每个元素加上key属性,这个key必须是每个元素唯一的标识-->
实战分析:评论功能(一)
组件划分
React中一切都是组件,用React构建的功能其实就是由各种组件组合而成。
组件实现
// 构建一个新的工程目录
create-react-app comment-app
// 在工程目录下的src/目录下新建四个文件
CommentApp.js
CommentInput.js
CommentList.js
Comment.js
// CommentApp.js
import React,{ Component } from 'react';
import CommentInput from './CommentInput';
import CommentList from './CommentList';
class CommentApp extends React.Component {
render() {
return (
<div>
<CommentInput />
<CommentList />
</div>
)
}
}
export default CommentApp
// CommentInput.js
import React,{ Component } from 'react';
class CommentInput extends React.Component {
render() {
return (
<div>CommentInput</div>
)
}
}
export default CommentInput
// CommentList.js
import React,{ Component } from 'react';
class CommentList extends React.Component {
render() {
return (
<div>CommentList</div>
)
}
}
export default CommentList
// 修改src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import CommentApp from './CommentApp';
import './index.css';
ReactDOM.render(
<CommentApp />,
document.getElementById('root')
)
// 进入工程目录启动工程
npm run start
添加样式
// 修改CommentApp中的render方法,给它添加一个wrapper类名
import React,{ Component } from 'react';
import CommentInput from './CommentInput';
import CommentList from './CommentList';
class CommentApp extends React.Component {
render() {
return (
<div className="wrapper">
<CommentInput />
<CommentList />
</div>
)
}
}
export default CommentApp
// index.css
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #fbfbfb;
}
.wrapper {
width: 500px;
margin: 10px auto;
font-size: 14px;
background-color: #fff;
border: 1px solid #f1f1f1;
padding: 20px;
}
实战分析:评论功能(二)
处理用户输入
// 修改ComponentInput.js
import React,{ Component } from 'react';
class CommentInput extends React.Component {
render() {
return (
<div className="comment-input">
<div className="comment-field">
<span className="comment-field-name">用户名:</span>
<div className="comment-field-input">
<input />
</div>
</div>
<div className="comment-field">
<span className="comment-field-name">评论内容:</span>
<div className="comment-field-input">
<textarea />
</div>
</div>
<div className="comment-field-button">
<button>
发布
</button>
</div>
</div>
)
}
}
export default CommentInput
// 修改index.css
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #fbfbfb;
}
.wrapper {
width: 500px;
margin: 10px auto;
font-size: 14px;
background-color: #fff;
border: 1px solid #f1f1f1;
padding: 20px;
}
/* 评论框样式 */
.comment-input {
background-color: #fff;
border: 1px solid #f1f1f1;
padding: 20px;
margin-bottom: 10px;
}
.comment-field {
margin-bottom: 15px;
display: flex;
}
.comment-field .comment-field-name {
display: flex;
flex-basis: 100px;
font-size: 14px;
}
.comment-field .comment-field-input {
display: flex;
flex: 1;
}
.comment-field-input input,
.comment-field-input textarea {
border: 1px solid #e6e6e6;
border-radius: 3px;
padding: 5px;
outline: none;
font-size: 14px;
resize: none;
flex: 1;
}
.comment-field-input textarea {
height: 100px;
}
.comment-field-button {
display: flex;
justify-content: flex-end;
}
.comment-field-button button {
padding: 5px 10px;
width: 80px;
border: none;
border-radius: 3px;
background-color: #00a3cf;
color: #fff;
outline: none;
cursor: pointer;
}
.comment-field-button button:active {
background: #13c1f1;
}
/* 评论列表样式 */
.comment-list {
background-color: #fff;
border: 1px solid #f1f1f1;
padding: 20px;
}
/* 评论组件样式 */
.comment {
display: flex;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 10px;
padding-bottom: 10px;
min-height: 50px;
}
.comment .comment-user {
flex-shrink: 0;
}
.comment span {
color: #00a3cf;
font-style: italic;
}
.comment p {
margin: 0;
/*text-indent: 2em;*/
}
// 加入处理逻辑
import React,{ Component } from 'react'
class CommentInput extends Component {
constructor() {
super();
this.state={
username:'',
content:''
};
}
handleUsernameChange(event) {
this.setState({
username:event.target.value
});
}
handleContentChange(event) {
this.setState({
content:event.target.value
});
}
handleSubmit() {
if(this.props.onSubmit) {
const {username,content}=this.state;
this.props.onSubmit({username,content});
};
this.setState({content:''});
}
render() {
return (
<div className="comment-input">
<div className="comment-field">
<span className="comment-field-name">用户名:</span>
<div className="comment-field-input">
<input value={this.state.username} onChange={this.handleUsernameChange.bind(this)} />
</div>
</div>
<div className="comment-field">
<span className="comment-field-name">评论内容:</span>
<div className="comment-field-input">
<textarea value={this.state.content} onChange={this.handleContentChange.bind(this)} />
</div>
</div>
<div className="comment-field-button">
<button onClick={this.handleSubmit.bind(this)}>
发布
</button>
</div>
</div>
)
}
}
export default CommentInput
// 要点:
1.<input />、<textarea />、<select />这样的输入控件被设置了value值,那么它们的值永远以被设置的值为准,值不变,value就不会变化。
2.类似于<input />、<select />、<textarea>这些元素的value值被React所控制、渲染的组件,在React当中被称为受控组件。
向父组件传递数据
// 修改CommentApp.js
import React,{ Component } from 'react'
import CommentInput from './CommentInput'
import CommentList from './CommentList'
class CommentApp extends Component {
handleSubmitComment(comment) {
console.log(comment);
}
render() {
return (
<div className="wrapper">
<CommentInput onSubmit={this.handleSubmitComment.bind(this)} />
<CommentList />
</div>
);
}
}
export default CommentApp
实战分析:评论功能(三)
// 修改CommentList.js
import React,{ Component } from 'react'
class CommentList extends Component {
render() {
const comments=[
{
username:'osoLife',
content:'i'
},
{
username:'moli',
content:'love'
},
{
username:'michael',
content:'react'
}
];
return (
<div>
{
comments.map(
(comment,i)=>{
return (
<div key={i}>
{comment.username}:{comment.content}
</div>
)
}
)
}
</div>
);
}
}
export default CommentList
// 修改Comment.js
import React,{ Component } from 'react';
class Comment extends Component {
render() {
return (
<div className="comment">
<div className="comment-user">
<span>{this.props.comment.username} </span>:
</div>
<p>{this.props.comment.content}</p>
</div>
)
}
}
export default Comment
// 修改CommentList.js
import React,{ Component } from 'react';
import Comment from './Comment';
class CommentList extends Component {
// 防止comments不传入的情况
static defaultProps={
comments:[]
}
// 删除测试数据,改成props获取评论数据
render() {
return (
<div>
{
this.props.comments.map(
(comment,i)=><Comment comment={comment} key={i} />
)
}
</div>
);
}
}
export default CommentList
// 修改CommentApp.js
import React,{ Component } from 'react'
import CommentInput from './CommentInput'
import CommentList from './CommentList'
class CommentApp extends Component {
constructor() {
super();
this.state={
comments:[]
}
}
handleSubmitComment(comment) {
this.state.comments.push(comment);
this.setState({
comments:this.state.comments
})
}
render() {
return (
<div className="wrapper">
<CommentInput onSubmit={this.handleSubmitComment.bind(this)} />
<CommentList comments={this.state.comments} />
</div>
);
}
}
export default CommentApp
// 给handleSubmitComment加入简单的数据检查
handleSubmitComment(comment) {
if(!comment) {
return;
}
if(!comment.username) {
return alert('请输入用户名');
}
if(!comment.content) {
return alert('请输入评论内容');
}
this.state.comments.push(comment);
this.setState({
comments:this.state.comments
});
}
前端应用状态管理——状态提升
当某个状态被多个组件依赖或者影响的时候,就把该状态提升到这些组件的最近公共组件中去管理,用props传递数据或者函数来管理这种依赖或者影响的行为。
挂载阶段的组件生命周期(一)
React将组件渲染,并且构造DOM元素然后塞入页面的过程称为组件的挂载。
// 一个组件的方法调用过程
-> constructor()
-> render()
// 然后构造DOM元素插入页面
-> constructor()
-> componentWillMount()
-> render()
// 然后构造DOM元素插入页面
-> componentDidMount()
// ...
// 即将从页面中删除
-> componentWillUnmount()
// 从页面中删除
<div id="box"></div>
<script type="text/babel">
class Header extends React.Component {
constructor() {
super();
console.log('constructor');
}
componentWillMount() {
console.log('component will mount');
}
componentDidMount() {
console.log('component did mount');
}
render() {
console.log('render');
return (
<div>
<h1 className="title">osoLife</h1>
</div>
);
}
}
ReactDOM.render(
<Header />,
document.getElementById("box")
)
</script>
<div id="box"></div>
<script type="text/babel">
class Header extends React.Component {
constructor() {
super();
console.log('constructor');
}
componentWillMount() {
console.log('component will mount');
}
componentDidMount() {
console.log('component did mount');
}
componentWillUnmount() {
console.log('component will unmount');
}
render() {
console.log('render');
return (
<div>
<h1 className="title">osoLife</h1>
</div>
)
}
}
class Index extends React.Component {
constructor() {
super();
this.state={
isShowHeader:true
};
}
handleShowOrHide() {
this.setState({
isShowHeader:!this.state.isShowHeader
});
}
render() {
return (
<div>
{this.state.isShowHeader ? <Header />:null}
<button onClick={this.handleShowOrHide.bind(this)}>
显示或者隐藏标题
</button>
</div>
)
}
}
ReactDOM.render(
<Index />,
document.getElementById('box')
)
</script>
<!--React控制了组件的删除过程,在组件删除之前调用组件定义的componentWillUnmount-->
挂载阶段的组件生命周期(二)
<div id="box"></div>
<script type="text/babel">
class Clock extends React.Component {
constructor() {
super();
this.state={
date:new Date()
};
}
componentWillMount() {
this.timer=setInterval(()=>{
this.setState({
date:new Date()
});
},1000)
}
render() {
return (
<div>
<h1>
<p>现在的时间是</p>
{this.state.date.toLocaleTimeString()}
</h1>
</div>
);
}
}
class Index extends React.Component {
render() {
return (
<div>
<Clock />
</div>
);
}
}
ReactDOM.render(
<Index />,
document.getElementById('box')
)
</script>
<div id="box"></div>
<script type="text/babel">
class Clock extends React.Component {
constructor() {
super();
this.state={
date:new Date()
};
}
// 在componentWillMount进行组件的启动工作,例如Ajax数据拉取、定时器的启动
componentWillMount() {
this.timer=setInterval(()=>{
this.setState({
date:new Date()
});
},1000);
}
// 组件从页面上销毁的时候,需要一些数据的清理,例如定时器的清理
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
return (
<div>
<h1>
<p>现在的时间是</p>
{this.state.date.toLocaleTimeString()}
</h1>
</div>
);
}
}
class Index extends React.Component {
constructor() {
super();
this.state={
isShowClock:true
};
}
handleShowOrHide() {
this.setState({
isShowClock:!this.state.isShowClock
});
}
render() {
return (
<div>
{this.state.isShowClock ? <Clock />:null}
<button onClick={this.handleShowOrHide.bind(this)}>
显示或隐藏时钟
</button>
</div>
);
}
}
ReactDOM.render(
<Index />,
document.getElementById('box')
)
</script>
更新阶段的组件生命周期
// 组件的挂载指的是将组件渲染并且构造DOM元素然后插入页面的过程,这是一个从无到有的过程。React提供一些生命周期函数可以给我们在这个过程中做一些操作。除了挂载阶段,还有一种“更新阶段”。
// 更新阶段的组件生命周期:
1.shouldComponentUpdate(nextProps,nextState):通过这个方法可以控制组件是否重新渲染,如果返回false组件就不会重新渲染。
2.componentWillReceiveProps(nextProps):组件从父组件接收到新的props之前调用。
3.componentWillUpdate():组件开始重新渲染之前调用。
4.componentDidUpdate():组件重新渲染并且把更改变更到真实的DOM以后调用。
ref和React中的DOM操作
// 通过ref属性来获取已经挂载的元素的DOM节点
// 原则:能不用ref就不用
<div id="box"></div>
<script type="text/babel">
class AutoFocusInput extends React.Component {
componentDidMount() {
this.input.focus();
}
render() {
return (
<input ref={(input)=>this.input=input} />
);
}
}
ReactDOM.render(
<AutoFocusInput />,
document.getElementById("box")
)
</script>
<!--可以给组件标签加上ref,这样获取到的是组件在React内部初始化的实例-->
props.children和容器类组件
<!--组件本身是一个不带任何内容的方形容器,我们可以在用这个组件的时候给它传入任意内容 -->
<!--通过给Card组件传入一个content属性,这个属性可以传入任意的JSX结构,然后在Card内部会通过{this.props.content}把内容渲染到页面上-->
<div id="box"></div>
<script type="text/babel">
class Card extends React.Component {
render() {
return (
<div className="card">
<div className="card-content">
{this.props.content}
</div>
</div>
)
}
}
ReactDOM.render(
<Card content={
<div>
<h2>osoLife</h2>
<div>一个前端工程师</div>
订阅我:<input />
</div>
} />,
document.getElementById("box")
)
</script>
<!--问题:如果Card除了content以外还能传入其它属性的话,那么这些JSX和其它属性就会混在一起,不好维护-->
// React会把嵌套的JSX元素一个个都放到数组当中,然后通过props.children传给了Card。
// 由于JSX会把插入表达式里面数组中的JSX一个个罗列下来显示。
// 所有嵌套在组件中的JSX结构都可以在组件内部通过props.children获取到。
<div id="box"></div>
<script type="text/babel">
class Card extends React.Component {
render() {
return (
<div className="card">
<div className="card-content">
{this.props.children}
</div>
</div>
)
}
}
ReactDOM.render(
<Card>
<h2>osoLife</h2>
<div>一个前端工程师</div>
订阅我:<input />
</Card>,
document.getElementById("box")
)
</script>
// 在组件内部把数组中的JSX元素安置在不同的地方示例:
<div id="box"></div>
<script type="text/babel">
class Layout extends React.Component {
render() {
return (
<div className="two-cols-layout">
<div className="sidebar">
{this.props.children[0]}
</div>
<div className="main">
{this.props.children[1]}
</div>
</div>
)
}
}
</script>
dangerouslySetHTML和style属性
dangerouslySetHTML
// 出于安全考虑的原因(XSS攻击),在React当中所有的表达式插入的内容都会被自动转义。
<div id="box"></div>
<script type="text/babel">
class Editor extends React.Component {
constructor() {
super();
this.state={
content:'<h1>osoLife</h1>'
}
}
render() {
return (
<div className="editor-wrapper">
{this.state.content}
</div>
)
}
}
</script>
// 通过dangerouslySetInnerHTML属性动态设置元素的innerHTML(给dangerouslySetInnerHTML传入一个对象,这个对象的__html属性值就相当于元素的innerHTML)
<div id="box"></div>
<script type="text/babel">
class Editor extends React.Component {
constructor() {
super();
this.state={
content:'<h1>osoLife</h1>'
}
}
render() {
return (
<div className="editor-wrapper"
dangerouslySetInnerHTML={{
__html:this.state.content
}} />
)
}
}
ReactDOM.render(
<Editor />,
document.getElementById("box")
)
</script>
style
// 需要把CSS属性变成一个对象再传给元素(原来CSS属性中带“-”的元素都必须去掉“-”换成驼峰命名)
<h1 style={{fontSize:'12px',color:'red'}}>osoLife</h1>
PropTypes和组件参数验证
// 安装第三方库:prop-types
npm install --save prop-types
// 给组件的配置参数加上类型验证
import React,{ Component } from 'react';
import PropTypes from 'prop-types';
class Comment extends Component {
static propTypes={
comment:PropTypes.object
}
render() {
const { comment }=this.props;
return (
<div className="comment">
<div className="comment-user">
<span>{comment.username} </span> :
</div>
<p>{comment.content}</p>
</div>
)
}
}
export default Comment
// 可选参数我们可以通过配置defaultProps,让它在不传入的时候有默认值。
import React,{ Component } from 'react';
import PropTypes from 'prop-types';
class Comment extends Component {
// 通过isRequired关键字来强制组件某个参数必须传入
static propTypes={
comment:PropTypes.object.isRequired
}
render() {
const { comment }=this.props;
return (
<div className="comment">
<div className="comment-user">
<span>{comment.username} </span> :
</div>
<p>{comment.content}</p>
</div>
)
}
}
export default Comment
// PropTypes提供了一系列的数据类型可以用来配置组件的参数
PropTypes.array
PropTypes.bool
PropTypes.func
PropTypes.number
PropTypes.object
PropTypes.string
PropTypes.node
PropTypes.element
...
实战分析:评论功能(四)
// index.css
body {
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #fbfbfb;
}
.wrapper {
width: 500px;
margin: 10px auto;
font-size: 14px;
background-color: #fff;
border: 1px solid #f1f1f1;
padding: 20px;
}
/* 评论框样式 */
.comment-input {
background-color: #fff;
border: 1px solid #f1f1f1;
padding: 20px;
margin-bottom: 10px;
}
.comment-field {
margin-bottom: 15px;
display: flex;
}
.comment-field .comment-field-name {
display: flex;
flex-basis: 100px;
font-size: 14px;
}
.comment-field .comment-field-input {
display: flex;
flex: 1;
}
.comment-field-input input,
.comment-field-input textarea {
border: 1px solid #e6e6e6;
border-radius: 3px;
padding: 5px;
outline: none;
font-size: 14px;
resize: none;
flex: 1;
}
.comment-field-input textarea {
height: 100px;
}
.comment-field-button {
display: flex;
justify-content: flex-end;
}
.comment-field-button button {
padding: 5px 10px;
width: 80px;
border: none;
border-radius: 3px;
background-color: #00a3cf;
color: #fff;
outline: none;
cursor: pointer;
}
.comment-field-button button:active {
background: #13c1f1;
}
/* 评论列表样式 */
.comment-list {
background-color: #fff;
border: 1px solid #f1f1f1;
padding: 20px;
}
/* 评论组件样式 */
.comment {
position: relative;
display: flex;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 10px;
padding-bottom: 10px;
min-height: 50px;
}
.comment .comment-user {
flex-shrink: 0;
}
.comment-username {
color: #00a3cf;
font-style: italic;
}
.comment-createdtime {
padding-right: 5px;
position: absolute;
bottom: 0;
right: 0;
padding: 5px;
font-size: 12px;
}
.comment:hover .comment-delete {
color: #00a3cf;
}
.comment-delete {
position: absolute;
right: 0;
top: 0;
color: transparent;
font-size: 12px;
cursor: pointer;
}
.comment p {
margin: 0;
/*text-indent: 2em;*/
}
code {
border: 1px solid #ccc;
background: #f9f9f9;
padding: 0px 2px;
}
自动聚焦到评论框
// 修改CommentInput.js
// 给输入框元素加上ref以便获取到DOM元素
...
<textarea ref={(textarea)=>this.textarea=textarea} value={this.state.content} onChange={this.handleContentChange.bind(this)} />
...
// 给CommentInput组件加上ComponentDidMount生命周期
static propTypes={
onSubmit:PropTypes.func
}
componentDidMount() {
this.textarea.focus()
}
持久化用户名
// 监听用户名输入框失去焦点事件onBlur
<input value={this.state.username} onBlur={this.handleUsernameBlur.bind(this)} onChange={this.handleUsernameChange.bind(this)} />
// 在handleUsernameBlur中把用户的输入内容保存到LocalStorage当中
// 在handleUsernameBlur中把用户输入的内容传给_saveUsername私有方法(所有私有方法都以_开头)。
_saveUsername(username) {
localStorage.setItem('username',username)
}
handleUsernameBlur(event) {
this._saveUsername(event.target.value)
}
handleUsernameChange(event) {
this.setState({
username:event.target.value
});
}
componentWillMount() {
this._loadUsername()
}
_loadUsername() {
const username=localStorage.getItem('username')
if(username) {
this.setState({
username
})
}
}
// 规范:
组件的私有方法都用“_”开头,所有事件监听的方法都用handle开头,把事件监听方法传给组件的时候,属性名用on开头。
// 组件的内容编写顺序:
1.static开头的类属性,如defaultProps、propTypes。
2.构造函数,constructor。
3.getter/setter。
4.组件生命周期。
5._开头的私有方法。
6.事件监听方法,handle*。
7.render*开头的方法,有时候render()方法里面的内容会分开到不同函数里面进行,这些函数都以render*开头。
8.render()方法。
完整代码
// CommentApp.js
import React, { Component } from 'react';
import CommentInput from './CommentInput';
import CommentList from './CommentList';
class CommentApp extends Component {
constructor() {
super();
this.state = {
comments: []
};
}
handleSubmitComment(comment) {
if(!comment) {
return;
}
if (!comment.username) {
return alert('请输入用户名');
}
if (!comment.content) {
return alert('请输入评论内容');
}
this.state.comments.push(comment);
this.setState({
comments: this.state.comments
});
}
render() {
return (
<div className='wrapper'>
<CommentInput onSubmit={this.handleSubmitComment.bind(this)} />
<CommentList comments={this.state.comments} />
</div>
)
}
}
export default CommentApp
// CommentInput.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class CommentInput extends Component {
static propTypes={
onSubmit:PropTypes.func
}
constructor() {
super();
this.state={
username: '',
content: ''
};
}
componentWillMount() {
this._loadUsername();
}
componentDidMount() {
this.textarea.focus();
}
_saveUsername(username) {
localStorage.setItem('username',username);
}
_loadUsername() {
const username=localStorage.getItem('username');
if(username) {
this.setState({
username
})
};
}
handleUsernameChange(event) {
this.setState({
username: event.target.value
})
}
handleUsernameBlur(event) {
this._saveUsername(event.target.value)
}
handleContentChange(event) {
this.setState({
content: event.target.value
})
}
handleSubmit() {
if (this.props.onSubmit) {
this.props.onSubmit({
username: this.state.username,
content: this.state.content,
})
}
this.setState({ content: '' })
}
render() {
return (
<div className='comment-input'>
<div className='comment-field'>
<span className='comment-field-name'>用户名:</span>
<div className='comment-field-input'>
<input
value={this.state.username}
onBlur={this.handleUsernameBlur.bind(this)}
onChange={this.handleUsernameChange.bind(this)} />
</div>
</div>
<div className='comment-field'>
<span className='comment-field-name'>评论内容:</span>
<div className='comment-field-input'>
<textarea
ref={(textarea)=>this.textarea=textarea}
value={this.state.content}
onChange={this.handleContentChange.bind(this)} />
</div>
</div>
<div className='comment-field-button'>
<button
onClick={this.handleSubmit.bind(this)}>
发布
</button>
</div>
</div>
)
}
}
export default CommentInput
// CommentList.js
import React, { Component } from 'react';
import Comment from './Comment';
class CommentList extends Component {
static defaultProps={
comments: []
}
render() {
return (
<div>
{this.props.comments.map((comment, i) =>
<Comment comment={comment} key={i} />
)}
</div>
)
}
}
export default CommentList
// Comment.js
import React,{ Component } from 'react';
class Comment extends Component {
render() {
return (
<div className='comment'>
<div className='comment-user'>
<span>{this.props.comment.username} </span>:
</div>
<p>{this.props.comment.content}</p>
</div>
)
}
}
export default Comment
实战分析:评论功能(五)
持久化评论
高阶组件
// 高阶组件就是一个函数,传给它一个组件,它返回一个新的组件
// 高阶组件的作用是用于代码复用,可以把组件之间可复用的代码、逻辑抽离到高阶组件当中。新的组件和传入的组件通过props传递信息
const NewComponent=higherOrderComponent(OldComponent)
// 一个简单的高阶组件
import React,{ Component } from 'react';
export default (WrappendComponent)=>{
class NewComponent extends Component {
render() {
return <WrappendComponent />
}
}
return NewComponent;
}
// 给NewComponent做一些数据启动工作
import React,{ Component } from 'react';
export default (WrappendComponent,name)=>{
class NewComponent extends Component {
constructor() {
super();
this.state={
data:null
}
}
componentWillMount() {
let data=localStorage.getItem(name);
this.setState({
data
})
}
render() {
return <WrappendComponent data={this.state.data} />
}
}
return NewComponent;
}
<!--现在NewComponent会根据第二个参数name在挂载阶段从LocalStorage加载数据-->
// 假设上面的代码是在src/wrapWithLoadData.js文件中,我们在别的地方这么用它
import wrapWithLoadData from './wrapWithLoadData';
class InputWithUserName extends Component {
render() {
return <input value={this.props.data} />
}
}
InputWithUserName=wrapWidthLoadData(InputWithUserName,'username')
export default InputWithUserName
// 别人用这个组件的时候实际是用了被加工过的组件
import InputWithUserName from './InputWithUserName';
class Index extends Component {
render() {
return (
<div>
用户名:<InputWithUserName />
</div>
)
}
}
context
// 一个组件可以通过getChildContext方法返回一个对象,这个对象就是子树的context,提供context的组件必须提供childContextTypes作为context的声明和验证。
// 一个组件的context只有它的子组件能够访问,它的父组件是不能访问的。
// 就如全局变量一样,context里面的数据能被随意接触就能被随意修改。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
class Index extends React.Component {
static childContextTypes={
themeColor:PropTypes.string
}
constructor() {
super();
this.state={
themeColor:'red'
};
}
getChildContext() {
return {
themeColor:this.state.themeColor
};
}
render() {
return (
<div>
<Header />
<Main />
</div>
);
}
}
class Header extends React.Component {
render() {
return (
<div>
<h2>This is header</h2>
<Title />
</div>
)
}
}
class Main extends React.Component {
render() {
return (
<div>
<h2>This is main</h2>
<Content />
</div>
)
}
}
class Title extends React.Component {
static contextTypes={
themeColor: PropTypes.string
}
render() {
return (
<h1 style={{color:this.context.themeColor}}>这里的标题是红色</h1>
)
}
}
class Content extends React.Component {
render() {
return (
<div>
<h2>这里是内容</h2>
</div>
)
}
}
ReactDOM.render(
<Index />,
document.getElementById('root')
)
export default Index
Redux(一):优雅地修改共享状态
// 用create-react-app新建一个项目make-redux
// 修改public/index.html
<body>
<div id="title"></div>
<div id="content"></div>
</body>
// 删除src/index.js,添加下面的代码
// 应用的状态
const appState={
title:{
text:'标题',
color:'red'
},
content:{
text:'内容',
color:'blue'
}
}
// 通过渲染函数把上面状态的数据渲染到页面上
function renderApp(appState) {
renderTitle(appState.title);
renderContent(appState.content);
}
function renderTitle(title) {
const titleDOM=document.getElementById('title');
titleDOM.innerHTML=title.text;
titleDOM.style.color=title.color;
}
function renderContent(content) {
const contentDOM=document.getElementById('content');
contentDOM.innerHTML=content.text;
contentDOM.style.color=content.color;
}
// 调用
renderApp(appState);
// 问题:
1.所有对共享状态的操作都是不可预料的。
2.“模块(组件)之间需要共享数据”,和“数据可能被任意修改导致不可预料的结果”之间的矛盾。
// 解决方法:
let appState={
title:{
text:'标题',
color:'red'
},
content:{
text:'内容',
color:'blue'
}
}
// 专门负责数据的修改
// 所有对数据的操作必须通过dispatch函数
// 参数action是一个对象,type字段来声明到底想干什么
function dispatch(action) {
switch(action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text=action.text;
break;
case 'UPDATE_TITLE_COLOR':
appState.title.color=action.color;
break;
default:
break;
}
}
function renderApp(appState) {
renderTitle(appState.title);
renderContent(appState.content);
}
function renderTitle(title) {
const titleDOM=document.getElementById('title');
titleDOM.innerHTML=title.text;
titleDOM.style.color=title.color;
}
function renderContent(content) {
const contentDOM=document.getElementById('content');
contentDOM.innerHTML=content.text;
contentDOM.style.color=content.color;
}
// 首次渲染页面
renderApp(appState);
// 修改标题文本
dispatch({
type:'UPDATE_TITLE_TEXT',
text:'《Helllo World》'
});
// 修改标题颜色
dispatch({
type:'UPDATE_TITLE_COLOR',
color:'blue'
});
// 把新的数据渲染到页面上
renderApp(appState);
Redux(二):抽离store和监控数据变化
抽离出store
let appState={
title:{
text:'标题',
color:'red'
},
content:{
text:'内容',
color:'blue'
}
};
function stateChanger(state,action) {
switch(action.type) {
case 'UPDATE_TITLE_TEXT':
state.title.text=action.text;
break;
case 'UPDATE_TITLE_COLOR':
state.title.color=action.color;
break;
default:
break;
}
};
function createStore(state,stateChanger) {
const getState=()=>state;
const dispatch=(action)=>stateChanger(state,action);
return {
getState,dispatch
};
};
function renderApp(appState) {
renderTitle(appState.title);
renderContent(appState.content);
};
function renderTitle(title) {
const titleDOM=document.getElementById('title');
titleDOM.innerHTML=title.text;
titleDOM.style.color=title.color;
};
function renderContent(content) {
const contentDOM=document.getElementById('content');
contentDOM.innerHTML=content.text;
contentDOM.style.color=content.color;
};
const store=createStore(appState,stateChanger);
renderApp(store.getState());
store.dispatch({
type:'UPDATE_TITLE_TEXT',
text:'《Helllo World》'
});
store.dispatch({
type:'UPDATE_TITLE_COLOR',
color:'blue'
});
renderApp(store.getState());
// 针对每个不同的App,我们可以给createStore传入初始的数据appState,和一个描述数据变化的函数stateChanger,然后生成一个store。需要修改数据的时候通过store.dispatch,需要获取数据的时候通过store.getState。
监控数据变化
let appState={
title:{
text:'标题',
color:'red'
},
content:{
text:'内容',
color:'blue'
}
};
function stateChanger(state,action) {
switch(action.type) {
case 'UPDATE_TITLE_TEXT':
state.title.text=action.text;
break;
case 'UPDATE_TITLE_COLOR':
state.title.color=action.color;
break;
default:
break;
}
};
function createStore(state,stateChanger) {
const listeners=[];
const subscribe=(listener)=>listeners.push(listener);
const getState=()=>state;
const dispatch=(action)=>{
stateChanger(state,action)
listeners.forEach((listener)=>listener())
};
return {
getState,dispatch,subscribe
};
};
function renderApp(appState) {
renderTitle(appState.title);
renderContent(appState.content);
};
function renderTitle(title) {
const titleDOM=document.getElementById('title');
titleDOM.innerHTML=title.text;
titleDOM.style.color=title.color;
};
function renderContent(content) {
const contentDOM=document.getElementById('content');
contentDOM.innerHTML=content.text;
contentDOM.style.color=content.color;
};
const store=createStore(appState,stateChanger);
store.subscribe(()=>renderApp(store.getState()));
renderApp(store.getState());
store.dispatch({
type:'UPDATE_TITLE_TEXT',
text:'《Helllo World》'
});
store.dispatch({
type:'UPDATE_TITLE_COLOR',
color:'blue'
});
Redux(三):纯函数
// 一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
// 函数的返回结果只依赖于它的参数。
Redux(四):共享结构的对象提高性能
// 解决性能问题:完整代码
let appState={
title:{
text:'标题',
color:'red'
},
content:{
text:'内容',
color:'blue'
}
};
function stateChanger(state,action) {
switch(action.type) {
case 'UPDATE_TITLE_TEXT':
state.title.text=action.text;
break;
case 'UPDATE_TITLE_COLOR':
state.title.color=action.color;
break;
default:
break;
}
};
function createStore(state,stateChanger) {
const listeners=[];
const subscribe=(listener)=>listeners.push(listener);
const getState=()=>state;
const dispatch=(action)=>{
stateChanger(state,action)
listeners.forEach((listener)=>listener())
};
return {
getState,dispatch,subscribe
};
};
function renderApp(newAppState,oldAppState={}) {
if(newAppState===oldAppState) {
return;
}
console.log('render app...');
renderTitle(newAppState.title,oldAppState.title);
renderContent(newAppState.content,oldAppState.content);
};
function renderTitle(newTitle,oldTitle={}) {
if(newTitle===oldTitle) {
return;
}
console.log('render title...');
const titleDOM=document.getElementById('title');
titleDOM.innerHTML=newTitle.text;
titleDOM.style.color=newTitle.color;
};
function renderContent(newContent,oldContent={}) {
if(newContent===oldContent) {
return;
}
console.log('render content...');
const contentDOM=document.getElementById('content');
contentDOM.innerHTML=newContent.text;
contentDOM.style.color=newContent.color;
};
const store=createStore(appState,stateChanger);
let oldState=store.getState();
store.subscribe(()=>{
const newState=store.getState();
renderApp(newState,oldState);
oldState=newState;
})
renderApp(store.getState());
store.dispatch({
type:'UPDATE_TITLE_TEXT',
text:'《Helllo World》'
});
store.dispatch({
type:'UPDATE_TITLE_COLOR',
color:'blue'
});
共享结构的对象
// 浅拷贝
const obj={a:1,b:2};
const obj2={...obj};
// 覆盖、拓展对象属性
const obj={a:1,b:2};
const obj2={...obj,b:3,c:4};
优化性能
// 修改stateChanger,让它修改数据的时候,并不会直接修改原来的数据state,而是产生上述的共享结构的对象
function stateChanger(state,action) {
switch(action.type) {
case 'UPDATE_TITLE_TEXT':
// 构建新的对象并且返回
return {
...state,
title:{
...state.title,
text:action.text
}
}
case 'UPDATE_TITLE_COLOR':
// 构建新的对象并且返回
return {
...state,
title:{
...state.title,
color:action.color
}
}
default:
// 没有修改,返回原来的对象
return state;
}
};
// 因为stateChanger不会修改原来对象,而是返回对象,所以需要修改一下createStore。
function createStore(state,stateChanger) {
const listeners=[];
const subscribe=(listener)=>listeners.push(listener);
const getState=()=>state;
const dispatch=(action)=>{
// 覆盖原对象
state=stateChanger(state,action)
listeners.forEach((listener)=>listener())
};
return {
getState,dispatch,subscribe
};
};
Redux(五):不要问为什么的reducer
// 合并appState和stateChanger
function stateChanger(state,action) {
if(!state) {
return {
title:{
text:'标题',
color:'红色'
},
content:{
text:'内容',
color:'蓝色'
}
}
}
switch(action.type) {
case 'UPDATE_TITLE_TEXT':
return {
...state,
title:{
...state.title,
text:action.text
}
}
case 'UPDATE_TITLE_COLOR':
return {
...state,
title:{
...state.title,
color:action.color
}
}
default:
return state;
}
};
Redux(六):Redux总结
// 定义reducer只能是纯函数,功能是负责初始化state,和根据
// state和action计算具有共享结构的新的state
function reducer(state,action) {
// 初始化state和switch case
};
// 生成store
const store=createStore(reducer);
// 监听数据变化重新渲染页面
store.subscribe(()=>renderApp(store.getState()));
// 首次渲染页面
renderApp(store.getState());
// 后面可以随意dispatch了,页面自动更新
store.dispatch(...)
React-redux(一):初始化工程
// 用create-react-app新建一个工程,然后在src/目录下新建三个文件:Header.js、Content.js、ThemeSwitch.js
// 修改Header.js
import React, { Component, PropTypes } from 'react';
class Header extends Component {
render () {
return (
<h1>React.js 小书</h1>
)
}
}
export default Header
// 修改ThemeSwitch.js
import React, { Component, PropTypes } from 'react';
class ThemeSwitch extends Component {
render () {
return (
<div>
<button>Red</button>
<button>Blue</button>
</div>
)
}
}
export default ThemeSwitch
// 修改Content.js
import React, { Component, PropTypes } from 'react';
import ThemeSwitch from './ThemeSwitch'
class Content extends Component {
render () {
return (
<div>
<p>React.js 小书内容</p>
<ThemeSwitch />
</div>
)
}
}
export default Content
// 修改index.js
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import Header from './Header'
import Content from './Content'
import './index.css'
class Index extends Component {
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
ReactDOM.render(
<Index />,
document.getElementById('root')
)
React-redux(二):结合context和store
// index.js
import React,{ Component,PropTypes } from 'react';
import ReactDOM from 'react-dom';
import Header from './Header';
import Content from './Content';
import './index.css';
function createStore(reducer) {
let state=null;
const listeners=[];
const subscribe=(listener)=>listeners.push(listener);
const getState=()=>state;
const dispatch=(action)=>{
state=reducer(state,action)
listeners.forEach((listener)=>listener())
}
dispatch({});
return {
getState,dispatch,subscribe
}
}
const themeReducer=(state,action)=>{
if(!state) {
return {
themeColor:'red'
}
}
switch(action.type) {
case 'CHANGE_COLOR':
return {
...state,themeColor:action.themeColor
}
default:
return state
}
}
const store=createStore(themeReducer)
class Index extends React.Component {
static childContextTypes={
store:PropTypes.object
}
getChildContext() {
return {
store
}
}
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
ReactDOM.render(
<Index />,
document.getElementById('root')
)
// Header.js
import React,{ Component,PropTypes } from 'react';
class Header extends React.Component {
static contextTypes={
store:PropTypes.object
}
constructor() {
super();
this.state={
themeColor:''
}
}
componentWillMount() {
this._updateThemeColor()
}
_updateThemeColor() {
const {store}=this.context;
const state=store.getState();
this.setState({
themeColor:state.themeColor
})
}
render() {
return (
<h1 style={{color:this.state.themeColor}}>标题</h1>
)
}
}
export default Header
// Content.js
import React,{ Component,PropTypes } from 'react';
import ThemeSwitch from './ThemeSwitch';
class Content extends React.Component {
static contextTypes={
store:PropTypes.object
}
constructor() {
super();
this.state={
themeColor:''
}
}
componentWillMount() {
this._updateThemeColor()
}
_updateThemeColor() {
const { store }=this.context;
const state=store.getState();
this.setState({ themeColor: state.themeColor })
}
render() {
return (
<div>
<p style={{color:this.state.themeColor}}>内容</p>
<ThemeSwitch />
</div>
)
}
}
export default Content
网友评论