+11

Bắt đầu từ cơ bản: Chức năng đăng ký, đăng nhập

Đây là bài đầu tiên trong series: Hôm nay ăn gì với Laravel, ReactJS, React Hook, Redux Saga. Nếu các bạn chưa rõ mục đích mình tạo series này thì vui lòng ấn vào link và đọc nha.

Thật ra ban đầu mình không định làm chức năng này vì chỉ muốn cho một người sử dụng web thôi. Nhưng rồi mình nghĩ có lẽ nên phân chia quản lý ra nhiều users thì sẽ thiết thực hơn. Hãy cùng đi từ phía backend trước nhé

Backend

Hiện tại laravel đã ra tới bản 7.0, và câu lệnh php artisan make:auth đã không còn sử dụng được nữa. Nhưng đương nhiên không phải là Laravel đã khai tử nó, đơn giản chỉ là chuyển sang 1 cách khác thôi. Đầu tiên bạn hãy chạy command composer require laravel/ui. Sau đó thì sử dụng php artisan ui react --auth, nếu bạn dùng vue thì có thể thay react = vue. Tiếp tới thì các bạn chỉnh sửa file migration sao cho phù hợp rồi chạy migrate là được. Giờ thì bạn có thể sử dụng các chức năng như đăng nhập hay tạo tài khoản, nhưng ở đây mình sử dụng reactjs nên mình cần custom lại chút thì mới có thể sử dụng được. Nếu bạn chưa cách kết hợp reactjs trong laravel thì có thể đọc bài viết Sử dụng ReactJs trong project Laravel của mình nhé.

Giờ mình sẽ tạo AuthController để xử lý việc đăng nhập và đăng ký. Đăng ký thì sẽ khá đơn giản, chỉ là chúng ta tạo ra một người dùng mới trên hệ thống, nếu các bạn có thời gian thì hãy làm cả chức năng xác nhận email nhé. Lưu ý vì chúng ta sử dụng react nên respone trả về phải là json nhé.

    public function register(RegisterRequest $request)
    {
        $data = $request->all();
        $data['password'] = bcrypt($request->password);
        $user = User::create($data);

        return response()->json([
            'user' => $user,
            'message' => 'Đăng ký thành công',
        ], 200);

    }

Còn đối với đăng nhập, ban đầu mình có sử dụng Auth::attempt thì không được, và mình đã vào hẳn trong function của laravel và xem

use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\Auth\RegisterRequest;
use App\User;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
    use AuthenticatesUsers;
    
    public function login(LoginRequest $request)
    {
        if ($this->attemptLogin($request)) {
            $user = Auth::user();

            return $this->sendLoginResponse($request, $user);
        }

        return $this->sendFailedLoginResponse($request);
    }

    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            $this->credentials($request), $request->filled('remember')
        );
    }

    protected function sendLoginResponse(Request $request, $user)
    {
        $request->session()->regenerate();

        $this->clearLoginAttempts($request);

        if ($response = $this->authenticated($request, $this->guard()->user())) {
            return $response;
        }

        return response()->json([
            'data' => $user,
            'message' => 'Đăng nhập thành công',
        ], 200);
    }

    protected function sendFailedLoginResponse()
    {
        return response()->json([
            'message' => 'Email hoặc mật khẩu của bạn không chính xác',
        ], 400);
    }
}

Ở trong file spa-view.blade.php bạn nhớ thêm đoạn này nhé

<script type="text/javascript">window.GLOBALS={"user":{!! json_encode($user) !!}}</script>
<script src="{{ asset('js/app.js') }}"></script>

Frontend

Phân chia thư mục

Vì phải sử dụng thêm Redux Saga nên bước này ban đầu sẽ mất khá lâu để setup. Mình sẽ nói qua về cách mình chia thư mục đã nhé:

Tất cả source code sẽ được lưu trong resources/js. Mình có chia nhỏ ra thêm thành các thư mục con như sau:

