Разработка клиентской части блога на DVA.Часть 3
В прошлых двух частях мы разработали простые страницы (часть 1) нашего приложения и создали главную страницу (часть 2). В этой статье мы рассмотрим более интересные кейсы:
- Подключение к API, разработанному на Loopback;
- Разработка авторизации на JWT (json web token).
Подключение к API, разработанному на Loopback
Итак, наконец-то мы добрались до момента работы с настоящим api, предоставляемым nodejs приложением. Для начала нам необходимо сделать две вещи:
- Добавить возможность переключения api с mock данных на nodejs приложение и обратно;
- Добавить настройку для проксирования запросов на nodejs приложение.
Однако, Вам еще понадобится загрузить доработанную версию loopback приложения. В данном приложении мы переключили хранение данных на MySQL базу данных в моделях, добавили автомиграции и заполнение тестовыми данными. Код приложения доступен в репозитории:
git clone https://github.com/dionic-i/lb-blog.git
После загрузки переходим в папку проекта:
npm i
git checkout part2-2
npm run dev
Чтобы добавить возможность переключения api с mock данных на nodejs приложение и обратно давайте добавим файл конфигурации в папку utils, где укажем текущий префикс api и два разных набора api. Вот что у нас получилось:
const APIV1 = '/api';
const config = {
name: 'Our blog',
apiPrefix: 'api_lp',
api: {
userLoginUrl : `${APIV1}/user/login`,
userLogoutUrl : `${APIV1}/user/logout`,
statusUrl : `${APIV1}/status`,
blogsUrl : `${APIV1}/blogs`,
blogByIdUrl : `${APIV1}/blogs/{id}`,
blogArticlesUrl: `${APIV1}/blogs/{id}/articles`,
articleUrl : `${APIV1}/articles/${id}`,
},
api_lp: {
userLoginUrl : `${APIV1}/Users/login`,
userLogoutUrl : `${APIV1}/Users/logout`,
statusUrl : `${APIV1}/Apps/status`,
blogsUrl : `${APIV1}/Blogs`,
blogByIdUrl : `${APIV1}/Blogs/{id}`,
blogArticlesUrl: `${APIV1}/Blogs/{id}/articles`,
articleUrl : `${APIV1}/Articles/{id}`,
},
};
export default config;
Теперь нам необходимо немного доработать методы в файлах сервисов blogs.js и articles.js, поправив их методы по загрузке данных. Рассмотрим на примере одного из методов:
import request from '../utils/request';
import config from '../utils/config';
import _ from 'lodash';
const {apiPrefix} = config; (1)
const {blogsUrl, blogByIdUrl, blogArticlesUrl} = config[apiPrefix]; (2)
export async function findById(id) {
const url = _.replace(blogByIdUrl, '{id}', id);
return request({
url : url,
method: 'get'
});
}
В данном случае мы сначала импортировали объект конфигурации, затем сделали деструктуризацию и получили текущий apiPrefix (1). А потом опять провели деструтуризацию, но уже объекта со списком url и получили в переменные наши url для запроса данных (2). Их мы уже и применяем в самих методах.
Теперь, чтобы наше клиентское приложение обращалось к nodejs api за данными, нам необходимо добавить настройку для проксирования запросов. Делается это добавлением следующих строчек в файл .webpackrc:
{
"proxy": {
"/api": {
"target": "http://127.0.0.1:3000",
"changeOrigin": true,
}
},
"extraBabelPlugins": [
["import", { "libraryName": "antd", "style": true }]
],
"env": {
"development": {
"extraBabelPlugins": [
"dva-hmr"
]
}
}
}
Все! Теперь наше клиентское приложение обращается за данными к nodejs приложению. Если будет необходимо переключиться обратно на mock данные, то мы просто меняем apiPrefix и убираем проксирование данных из .webpackrc.
Скачать исходный код можно по ссылке:
git clone https://github.com/dionic-i/lb-blog-client.git
После загрузки переходим в папку проекта
npm i
git checkout part2-2
npm run dev
Разработка авторизации на JWT (json web token)
Мы добрались до самой интересной части нашей статьи - авторизации пользователя по JWT.
Loopback из коробки поддерживает методы для авторизации пользователей. У модели User есть для этого соответствующие методы: login и logout, которые доступны по url /Users/login и Users/logout. Остальные методы, которые есть у данной модели можно посмотреть в explorer.
Метод /Users/login в качестве параметров принимает username и password и возвращает json токен, который используется для дальнейшей авторизации в приложении.
Вот так выглядит успешный ответ при авторизации:
{
"id": "ek33csbgM77V48fpiYSlFy7GyH8tZm5MTCQBA6UzXrGGL3arSbRS5IQBnlfYUy0N",
"ttl": 1209600,
"created": "2018-03-01T10:48:38.585Z",
"userId": 1
}
Нам необходимо будет сохранить этот токен (id) и в дальнейшем отправлять его в качестве параметра при каждом запросе. Сохранить его можно в cookies или localStorage. Мы сохраним его в куке под названием authorized. А затем при каждом запросе будем его добавлять в качестве query параметра к каждому запросу. Для тех страниц, где требуются права авторизованного пользователя, это откроет доступ. Конечно, если пользователь успешно прошел авторизацию.
Давайте определим что нам предстоит сделать:
- Добавить модель с методом авторизации, проверки статуса пользователя и выхода из приложения (app);
- Добавить сервис, который будет отправлять запросы на сервер по авторизации, получении данных текущего пользователя и т.д.;
- Изменить верхнее меню приложения, добавив кнопки для входа и выхода из приложения;
- Добавить страницу для прохождения авторизации;
- Добавить новый роут.
Для начала следует сказать, что в последней версии приложения на loopback добавлен один новый метод: метод проверки статуса пользователя, который возвращает информацию о пользователе (/Apps/status). Мы его сделали методом модели App, которая имеет только этот метод и не подключена к базе данных. Если пользователь не авторизирован, то метод возвращает пустой объект:
{
"currentUser":
{
"id":0,"realname":"",
"username":"",
"email":"",
"permissions":[],
"auth":false
},
"success":true
}
Итак, модель приложения (app).
import {routerRedux} from 'dva/router';
import {login, logout, status} from '../services/app';
import Cookies from 'js-cookie';
export default {
namespace: 'app',
state: {
user: {},
auth: {},
},
subscriptions:
setup ({dispatch}) {
dispatch({type: 'status'});
},
},
effects: {
* login ({payload,}, {put, call, select}) {
const auth = yield call(login, payload);
const {id, ttl, created, userId, success} = auth;
if (success) {
// Устанавливаем cookies токена.
Cookies.set('authorized', auth.id, {expires: 7});
yield put({type: 'updateState', payload: {id, ttl, created, userId}});
}
yield put({type: 'status'});
yield put(routerRedux.push('/'))
},
* status ({payload,}, {call, put, select}) {
const {success, currentUser: user} = yield call(status, payload);
if (success && user) {
yield put({
type : 'updateState',
payload: {
user
},
});
}
},
* logout ({payload}, {call, put}) {
const data = yield call(logout, payload);
if (data.success) {
Cookies.remove('authorized');
yield put({type: 'status'});
}
else {
throw (data);
}
}
},
reducers: {
updateState (state, {payload}) {
return {
...state,
...payload
}
}
}
В данной модели у нас имеется один синхронный метод, который просто обновляет state переданными значениями и три асинхронных - авторизация (login), выход из приложения (logout) и запроса данных пользователя (status). Важной особенностью данной модели является метод setup, который при первой ее загрузке запускает отправку запроса о статусе пользователя на сервер. И когда он будет выполнен мы получим в состоянии нашего приложения информацию о текущем пользователе или ее отсутствие. После создания модели ее надо подключить как и все модели в index файле нашего приложения:
app.model(require('./models/app').default);
Модель готова и подключена к приложению. Настало время внести небольшие правки в компонент меню. Сначала переделаем маппинг state в компонент. В отличии от того как мы делали раньше, в данной модели мы еще добавили два селектора:
- isAuth - проверяет авторизован ли пользователь;
- getUsername - возвращает имя пользователя, если он авторизован.
const mapStateToProps = state => {
return {
menu : state.menu,
isAuth : isAuth(state),
username: getUsername(state),
}
};
export default connect(mapStateToProps)(MyMenu);
Сами селекторы - достаточно простые функции, которые используются для получения какой-либо информации из нашего глобального state. В данном случае isAuth проверяет авторизорован ли пользователь, а getUsername - получает имя пользователя:
export const isAuth = (state) => {
const {app} = state;
const {user} = app;
return user && user.id && user.id !== 0;
};
export const getUsername = (state) => {
const {app} = state;
const {user} = app;
return user && user.username ? user.username : '';
};
Мы их сохранили в файле /selectors/app.js.
В методе render компонента Menu мы добавим две константы:
- первая для отображения кнопки "Вход" или "Выход";
- вторая для отображения имени пользователя.
Заметьте, что селекторы, которые мы отправили в компонент меню, уже вызываются, как просто свойства объекты props:
const style = {float: 'right'};
const authItem = !me.props.isAuth
? (
<Menu.Item key="login" style={style}>
<NavLink activeClassName="active" to="/login">Вход</NavLink>
</Menu.Item>
)
:(
<Menu.Item key="logout" style={style}>
<span onClick={me.onLogout}>Выход</span>
</Menu.Item>
);
const profile = (
<Menu.Item key="username" style={style}>
<span>{me.props.username}</span>
</Menu.Item>
);
Добавим эти две константы в return поле кнопки для отображения блогов. Кнопка "Вход" отправляет нас на новый роут, где мы в дальнейшем отобразим форму для авторизации, а кнопка "Выход" по событию onClick будет вызывать метод компонента onLogout, в котором мы вызовем действие logout нашей модели app:
onLogout = (e) => {
e.preventDefault();
const {dispatch} = this.props;
dispatch({type: 'app/logout'});
};
Новый файл с сервисом app ничего необычного не представляет, он очень похож на сервисы blogs и articles. Все они просто являются набором простых функций для отправки запросов на те или иные урлы нашего api:
import request from '../utils/request';
import config from '../utils/config';
const {apiPrefix} = config;
const {userLoginUrl, userLogoutUrl, statusUrl} = config[apiPrefix];
export async function login(params) {
return request({
url : userLoginUrl,
method: 'post',
data : params,
})
}
export async function logout(params) {
return request({
url : userLogoutUrl,
method: 'post',
data : params,
})
}
export async function status(params) {
return request({
url : statusUrl,
method: 'get',
data : params,
})
}
Нам осталось только сделать саму страницу авторизации и подключить ее к нашему роутингу. Назовем ее LoginPage. Эта страница будет содержать в себе компонент формы для авторизации LoginForm. Нам больше интересен сам код компонента LoginForm. В данном компоненте мы будет использовать формы, которые нам предоставляет фреймворк ant.design. Он содержит довольно широкое api для работы с формами. Для того, чтобы использовать все возможности по работе с формами, нам надо завернуть наш компонент формы в декоратор Form.create():
export default connect(({app}) => ({app}))(Form.create()(LoginForm));
Для добавления роута в наше приложение немного подправим файл router.js, добавив еще один роут ко всем остальным:
<Route path="/login" exact component={LoginPage}/>
Давайте напоследок рассмотрим, как происходит авторизация в приложении по пунктам для большей ясности.
1. Как только приложение загружается, наша модель отправляет запрос на урл /Apps/status, откуда получает информацию о пользователе. Если пользователь авторизован, то придет нужная информация, если же нет, то придет пустой объект.
2. Допустим, пользователь не авторизован, тогда мы нажимаем кнопку войти и попадаем на страницу авторизации, где вводим имя пользователя и пароль.
3. После нажатия кнопки приложение отправляет запрос на урл /Users/login и при успешной авторизации получает токен, который сохраняет в куке (authorized).
4. Также после этого отправляется запрос на /Apps/status для получении информации о текущем пользователе.
5. Данные о пользователе попадают в компонент Menu и он меняется: скрывает кнопку "Вход" и отображает имя пользователя и кнопку "Выход".
6. Также после успешной авторизации пользователь отправляется на главную страницу приложения.
Итак, наше приложение готово. Конечно, оно получилось довольно простое, но для обучающих и ознакомительных целей вполне себе познавательное.
Скачать исходный код, как всегда, можно по ссылке:
git clone https://github.com/dionic-i/lb-blog-client.git
После загрузки переходим в папку проекта:
npm i
git checkout part2-3
npm run dev
Успехов!