前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >构建具有用户身份认证的 React + Flux 应用程序

构建具有用户身份认证的 React + Flux 应用程序

作者头像
叙帝利
发布于 2018-01-17 08:43:27
发布于 2018-01-17 08:43:27
11.2K00
代码可运行
举报
文章被收录于专栏:前端新视界前端新视界
运行总次数:0
代码可运行

原文:Build a React + Flux App with User Authentication 译者: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 项目

在这篇教程中,我们将使用 React 以及 ES2015,这意味着需要一个编译器才能使用所有特性并兼容所有浏览器。我们会使用 webpack 编译,而使用 React + Webpack 构建一个新项目最简单的方式就是使用 Yeoman 的生成器。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm install -g yo
npm install -g generator-react-webpack
mkdir react-auth && cd react-auth
yo react-webpack

根据 Yeoman 的提示一步步安装,最后会得到一个搭配 webpack 的 React 新项目。

还需要安装一些 Yeoman 中没有的依赖包,快开始吧。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm install flux react-router bootstrap react-bootstrap keymirror superagent

为了使用 React Bootstrap,需要对 webpack 配置文件中的 url-loader 稍作调整。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// cfg/defaults.js

...

{
  test: /\.(png|woff|woff2|eot|ttf|svg)$/,
  loader: 'url-loader?limit=8192'
},

...

另外,要改一下 webpack 用于保存项目的路径,否则使用 React Router 会出问题。打开 server.js ,在最底部,将

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
open('http://localhost:' + config.port + '/webpack-dev-server/');

改成

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
open('http://localhost:' + config.port);

创建一个 Express 服务器

项目开始之前先创建 Express 服务器,保证 React 应用程序可以获取数据。这个服务器非常简单,只需要几个依赖模块。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mkdir react-auth-server && cd react-auth-server
npm init
npm install express express-jwt cors
touch server.js

安装 express-jwt 包是为了创建用户身份验证的中间件来保护 API 端口。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 方法获取数组中对象的 idname 字段。而在 /api/contacts/:id 端口,我们通过特殊的 id 字段检索数组并获得对应的对象。为了简单起见,我们只是使用模拟数据。在真实的应用中,这些数据是从服务器返回的。

注册 Auth0

你可能注意到我们在 Express 服务器中定义的 authCheck 。这是应用于 /api/contacts/:id 路由的中间件,它需要从我们这里获取验证信息。很显然,我们需要设置一个密钥,它会对比发送给 API 的解码 JWT 验证合法性。如果使用 Auth0,我们只需要将我们的密钥及用户 ID 提供给中间件。

如果你还没有 注册 Auth0,那现在就去注册一个。在你注册之后,你会在 management area 中找到用户密码及用户 ID。拿到这些关键信息之后,你要把它们放到中间件的合适位置,这样就大功告成了。

你要在 “Allowed Origins” 输入框中输入 localhost 域名及端口,这样 Auth0 才允许从测试域名获取请求。

创建 Index 文件和路由

先设置 index.js 文件,我们需要修改 Yeoman 生成器提供的文件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 的属性,渲染到名为 app 的 DOM 节点上。

为了完成路由设置,我们需要创建一个设置路由的 Root.js 文件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 包裹私有的 Routes ,然后给它们指定路径及组件。 Router 有一个名为 history 的参数,它可以解析 URL 并构建路径对象。之前我们在index.js 文件中也传递了一个 history 属性。

现在我们还应该添加 Lock 组件。可以使用 npm 安装,然后通过 webpack 构建的方式添加,或者作为 script 标签插入。为了简单一点,我们直接使用一个 script 标签插入。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    <!-- 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 组件

我们设置的第一个组件是 App 根组件。将 Main.js 命名为 App.js ,然后从 React Bootstrap 导入组件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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;

我们调用了名为 HeaderSidebar 的组件。之后会创建这些组件,但是现在,让我们看一看 componentWillMount  中发生的变化。我们在这里可以设置 Auth0Lock 实例,只需要调用 new Auth0Lock 然后传入用户 ID 以及用户域名。提醒一下,这两项可以在 Auth0 的 management area 中获得。

需要注意的一点是我们在第二个 Col 组件中调用了 {this.props.children} 。这个地方会展示 React Router 中的子路由, 通过这种方式,我们的应用程序会有一个侧边栏及动态视图。

我们已经将 Auth0Lock 实例作为 prop 传递到 Header 中,所以接下来创建 Header。

创建 Header 组件