components

Đây sẽ là nơi chứa các components của mình. Mình có phân ra thành 2 loại nhỏ nữa là pagesroutes. pages chính là nơi lưu trữ các view mà các bạn có thể truy cập. Còn routes sẽ là nơi chưa guest routes (Route sử dụng cho nhưng user không đăng nhập, ở đây hiện tại sẽ chỉ có view đăng nhập và đăg ký) và authenticated-route (Route sử dụng cho các user đã đăng nhập)

shared

Đây sẽ là nơi mình lưu nhưng config để dùng

stores

Nếu bạn sử dụng Redux Saga thì đã biết răng nó sẽ tạo ra 1 stores chung để lấy dữ liệu, và đây chính là nơi để mình code những chức năng liên quan

utils

Đây là nhưng function mà mình sẽ sử dụng lại ở nhiều nơi

Redux saga

Để sự dụng được redux saga thì chúng ta sẽ phải thay đổi 1 chút ở file App.js. Trước tiên cần tạo 1 file configStore.js trong shared

import 'babel-polyfill'; // Needed for redux-saga es6 generator support
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware, { END } from 'redux-saga';
import { fromJS } from 'immutable';

const sagaMiddleware = createSagaMiddleware();

export default function configStore(initialState = {}, history, rootReducer) {
    const middlewares = [
        sagaMiddleware
    ];

    const enhancers = [
        applyMiddleware(...middlewares)
    ];

    /* eslint-disable no-underscore-dangle */
    const composeEnhancers =
        process.env.APP_ENV !== 'production' &&
        typeof window === 'object' &&
        window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
            window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;
    /* eslint-enable */

    const store = createStore(
        rootReducer(),
        fromJS(initialState),
        composeEnhancers(...enhancers)
    );

    store.runSaga = sagaMiddleware.run;
    store.close = () => store.dispatch(END);

    return store;
}

Ở phần import là những packet mà mình sử dụng, các bạn nhớ cài đặt nhé. Ở đây mình có sử dụng thư viện immutable. Nếu bạn chưa biết về khái niệm này thì có thể đọc bài này nha: Immutability và Immutable.js trong ReactJs. Xuyên suốt dự án mình sẽ sử dụng nó đó.

Giờ hãy cùng thiết lập store nhé, trong thư mục stores sẽ bao gồm những files và folder sau:

Trong folder modules sẽ là những file tương ứng với từng view. Ví dụ mình làm cho chức năng đăng nhập và đăng ký thì sẽ tạo file authenticate.js. rootReducer là nơi để mình sẽ combine tất cả những reducers lại. Trong rootSaga thì mình sẽ yield tất cả các saga. Nói thì khá khó hình dung với những bạn mới, chút nữa mình sẽ đưa ra code cụ thể có lẽ các bạn sẽ dễ hình dung hơn. Hiện tại chưa có gì nên các file sẽ chỉ như sau:

rootReducer.js:

import { combineReducers } from 'redux-immutable';

export default function rootReducer(asyncReducers) {
}

rootSaga.js:

import { all, fork } from 'redux-saga/effects';

export default function* rootSaga() {
}

Giờ sẽ tới file App.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import {createBrowserHistory} from 'history';
import 'antd/dist/antd.css';
import configStore from '../shared/configStore';
import rootReducer from '../stores/rootReducer';
import rootSaga from '../stores/rootSaga';
import Routes from './routes';


const initialState = {};
const history = createBrowserHistory();
const store = configStore(initialState, history, rootReducer);
store.runSaga(rootSaga);

const App = () => (
    <Provider store={store}>
        <Router history={history}>
            <Routes />
        </Router>
    </Provider>
);


ReactDOM.render(<App/>, document.getElementById('app'));

Tạo modules phục vụ

Ở trong folder utils, các bạn hãy tạo 3 file

prop-to-js.js

