React 应用
React是一个声明式、基于组件的javascript
库,可以非常轻松地创建用户交互界面。为你应用的每一个状态设计简洁的视图,在数据改变时React
也可以高效地更新渲染界面。
如果你还没接触过React
的话,建议你先阅读Why did we build React?和React 快速入门两篇文章,当然我也建议你能学学Babel和Webpack的相关知识,这些知识点结合在一起可以非常完美的帮助你进行React
应用的开发。
$ tdd100 node -v v8.5.0 $ tdd100 npm -v 5.4.2
如果你没安装的话,根据上面两个连接你也可以非常方便的安装,这里就不详细说明了。
初始化项目¶
这里我们使用一个非常牛逼的创建React
项目的脚手架工具:Create React App。 首先我们安装Create React App
工具到全局环境中:
$ npm install create-react-app --global
安装完成后,我们在我们的flask-microservices-users
项目根目录下面创建一个新的文件夹:client
,然后初始化我们的项目结构:
$ mkdir client && cd client $ create-react-app .
项目结构创建完成后,相关的依赖包也已经安装好,完成后,我们可以启动服务:
$ npm start
服务启动完成后,Create React App
工具会自动用默认的浏览器打开:http://localhost:3000。
确保上面这些都正常后,关掉服务(Crtl + C),为了简化我们的开发过程,我们可以通知npm
不要为项目创建package-lock.json
文件:
$ echo 'package-lock=false' >> .npmrc
- 可以通过文档npm docs查看关于配置文件
.npmrc
的更多信息。 - 对
package-lock.json
文件不太清楚的,可以查看这篇文章:Understanding lock files in NPM 5进行了解。
现在来开始创建创我们的第一个组件吧~~~
第一个组件¶
为了让我们的项目结构看起来更加简单,我们移除src目录下面的App.css,App.js,App.test.js以及index.css文件,然后更新index.js文件:
import React from 'react'; import ReactDOM from 'react-dom'; const App = () => { return ( <div className="container"> <div className="row"> <div className="col-md-4"> <br /> <h1>All Users</h1> <hr /><br /> </div> </div> </div> ) }; ReactDOM.render( <App />, document.getElementById('root') );
上面代码做了几件事:
- 导入
React
和ReactDom
类后,我们创建了一个叫App
的函数,这是ES6
里面的箭头函数的写法,然后该函数返回的是一个JSX格式的对象。 - 我们用
ReactDOM
的render
方法将我们的App
组件挂载到了一个ID为root
的HTML元素上。
注意
public
目录下面的index.html
文件的<div id="root"></div>
,这就是被挂载的地方。另外对于还不太了解ES6
的同学,不用担心,实际上是可以直接跳过javascript
的语法,直接学习ES6
的(虽然不太推荐这样),ES6
比原生的javascript
更加系统、更加容易学习、也更加接近你所学习过的其他编程语言(点击这里前去学习吧)
然后在public
目录下的index.html文件的head区域添加bootstrap
的css样式文件:
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
基于类的组件¶
现在我们来将我们的App
组件更改成基于类的形式:
import React, {Component} from 'react'; import ReactDOM from 'react-dom'; class App extends Component { constructor() { super(); } render() { return ( <div className="container"> <div className="row"> <div className="col-md-4"> <br /> <h1>All Users</h1> <hr /><br /> </div> </div> </div> ) } } ReactDOM.render( <App />, document.getElementById('root') );
然后我们执行npm start
可以发现其实上面的基于类的组件和前面的函数组件,最后的输出都是一模一样的,我们在后面会慢慢发现二者之间的区别的。
AJAX请求¶
为了连接客户端和服务端数据,我们在App
类中增加一个getUsers()
的方法,我们用一个非常流行的库:Axios来进行网络请求: 首先先安装axios
包:
$ npm install axios --save
然后在App
类中添加getUsers
函数:
import React, {Component} from 'react'; import ReactDOM from 'react-dom'; import axios from 'axios'; class App extends Component { constructor() { super(); } getUsers() { axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`).then(res => { console.log(res); }).catch(err => { console.log(err); }); } render() { return ( <div className="container"> <div className="row"> <div className="col-md-4"> <br /> <h1>All Users</h1> <hr /><br /> </div> </div> </div> ) } } ReactDOM.render( <App />, document.getElementById('root') );
当然不要忘记在顶部导入axios
包哦~
为了验证上面的功能,我们要先开启我们的服务端,打开一个新的终端窗口,然后定位到flask-microservices-users
项目根目录,还记得前面的启动docker-compose
的命令吗?
$ docker-compose -f docker-compose.yml up -d
为了确保我们服务端的代码是正常工作的,我们还需要执行下我们的测试命令:
$ docker-compose -f docker-compose.yml run users-service python manage.py test
测试通过后,我们回到React
项目,从上面的代码中可以看出我们需要增加一个process.env.REACT_APP_USERS_SERVICE_URL
的环境变量,首先杀掉React
的服务(Ctrl + C),然后执行下面的命令:
$ export REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001
注意所有的自定义的环境变量必须要已
REACT_APP_
开头,更多信息可以查看官方文档
现在为了验证getUsers()
方法,我们可以在构造函数constructor()
中先调用:
constructor() { super(); this.getUsers(); }
这样当我们的App
组件被实例化的时候就会调用getUsers()
方法了。我们来运行命令:npm start
打开我们的React
应用,然后打开Chrome 开发者工具(强烈推荐把chrome浏览器设置为默认浏览器,这货对于前端开发者来说真的是神器,在页面右键选择审查元素即可打开),然后打开JavaScript Console
控制台,你将能看到下面的错误信息: 简单来说,就是我们正在发起一个跨域的AJAX请求(从http://localhost:3000
到http://127.0.0.1:5001
),这是违反浏览器的同源策略的,所以该请求被拒绝了。幸运的时候我们可以通过Flask-CORS扩展在服务端来处理跨域的请求。
回到flaks-users-service的目录,增加Flask-CORS包在requirements.txt
文件中:
flask-cors==3.0.3
为方便我们开发调试,我们设置所有的请求都运行跨域操作(切记,生产环境绝对不能这样做),更新flaks-users-service/project/__init__.py
文件的create_app()
方法:
from flask_cors import CORS def create_app(): # 初始化应用 app = Flask(__name__) # 运行跨域 CORS(app) # 环境配置 app_settings = os.getenv('APP_SETTINGS') app.config.from_object(app_settings) # 安装扩展 db.init_app(app) # 注册blueprint from project.api.views import users_blueprint app.register_blueprint(users_blueprint) return app
不要忘记在文件的顶部导入flask_cors
包:
from flask_cors import CORS
由于我们新增了依赖包,所以我们需要重新构建我们的镜像:
$ docker-compose -f docker-compose.yml up -d --build
然后更新、初始化数据库:
$ docker-compose -f docker-compose.yml run users-service python manage.py recreate_db $ docker-compose -f docker-compose.yml run users-service python manage.py seed_db
然后将我们的React
应用服务打开(npm start
),一样的操作在浏览器中打开JavaScript Console
控制终端,这下我们应该可以看到正常的网络请求的打印结果:console.log(res)
。
我们前面在写获取用户列表的API
的时候,返回的数据结构是这样的:
{ 'status': 'success', 'data': { 'users': users_list } }
还记得吗(project/api/views.py
文件中的get_users
方法)?我们来更改axios
获取成功后的打印语句,可以很方便的拿到用户列表数据:
getUsers() { axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`) .then((res) => { console.log(res.data.data.users); }) .catch((err) => { console.log(err); }) }
(想想为什么是res.data.data.users
?)现在你可以在Javascript Console终端中看到两个用户对象的数组打印出来了:
[ {"created_at":"Sat, 13 Jan 2018 06:12:40 GMT","email":"qikqiak@gmail.com","id":1,"username":"cnych"}, {"created_at":"Sat, 13 Jan 2018 06:12:40 GMT","email":"icnych@gmail.com","id":2,"username":"chyang"} ]
现在有个问题是,我们是在构造函数中调用的getUsers()
方法,而构造函数constructor()
是在组件被挂载到DOM
之前调用的,如果AJAX
请求在组件被挂载完成之前比预期花费了更多的时间会出现什么情况呢?这有可能会造成竞争危害,什么意思?就是页面上的数据可能现在能够渲染出来,另外一次又可能渲染不出来(因为这个时候数据在组件挂载完成后还没完成请求,明白了吗?),这取决与我们的AJAX
请求是否能够在组件挂载完成之前完成请求,对吧。不过不用担心,React
定义了一系列的生命周期函数,可以很方便的来解决这个问题。
组件声明周期¶
基于类的组件有一个特定的函数,它们在组件的生命周期各个阶段执行。这些函数被称作生命周期方法,我们可以先花点时间看看官方文档来简单的学习下每个声明周期方法,看下这些方法都是在什么地方被调用的。 简单总结下:方法中带有前缀will的在特定环节之前被调用,而带有前缀did的方法则会在特定环节之后被调用。
挂载
下面这些方法会在组件实例被创建和插入DOM
中时被调用:
- constructor()
- componentWillMount():会在组件
render
之前执行,并且永远都只执行一次。 - render()
- componentDidMount():会在组件加载完毕之后立即执行。这个时候组件已经生成了对应的
DOM
结构
更新
属性或者状态的改变会触发一次更新。当一个组件在被重新渲染时,下面这些方法会被调用:
- componentWillReceiveProps():在组件接收到一个新的
prop
时被执行 - shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
卸载
当一个组件从DOM
中移除时,会调用下面的方法:
- componentWillUnmount()
说了这么多,我们应该在哪个方法里面来做我们的网络请求呢?实际上ES6
中的构造函数和componentWillMount
函数是一致的,上面我们已经知道构造函数中执行网络请求甚至是所有的异步操作都不是好的选择,在componentDidMount
函数中执行异步操作是最好的时机,可以通过Where to Fetch Data: componentWillMount vs componentDidMount了解到原因。
更改client/src/index.js
代码,在App
类中增加方法:
class App extends Component { constructor() { super(); }; componentDidMount() { this.getUsers(); }; getUsers() { axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`) .then((res) => { console.log(res.data.data.users); }) .catch((err) => { console.log(err); }) }; render() { return ( <div className="container"> <div className="row"> <div className="col-md-4"> <br/> <h1>All Users</h1> <hr/><br/> </div> </div> </div> ) } }; ReactDOM.render( <App />, document.getElementById('root') );
确保上面的React
应用仍然能够正常工作
State(状态)¶
state
是React
中非常重要的一个概念,一个组件的显示形态可以由它的数据状态和配置参数来决定,一个组件可以拥有自己的状态,状态的改变可以让React
高效的更新界面。 现在我们为组件App
增加一个users的状态数据,然后我们可以使用setState()
方法来更新状态数据,更新getUsers()
方法:
getUsers() { axios.get(`${process.env.REACT_APP_USERS_SERVICE_URL}/users`).then(res => { this.setState({ users: res.data.data.users }); }).catch(err => { console.log(err); }); }
然后我们在构造函数中增加状态users
:
constructor() { super(); this.state = { users: [] } }
我们可以看到,默认初始化的时候users
数据是一个空数组,在getUsers()
方法调用成功后,我们调用setState()
方法更新了users
状态。
查看官方文档学习正确的使用
state
。
然后我们就可以更新rendor()
方法来将状态数据渲染到页面中:
render() { return ( <div className="container"> <div className="row"> <div className="col-md-4"> <br /> <h1>All Users</h1> <hr /><br /> { this.state.users.map(user => { return ( <h4 key={ user.id } className="well" >{ user.username } </h4> ) }) } </div> </div> </div> ) }
上面的render()
函数:
- 我们循环(用的ES6的map方法)
users
状态数据,每次循环中创建了一个新的H4元素,这也是为什么我们初始化的users
是一个空数组的原因,避免初始化的时候出错。 - 注意H4元素中我们增加了一个属性
key
:React
通过该值来跟踪每一个元素,每个key
对应一个组件,相同的key
React会认为是同一个组件,这样后续相同的key
对应组件都不会被创建,简单的来说就是该属性是用来保证React
高效的一个重要标识,在循环中一定要加上该属性。查看官方文档了解更多关于key
的概念。
到这里,其实我们已经可以看到React
应用的效果了,如下图:
组件¶
我们知道React
是一个组件化的库,我们这里可以将用户列表做成一个组件,这样其他任何地方要使用用户列表的话,只需要将这个组件引入就行了,是不是很方便~~~
首先在src目录下面新建components目录,在该目录下新建文件:UserList.jsx:
import React from 'react'; const UserList = (props) => { return ( <div> { props.users.map(user => { return ( <h4 key={ user.id } className="well" >{ user.username } </h4> ) }) } </div> ) }; export default UserList;
注意这里,为什么我们使用一个函数组件而不是基于类的组件呢?注意在该组件中我们使用props
代替了state
:
Props
:数据通过props
向下传递,是只读的State
:数据绑定到一个组件上,是可读写的
可以查看文章ReactJS: Props vs. State了解更多属性和状态的区别。
限制基于类的(有状态)组件数量是一个好的习惯,因为它们可以操作状态,因为不太可预测,不太可控制。如果你只需要渲染数据(就像我们的UserList组件),使用一个函数类的组件是一个更好的选择。
现在我们需要在父组件中通过状态数据传递给子组件,首先在index.js
文件中先引入我们的用户列表组件:
import UserList from './components/UserList';
然后更新render()
方法:
render() { return ( <div className="container"> <div className="row"> <div className="col-md-4"> <br /> <h1>All Users</h1> <hr /><br /> <UserList users={this.state.users} /> </div> </div> </div> ) }
注意看上面是怎样引入用户列表组件的:,这样App
组件将状态数据users
通过属性的形式传递给了子组件UserList
,然后子组件中通过属性users
进行数据渲染,整个流程明白了吗?
到这里请确保React
应用能够正常的工作,在浏览器中打开http://localhost:3000
能得到上面相同的结果,然后我们对我们的代码进行一些Review
,然后提交到github
上去。