导航条可以放置用户用来登录及注销应用程序的按钮。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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。但是,现在已经可以看到程序的工作流程。 login 方法可以弹出 Lock 组件,它由 “Login” NavItem 控制。现在我们只是简单的设置 authenticated 的状态为 true 或者 false,但是之后它的状态将由用户的 JWT 决定。

在我们看到屏幕上的东西之前,我们需要先创建 SidebarIndex 组件。

创建 Sidebar 和 Index 组件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 。这个组件只是展示点击的用户信息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 非常适合状态管理,但是它的缺点就是需要大量代码,这意味着这一部分有些啰嗦。为了尽可能简洁,我们不会详细讨论 Flux 是什么以及如何工作,如果你想深入了解,你可以阅读 Ken 的文章

简单介绍一下 Flux,它是一种帮助我们处理应用程序中单向数据流的结构。当应用程序变得庞大时,拥有一个单向流动的数据结构非常重要,因为相比混乱的双向数据流更容易理解。

为了做到这一点,Flux 需要 actions, dispatcher 以及 stores

创建 Dispatcher

先创建一个 dispatcher 。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/dispatcher/AppDispatcher.js

import { Dispatcher } from 'flux';

const AppDispatcher = new Dispatcher();

export default AppDispatcher;

在 React + Flux 应用中只有一个 dispatcher,可以通过调用 new Dispatcher() 创建。

创建 Actions

接下来,我们创建 actions 检索从 API 获取的联系人数据。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 需要 dispatch 一个 action type 和一个 payload 。payload 通常是与 action 有关的数据,而这些数据是从相关联的 store 中获得。

人们对于是否在应该在 actions 中调用 API 等操作有不同的看法,有些人认为应该保存在 stores 中。最终,你选择的方式取决于它是否适合你的应用程序,在 actions 中调用 API 是处理远程数据比较好的方式。

该组件依赖还没有创建的 ContactsAPIContactConstants ,所以现在就开始创建吧。

创建 Contact Constants

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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,之后会看到。我们使用 keyMirror 来保证 constants 的值可以匹配键值。

创建 Contacts API

我们已经从 ContactActions 组件中简单了解了 ContactsAPI 的功能。我们想创建一些向服务器端发送 XHR 请求的方法,用于接收数据并处理返回的 Promise 。对于 XHR 请求,我们将使用 superagent ,它是封装 XHR 比较好的一个库并且提供了处理 HTTP 请求的简单方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 方法中有一个处理错误或者响应的回调函数,我们可以用这些方法做任何事情。

如果我们在请求中遇到任何错误, 我们可以 reject (排除)错误。排除操作在 actions 的 catch 方法中。另外,我们可以 resolve (处理)从 API 获取的数据。

创建 Contact Store

在我们将通讯录数据渲染到屏幕上之前,我们需要创建 store 。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 注册了一个 switch 状态,可以响应程序中派发的各种 actions 。当 RECIEVE_CONTACTS action 被派发的时候,意味着我们正在从 API 获取联系人数据,而且我们想将联系人数据转成数组。这个功能由 setContacts 函数实现,之后通知 EventListener 发生变化,这样应用程序就知道发生了变化。

我们已经有了获取单个联系人或者整个列表的逻辑,这些方法会用在组件中。

在看到通讯录之前,我们需要创建几个组件来专门处理我们的列表。

创建 Contacts 组件

Contacts 组件将用于在侧边栏中展示联系人列表。我们将在列表中设置 Link 链接,稍后详细说明。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 这样处理其它操作非常重要。

当组件加载后,我们通过直接调用 ContactActions.recieveContacts action 来请求原始列表。这会向服务器发送一个 XHR (和在 ContactsAPI 定义的一样) 并触发 ContactStore 来处理数据。 我们需要在 componentWillMount 生命周期方法中添加一个 change 监听器,它将 onChange 方法作为回调函数。 onChange 方法负责设置 store 中当前联系人列表的状态。

我们使用 map 方法循环设置了状态的 contacts 数据,为每一项都创建一个列表项,这样可以很好的使用 ListGroup (React Bootstrap 的组件)展示。所以我们需要另外一个名为 ContactListItem 的组件,快开始吧。

创建 Contact List Item 组件

ContactListItem 组件会创建一个带有 React Router LinkListGroupItem (另一个 React Bootstrap 组件) ,它最终会展示联系人的详细信息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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;

我们通过 prop 获取 contact ,然后很容易地渲染出 name 属性。