import React from 'react';
import { Iterable } from 'immutable';
import getDisplayName from 'recompose/getDisplayName';
import { toPairs } from 'lodash';

export default function propsToJS(WrappedComponent) {
    function PropsToJS(wrappedComponentProps) {
        const KEY = 0;
        const VALUE = 1;

        const propsJS = toPairs(wrappedComponentProps)
            .reduce((newProps, wrappedComponentProp) => {
                newProps[wrappedComponentProp[KEY]] = Iterable.isIterable(wrappedComponentProp[VALUE]) // eslint-disable-line
                    ? wrappedComponentProp[VALUE].toJS()
                    : wrappedComponentProp[VALUE];
                return newProps;
            }, {});

        return <WrappedComponent {...propsJS} />;
    }

    PropsToJS.displayName = `PropsToJS${getDisplayName(WrappedComponent)}`;

    return PropsToJS;
}

axios.js:

import axios from 'axios';

const csrfToken = document.head.querySelector('meta[name=csrf-token]').getAttribute('content');

export default axios.create({
    headers: {
        'X-CSRF-TOKEN': csrfToken
    }
});

handle-response.js:

import {toastr} from 'react-redux-toastr';

function handleResponse(response) {
    switch (response.status) {
        case 200:
            toastr.success(response.data.message);
            break;
        case 422:
            const error = response.data[Object.keys(response.data)[1]];
            const message = error[Object.keys(error)[0]][0];
            toastr.error(message);
            break;
        case 400:
            if (typeof response.data.token != 'undefined') {
                window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.token;
            } else {
                toastr.error(typeof response.data == 'string'
                    ? response.data
                    : response.data[Object.keys(response.data)[0]]
                );
            }
            break;
        default:
            toastr.error(response.statusText ? response.statusText : response.data);
            break;
    }
}

export default handleResponse;

Giờ ở trong stores/modules chúng ta sẽ tạo file authenticate.js.

import {createAction, handleActions} from 'redux-actions';
import {put, call, takeLatest} from 'redux-saga/effects';
import {fromJS} from 'immutable';
import axios from '../../utils/axios';
import handleResponse from '../../utils/handle-respone';
import history from '../../components/routes/history';


//Action types
export const REGISTER = 'authenticate/REGISTER';

export const LOGIN = 'authenticate/LOGIN';
export const LOGIN_SUCCESSFULLY = 'authenticate/LOGIN_SUCCESSFULLY';


//Action creators
export const register = createAction(REGISTER);

export const login = createAction(LOGIN);
export const loginSuccessfully = createAction(LOGIN_SUCCESSFULLY);

// Reducer
const authenticateInitialState = fromJS({
    currentUser: window.GLOBALS.user || {},
});

const setLoginUser = (state, action) => state.setIn(['currentUser'], fromJS(action.payload));


export default handleActions({
    [LOGIN_SUCCESSFULLY]: setLoginUser,
}, authenticateInitialState);

// Selectors
export const getAuthenticate = state => state.get('authenticate');
export const getCurrentUser = state => getAuthenticate(state).get('currentUser');

// Sagas
export function* authenticateSagas() {
    yield takeLatest(register, onRegister);
    yield takeLatest(login, onLogin);
}

function* onRegister(action) {
    const response = yield call(onRegisterApi, action.payload);
    handleResponse(response);
}

function* onLogin(action) {
    const response = yield call(onLoginApi, action.payload);

    if (response.status === 200) {
        const {data} = response.data;
        yield put(loginSuccessfully(data));
        handleResponse(response);
        history.push('/');

        return;
    }

    handleResponse(response);
}

// Apis
function onRegisterApi(data) {
    const url = '/api/register';

    return axios.post(url, data)
        .then(response => response)
        .catch(error => error.response);
}

function onLoginApi(data) {
    const url = 'api/login';

    return axios.post(url, data)
        .then(response => response)
        .catch(error => error.response);
}

Cuối cùng là update lại rootReducerrootSaga

rootReducer.js

