和taro一起做SPA 2.技术栈框架:react
从这章开始,我们开始逐渐讲解相关的技术栈框架,让我们先从react开始
1.第一个react程序
由于react需要依赖大量的依赖库,如通过babel对es6的转化,css的渲染…对于新手来说,可能这一大堆配置就让人望而却步。为了简化开发环境的搭建,让我们直接用create-react-app 搭建脚手架(具体命令含义可以先不用理解):
首先,我们安装create-react-app
npm install -g create-react-app
安装成功后,就可以开始使用
mkdir basic
create-react-app basic
basic是我们第一个react程序的名称,create-react-app命令会运行一段时间,帮我们搭建脚手架和开发环境.命令执行后,我们开始启动我们的应用:
cd basic
yarn start
yarn start命令后,webpack会启动webpack-dev-server跟踪我们代码修改,然后自动启动一个端口为3000的HttpServer帮我们进行调试,并且很贴心的弹出URL为http://localhost:3000的浏览器页面,显示我们的第一个react应用.
好,让我们开始学习第一个react程序,首先,让我们看一下脚手架帮我们搭建的目录结构。
其中/public/index.html就是我们的页面模板,所有的组件都会渲染在这个文件上.
让我们看一下第一个组件:/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
这个组件很简单,重要的是这一句:
ReactDOM.render(<App />, document.getElementById('root'));
这个是React的语句,意思是在index.html页面ID为root的元素上渲染App组件.其中index.html页面放置在public目录下.
而App就是React组件,系统如何区分React组件和DOM组件呢?很简单,在React中,首字母为大写的就是React组件,小写字母为DOM组件.
App组件非常简单,让我们也看一下:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
}
export default App;
好,现在让我们开发一个计数器程序来学习一下React开发.首先,让我们自己定义一个Counter组件.
首先,我们在src根目录下新建一个counter.js文件
import React,{Component} from 'react'
class Counter extends Component{
constructor(){
super()
this.state={value:0}
}
render(){
return (
<div>
<span>{this.state.value}</span>
<button onClick={this.add.bind(this)}>+</button>
<button onClick={this.desc.bind(this)}>-</button>
</div>
)
}
desc(){
let value=this.state.value-1;
this.setState({value})
}
add(){
let value=this.state.value+1;
this.setState({value})
}
}
//将组件导出模块
export default Counter
然后,修改index.js文件,渲染我们新开发的Counter组件:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
-import App from './App';
+import Counter from './counter'
import * as serviceWorker from './serviceWorker';
-ReactDOM.render(<App />, document.getElementById('root'));
+ReactDOM.render(<Counter />, document.getElementById('root'));
serviceWorker.unregister();
页面会自动刷新修改后的内容,(是不是很惊讶)显示结果.点击 + 和 - 按钮系统会自动显示最新的counter.
让我们来分析一下代码:
首先 ,我们的Counter组件继承自React.Component.我们的Counter组件继承Component组件,并且实现了构建器.在构建器中,完成了state值的初始化.
class Counter extends Component{
constructor(){
super()
this.state={value:0}
}
}
让我们看接下来的渲染部分的处理:
render(){
return (
<div>
<span>{this.state.value}</span>
<button onClick={this.add.bind(this)}>+</button>
<button onClick={this.desc.bind(this)}>-</button>
</div>
)
}
render方法是React组件的核心部分,主要完成组件的表现.在这里,render方法返回了一个jxs的代码段.所谓的jxs,简单说就是嵌入了react语句的html代码段.
在这里,你特别需要注意的是,React会自动跟踪state值的变化进行渲染,因此,你不需要像传统开发一样手动渲染数据,只需要简单的标明会发成变更的数据即可:
<span>{this.state.value}</span>
在这里,{}表示的是里面的部分是由react代码构成.
这个代码段有几个特别需要注意的地方:
- return语句直接返回JSX时,必须用()进行包裹,下面的语句由于<div>前没有(),会直接报错:
render(){
return <div>{this.state.value}</div>
}
- html代码段必须有一个根元素,因此,以下的代码是错误的:
render(){
return (
<span>{this.state.value}</span>
<button onClick={this.add.bind(this)}>+</button>
<button onClick={this.desc.bind(this)}>-</button>
)
}
- 和HTML事件命名机制不同,让我们对比一下React的写法:
<button onClick={this.add.bind(this)}>+</button>
下面是HTML的写法:
<button onclick="test()">test</button>
有三个重要的区别:
1.React事件触发是骆驼命名方式.而HTML的触发方式是全部小写;
2.React事件触发是函数名,而HTML的触发方式是函数执行代码块;
3.React事件处理函数this指针不会绑定任何对象,而HTML指针会自动绑定到window对象;
- 如果处理state,则React事件处理函数需要绑定this指针到组件
这就是以下代码的原因,通过bind方法将函数this指针绑定到React Component:
<button onClick={this.add.bind(this)}>+</button>
- state的处理原则
让我们看一下事件处理方法的实现:
desc(){
let value=this.state.value-1;
this.setState({value})
}
add(){
let value=this.state.value+1;
this.setState({value})
}
代码很简单,但是有几个需要注意的地方:
- 不能直接修改state的值,必须通过this.setState方法修改,否则,系统不会进行渲染.
let value=this.state.value+1
this.state.value=value
- state的原始值不能修改,因此,以下代码是无效的:
let value=this.state.value++; //this.state.value++修改了原始的state的值
this.setState({value})
第一个react程序结束了,React的逻辑很简单,每个组件都有一个state值,组件通过监控state的状态变化实现页面渲染.
最后,别忘了需要从模块中导出我们的组件:
export default Counter
导出是,如果不增加default参数,导入时需要将组件名称用{}括起来.一个模块中智能有唯一的一个default组件.
2.3.2 React组件间通讯
从第一个例子我们可以发现,React组件开发很容易,通过监控组件state值的变化,实现自动的渲染,极大的减轻了开发的工作量.但是每个组件都有自己的state,如果多个组件需要通讯,问题就变得复杂了.
让我们看下面的这个例子,在这个例子,组件Control由3个Counter构成,每个Counter都可以自动增减,CounterControl显示的值是3个Counter的累加值.
这个例子显示了组件间如何进行通信.
先让我们看一下CounterControl组件的代码:
import React,{Component } from "react";
import Counter from "./counter"
class counterControl extends Component{
constructor(){
super();
//设置组件的初始值
this.state={value:0}
}
//这个地方需要特别注意,change是Counter组件每次点击发生变化的值
change(change){
//不可以修改state的原始值
let value=this.state.value;
value+=change;
//必须通过this.setState方法进行state的修改
this.setState({value:value})
}
render(){
return(
<div>
<Counter name={"one"} change={this.change.bind(this)}/>
<Counter name={"two"} change={this.change.bind(this)}/>
<Counter name={"three"} change={this.change.bind(this)}/>
<div>value:{this.state.value}</div>
</div>
)
}
}
export default counterControl;
Counter组件也发生了变化,增加了name属性和change方法.
<Counter name={"three"} change={this.change.bind(this)}/>
由于组件彼此state独立,因此,组件之间的通讯就落到了change方法里.让我们看一下change方法的实现:
//这个地方需要特别注意,change是Counter组件每次点击发生变化的值
change(change){
//不可以修改state的原始值
let value=this.state.value;
value+=change;
//必须通过this.setState方法进行state的修改
this.setState({value:value})
}
change方法实际是CounterControl通过属性传递给Counter子组件的回调函数.他的实现原理是每次Counter组件被点击时,把Counter组件state值的变化回调至CounterControl,从而实现CounterControl的State值的变化.
让我们看一下Counter组件:
import React,{Component} from 'react'
class Counter extends Component{
constructor(props){
super(props)
this.state={value:0}
}
desc(){
let value=this.state.value-1;
this.setState({value})
this.props.change(-1)
}
add(){
let value=this.state.value+1;
this.setState({value})
this.props.change(1);
}
render(){
return (
<div>
<span>{this.props.name}:{this.state.value}</span>
<button onClick={this.add.bind(this)}>+</button>
<button onClick={this.desc.bind(this)}>-</button>
</div>
)
}
}
export default Counter
重点看一下组件的onClick方法:
desc(){
let value=this.state.value-1;
this.setState({value})
this.props.change(-1)
}
add(){
let value=this.state.value+1;
this.setState({value})
this.props.change(1);
}
无论是add还是desc方法,Counter在完成自己state变更的同时,都需要调用通过props属性传递的change方法回调CounterControl提供的chanage方法,实现state变化的通知.
2.3.3 优化
上面的例子我们发现,Counter组件如果需要整合到CounerControl组件中,就必须进行修改.
原来Counter组件的事件如下:
desc(){
let value=this.state.value-1;
this.setState({value})
}
add(){
let value=this.state.value+1;
this.setState({value})
}
修改后
desc(){
let value=this.state.value-1;
this.setState({value})
//增加了change的回调方法
this.props.change(-1)
}
add(){
let value=this.state.value+1;
this.setState({value})
//增加了change的回调方法
this.props.change(1);
}
也就是说,Counter组件并不是一个通用的组件.造成这种情况的原因,是由于Counter组件增加了过多的业务逻辑.
如果把组件的显示和业务逻辑进行剥离成内部组件和容器组件,就可以解决这个问题.内部组件只负责显示,容器组件则负责具体的业务逻辑和state处理.让我们看一下如何进行剥离:
新的NgCounter内部组件代码如下:
class NgCounter extends Component{
render(){
return (
<div>
<span>{this.props.name}:{this.props.value}</span>
<button onClick={this.props.add}>+</button>
<button onClick={this.props.sub}>-</button>
</div>
)
}
}
剥离后的NgCounter组件不再进行任何业务逻辑的处理,也不处理state相关的数据.它只是按照传递的属性值进行显示或回调.
我们管这种不处理任何state的组件称之为无状态组件.无状态组件可以进一步简化为函数,如下所示:
import React,{Component} from 'react'
function NgCounter(props){
return (
<div>
<span>{props.name}:{props.value}</span>
<button onClick={props.add}>+</button>
<button onClick={props.sub}>-</button>
</div>
)
}
无状态组件函数由于没有this指针,属性props由容器组件传递.
让我们看一下容器组件如何进行处理
import React,{Component} from 'react'
class Container extends Component{
constructor(props){
super(props);
this.state={value:0}
}
add(){
this.setState({value:this.state.value+1})
//如果嵌入CounterControl则需要增加以下方法
this.props.add();
}
sub(){
this.setState({value:this.state.value-1})
//如果嵌入CounterControl则需要增加以下方法
this.props.sub();
}
render(){
return(
<NgCounter name="ngCounter" add={this.add.bind(this)} sub={this.sub.bind(this)} value={this.state.value}/>
)
}
}
export default Container;
通过内部组件和容器组件的拆分,如果组件需要嵌入其他组件,则只需要修改容器组件即可.内部组件不需要进行任何调整.
需要特别注意的是,此时导出的组件为容器组件.
我们把CounterControl也进行了改造,相应的代码如下:
import React,{Component} from 'react';
import NgCounter from './ngCounter'
//内部无状态组件退化为函数
function NgCounterControl(props){
return(
<div>
<NgCounter name={"one"} add={props.add} sub={props.sub}/>
<NgCounter name={"two"} add={props.add} sub={props.sub}/>
<NgCounter name={"three"} add={props.add} sub={props.sub}/>
<div>value:{props.value}</div>
</div>
)
}
//容器组件负责具体的业务逻辑和state的处理
class Container extends Component{
constructor(props){
super(props);
this.state={value:0}
}
add(){
this.setState({value:this.state.value+1})
}
sub(){
this.setState({value:this.state.value-1})
}
render(){
return (
//返回内部组件
<NgCounterControl add={this.add.bind(this)}
sub={this.sub.bind(this)}
value={this.state.value} />
)
}
}
//导出容器组件
export default Container;
2总结
通过上面的例子,我们可以发现,虽然我们把组件拆分为内部组件和容器组件,实现了内部组件的独立性,但是,由于React组件彼此都维护自己的state,当多个组件需要同步state值的时候,情况还是变得很复杂,组件必须通过props属性传递回调方法层层调用.因此,对于多个组件协调工作时,这种实现方法就显得很笨拙而且效率低下.