修改 Sidebar

在预览应用之前做最后一次调整,就是修改 Sidebar ,这样一来之前的信息就会被替换成联系人列表。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/components/Sidebar.js

import React, { Component } from 'react';
import Contacts from './Contacts';

class SidebarComponent extends Component {
  render() {
    return (
      <Contacts />
    );
  }
}

export default SidebarComponent;

完成这一步,我们就可以查看联系人列表了。

创建 Contact Detail 组件

应用程序的最后一部分是联系人详情区域,它占据页面的主要部分。当点击联系人姓名时,会向服务器端发送请求,然后接收联系人信息并显示出来。

你已经注意到,在我们设置 Express 应用时,一开始我们就向 /contacts/:id 路由申请 JWT 中间件 (authCheck) ,这就意味着只有获得有效的 JWT,我们才能获取资源。也许这并不是你的应用程序的真实场景, 但是在这个例子中,限制用户信息很好的演示了需要认证的应用程序是如何工作的。

我们已经有了处理单个联系人的 action 和 store,所以让我们开始编写组件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 组件很像,但是它只处理单个联系人对象。注意我们向 ContactActionsContactStore 组件的 getContact 方法传递了一个 id 参数。这个 id 来自于 React Router,由 params 提供。当我们在列表中的联系人之间切换时,或者换句话说,当我们想查看“下一个”联系人时, componentWillReceiveProps 方法用于提取 params 中的 id

回顾 Contact Detail 路由

在预览这个组件之前,我们回顾 Root.js 文件中的 ContactDetail 路由。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/ 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 登录后会发生什么? 回调函数会返回很多内容,其中最重要的是 id_token ,它是一个 JWT 。其它内容还包括用户配置文件, access token,  refresh token  等等。

好消息是, 由于大部分的工作在 Auth0 的沙盒中完成,所以我们已经完成了身份认证。我们需要做的认证部分就是提供处理用户信息数据的逻辑以及成功登陆后返回的 JWT。

我们将遵循 Flux 的架构,为认证创建一系列的 actions, constants 以及 store 。

创建 AuthActions

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 组件类似,但在这里,我们关注用户的登录和注销。在 logUserIn 方法中,当我们调用 action 的时候,我们分发了来自 Header 组件的用户信息和 token

创建 Auth Constants

我们的用户身份认证需要一些新的 constants (静态变量)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/constants/AuthConstants.js

import keyMirror from 'keymirror';

export default keyMirror({
  LOGIN_USER: null,
  LOGOUT_USER: null
});

创建 Auth Store

AuthStore 是成功登陆后处理用户信息 及 JWT 的组件。那么我们到底需要做些什么呢?处理用户信息和 token 最简单的方式就是把它们保存在 local storage 中,这样它们在之后可以被重新利用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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;

setUser 是登陆成功之后使用的函数, 它的功能是将用户信息和 token 保存在 local storage 中。我们在组件中也写了一些有助于我们的工具类方法。其中 isAuthenticated 方法可以根据用户是否登录来隐藏或显示一些元素。

但是让我们再考虑一下。在传统的身份认证设置中,当用户成功登录时,服务器会生成一个 session ,这个 session 稍后用于检查用户是否经过身份认证。然而,JWT 认证是无状态的,它的工作原理是通过服务器去检查请求中的 token 令牌是否与密钥匹配。没有会话或也没有必要的状态。 出于很多原因 ,这是一种很好的方式,但是在我们的前端应用中应该如何验证用户的身份。

好消息是,我们真正需要做的是检查令牌是否保存在本地存储中。如果令牌无效,则请求将被拒绝,用户将需要重新登录。我们可以进一步检查令牌是否已经过期,但是现在只需要检查 JWT 是否存在。

修改 Header 组件

让我们赶快修改 header 组件,这样它就可以使用 AuthActions 以及 AuthStore 来分发正确的 actions 。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 。我们还需要在发送请求时将令牌添加到 Authorization header 中。通过 superagent,很容易在请求中设置。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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));
        })
    });
  }
}

我们在 Authorization header 中添加了 Bearer scheme 以及从 store 中获取的 JWT 。做完这一步,我们就可以访问受保护的内容了。

最后:根据条件显示和隐藏元素

我们的应用程序已经做的差不多了!最后,让我们根据条件展示和隐藏一些元素。 我们将在用户未验证时显示“Login”导航项,而验证之后将其隐藏起来。 “Logout”导航项正好相反。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 状态显示或隐藏 NavItems 。

