Trello clone with Phoenix and React (第四章节)

这篇文章属于基于Phoenix Framework 和React的Trello系列

用户注册

上一章节,我们创建了User模型,并完成了相关验证以及加密密码等变更,同时更新了路由文件,创建了RegistrationControlle以便于处理新用户请求和返回验证需要的 JSON数据以及 jwt token。现在让我们转移到前端这边。

准备Recat路由

我们的主要目标拥有两种公共路由,一种是/sign_in/sign_up,任何访问者将能够通过他们登录到应用程序或这注册新的用户帐户。

另一方面我们需要/作为根路由,用于显示所有属于用户的卡片,而/boards/:id路由用于显示用户所选择的开片。访问最后两个路由,用户必须是验证过的,否则,我们将他重定向到注册页面。

让我们更新react-router路由文件,如下所示:

  1. // web/static/js/routes/index.js
  2. import { IndexRoute, Route } from 'react-router';
  3. import React from 'react';
  4. import MainLayout from '../layouts/main';
  5. import AuthenticatedContainer from '../containers/authenticated';
  6. import HomeIndexView from '../views/home';
  7. import RegistrationsNew from '../views/registrations/new';
  8. import SessionsNew from '../views/sessions/new';
  9. import BoardsShowView from '../views/boards/show';
  10. export default (
  11. <Route component={MainLayout}>
  12. <Route path="/sign_up" component={RegistrationsNew} />
  13. <Route path="/sign_in" component={SessionsNew} />
  14. <Route path="/" component={AuthenticatedContainer}>
  15. <IndexRoute component={HomeIndexView} />
  16. <Route path="/boards/:id" component={BoardsShowView} />
  17. </Route>
  18. </Route>
  19. );

复杂的部分是AuthenticatedContainer,让我们看看:

  1. // web/static/js/containers/authenticated.js
  2. import React from 'react';
  3. import { connect } from 'react-redux';
  4. import { routeActions } from 'redux-simple-router';
  5. class AuthenticatedContainer extends React.Component {
  6. componentDidMount() {
  7. const { dispatch, currentUser } = this.props;
  8. if (localStorage.getItem('phoenixAuthToken')) {
  9. dispatch(Actions.currentUser());
  10. } else {
  11. dispatch(routeActions.push('/sign_up'));
  12. }
  13. }
  14. render() {
  15. // ...
  16. }
  17. }
  18. const mapStateToProps = (state) => ({
  19. currentUser: state.session.currentUser,
  20. });
  21. export default connect(mapStateToProps)(AuthenticatedContainer);

在这里我们主要做的是,当组件mount时,检测在当前浏览器是否本地存储有 jwt token。后面我们将看到如何设置它,但现在,让我们试想一下,它不存在,这要感谢redux-simple-router库将用户重定向到注册页面。

注册视图组件

一旦发现他没有通过验证,我们将呈现给用户:

  1. // web/static/js/views/registrations/new.js
  2. import React, {PropTypes} from 'react';
  3. import { connect } from 'react-redux';
  4. import { Link } from 'react-router';
  5. import { setDocumentTitle, renderErrorsFor } from '../../utils';
  6. import Actions from '../../actions/registrations';
  7. class RegistrationsNew extends React.Component {
  8. componentDidMount() {
  9. setDocumentTitle('Sign up');
  10. }
  11. _handleSubmit(e) {
  12. e.preventDefault();
  13. const { dispatch } = this.props;
  14. const data = {
  15. first_name: this.refs.firstName.value,
  16. last_name: this.refs.lastName.value,
  17. email: this.refs.email.value,
  18. password: this.refs.password.value,
  19. password_confirmation: this.refs.passwordConfirmation.value,
  20. };
  21. dispatch(Actions.signUp(data));
  22. }
  23. render() {
  24. const { errors } = this.props;
  25. return (
  26. <div className="view-container registrations new">
  27. <main>
  28. <header>
  29. <div className="logo" />
  30. </header>
  31. <form onSubmit={::this._handleSubmit}>
  32. <div className="field">
  33. <input ref="firstName" type="text" placeholder="First name" required={true} />
  34. {renderErrorsFor(errors, 'first_name')}
  35. </div>
  36. <div className="field">
  37. <input ref="lastName" type="text" placeholder="Last name" required={true} />
  38. {renderErrorsFor(errors, 'last_name')}
  39. </div>
  40. <div className="field">
  41. <input ref="email" type="email" placeholder="Email" required={true} />
  42. {renderErrorsFor(errors, 'email')}
  43. </div>
  44. <div className="field">
  45. <input ref="password" type="password" placeholder="Password" required={true} />
  46. {renderErrorsFor(errors, 'password')}
  47. </div>
  48. <div className="field">
  49. <input ref="passwordConfirmation" type="password" placeholder="Confirm password" required={true} />
  50. {renderErrorsFor(errors, 'password_confirmation')}
  51. </div>
  52. <button type="submit">Sign up</button>
  53. </form>
  54. <Link to="/sign_in">Sign in</Link>
  55. </main>
  56. </div>
  57. );
  58. }
  59. }
  60. const mapStateToProps = (state) => ({
  61. errors: state.registration.errors,
  62. });
  63. export default connect(mapStateToProps)(RegistrationsNew);

