虽然别人整理的入门知识资料已经挺多的了,但不一定适合自己,还是重新整理下,理一理React的开发生态。
使用react,只需要引入react.min.js(React 的核心库)和react-dom.min.js(提供与 DOM 相关的功能)即可。
<script src="../js/lib/react.min.js"></script>
<script src="../js/lib/react-dom.min.js"></script>
<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
// 这段代码将一个h1标题,插入id="example"节点中
</script>
通常为了使用JSX语法,我们需要在文件打包时使用插件将含有JSX语法的文件解析成普通的JavaScript语法,例如fis3-parser-react;如果使用到了ES6的写法,也需要将其转为ES5的格式,例如使用fis3-parser-babel。(现在前端已经不用ES6了,babel太慢影响效率,ES6在Node上使用)
React使用JSX来替代常规的 JavaScript,以为JSX执行时进行了优化,含有错误检测而且方便我们书写模板。
var arr = [
<h1>W3Cschool教程</h1>,
<h2>从W3Cschool开始!</h2>,
];
var mystyle = {
fontSize: 100,
color: '#FF0000'
};
ReactDOM.render(
<div style={myStyle}>{arr}</div>,
document.getElementById('example')
);
JSX语法代码实际上是JavaScript代码,所以html中的部分标签属性在JSX中不能直接使用,例如html class属性在JSX中需要使用className表示,for属性需要使用htmlFor代替,style属性标签中内容需要写成JSON格式。同时,JSX使用单个{}来包含变量模板,一次返回的JSX render内容必须放在单独一个标签内,返回统计的多个标签是不允许的,例如下面这样是不对的:
// 不正确
render: function(){
return (<div>{this.props.name}</div>
<div>{this.props.site}</div>
);
}
实现了输出网站名字和网址的组件,另外if和for循环的输出大家也可以注意下:
var Name = require('Name');
var Link = require('LINK');
var list = ['list-item-1', 'list-item-2', 'list-item-3'];
var WebSite = React.createClass({
getDefaultProps: function() {
return {
name: '极限前端',
site: "http://www.jixianqianduan.com"
};
},
render: function() {
var hasList = list.length > 0;
if(hasList){
return (
<div>
<Name name={this.props.name} />
<Link site={this.props.site} />
<ul>
{
this.props.list.map(function(item, index){
return <li key={index}>item</li>;
});
}
</ul>
</div>
)
}else{
return <div>没有数据</div>;
}
}
});
React.render(
<WebSite name="极限前端" site="http://www.jixianqianduan.com" />,
document.getElementById('example')
);
// 文件Name.js
var Name = React.createClass({
render: function() {
return (
<h1>{this.props.name}</h1>
);
}
});
module.exports = Name;
// 文件Link.js
var Link = React.createClass({
render: function() {
return (
<a href={this.props.site}>
{this.props.site}
</a>
);
}
});
module.exports = Link;
最新版的React组件名称只允许使用大些字母开头的变量命名,例如x-button、my-button等命名方式都是不正确的。另外注意上面if模板和for循环输出模板的用法,循环输出时React要求列表项便签需要加上key的属性。
下面展示的一个典型实例中创建了 LikeButton 组件,getInitialState 方法用于定义初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。当用户点击组件,导致状态变化,this.setState 方法就修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。
var LikeButton = React.createClass({
getInitialState: function() {
return {liked: 0};
},
handleClick: function(event) {
this.setState({liked: this.state.liked + 1});
},
render: function() {
var text = this.state.liked ? this.state.liked : '不喜欢';
return (
<p onClick={this.handleClick}>
你<b>{text}</b>我。点我切换状态。
</p>
);
}
});
React.render(
<LikeButton />,
document.getElementById('example')
);
上面的例子也可以这样来做:
var Name = require('Name');
var Link = require('LINK');
var list = ['list-item-1', 'list-item-2', 'list-item-3'];
var WebSite = React.createClass({
getDefaultProps: function() {
return {
name: '极限前端',
site: "http://www.jixianqianduan.com"
};
},
// 属性类型检测
propTypes: {
name: React.PropTypes.string.isRequired,
site: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
name: '极限前端',
site: "http://www.jixianqianduan.com"
};
},
render: function() {
var hasList = list.length > 0;
if(hasList){
return (
<div>
<Name name={this.state.name} />
<Link site={this.state.site} />
<ul>
{
this.props.list.map(function(item, index){
return <li key={index}>item</li>;
});
}
</ul>
</div>
)
}else{
return <div>没有数据</div>;
}
}
});
React.render(
<WebSite name="极限前端" site="http://www.jixianqianduan.com" />,
document.getElementById('example')
);
state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变。这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 state 来传递数据。
组件的state和props常见有下面的管理方法:
设置状态:setState, 不能在组件内部通过this.state修改状态,因为该状态会在调用setState()后被替换,setState()并不会立即改变this.state,而是创建一个即将处理的state。setState()并不一定是同步的,为了提升性能React会批量执行state和DOM渲染。 setState()总是会触发一次组件重绘,除非在shouldComponentUpdate()中实现了一些条件渲染逻辑。 替换状态:replaceState,replaceState()方法与setState()类似,但是方法只会保留nextState中状态,原state不在nextState中的状态都会被删除。 设置属性setProps,props相当于组件的数据流,它总是会从父组件向下传递至所有的子组件中。当和一个外部的JavaScript应用集成时,我们可能会需要向组件传递数据或通知React.render()组件需要重新渲染,可以使用setProps()。 替换属性replaceProps,replaceProps()方法与setProps类似,但它会删除原有 强制更新:forceUpdate,forceUpdate()方法会使组件调用自身的render()方法重新渲染组件,组件的子组件也会调用自己的render()。但是,组件重新渲染时,依然会读取this.props和this.state,如果状态没有改变,那么React只会更新DOM。 获取DOM节点:findDOMNode,如果组件已经挂载到DOM中,该方法返回对应的本地浏览器 DOM 元素。 判断组件挂载状态:isMounted,isMounted()方法用于判断组件是否已挂载到DOM中。可以使用该方法保证了setState()和forceUpdate()在异步场景下的调用不会出错。
getDefaultProps:获取实例的默认属性(即使没有生成实例,组件的第一个实例被初始化CreateClass的时候调用,只调用一次,) getInitialState:获取每个实例的初始化状态(每个实例自己维护) componentWillMount:组件即将被装载、渲染到页面上(render之前最好一次修改状态的机会) render:组件在这里生成虚拟的DOM节点(只能访问this.props和this.state;只有一个顶层组件,也就是说render返回值值职能是一个组件;不允许修改状态和DOM输出) componentDidMount:组件真正在被装载之后,可以修改DOM
componentWillReceiveProps:组件将要接收到属性的时候调用(赶在父组件修改真正发生之前,可以修改属性和状态) shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了) componentWillUpdate:不能修改属性和状态 render:只能访问this.props和this.state;只有一个顶层组件,也就是说render返回值只能是一个组件;不允许修改状态和DOM输出 componentDidUpdate:可以修改DOM
React的网络请求通常是将请求的url放在组件props中传入,然后在componentDidMount时发送ajax请求。
var UserGist = React.createClass({
getInitialState: function() {
return {
username: '',
lastGistUrl: ''
};
},
componentDidMount: function() {
this.serverRequest = $.get(this.props.source, function (result) {
var lastGist = result[0];
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}.bind(this));
},
componentWillUnmount: function() {
this.serverRequest.abort();
},
render: function() {
return (
<div>
{this.state.username} 用户最新的 Gist 共享地址:
<a href={this.state.lastGistUrl}>{this.state.lastGistUrl}</a>
</div>
);
}
});
ReactDOM.render(
<UserGist source="https://api.github.com/users/ouven/gists" />,
mountNode
);
如果绑定一个 ref 属性到render内容的返回值上,组件或其它组件就可以这样来引用这个内容关联的DOM元素。
<input ref="myInput" /> var input = this.refs.myInput; var inputValue = input.value; var inputRect = input.getBoundingClientRect();
我们也可以使用getDOMNode()方法获取当前真实DOM元素,但是findDOMNode()不能用在无状态组件上。此外也可以使用bind的方法来实现当前事件的处理。
componentDidMound() {
const el = findDOMNode(this);
}
// do something ...
onClick = {this.handleClick.bind(this, value1, value2)}
// do something
handleClick(value1, value2, ..., event) {
// 事件处理函数
}
React服务端渲染需要用到react-dom/server模块,以koa(koa使用教程省略)为例,我们渲染一个服务端返回的页面就可以这样写:
/**
* react前后端同构页面
* @param {[type]} req [description]
* @param {[type]} res [description]
* @yield {[type]} [description]
*/
const reactController = function*(req, res) {
let ctx = this;
let helloProps = {
type: 'hello',
data: {
name: 'hello-name-init',
address: 'hello-address-init',
age: '26',
job: 'hello-job-init'
}
};
let contentProps = {
type: 'content',
data: {
name: 'content-name-init',
address: 'content-address-init',
age: '26',
job: 'content-job-init'
}
}
let reactHello = reactComponent.renderPath(ctx, 'react/react-hello/main.jsx', helloProps);
let reactContent = reactComponent.renderPath(ctx, 'react/react-content/main.jsx', contentProps);
ctx.body = yield render(ctx, 'pages/react', {
reactHello,
reactContent,
storeData: {
helloProps,
contentProps
}
});
};
这里用到了一个统一react的render处理模块:
'use strict';
const React = require('react');
const ReactDOM = require('react-dom');
const ReactDOMServer = require('react-dom/server');
/**
* 根据开发或正式环境读取不同目录先的jsx组件
* @param {[type]} ctx [当前运行环境,用于判断使用开发环境路径还是线上环境路径]
* @param {[type]} componentPath [接受组件路径名]
* @param {[type]} props [组件接受的数据]
* @return {[type]} [description]
*/
const renderPath = function(ctx, componentPath, props) {
let componentFactory,
jsxPath;
// 如果是本地则使用dev环境目录,否则使用page的构建目录
if (ctx.hostname === '127.0.0.1' || ctx.hostname === 'localhost') {
jsxPath = '../dev/component/';
} else {
jsxPath = '../pages/component/';
}
componentFactory = React.createFactory(require(jsxPath + componentPath));
/**
* renderToStaticMarkup不会避免前端重渲染
* renderToString会避免前端重渲染
*/
return ReactDOMServer.renderToString(componentFactory(props));
};
module.exports = {
renderPath: renderPath
}
这里这样做还不够,我们还需要将renderToString的字符串内容填充到真正的页面模板中,这里以swig模板为例,同时后端渲染的初始数据状态也要通过storeData变量带到前端页面上,保证初始的页面组件状态和后端直出是一样的。
<div id="testHello">
<div></div>
</div>
<div id="test">
<div></div>
</div>
<script>
var storeData = ;
</script>
React组件间的通信分为几种,父组件向子组件通信、子组件向父组件通信、同级组件间通信。第一种通常是将父组件的state传给子组件props来实现,父组件state变化,子组件的状态就直接变化;子组件向父组件通信是将父组件的方法通过props传入的子组件中,然后再子组件中调用来通知父组件;同级组件通信则是创建一个共同父组件,先发起子组件向父组件通信,然后再让父组件向另一子组件发起通信。 另一种通用的机制就是Redux,Redux可以创建一个全局的store,统一保存管理不同组件的状态,然后通过subscribe订阅事件,当dispatch调用时可以修改组件状态触发订阅事件重新渲染视图。
var reactContent = require('react-content');
var reactHello = require('react-hello');
var store = Redux.createStore(reducer);
function reducer(state={}, action) {
if(action.type){
state[action.type] = action.data;
}
return state;
};
// do somthing...
for(var key in storeData){
store.dispatch(storeData[key]);
}
// do somthing...
reactContent.init(store);
reactHello.init(store);
此时如果react-content组件重要控制react-hello组件状态的变化,reactHello中就可以这样监听
componentDidMount: function() {
var store = this.props.store;
function handleChange() {
self.setState({ data: store.getState()['hello'] });
}
// 订阅store变化,如果有dispatch,handleChange里面的状态设置就会执行
let unsubscribe = store.subscribe(handleChange);
// unsubscribe(); 取消订阅
}
reactContent中控制reactHello变化的动作中则需要这样写。
// 触发reactHello组件变化
_triggerHello: function(){
// 修改hello组件的store
var hello = this.props.store.getState()['hello'];
hello.name = 'ouvenzhang';
// dispatch后会触发reactHello中的handleChange
this.props.store.dispatch({
type: 'hello',
data: hello
});
}
其实我们可以在SSR时也使用Redux管理,可以增强组件的一部分复用性,但目前还没有用到,其实redux是用来管理组件状态变化的,更推荐在前端使用,服务端使用感觉有点不大必要,服务端只用来做首次内容的渲染。
总得来说react生态的还是很健全的,解决的了实际开发中组件和组件状态管理的问题,前后端同构的模式也解决了React库本身较大前端加载缓慢的弊端,但是如果实际项目中没有Node服务层,个人建议还是不要直接使用React,React库文件比较大,还需要其它的依赖,会大大延后页面渲染时机,这是例如使用Vue会显得更轻量级。
工程化项目代码样例:https://github.com/ouvens/fis3-koa-node (内含react的页面,可直接用来开发大型项目)