import { combineReducers } from 'redux-immutable';
import { reducer as toastrReducer } from 'react-redux-toastr';
import authenticate from './modules/authenticate';

export default function rootReducer(asyncReducers) {
    return combineReducers({
        toastr: toastrReducer,
        authenticate,
        ...asyncReducers
    });
}

rootSaga.js

import { all, fork } from 'redux-saga/effects';
import {authenticateSagas} from './modules/authenticate';

export default function* rootSaga() {
    yield all([
        fork(authenticateSagas)
    ]);
}

Thiết lập routes

Ở trong App.js bạn sẽ thấy có <Routes /> được import từ './routes'. Giờ chúng ta hãy cùng xem trong đó có gì nhé

index.js:

import React from 'react';
import {Switch, Route, withRouter} from 'react-router-dom';
import Authenticate from '../pages/authenticate';
import GuestRoute from '../routes/guest-route/guest-route';
import AuthenticatedRoute from '../routes/authenticated-route';
import {connect} from "react-redux";
import {getCurrentUser} from "../../stores/modules/authenticate";
import propsToJS from "../../utils/prop-to-js";

const Routes = () => (
    <Switch>
        {/* Home page */}
        <AuthenticatedRoute
            exact
            path='/'
            component={() => {
                return (
                    <div>
                        Đây là trang chủ
                    </div>
                )
            }}
        />

        <GuestRoute
            exact
            path='/login'
            component={Authenticate}
        />
    </Switch>
);

const mapStateToProps = state => ({
    currentUser: getCurrentUser(state)
});

export default withRouter(connect(mapStateToProps, null)(propsToJS(Routes)));

Ở đây các bạn sẽ thấy mình có chia ra làm 2 loại routes nư mình nói ở trên

history.js:

import {createBrowserHistory} from 'history';

export default createBrowserHistory();

Giờ mình sẽ tạo AuthenticatedRoute. Trong folder routes/authenticated-route trông sẽ như sau:

index.js sẽ để export 2 file kia ra nên trông rất đơn giản

export {default} from './authenticated-route-container';
export {default as AuthenticatedRoute} from './authenticated-route';

Ở trong authenticated-route-container.js sẽ chứa phần để xử lý với store, như việc chuyển state sang props và dispatch các action

import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import AuthenticatedRoute from './authenticated-route';
import {getCurrentUser} from '../../../stores/modules/authenticate'
import propsToJS from '../../../utils/prop-to-js';

const mapStateToProps = state => ({
    currentUser: getCurrentUser(state)
});

export default withRouter(connect(mapStateToProps, null)(propsToJS(AuthenticatedRoute)));

authenticated-route.js sẽ chứa code xử lý kiểm tra curentUser trong store

import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {isEmpty} from 'lodash';

const AuthenticatedRoute = ({component: Component, render: Render, ...rest}) => {
    if (isEmpty(rest.currentUser)) {
        return <Redirect to={{pathname: '/login', state: {from: rest.location}}}/>;
    }

    return <Route {...rest} render={props => <Component {...props} />}/>;
};

export default AuthenticatedRoute;

Với guest-route thì cũng tương tự, chỉ là đổi logic xử lý 1 chút.

Tạo view

Ở trong folder components/pages, mình sẽ tạo 1 folder authenticate có cấu trúc như sau

Mình sẽ chỉ đi vào những file quan trọng thôi nhé:

authenticate.js

import React, {useState} from 'react';
import {Row, Col} from 'antd';
import ReduxToastr from 'react-redux-toastr';
import 'react-redux-toastr/lib/css/react-redux-toastr.min.css';
import Login from './login';
import './login.scss';
import Register from "./register";