我们可以用同样的方法设置 Index 组件中的提示信息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 。几乎所有流行的语言和框架都有集成,包括:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-09-14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
SAP—IDoc操作步骤
在ALE过程中,消息在系统之间,每一个ALE分布处理的参与系统必须拥有唯一的ID,这个ID即为逻辑系统。一般一个逻辑系统代指一个集团如果没有两个SAP系统,可以在一个SAP系统中的两个不同的Client端完成。我选择了800作为发送方,而810作为接收方。我将从800发送物料主数据到810中
用户5495712
2020/02/17
3.6K0
SAP—IDoc操作步骤
ActiveMQ消息传递的两种方式
1.什么是ActiveMQ?   ActiveMQ是apache提供的开源的,实现消息传递的一个中间插件,可以和spring整合,是目前最流行的开源消息总线,ActiveMQ是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现。较相似的还有rabbitMQ和kafka等,都是最为消息传递的插件 2.ActiveMQ传递消息的两种方式 前提:需要引入activemq的jar包 点对点方式(PTP):一个消费者对应一个生产者 发布/订阅模式(Publish/Sub):一个生产者产生消
用户2146856
2018/05/18
8230
爆赞!当年跳槽高级高发就是这么回答的!谈谈你对RocketMQ分布式事务原理的理解
有位工作五年的小伙伴在面试的时候被问到RocketMQ的分布式事务实现原理。他说他只知道RocketMQ能够支持事务,但是没有了解过它的事务实现原理。
Tom弹架构
2023/04/28
2580
爆赞!当年跳槽高级高发就是这么回答的!谈谈你对RocketMQ分布式事务原理的理解
什么是飞书机器人?如何定时发送飞书机器人消息?
机器人是飞书应用的一种能力类型。基于飞书的机器人能力,开发者能够以较低的开发成本(只需在服务端开发),实现在飞书单聊或群组中的消息推送和简单互动,完成企业系统数据与飞书的互联互通。
阿那个沫
2022/12/16
2.5K0
什么是飞书机器人?如何定时发送飞书机器人消息?
【SAP ABAP系列】SAP RFC通信模式详细解析
其本义是:异步通信时,通信双方时钟允许存在一定误差;同步通信时,双方时钟的允许误差较小。在SAP的系统间的通信过程中,也借用术语同步通信和异步通信,但其主要差异在于调用系统是否需要立即接受返回结果。这两种通信模式各有局限性,不同的应用适用于不同的通信模式。
matinal
2020/11/11
5.1K1
【SAP ABAP系列】SAP RFC通信模式详细解析
day75_淘淘商城项目_08_同步索引库问题分析 + ActiveMQ介绍/安装/使用 + ActiveMQ整合spring + 使用ActiveMQ实现添加商品后同步索引库_匠心笔记
方案一:在taotao-manager中,添加商品的业务逻辑中,添加一个同步索引库的业务逻辑。
黑泽君
2018/12/18
1K0
Spring boot+LayIM+t-io 好友申请通知的实现
企鹅号小编
2017/12/28
1.4K0
Spring boot+LayIM+t-io 好友申请通知的实现
常见技术类缺陷及解决方案
不论是通过F12分析页面请求,还是查看Skywalking做链路分析,经常会发现接口超时的问题。简单地调用流程图如下:
CKL的思考
2023/08/28
2770
常见技术类缺陷及解决方案
IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?
上一篇文章《IM群聊消息的已读回执功能该怎么实现?》是说,“很容易想到,是存一份”,被网友们骂了,大家争论的很激烈(见下图)。
JackJiang
2018/08/29
1.7K0
【SAP HANA系列】HANA计算视图中的RANK使用方法
2、当我们必须从源集中的多个记录或前N个或后N个记录中选择最新记录时,这将非常有用。
matinal
2020/11/13
1.7K0
【SAP HANA系列】HANA计算视图中的RANK使用方法
TCP/IP协议之三次握手过程及原因
三次握手 建立起 TCP连接 的 reliable,分配初始序列号和资源,在相互确认之后开始数据的传输。有 主动打开(一般是client) 和 被动打开(一般是server)。
灰子学技术
2023/10/30
4230
TCP/IP协议之三次握手过程及原因
Android程序后台开启服务,显示通知栏
一个Android程序仅仅只能前台 运行是远远不够的,我们更希望它在后台运行,既可以接收消息,又不耽误我们去使用别的软件,这就要求我们要实现两点:
fanfan
2022/05/07
1.7K0
Android程序后台开启服务,显示通知栏
Nginx运行FastCGI程序(ngx_http_fastcgi_module模块、fcgi库、spwan-fcgi进程管理器)
typedef struct { unsigned char version; // 版本号 unsigned char type; // 数据包类型 unsigned char requestIdB1; // 记录 id 高 8 位 unsigned char requestIdB0; // 记录 id 低 8 位 unsigned char contentLengthB1; // 记录内容长度高 8 位(body 长度高 8 位) unsigned char contentLengthB0; // 记录内容长度低 8 位(body 长度低 8 位) unsigned char paddingLength; // 补齐位长度(body 补齐长度) unsigned char reserved; // 补齐位 }FCGI_Header;
全栈程序员站长
2022/09/08
2.7K0
Nginx运行FastCGI程序(ngx_http_fastcgi_module模块、fcgi库、spwan-fcgi进程管理器)
消息队列:第五章:RabbitMQ的使用
安装配置RabbitMQ:https://blog.csdn.net/qq_33450681/article/details/85339315
Java廖志伟
2022/09/28
2610
消息队列:第五章:RabbitMQ的使用
Windows 7 连接 Windows 10 共享打印机,Windows 无法连接打印机,操作失败,错误为0x0000011b 的终极解决办法
Windows 7 连接 Windows 10 共享打印机出现错误 0x000001b,建议不要通过卸载Windows10系统的KB5005565安全更新来解决该问题(犹如削足适履),正确的处理方法是手工添加一个本地打印机,本方法是安全可靠的。本文详述了该方法的操作步骤。
全栈程序员站长
2022/09/30
8.4K0
Windows 7 连接 Windows 10 共享打印机,Windows 无法连接打印机,操作失败,错误为0x0000011b 的终极解决办法
安全声明标记语言SAML2.0初探
SAML的全称是Security Assertion Markup Language, 是由OASIS制定的一套基于XML格式的开放标准,用在身份提供者(IdP)和服务提供者 (SP)之间交换身份验证和授权数据。
程序那些事
2020/12/31
1.8K0
白话讲解:消息队列到底解决了什么问题?
2020年处于移动互联网的下半场,各种技术层出不穷,虽然数据也在爆发式增长,但是高并发、高吞吐已经不再是首要的痛点,稳定、可靠才是王道。 本文作为一篇消息队列入门级介绍,帮助大家对消息队列有一个大致的了解,并对对时下流行的消息队列组件进行了简单的比较,供大家做技术选型的参考。 1  什么是消息队列 消息队列(Message Queue),从广义上讲是一种消息队列服务中间件,提供一套完整的信息生产、传递、消费的软件系统。 消息队列所涵盖的功能远不止于队列(Queue),其本质是两个进程传递信息的一种方法。两
博文视点Broadview
2023/05/19
5170
白话讲解:消息队列到底解决了什么问题?
Yate教程1
From:http://yate.null.ro/pmwiki/index.php?n=Main.CppTutorial1 Yate可分为两个部分     * Yate内核     * Yate模
雪影
2018/08/02
8170
大数据开发工程师基本功修炼之史上最全Linux学习笔记(建议收藏)
Linux是大数据中的基础,无论是运维或开发,都免不了要学,而且学的越扎实越好,下面为大家带来Linux学习笔记
Maynor
2021/06/29
1.7K0
微信公众号模板消息
模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。
神葳
2021/01/22
4.6K0
推荐阅读
相关推荐
SAP—IDoc操作步骤
更多 >
LV.3
研发Leader
目录
  • 创建一个新的 React 项目
  • 创建一个 Express 服务器
  • 注册 Auth0
  • 创建 Index 文件和路由
  • 创建 App 组件
  • 创建 Header 组件
  • 创建 Sidebar 和 Index 组件
  • 使用 Flux
    • 创建 Dispatcher
    • 创建 Actions
    • 创建 Contact Constants
    • 创建 Contacts API
    • 创建 Contact Store
  • 创建 Contacts 组件
    • 创建 Contact List Item 组件
    • 修改 Sidebar
  • 创建 Contact Detail 组件
    • 回顾 Contact Detail 路由
  • 完成用户身份认证
    • 创建 AuthActions
    • 创建 Auth Constants
    • 创建 Auth Store
    • 修改 Header 组件
  • 发送身份认证请求
  • 最后:根据条件显示和隐藏元素
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档