ReactJs是目前比较火的前端框架,作为一个前端开发者,对这一块迟迟没有了解实在说不过去。今次文章的目的在于快速对其做一个使用层面的了解(通俗点讲,就是小白文,好吧,现在也只能写小白文),主要参考阮一峰老师的blog,做一些适于自己的整理和分析。
1. HTML模板
React网页源码的结构
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../build/browser.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
</body>
</html>
这段代码需要注意两个地方:
1. 引用文件: 用到了三个库。这三个库必须首先加载。其中,react.js是React的核心库,react-dom.js提供与DOM相关的功能。browser.js的作用是将JSX语法转换为javascript语法。
2. script标签: 最后一个script标签的type属性为 text/babel。这是因为JSX语法跟javascript不兼容。凡是用到JSX的地方,都要加上type=’text/babel’
关于JSX:
JSX是一个看起来很像XML的JS语法扩展。
React建议使用JSX,因为它能定义简洁且我们熟知的包含属性的树状结构语法。
HTML标签与React组件的对比:
要渲染HTML标签,只需要在JSX里使用小写字母开头的标签名:
var myDivElement = <div className="foo" />;
React.render(myDivElement, document.body);
要渲染React组件,只需创建一个大写字母开头的本地变量:
var MyComponent = React.createClass({/*...*/});
var myElement = <MyComponent someProperty={true} />;
React.render(myElement, document.body);
React 的 JSX 里约定分别使用首字母大、小写来区分本地组件的类和 HTML 标签。
JSX 把类 XML 的语法转成纯粹 JavaScript,XML 元素、属性和子节点被转换成 React.createElement 的参数。
var Nav;
// 输入 (JSX):
var app = <Nav color="blue" />;
// 输出 (JS):
var app = React.createElement(Nav, {color:"blue"});
2. ReactDOM.render()
ReactDOM.render()是React的基本方法,用于将模板转换为HTML语言,并插入指定DOM节点。
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
还是一中的例子,如上语法作用即将h1标签插入example节点。
3. JSX 语法
上面已经接触到,HTML语言直接写在JavaScript语言之中,不加任何引号,这就是JSX的语法。它允许HTML与javascript的混写。
看下面一段代码:
var names = ['Alice', 'Emily', 'Kate'];
ReactDOM.render(
<div>
{
names.map(function (name) {
return <div>Hello, {name}!</div>
})
}
</div>,
document.getElementById('example')
);
上面代码体现了JSX的基本语法规则:遇到HTML标签(以<开头),就用HTML规则解析;遇到代码块(以{开头),就用JS规则解析。
JSX允许直接在模板插入JS变量。若这个变量是一个数组,会展开数组所有成员:
var arr = [
<h1>Hello world!</h1>,
<h2>React is awesome</h2>,
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('example')
);
4. 组件
React允许将代码封装成组件(component),然后像插入普通html标签一样,在网页中插入这个组件。
React.creatClass方法就用于生成一个组件类。
var HelloMessage = React.createClass({
render: function() {
return <h1>Hello {this.props.name}</h1>;
}
});
ReactDOM.render(
<HelloMessage name="John" />,
document.getElementById('example')
);
上面实例中,变量HelloMessage就是一个组件类。模板插入时,会自动生成HelloMessage一个实例。所有组件类都必须有自己的render方法,用于输出组件。
组件的用法与原生的HTML标签完全一致。可任意添加属性(EX:除了class属性要写成className,for要写成htmlFor,因为class和for是javascript的保留字)
组件的属性可在组件类的this.props对象上获取。
PA:
- 组件类的第一个字母必须大写
组件类只能包含一个顶层标签
var HelloMessage = React.createClass({ render: function() { return <h1>Hello {this.props.name}</h1> <p>gogo!hangview</p>; } });
上面这段代码是会报错的,因为包含两个顶层标签h1和p
5. this.props.children
上面提到组件的属性可在组件类的this.props对象上获取。事实上对象的属性和组件的属性一一对应,但有一个例外,就是this.props.children属性。它表示组件的所有子节点。
var NotesList = React.createClass({
render: function() {
return (
<ol>{
React.Children.map(this.props.children, function (child) {
return <li>{child}</li>;
})
}</ol>
);
}
});
ReactDOM.render(
<NotesList>
<span>hello</span>
<span>world</span>
</NotesList>,
document.body
);
输出结果:
- hello
- world
上面代码中NodeList组件有两个span节点,它们都可通过this.props.children读取。但是,当组件没有子节点时,其值是undefined,有一个子节点,是Object,只有多个子节点是才为Array。
React提供一个工具方法:React.Children来处理this.props.childen。可以使用React.Children.map来遍历子节点而不必关注数据类型。
6. PropTypes
组件属性可以接受任意值。但有时在验证别人使用组件时,需要看其提供的参数是否符合要求。
组件的ProtoType属性,就是用来验证组件实例的属性是否符合要求。
var MyTitle = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired, //title属性是必需的,而且必须为字符串 },
render: function() {
return <h1> {this.props.title} </h1>;
}
});
var data = 123;
ReactDom.render(
<MyTitle title={data}>,
document.body
);
//控制台会报错,组件属性类型不对
此外,**getDefaultProps**方法可用来设置组件属性的默认值。
var MyTitle = React.createClass({
getDefaultProps : function () {
return {
title : 'Hello World'
};
},
render: function() {
return <h1> {this.props.title} </h1>;
}
});
ReactDOM.render(
<MyTitle />, //未设置值,读取默认值
document.body
);
7. 获取真实的DOM节点
组件并非真实的DOM节点,它只是存在于内存的一种数据结构,叫做虚拟DOM。只有被插入文档后,才会变成真实的DOM。
在React的设计里,所有的DOM变动,都先在虚拟DOM发生,然后再将实际变动部分反映在真实的DOM上,它可以极大程度地提高网页的性能表现。
当需要从组件获取真实的DOM节点时,要用到ref属性:
var MyComponent = React.createClass({
handleClick: function() {
this.refs.myTextInput.focus();
},
render: function(){
return (
<div>
<input type="text" ref="myTextInput" /> //ref属性
<input type="button" value="Focus the text input" onClick={this.handleClick} />
</div>
);
}
});
ReactDOM.render(
<MyComponent />,
document.getElementById('example')
);
子节点有一个文本输入框,用于获取用户输入。这时需要真正的DOM节点,虚拟DOM是拿不到用户输入的。为了做到这一点,文本框要有一个ref属性(ref=myTextInput),然后this.refs.[refName]就会返回真实的DOM节点。
需要注意的是,因为this.refs.[refName]属性获取的是真实DOM,所以必须等到虚拟DOM插入文档后才能使用这个属性。上面实例中通过为组件指定click的回调函数,确保只有等到真实DOM发生click时间后,才会读取ref属性。
8. this.state
组件要跟用户互动,React的一大创新就是将组件看成一个状态机,一开始有一个初始状态,然后用户互动,导致状态变化,从而触发重新渲染UI。
var LikeButton = React.createClass({
getInitialState: function() {
return {liked: false};
},
handleClick: function(event) {
this.setState({liked: !this.state.liked});
},
render: function() {
var text = this.state.liked ? 'like' : 'haven\'t liked';
return (
<p onClick={this.handleClick}>
You {text} this. Click to toggle.
</p>
);
}
});
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
上面实例的组件中,getInitialState方法用户定义初始状态,也就是一个对象。这个对象可以通过this.state属性获取。当用户点击组件,导致状态变化,this.setState方法就修改状态值,每次修改会自动调用this.render方法,再次渲染组件。
this.props与this.state的区别:
this.props标示那些一旦定义,就不再改变的属性,而this.state是会随用户互动而产生变化的特性。
9. 表单
用户在表单输入的内容,属于用户跟组件的互动,不能用this.props获取
var Input = React.createClass({
getInitialState: function() {
return {value: 'Hello!'};
},
handleChange: function(event) {
this.setState({value: event.target.value});
},
render: function () {
var value = this.state.value;
return (
<div>
<input type="text" value={value} onChange={this.handleChange} />
<p>{value}</p>
</div>
);
}
});
ReactDOM.render(<Input/>, document.body);
上面例子中,文本框输入的值,通过定义一个onChange事件的回调函数,用event.target.value读取用户输入的值
。textarea、select、radio元素都属于这种情况。
10. 组件的生命周期
组件的生命周期主要分为三个状态
React为每个状态提供两种处理函数,will函数在进入状态前调用,did函数在进入状态后调用。
- Mounting 已插入真实DOM componentWillMount() componentDidMount()
- Updating 正在被重新渲染 componentWillUpdate() componentDidUpdate()
- Unmounting 已移除真实DOM 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 );
上面例子中,在组件加载后,通过componentDidMount方法设置一个定时器,作用是定时更新文字透明度。当状态被更新时,页面也就重新被渲染。
11. Ajax
组件的数据来源,通常是通过Ajax请求从服务器获取。主要实现思路是通过 componentDidMount方法设置Ajax请求,等到请求成功,通过this.setState方法重新渲染UI。
var UserGist = React.createClass({
getInitialState: function() { //状态初始化
return {
username: '',
lastGistUrl: ''
};
},
componentDidMount: function() {
$.get(this.props.source, function(result) { //组件加载后,设置Ajax请求
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
);
12. 代码分析
下面来具体分析一段代码,如下是reactJs官方文档的一段实例代码。通过分析回顾一下前面整合的内容。
//Comment组件
var Comment = React.createClass({
rawMarkup: function() { //markdown文本格式处理
var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return { __html: rawMarkup };
},
render: function() {
return (
<div className="comment"> //只有一个顶层标签
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
});
//父级组件CommentBox(包含子组件:CommentList和CommentForm)
var CommentBox = React.createClass({
loadCommentsFromServer: function() { //加载数据的Ajax请求
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data}); //更新组件状态,data数据来源
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleCommentSubmit: function(comment) { //Ajax处理新增数据
var comments = this.state.data;
// Optimistically set an id on the new comment. It will be replaced by an
// id generated by the server. In a production application you would likely
// not use Date.now() for this and would have a more robust system in place.
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
this.setState({data: comments});
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() { //初始化状态,数据源头
return {data: []};
},
componentDidMount: function() { //组件加载完后调用的方法
this.loadCommentsFromServer(); //加载数据
setInterval(this.loadCommentsFromServer, this.props.pollInterval); //设置定时器定时刷新
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} /> //包含两个子组件
<CommentForm onCommentSubmit={this.handleCommentSubmit} />
</div>
);
}
});
//CommentList组件(包含子组件Comment)
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function(comment) { //从父组件传入的数据(data)会作为子组件的属性,这些属性可以通过this.props访问到。
return (
<Comment author={comment.author} key={comment.id}> //遍历输出每一条数据
{comment.text}
//子节点 ,Comment组件通过this.props.children访问到这些数据
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
var CommentForm = React.createClass({
getInitialState: function() {
return {author: '', text: ''}; //初始化状态
},
handleAuthorChange: function(e) { //绑定事件的回调函数,下同
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
//父组件属性,当事件触发时(submit),调用父组件方法,处理新增评论数据,这样整个父组件状态会被重置,可以重新渲染整个组件相关UI
this.setState({author: '', text: ''}); //状态初始化
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}> //绑定事件,下同
<input
type="text"
placeholder="Your name"
value={this.state.author}
onChange={this.handleAuthorChange}
/>
<input
type="text"
placeholder="Say something..."
value={this.state.text}
onChange={this.handleTextChange}
/>
<input type="submit" value="Post" />
</form>
);
}
});
ReactDOM.render( //最终render CommentBox这个组件,设置定时加载时间和Ajax url
<CommentBox url="/api/comments" pollInterval={2000} />,
document.getElementById('content')
);
上面的这个例子很明显地看出,ReactJs组件化的魅力,它将每一个小模块都抽象成一个个组件,通过state控制组件的渲染,其数据沿着组件树从上到下流动,也可以通过设置触发事件更新上级state达到反向数据流的目的。
后续:
接下来会继续深入梳理reactJs相关内容,更多做一些偏实践的总结。
另外4月份的主要学习计划如下
js前端框架、MVC模块化开发、react的进一步梳理
css基础的回顾和整理,Sass/Less的了解和使用学习
Http协议学习
继续回顾学习JS基础
buffer或工作需求:对yii2和LNMP环境做一些使用层面的了解和总结