译者:nzbin
译者的话:这是一篇内容详实的 React + Flux 教程,文章主要介绍了如何使用 API 获取远程数据以及如何使用 JSON Web Tokens 进行用户身份认证。在阅读本文之后,我一直使用文章介绍的方法,通过搭建 Node 服务器,模拟接口数据进行前端开发。这篇文章发表于 2016 年 5 月,我是去年读的本文,但迟迟没有翻译,而现在准备重新学习 React ,所以把这篇文章翻出来与大家共勉。
React 的生态系统很大,为了解决 React 中比较困难的问题,你可以选择多种模块。大多数实际的 React 应用程序都有一些共同的需求,这些需求主要包括状态管理及路由。而解决这些需求最常用的是 Flux 及 React Router。
在 Scotch 上, Ken 有一些关于React 和 Flux 的 awesome series,当然,网上也有很多关于这些话题的教程。但是,在构建一个真实的 React 应用程序时,我们还需要考虑其它一些不经常讨论的事情:如何调用远程 API 以及如何验证用户身份。
在这篇教程中,我们将通过 API 获取数据的方式制作一个简单的通讯录应用。我们会使用 Express (NodeJS)服务器发送数据,需要说明的是并不一定非要使用 Node。只要能输出 JSON 数据,我们可以使用任何服务器。
单页应用中进行用户身份验证的最好方式就是 JSON Web Tokens (JWT) 。从头开始设置 JWT 身份验证非常繁琐,所以我们将使用 Auth0 。
使用 Auth0,我们只需要放置一个 script 标签就可以立即得到一个 登录框 ,它具有 社交登录 ,多重身份认证 等等。
当我们 注册 Auth0 之后,我们会得到一个免费账户,它提供 7,000 个免费用户以及两个社交认证供应商。最好的一点是这个账户是针对产品就绪的,所以我们可以开发真正的应用程序。
开始吧!
在这篇教程中,我们将使用 React 以及 ES2015,这意味着我们需要一个编译器才能使用所有特性并兼容所有浏览器。我们会使用 webpack 编译,而使用 React + Webpack 构建一个新项目最简单的方式就是使用 Yeoman 的生成器。
- npm install -g yo
- npm install -g generator-react-webpack
- mkdir react-auth && cd react-auth
- yo react-webpack
根据 Yeoman 的提示一步步安装,最后会得到一个搭配 webpack 的 React 新项目。
还需要安装一些 Yeoman 中没有的依赖包,快开始吧。
- npm install flux react - router bootstrap react - bootstrap keymirror superagent
为了使用 React Bootstrap,我们需要对 webpack 配置文件中的
稍作调整。
- url-loader
- // cfg/defaults.js
- ...
- {
- test: /\.(png|woff|woff2|eot|ttf|svg)$/,
- loader: 'url-loader?limit=8192'
- },
- ...
另外,我们要改一下 webpack 用于保存项目的路径,否则使用 React Router 会出问题。打开
,在最底部,将
- server.js
- open('http://localhost:' + config.port + '/webpack-dev-server/');
改成
- open('http://localhost:' + config.port);
项目开始之前我们先创建 Express 服务器,保证 React 应用程序可以获取数据。这个服务器非常简单,我们只需要几个依赖模块。
- mkdir react-auth-server && cd react-auth-server
- npm init
- npm install express express-jwt cors
- touch server.js
安装 express-jwt 包是为了创建用户身份验证的中间件来保护 API 端口。
- // server.js
- const express = require('express');
- const app = express();
- const jwt = require('express-jwt');
- const cors = require('cors');
- app.use(cors());
- // Authentication middleware provided by express-jwt.
- // This middleware will check incoming requests for a valid
- // JWT on any routes that it is applied to.
- const authCheck = jwt({
- secret: new Buffer('YOUR_AUTH0_SECRET', 'base64'),
- audience: 'YOUR_AUTH0_CLIENT_ID'
- });
- var contacts = [{
- id: 1,
- name: 'Chris Sevilleja',
- email: 'chris@scotch.io',
- image: '//gravatar.com/avatar/8a8bf3a2c952984defbd6bb48304b38e?s=200'
- },
- {
- id: 2,
- name: 'Nick Cerminara',
- email: 'nick@scotch.io',
- image: '//gravatar.com/avatar/5d0008252214234c609144ff3adf62cf?s=200'
- },
- {
- id: 3,
- name: 'Ado Kukic',
- email: 'ado@scotch.io',
- image: '//gravatar.com/avatar/99c4080f412ccf46b9b564db7f482907?s=200'
- },
- {
- id: 4,
- name: 'Holly Lloyd',
- email: 'holly@scotch.io',
- image: '//gravatar.com/avatar/5e074956ee8ba1fea26e30d28c190495?s=200'
- },
- {
- id: 5,
- name: 'Ryan Chenkie',
- email: 'ryan@scotch.io',
- image: '//gravatar.com/avatar/7f4ec37467f2f7db6fffc7b4d2cc8dc2?s=200'
- }];
- app.get('/api/contacts', (req, res) = >{
- const allContacts = contacts.map(contact = >{
- return {
- id: contact.id,
- name: contact.name
- }
- });
- res.json(allContacts);
- });
- app.get('/api/contacts/:id', authCheck, (req, res) = >{
- res.json(contacts.filter(contact = >contact.id === parseInt(req.params.id)));
- });
- app.listen(3001);
- console.log('Listening on http://localhost:3001');
我们得到了从两个端口返回的联系人数据数组。在
端口,我们使用
- /api/contacts
方法获取数组中对象的
- map
和
- id
字段。而在
- name
端口,我们通过特殊的
- /api/contacts/:id
字段检索数组并获得对应的对象。为了简单起见,我们只是使用模拟数据。在真实的应用中,这些数据是从服务器返回的。
- id
你可能注意到我们在 Express 服务器中定义的
。这是应用于
- authCheck
路由的中间件,它需要从我们这里获取验证信息。很显然,我们需要设置一个密钥,它会对比发送给 API 的解码 JWT 验证合法性。如果使用 Auth0,我们只需要将我们的密钥及用户 ID 提供给中间件。
- /api/contacts/:id
如果你还没有 注册 Auth0,那现在就去注册一个。在你注册之后,你会在 management area 中找到用户密码及用户 ID。拿到这些关键信息之后,你要把它们放到中间件的合适位置,这样就大功告成了。
你要在 “Allowed Origins” 输入框中输入 localhost 域名及端口,这样 Auth0 才允许从测试域名获取请求。
先设置
文件,我们需要修改 Yeoman 生成器提供的文件。
- index.js
- // src/index.js
- import 'core-js/fn/object/assign';
- import React from 'react';
- import ReactDOM from 'react-dom';
- import { browserHistory } from 'react-router';
- import Root from './Root';
- // Render the main component into the dom
- ReactDOM.render(<Root history={browserHistory} />, document.getElementById('app'));
我们渲染了一个名为
的组件,这个组件有一个名为
- Root
的属性,渲染到名为
- browserHistory
的 DOM 节点上。
- app
为了完成路由设置,我们需要创建一个设置路由的
文件。
- Root.js
- // Root.js
- import React, { Component } from 'react';
- import { Router, Route, IndexRoute } from 'react-router';
- import Index from './components/Index';
- import ContactDetail from './components/ContactDetail';
- import App from './components/App';
- class Root extends Component {
- // We need to provide a list of routes
- // for our app, and in this case we are
- // doing so from a Root component
- render() {
- return (
- <Router history={this.props.history}>
- <Route path='/' component={App}>
- <IndexRoute component={Index}/>
- <Route path='/contact/:id' component={ContactDetail} />
- </Route>
- </Router>
- );
- }
- }
- export default Root;
通过 React Router ,我们可以使用
包裹私有的
- Router
s ,然后给它们指定路径及组件。
- Route
有一个名为
- Router
的参数,它可以解析 URL 并构建路径对象。之前我们在
- history
文件中也传递了一个
- index.js
属性。
- history
现在我们还应该添加 Lock 组件。可以使用 npm 安装,然后通过 webpack 构建的方式添加,或者作为 script 标签插入。为了简单一点,我们直接使用一个 script 标签插入。
- <!-- src/index.html -->
- ...
- <!-- Auth0Lock script -->
- <script src="//cdn.auth0.com/js/lock-9.1.min.js">
- </script>
- <!-- Setting the right viewport -->
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
- />
- ...
我们设置的第一个组件是
根组件。将
- App
命名为
- Main.js
,然后从 React Bootstrap 导入组件。
- App.js
- // src/components/App.js
- import 'normalize.css/normalize.css';
- import 'bootstrap/dist/css/bootstrap.min.css';
- import React, { Component } from 'react';
- import Header from './Header';
- import Sidebar from './Sidebar';
- import { Grid, Row, Col } from 'react-bootstrap';
- class AppComponent extends Component {
- componentWillMount() {
- this.lock = new Auth0Lock('YOUR_AUTH0_CLIENT_ID', 'YOUR_AUTH0_DOMAIN);
- }
- render() {
- return (
- <div>
- <Header lock={this.lock}></Header>
- <Grid>
- <Row>
- <Col xs={12} md={3}>
- <Sidebar />
- </Col>
- <Col xs={12} md={9}>
- {this.props.children}
- </Col>
- </Row>
- </Grid>
- </div>
- );
- }
- }
- export default AppComponent;
我们调用了名为
和
- Header
的组件。之后会创建这些组件,但是现在,让我们看一看
- Sidebar
中发生的变化。我们在这里可以设置
- componentWillMount
实例,只需要调用
- Auth0Lock
然后传入用户 ID 以及用户域名。提醒一下,这两项可以在 Auth0 的 management area 中获得。
- new Auth0Lock
需要注意的一点是我们在第二个
组件中调用了
- Col
。这个地方会展示 React Router 中的子路由, 通过这种方式,我们的应用程序会有一个侧边栏及动态视图。
- {this.props.children}
我们已经将
实例作为 prop 传递到
- Auth0Lock
中,所以接下来创建 Header。
- Header
导航条可以放置用户用来登录及注销应用程序的按钮。
- // src/components/Header.js
- import React, { Component } from 'react';
- import { Nav, Navbar, NavItem, Header, Brand } from 'react-bootstrap';
- // import AuthActions from '../actions/AuthActions';
- // import AuthStore from '../stores/AuthStore';
- class HeaderComponent extends Component {
- constructor() {
- super();
- this.state = {
- authenticated: false
- }
- this.login = this.login.bind(this);
- this.logout = this.logout.bind(this);
- }
- login() {
- // We can call the show method from Auth0Lock,
- // which is passed down as a prop, to allow
- // the user to log in
- this.props.lock.show((err, profile, token) => {
- if (err) {
- alert(err);
- return;
- }
- this.setState({authenticated: true});
- });
- }
- logout() {
- // AuthActions.logUserOut();
- this.setState({authenticated: false});
- }
- render() {
- return (
- <Navbar>
- <Navbar.Header>
- <Navbar.Brand>
- <a href="#">React Contacts</a>
- </Navbar.Brand>
- </Navbar.Header>
- <Nav>
- <NavItem onClick={this.login}>Login</NavItem>
- <NavItem onClick={this.logout}>Logout</NavItem>
- </Nav>
- </Navbar>
- );
- }
- }
- export default HeaderComponent;
不可否认,我们省略了用户验证的一些细节,因为我们还没有创建 actions 和 stores。但是,现在已经可以看到程序的工作流程。
方法可以弹出 Lock 组件,它由 “Login”
- login
控制。现在我们只是简单的设置
- NavItem
的状态为
- authenticated
或者
- true
,但是之后它的状态将由用户的 JWT 决定。
- false
在我们看到屏幕上的东西之前,我们需要先创建
和
- Sidebar
组件。
- Index
- // src/components/Sidebar.js
- import React, { Component } from 'react';
- class SidebarComponent extends Component {
- render() {
- return (
- <h1>Hello from the sidebar</h1>
- );
- }
- }
- export default SidebarComponent;
最终这个组件会渲染从服务器返回的联系人列表,但是现在只展示一条简单的信息。
我们需要一个
组件作为路由的
- Index
。这个组件只是展示点击的用户信息。
- IndexRoute
- // src/components/Index.js
- import React, { Component } from 'react';
- class IndexComponent extends Component {
- constructor() {
- super();
- }
- render() {
- return (
- <h2>Click on a contact to view their profile</h2>
- );
- }
- }
- export default IndexComponent;
现在准备查看我们的应用程序。但是首先我们要删除或者注释掉
中的一些内容。我们还没有
- Root.js
组件,所以先临时删除导入部分及这个组件的
- ConactDetail
。
- Route
如果一切顺利,我们应该能看到渲染出的应用程序。
当我们点击 Login,应该可以看到 Lock 组件。
Flux 非常适合状态管理,但是它的缺点就是需要大量代码,这意味着这一部分有些啰嗦。为了尽可能简洁,我们不会详细讨论 Flux 是什么以及如何工作,如果你想深入了解,你可以阅读 Ken 的文章 。
简单介绍一下 Flux,它是一种帮助我们处理应用程序中单向数据流的结构。当应用程序变得庞大时,拥有一个单向流动的数据结构非常重要,因为相比混乱的双向数据流更容易理解。
为了做到这一点,Flux 需要 actions, dispatcher 以及 stores。
先创建一个 dispatcher 。
- // src/dispatcher/AppDispatcher.js
- import { Dispatcher } from 'flux';
- const AppDispatcher = new Dispatcher();
- export default AppDispatcher;
在 React + Flux 应用中只有一个 dispatcher,可以通过调用
创建。
- new Dispatcher()
接下来,我们创建 actions 检索从 API 获取的联系人数据。
- // src/actions/ContactActions.js
- import AppDispatcher from '../dispatcher/AppDispatcher';
- import ContactConstants from '../constants/ContactConstants';
- import ContactsAPI from '../utils/ContactsAPI';
- export
- default {
- recieveContacts:
- () = >{
- ContactsAPI.getContacts('http://localhost:3001/api/contacts').then(contacts = >{
- AppDispatcher.dispatch({
- actionType: ContactConstants.RECIEVE_CONTACTS,
- contacts: contacts
- });
- }).
- catch(message = >{
- AppDispatcher.dispatch({
- actionType: ContactConstants.RECIEVE_CONTACTS_ERROR,
- message: message
- });
- });
- },
- getContact: (id) = >{
- ContactsAPI.getContact('http://localhost:3001/api/contacts/' + id).then(contact = >{
- AppDispatcher.dispatch({
- actionType: ContactConstants.RECIEVE_CONTACT,
- contact: contact
- });
- }).
- catch(message = >{
- AppDispatcher.dispatch({
- actionType: ContactConstants.RECIEVE_CONTACT_ERROR,
- message: message
- });
- });
- }
- }
在一个 Flux 架构中,actions 需要
一个 action type 和一个 payload 。payload 通常是与 action 有关的数据,而这些数据是从相关联的 store 中获得。
- dispatch
人们对于是否在应该在 actions 中调用 API 等操作有不同的看法,有些人认为应该保存在 stores 中。最终,你选择的方式取决于它是否适合你的应用程序,在 actions 中调用 API 是处理远程数据比较好的方式。
该组件依赖还没有创建的
和
- ContactsAPI
,所以现在就开始创建吧。
- ContactConstants
- // src/constants/ContactConstants.js
- import keyMirror from 'keymirror';
- export
- default keyMirror({
- RECIEVE_CONTACT:
- null,
- RECIEVE_CONTACTS: null,
- RECIEVE_CONTACT_ERROR: null,
- RECIEVE_CONTACTS_ERROR: null
- });
Constants 可以识别 action 的类型,可以同步 actions 及 stores,之后会看到。我们使用
来保证 constants 的值可以匹配键值。
- keyMirror
我们已经从
组件中简单了解了
- ContactActions
的功能。我们想创建一些向服务器端发送 XHR 请求的方法,用于接收数据并处理返回的
- ContactsAPI
。对于 XHR 请求,我们将使用 superagent ,它是封装 XHR 比较好的一个库并且提供了处理 HTTP 请求的简单方法。
- Promise
- // src/utils/ContactsAPI.js
- import request from 'superagent/lib/client';
- export default {
- // We want to get a list of all the contacts
- // from the API. This list contains reduced info
- // and will be be used in the sidebar
- getContacts: (url) => {
- return new Promise((resolve, reject) => {
- request
- .get(url)
- .end((err, response) => {
- if (err) reject(err);
- resolve(JSON.parse(response.text));
- })
- });
- },
- getContact: (url) => {
- return new Promise((resolve, reject) => {
- request
- .get(url)
- .end((err, response) => {
- if (err) reject(err);
- resolve(JSON.parse(response.text));
- })
- });
- }
- }
通过 superagent,我们可以调用
方法发送
- get
请求。在
- GET
方法中有一个处理错误或者响应的回调函数,我们可以用这些方法做任何事情。
- end
如果我们在请求中遇到任何错误, 我们可以
(排除)错误。排除操作在 actions 的
- reject
方法中。另外,我们可以
- catch
(处理)从 API 获取的数据。
- resolve
在我们将通讯录数据渲染到屏幕上之前,我们需要创建 store 。
- // src/stores/ContactStore.js
- import AppDispatcher from '../dispatcher/AppDispatcher';
- import ContactConstants from '../constants/ContactConstants';
- import {
- EventEmitter
- }
- from 'events';
- const CHANGE_EVENT = 'change';
- let _contacts = [];
- let _contact = {};
- function setContacts(contacts) {
- _contacts = contacts;
- }
- function setContact(contact) {
- _contact = contact;
- }
- class ContactStoreClass extends EventEmitter {
- emitChange() {
- this.emit(CHANGE_EVENT);
- }
- addChangeListener(callback) {
- this.on(CHANGE_EVENT, callback)
- }
- removeChangeListener(callback) {
- this.removeListener(CHANGE_EVENT, callback)
- }
- getContacts() {
- return _contacts;
- }
- getContact() {
- return _contact;
- }
- }
- const ContactStore = new ContactStoreClass();
- // Here we register a callback for the dispatcher
- // and look for our various action types so we can
- // respond appropriately
- ContactStore.dispatchToken = AppDispatcher.register(action = >{
- switch (action.actionType) {
- case ContactConstants.RECIEVE_CONTACTS:
- setContacts(action.contacts);
- // We need to call emitChange so the event listener
- // knows that a change has been made
- ContactStore.emitChange();
- break
- case ContactConstants.RECIEVE_CONTACT:
- setContact(action.contact);
- ContactStore.emitChange();
- break
- case ContactConstants.RECIEVE_CONTACT_ERROR:
- alert(action.message);
- ContactStore.emitChange();
- break
- case ContactConstants.RECIEVE_CONTACTS_ERROR:
- alert(action.message);
- ContactStore.emitChange();
- break
- default:
- }
- });
- export
- default ContactStore;
和大多数 stores 的功能一样,我们在
注册了一个
- AppDispatcher
状态,可以响应程序中派发的各种 actions 。当
- switch
action 被派发的时候,意味着我们正在从 API 获取联系人数据,而且我们想将联系人数据转成数组。这个功能由
- RECIEVE_CONTACTS
函数实现,之后通知
- setContacts
发生变化,这样应用程序就知道发生了变化。
- EventListener
我们已经有了获取单个联系人或者整个列表的逻辑,这些方法会用在组件中。
在看到通讯录之前,我们需要创建几个组件来专门处理我们的列表。
组件将用于在侧边栏中展示联系人列表。我们将在列表中设置
- Contacts
链接,稍后详细说明。
- Link
- // src/components/Contacts.js
- import React,
- {
- Component
- }
- from 'react';
- import {
- ListGroup
- }
- from 'react-bootstrap';
- // import { Link } from 'react-router';
- import ContactActions from '../actions/ContactActions';
- import ContactStore from '../stores/ContactStore';
- import ContactListItem from './ContactListItem';
- // We'll use this function to get a contact
- // list item for each of the contacts in our list
- function getContactListItem(contact) {
- return ( < ContactListItem key = {
- contact.id
- }
- contact = {
- contact
- }
- />
- );
- }
- class ContactsComponent extends Component {
- constructor() {
- super();
- / / For our initial state, we just want
- // an empty array of contacts
- this.state = {
- contacts: []
- }
- // We need to bind this to onChange so we can have
- // the proper this reference inside the method
- this.onChange = this.onChange.bind(this);
- }
- componentWillMount() {
- ContactStore.addChangeListener(this.onChange);
- }
- componentDidMount() {
- ContactActions.recieveContacts();
- }
- componentWillUnmount() {
- ContactStore.removeChangeListener(this.onChange);
- }
- onChange() {
- this.setState({
- contacts: ContactStore.getContacts()
- });
- }
- render() {
- let contactListItems;
- if (this.state.contacts) {
- // Map over the contacts and get an element for each of them
- contactListItems = this.state.contacts.map(contact = >getContactListItem(contact));
- }
- return ( < div > <ListGroup > {
- contactListItems
- } < /ListGroup>
- </div > );
- }
- }
- export
- default ContactsComponent;
我们需要有一个初始状态, 如果使用 ES2015,可以在 constructor 中设置
。我们给
- this.state
方法绑定了
- onChange
,所以在方法中我们可以获得正确的
- this
上下文环境。 在组件方法中像
- this
这样处理其它操作非常重要。
- this.setState
当组件加载后,我们通过直接调用
action 来请求原始列表。这会向服务器发送一个 XHR (和在
- ContactActions.recieveContacts
定义的一样) 并触发
- ContactsAPI
来处理数据。 我们需要在
- ContactStore
生命周期方法中添加一个 change 监听器,它将
- componentWillMount
方法作为回调函数。
- onChange
方法负责设置 store 中当前联系人列表的状态。
- onChange
我们使用 map 方法循环设置了状态的
数据,为每一项都创建一个列表项,这样可以很好的使用
- contacts
(React Bootstrap 的组件)展示。所以我们需要另外一个名为
- ListGroup
的组件,快开始吧。
- ContactListItem
组件会创建一个带有 React Router
- ContactListItem
的
- Link
(另一个 React Bootstrap 组件) ,它最终会展示联系人的详细信息。
- ListGroupItem
- // src/components/ContactListItem.js
- import React, { Component } from 'react';
- import { ListGroupItem } from 'react-bootstrap';
- import { Link } from 'react-router';
- class ContactListItem extends Component {
- render() {
- const { contact } = this.props;
- return (
- <ListGroupItem>
- <Link to={`/contact/${contact.id}`}>
- <h4>{contact.name}</h4>
- </Link>
- </ListGroupItem>
- );
- }
- }
- export default ContactListItem;
我们通过
获取 contact ,然后很容易地渲染出
- prop
属性。
- name
在预览应用之前做最后一次调整,就是修改
,这样一来之前的信息就会被替换成联系人列表。
- Sidebar
- // src/components/Sidebar.js
- import React, { Component } from 'react';
- import Contacts from './Contacts';
- class SidebarComponent extends Component {
- render() {
- return (
- <Contacts />
- );
- }
- }
- export default SidebarComponent;
完成这一步,我们就可以查看联系人列表了。
应用程序的最后一部分是联系人详情区域,它占据页面的主要部分。当点击联系人姓名时,会向服务器端发送请求,然后接收联系人信息并显示出来。
你已经注意到,在我们设置 Express 应用时,一开始我们就向
路由申请 JWT 中间件 (
- /contacts/:id
) ,这就意味着只有获得有效的 JWT,我们才能获取资源。也许这并不是你的应用程序的真实场景, 但是在这个例子中,限制用户信息很好的演示了需要认证的应用程序是如何工作的。
- authCheck
我们已经有了处理单个联系人的 action 和 store,所以让我们开始编写组件。
- // src/components/ContactDetail.js
- import React,
- {
- Component
- }
- from 'react';
- import ContactActions from '../actions/ContactActions';
- import ContactStore from '../stores/ContactStore';
- class ContactDetailComponent extends Component {
- constructor() {
- super();
- this.state = {
- contact: {}
- }
- this.onChange = this.onChange.bind(this);
- }
- componentWillMount() {
- ContactStore.addChangeListener(this.onChange);
- }
- componentDidMount() {
- ContactActions.getContact(this.props.params.id);
- }
- componentWillUnmount() {
- ContactStore.removeChangeListener(this.onChange);
- }
- componentWillReceiveProps(nextProps) {
- this.setState({
- contact: ContactActions.getContact(nextProps.params.id)
- });
- }
- onChange() {
- this.setState({
- contact: ContactStore.getContact(this.props.params.id)
- });
- }
- render() {
- let contact;
- if (this.state.contact) {
- contact = this.state.contact;
- }
- return ( < div > {
- this.state.contact && <div > <img src = {
- contact.image
- }
- width = "150" / ><h1 > {
- contact.name
- } < /h1>
- <h3>{contact.email}</h3 > </div>
- }
- </div > );
- }
- }
- export
- default ContactDetailComponent;
这个组件看上去和
组件很像,但是它只处理单个联系人对象。注意我们向
- Contacts
和
- ContactActions
组件的
- ContactStore
方法传递了一个
- getContact
参数。这个
- id
来自于 React Router,由
- id
提供。当我们在列表中的联系人之间切换时,或者换句话说,当我们想查看“下一个”联系人时,
- params
方法用于提取
- componentWillReceiveProps
中的
- params
。
- id
在预览这个组件之前,我们回顾
文件中的
- Root.js
路由。
- ContactDetail
- / src/Root.js
- ...
- render() {
- return (
- <Router history={this.props.history}>
- <Route path='/' component={App}>
- <IndexRoute component={Index}/>
- <Route path='/contact/:id' component={ContactDetail} />
- </Route>
- </Router>
- );
- }
- ...
现在我们可以点击联系人查看详情,但是无权访问。
这个无权访问的错误是因为服务器端的中间件在保护联系人的详情资源。服务器需要一个有效的 JWT 才允许请求。为了做到这一点,我们首先需要对用户进行身份验证。让我们完成验证部分。
当用户使用 Auth0 登录后会发生什么? 回调函数会返回很多内容,其中最重要的是
,它是一个 JWT 。其它内容还包括用户配置文件, access token, refresh token 等等。
- id_token
好消息是, 由于大部分的工作在 Auth0 的沙盒中完成,所以我们已经完成了身份认证。我们需要做的认证部分就是提供处理用户信息数据的逻辑以及成功登陆后返回的 JWT。
我们将遵循 Flux 的架构,为认证创建一系列的 actions, constants 以及 store 。
- // src/actions/AuthActions.js
- import AppDispatcher from '../dispatcher/AppDispatcher';
- import AuthConstants from '../constants/AuthConstants';
- export
- default {
- logUserIn:
- (profile, token) = >{
- AppDispatcher.dispatch({
- actionType: AuthConstants.LOGIN_USER,
- profile: profile,
- token: token
- });
- },
- logUserOut: () = >{
- AppDispatcher.dispatch({
- actionType: AuthConstants.LOGOUT_USER
- });
- }
- }
以上设置和
组件类似,但在这里,我们关注用户的登录和注销。在
- ContactActions
方法中,当我们调用 action 的时候,我们分发了来自
- logUserIn
组件的用户信息和
- Header
。
- token
我们的用户身份认证需要一些新的 constants (静态变量)
- // src/constants/AuthConstants.js
- import keyMirror from 'keymirror';
- export default keyMirror({
- LOGIN_USER: null,
- LOGOUT_USER: null
- });
是成功登陆后处理用户信息 及 JWT 的组件。那么我们到底需要做些什么呢?处理用户信息和 token 最简单的方式就是把它们保存在 local storage 中,这样它们在之后可以被重新利用。
- AuthStore
- // src/stores/AuthStore.js
- import AppDispatcher from '../dispatcher/AppDispatcher';
- import AuthConstants from '../constants/AuthConstants';
- import {
- EventEmitter
- }
- from 'events';
- const CHANGE_EVENT = 'change';
- function setUser(profile, token) {
- if (!localStorage.getItem('id_token')) {
- localStorage.setItem('profile', JSON.stringify(profile));
- localStorage.setItem('id_token', token);
- }
- }
- function removeUser() {
- localStorage.removeItem('profile');
- localStorage.removeItem('id_token');
- }
- class AuthStoreClass extends EventEmitter {
- emitChange() {
- this.emit(CHANGE_EVENT);
- }
- addChangeListener(callback) {
- this.on(CHANGE_EVENT, callback)
- }
- removeChangeListener(callback) {
- this.removeListener(CHANGE_EVENT, callback)
- }
- isAuthenticated() {
- if (localStorage.getItem('id_token')) {
- return true;
- }
- return false;
- }
- getUser() {
- return localStorage.getItem('profile');
- }
- getJwt() {
- return localStorage.getItem('id_token');
- }
- }
- const AuthStore = new AuthStoreClass();
- // Here we register a callback for the dispatcher
- // and look for our various action types so we can
- // respond appropriately
- AuthStore.dispatchToken = AppDispatcher.register(action = >{
- switch (action.actionType) {
- case AuthConstants.LOGIN_USER:
- setUser(action.profile, action.token);
- AuthStore.emitChange();
- break
- case AuthConstants.LOGOUT_USER:
- removeUser();
- AuthStore.emitChange();
- break
- default:
- }
- });
- export
- default AuthStore;
是登陆成功之后使用的函数, 它的功能是将用户信息和 token 保存在 local storage 中。我们在组件中也写了一些有助于我们的工具类方法。其中
- setUser
方法可以根据用户是否登录来隐藏或显示一些元素。
- isAuthenticated
但是让我们再考虑一下。在传统的身份认证设置中,当用户成功登录时,服务器会生成一个 session ,这个 session 稍后用于检查用户是否经过身份认证。然而,JWT 认证是无状态的,它的工作原理是通过服务器去检查请求中的 token 令牌是否与密钥匹配。没有会话或也没有必要的状态。 出于很多原因 ,这是一种很好的方式,但是在我们的前端应用中应该如何验证用户的身份。
好消息是,我们真正需要做的是检查令牌是否保存在本地存储中。如果令牌无效,则请求将被拒绝,用户将需要重新登录。我们可以进一步检查令牌是否已经过期,但是现在只需要检查 JWT 是否存在。
让我们赶快修改 header 组件,这样它就可以使用
以及
- AuthActions
来分发正确的 actions 。
- AuthStore
- // src/components/Header.js
- ...
- import AuthActions from '../actions/AuthActions';
- import AuthStore from '../stores/AuthStore';
- class HeaderComponent extends Component {
- ...
- login() {
- this.props.lock.show((err, profile, token) = >{
- if (err) {
- alert(err);
- return;
- }
- AuthActions.logUserIn(profile, token);
- this.setState({
- authenticated: true
- });
- });
- }
- logout() {
- AuthActions.logUserOut();
- this.setState({
- authenticated: false
- });
- }
- ...
正确修改文件之后,如果用户已经登录,用户信息及 JWT 会被保存。
联系人详情资源受 JWT 身份认证的保护,现在我们为用户添加了有效的 JWT 。我们还需要在发送请求时将令牌添加到
header 中。通过 superagent,很容易在请求中设置。
- Authorization
- // src/utils/ContactsAPI.js
- import AuthStore from '../stores/AuthStore';
- ...
- getContact: (url) => {
- return new Promise((resolve, reject) => {
- request
- .get(url)
- .set('Authorization', 'Bearer ' + AuthStore.getJwt())
- .end((err, response) => {
- if (err) reject(err);
- resolve(JSON.parse(response.text));
- })
- });
- }
- }
我们在
header 中添加了
- Authorization
scheme 以及从 store 中获取的 JWT 。做完这一步,我们就可以访问受保护的内容了。
- Bearer
我们的应用程序已经做的差不多了!最后,让我们根据条件展示和隐藏一些元素。 我们将在用户未验证时显示“Login”导航项,而验证之后将其隐藏起来。 “Logout”导航项正好相反。
- // src/components/Header.js
- ...
- constructor() {
- super();
- this.state = {
- authenticated: AuthStore.isAuthenticated()
- }
- ...
- }
- ...
- render() {
- return (
- <Navbar>
- <Navbar.Header>
- <Navbar.Brand>
- <a href="#">React Contacts</a>
- </Navbar.Brand>
- </Navbar.Header>
- <Nav>
- { !this.state.authenticated ? (
- <NavItem onClick={this.login}>Login</NavItem>
- ) : (
- <NavItem onClick={this.logout}>Logout</NavItem>
- )}
- </Nav>
- </Navbar>
- );
- }
- ...
当组件加载后,我们从 store 中获得用户的身份验证状态。根据
状态显示或隐藏
- authenticated
s 。
- NavItem
我们可以用同样的方法设置
组件中的提示信息。
- Index
- // src/components/Index.js
- ...
- constructor() {
- super();
- this.state = {
- authenticated: AuthStore.isAuthenticated()
- }
- }
- render() {
- return (
- <div>
- { !this.state.authenticated ? (
- <h2>Log in to view contact details</h2>
- ) : (
- <h2>Click on a contact to view their profile</h2>
- )}
- </div>
- );
- }
- ...
如果你跟着本教程做完,现在你已经有了一个 React + Flux 的应用,它调用 API 获取数据以及使用 Auth0 完成用户身份认证。非常棒!
毫无疑问: 创建一个 React + Flux 应用程序需要写大量代码,而构建小项目很难看到它的优势。但是,随着应用程序体量的增长,单向数据流以及 Flux 遵循的应用结构变得非常重要。当应用程序变得越来越大时,有必要消除双向绑定带来的困惑。
幸运的是,令人棘手的身份验证部分使用 Auth0 来做非常简单。如果你的应用程序没有使用 Node 作为后端,务必选择适合你的 Auth0 SDK 。几乎所有流行的语言和框架都有集成,包括:
来源: http://www.cnblogs.com/nzbin/p/7397604.html