对于这个组件,不想多的太多,当它加载的时候,改变了document标题,同时呈现给注册表单并dispatch注册登记 action creator的结果。

Action creator

当前面的表单确认后,我们想把数据发送到服务器并处理:

  1. // web/static/js/actions/registrations.js
  2. import { pushPath } from 'redux-simple-router';
  3. import Constants from '../constants';
  4. import { httpPost } from '../utils';
  5. const Actions = {};
  6. Actions.signUp = (data) => {
  7. return dispatch => {
  8. httpPost('/api/v1/registrations', {user: data})
  9. .then((data) => {
  10. localStorage.setItem('phoenixAuthToken', data.jwt);
  11. dispatch({
  12. type: Constants.CURRENT_USER,
  13. currentUser: data.user,
  14. });
  15. dispatch(pushPath('/'));
  16. })
  17. .catch((error) => {
  18. error.response.json()
  19. .then((errorJSON) => {
  20. dispatch({
  21. type: Constants.REGISTRATIONS_ERROR,
  22. errors: errorJSON.errors,
  23. });
  24. });
  25. });
  26. };
  27. };
  28. export default Actions;

RegistrationsNew组件调用action creator传递表单数据,新的POST请求发送到服务器。请求首先被Phoenix路由过滤,并由RegistrationController处理(上一章节创建的)。如果成功,将返回jwt token,并存储到localStorage中,创建的用户数据将dispatch到CURRENT_USER动作中,最后重定向用户到根路由页面。相反,注册数据有任何错误,将产生REGISTRATIONS_ERROR动作与错误,最终就可以以表格呈现给用户。

为了处理这些http请求,我们将借助于isomorphic-fetch包来处理这些utils文件,其中一些有助于完成这个目的。

  1. // web/static/js/utils/index.js
  2. import React from 'react';
  3. import fetch from 'isomorphic-fetch';
  4. import { polyfill } from 'es6-promise';
  5. export function checkStatus(response) {
  6. if (response.status >= 200 && response.status < 300) {
  7. return response;
  8. } else {
  9. var error = new Error(response.statusText);
  10. error.response = response;
  11. throw error;
  12. }
  13. }
  14. export function parseJSON(response) {
  15. return response.json();
  16. }
  17. export function httpPost(url, data) {
  18. const headers = {
  19. Authorization: localStorage.getItem('phoenixAuthToken'),
  20. Accept: 'application/json',
  21. 'Content-Type': 'application/json',
  22. }
  23. const body = JSON.stringify(data);
  24. return fetch(url, {
  25. method: 'post',
  26. headers: headers,
  27. body: body,
  28. })
  29. .then(checkStatus)
  30. .then(parseJSON);
  31. }
  32. // ...

Reducers

最后的步骤是处理这些带reducers动作返回的结果,因此我们创建了程序需要的状态树。首先,让我们看看session reducer,用于设置currentUser:

  1. // web/static/js/reducers/session.js
  2. import Constants from '../constants';
  3. const initialState = {
  4. currentUser: null,
  5. };
  6. export default function reducer(state = initialState, action = {}) {
  7. switch (action.type) {
  8. case Constants.CURRENT_USER:
  9. return { ...state, currentUser: action.currentUser };
  10. default:
  11. return state;
  12. }
  13. }

在这种情况下,任何注册错误都会更新状态,同时显示给用户。让我们添加注册reducer:

  1. // web/static/js/reducers/registration.js
  2. import Constants from '../constants';
  3. const initialState = {
  4. errors: null,
  5. };
  6. export default function reducer(state = initialState, action = {}) {
  7. switch (action.type) {
  8. case Constants.REGISTRATIONS_ERROR:
  9. return {...state, errors: action.errors};
  10. default:
  11. return state;
  12. }
  13. }

注意:我们从utils文件中调用renderErrorsFor函数用于显示错误:

  1. // web/static/js/utils/index.js
  2. // ...
  3. export function renderErrorsFor(errors, ref) {
  4. if (!errors) return false;
  5. return errors.map((error, i) => {
  6. if (error[ref]) {
  7. return (
  8. <div key={i} className="error">
  9. {error[ref]}
  10. </div>
  11. );
  12. }
  13. });
  14. }

这就是所有的注册过程。下一章节,我们将看到已存在的用户如何登录,并且访问个人的内容。同样,别忘记查看运行演示和下载最终的源代码:

演示 源代码