const Authenticate = (props) => {
    const [isShowRegister, showRegisterForm] = useState(false);
    const {register, login} = props;

    return (
        <div>
            <ReduxToastr
                getState={(state) => state.get('toastr')}
                timeOut={4000}
                transitionIn='fadeIn'
                transitionOut='fadeOut'
                progressBar
                closeOnToastrClick
            />
            <Row type="flex" justify="center" align="middle">
                {
                    isShowRegister ? (
                        <Register
                            showLoginForm={showRegisterForm}
                            register={register}
                        />
                    ) : (
                        <Login
                            showRegisterForm={showRegisterForm}
                            login={login}
                        />
                    )
                }
                <Col span={16} className='login_background'>

                </Col>
            </Row>
        </div>
    )
};

export default Authenticate;

Như các bạn đã thấy ở đây mình đã sử dụng hook để quản lý tate thay vì cách cũ, code đã ngắn đi rất nhiều. Ở đây thì mình sẽ làm view đăng nhập và đăng ký chung 1 địa chỉ đường dẫn. Bạn có thể tách ra hoặc làm giống mình, đó là tùy ở quyết định của bạn.

register/index.js

import React from 'react';
import {Col, Form, Input, Button, Checkbox} from 'antd';

const layout = {
    labelCol: {span: 8},
    wrapperCol: {span: 14},
};


const Register = (props) => {
    const [form] = Form.useForm();

    const showLoginForm = () => {
        props.showLoginForm(false);
    };

    const onFinish = (values) => {
        const {register} = props;
        register(values);
    };

    return (
        <Col span={8} className='register_form'>
            <h3>Đăng ký</h3>
            <div className='register_form--wrap'>
                <Form
                    {...layout}
                    name="basic"
                    onFinish={onFinish}
                >
                    <Form.Item
                        label="Tên"
                        name="name"
                        rules={[
                            {required: true, message: 'Vui lòng nhập tên'},
                            {max: 191, message: 'Vui lòng không nhập quá 191 ký tự'}
                        ]}
                    >
                        <Input/>
                    </Form.Item>
                    <Form.Item
                        label="Email"
                        name="email"
                        rules={[
                            {required: true, message: 'Vui lòng nhập email'},
                            {type: 'email', message: 'Vui lòng nhập đúng định dạng email'},
                            {max: 191, message: 'Vui lòng không nhập quá 191 ký tự'}
                        ]}
                    >
                        <Input/>
                    </Form.Item>

                    <Form.Item
                        label="Mật khẩu"
                        name="password"
                        rules={[
                            {required: true, message: 'Vui lòng nhập mật khẩu'},
                            {max: 16, message: 'Vui lòng không nhập quá 16 kí tự'},
                            {min: 6, message: 'Vui lòng không nhập dưới 6 kí tự'}
                        ]}
                    >
                        <Input.Password/>
                    </Form.Item>
                    <Form.Item
                        label="Nhập lại mật khẩu"
                        name="password_confirmation"
                        dependencies={['password']}
                        rules={[
                            {required: true, message: 'Vui lòng nhập đúng mật khẩu'},
                            ({getFieldValue}) => ({
                                validator(rule, value) {
                                    if (!value || getFieldValue('password') === value) {
                                        return Promise.resolve();
                                    }
                                    return Promise.reject('Mật khẩu nhập lại không khớp');
                                },
                            }),
                        ]}
                    >
                        <Input.Password/>
                    </Form.Item>

                    <Form.Item>
                        <Button type="primary" htmlType="submit">
                            Đăng ký
                        </Button>
                        <Button className='register_form--wrap__register-btn' type="danger" onClick={showLoginForm}>
                            Đăng nhập
                        </Button>
                    </Form.Item>
                </Form>
            </div>
        </Col>
    )
};

export default Register;

onFinish mình đã dispatch action register để gọi tới api thực hiện việc thêm người dùng. Và View đăng nhập thì cũng làm gần như tương tự nha.

Bài viết này hôm nay sẽ kết thục tại đây, các bạn có gì góp ý thì hãy để lại comment cho mình nhé. Hẹn gặp bạn lại ở bài viết tiếp theo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí