Разработка клиентской части блога на DVA.Часть 2
В прошлой части мы разработали простые страницы нашего приложения. В этой статье мы рассмотрим более интересный кейс, а именно - создание главной страницы.
Для начала давайте немного изменим функцию получения данных с сервера. На данный момент она использует библиотеку isomorphic-fetch. А мы ее заменим на axios. Также подключим библиотеку для работы с куками, так как нам это пригодится при реализации авторизации в приложении.
Подключение библиотеки для работы с куками:
npm i js-cookie --save
Код для функции request получился такой:
import axios from 'axios';
import lodash from 'lodash';
import Cookies from 'js-cookie';
export default function axioRequest(options) {
const token = Cookies.get('authorized');
if (token) {
options = lodash.extend(options, {
headers: {
'authorized': token
},
params: lodash.extend({}, options.params || {}, {
access_token: token
})
});
}
return axios.request(options).then((response) => {
const {statusText, status} = response;
let data = response.data;
if (data instanceof Array) {
data = {
list: data,
}
}
return Promise.resolve({
success : true,
message : statusText,
statusCode: status,
...data
});
}).catch((error) => {
const {response} = error;
let msg;
let statusCode;
if (response && response instanceof Object) {
const {data, statusText} = response;
statusCode = response.status;
msg = data.message || statusText
} else {
statusCode = 600;
msg = error.message || 'Network Error';
}
return Promise.reject({success: false, statusCode, message: msg})
})
}
В нем есть две небольшие особенности. Первая - это добавление параметра для авторизации по JWT в заголовок и query параметр. Вторая - если нам придет ответ в виде массива, то он будет автоматически преобразован в объект со свойством list.
Далее необходимо немного поправить методы моделей blogs и articles, приведя функции получения данных с сервера в соответствии с нашим методом request.
blogs.js
import request from '../utils/request';
export async function query(filter) {
const fOptions = filter || {};
return request({
url : '/api/blogs',
method: 'get',
params: {
filter: JSON.stringify(fOptions)
}
});
}
export async function findById(id) {
return request({
url : `/api/blogs/${id}`,
method: 'get'
});
}
export async function blogArticles(id, filter) {
const fOptions = filter || {};
return request({
url : `/api/blogs/${id}/articles`,
method: 'get',
params: {
filter: JSON.stringify(fOptions)
}
});
}
articles.js
import request from '../utils/request';
export async function findOne(id) {
return request({
url : `/api/articles/${id}`,
method: 'get'
});
}
Главная страница
На главной странице мы отобразим две колонки. В левой будет располагаться список блогов , а в правой мы поместим список статей текущего выделенного блога. Т.е. после того, как список блогов будет загружен, если их больше нуля, то выбирается первый по списку блог и далее выводится список статей данного блога в правой колонке страницы.
В список блогов на главной странице мы будет выводить только 5 последних блогов, а в список статей мы будем выводить только 5 лучших статей выделенного блога.
Для реализации данной страницы нам необходимо:
- Создать компоненты для вывода блогов статей на главной странице в виде списка;
- Создать модель состояния для подключения к главной странице;
- Добавить параметры к сервисам запроса блогов и статей, чтобы они учитывали порядок сортировки и лимит вывода;
- Установить компоненты на главную страницу и подключить их через модель.
Начнем с создания компонентов для вывода списка блогов и статей на главной странице приложения. Назовем их соответственно MainBlogsList и MainArticlesList. Для отображения обоих списков воспользуемся компонентом ant design - List.
Замечание. В принципе, данные компоненты похожи на компонент списка статей. Можно было бы выделить какой-то общий подход для отображения таких списков и вынести часть кода в общий класс (функцию) и с помощью библиотеки compose сделать компоненты высшего порядка с передачей необходимых параметров в тех или иных случаях, но не будем сейчас заострять на этом внимание.
Хотя MainBlogsList и MainArticlesList - похожие по функционалу компоненты, но первый будет немного сложнее. В компонент MainBlogsList мы отправим три свойства:
- этот список блогов;
- идентификатор текущего блога;
- функцию, которая будет вызываться при клике на запись блога.
Также мы определим там функцию, которая будет отображать иконку напротив выбранного элемента списка. Вот код получившегося компонента:
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {List, Icon} from 'antd';
import {Link} from 'react-router-dom';
class MainBlogsList extends Component {
render() {
let me = this;
const {items} = me.props;
const view = items.length
? <List className="main-list"
itemLayout="horizontal"
dataSource={items}
renderItem={item => (
<div onClick={() => me.onSelectItem(item)}>
<List.Item extra={me.getExtraContent(item)}>
<List.Item.Meta
title={<Link to={`/blog/${item.id}`}>{item.title}
</Link>}
description={item.description}
/>
</List.Item>
</div>
)}
/>
: <h4>На данный момент не заведено ни одного блога!</h4>;
return (
<div>
{view}
</div>
);
}
onSelectItem = (item) => {
let me = this;
const {selectBlog} = me.props;
selectBlog(item);
};
getExtraContent = (item) => {
let me = this;
const {blogId} = me.props;
return item.id === blogId ? <Icon type="check" />: '';
};
}
MainBlogsList.propTypes = {
items: PropTypes.array,
blogId: PropTypes.number,
selectBlog: PropTypes.func
};
export default MainBlogsList;
Код компонента MainArticlesList достаточно простой:
import React from 'react';
import PropTypes from 'prop-types';
import {List} from 'antd';
import {Link} from 'react-router-dom';
function MainArticlesList({items, blogId}) {
const message = (blogId === 0)
? 'Выберите блог для отображения статей!'
: 'На данный момент в текущем блоге нет ни одной статьи!';
const view = items.length
? <List
itemLayout="horizontal"
dataSource={items}
renderItem={item => (
<List.Item>
<List.Item.Meta
title={<Link to={`/article/${item.id}`}>{item.title}</Link>}
description={item.except}
/>
</List.Item>
)}
/>
: <h4>{message}</h4>;
return (
<div>
{view}
</div>
);
}
MainArticlesList.propTypes = {
items: PropTypes.array,
blogId: PropTypes.number
};
export default MainArticlesList;
Теперь перейдем к созданию модели состояния главной страницы. Назовем ее dashboard. Конечно, нам понадобится список блогов (blogs) и список статей (articles), а также идентификатор текущего выбранного блога. В редьюсерах у нас будут следующие функции - updateState для обновления списка блогов или статей после загрузки, selectBlog для выбора текущего активного блога. Для асинхронных вызовов мы определим функции-генераторы fetch и fetchArticles.
import pathToRegexp from 'path-to-regexp'
import {query, blogArticles} from '../services/blogs';
export default {
namespace: 'dashboard',
state: {
blogId: 0,
blogs:[],
articles: []
},
subscriptions: {
setup ({dispatch, history}) {
history.listen(({pathname}) => {
const match = pathToRegexp('/').exec(pathname);
if (match) {
dispatch({type: 'fetch'})
}
})
},
},
effects: {
*fetch({payload}, {call, put}) {
const filter = {
order: 'id DESC',
limit: 5
};
const {list} = yield call(query, filter);
yield put({type: 'updateState', payload: {blogs: list}});
},
*fetchArticles({payload}, {call, put}) {
const filter = {
order: 'votes DESC',
limit: 5
};
const {list} = yield call(blogArticles, payload.id, filter);
yield put({type: 'updateState', payload: {articles: list}});
},
},
reducers: {
updateState(state, action) {
return {...state, ...action.payload};
},
selectBlog(state, action) {
const {id: blogId} = action.payload;
return {...state, blogId};
},
},
};
После реализации этих компонентов нам необходимо соединить все это вместе с помощью функции connect в компоненте IndexPage для главной страницы:
import React from 'react';
import {connect} from 'dva';
import {Link} from 'react-router-dom';
import {Row, Col} from 'antd';
import PropTypes from 'prop-types';
import MainBlogsList from '../components/MainBlogsList';
import MainArticlesList from '../components/MainArticlesList';
function IndexPage({dashboard, dispatch}) {
const {blogs, articles, blogId} = dashboard;
const onSelectBlog = function (item) {
dispatch({type: 'dashboard/selectBlog', payload: item});
dispatch({type: 'dashboard/fetchArticles', payload: {id: item.id}});
};
return (<div>
<h1>Главная страница</h1>
<Row gutter={8}>
<Col span="12">
<h2>Последние блоги</h2>
<MainBlogsList items={blogs} blogId={blogId} selectBlog={onSelectBlog}/>
</Col>
<Col span="12">
<h2>Лучшие статьи</h2>
<MainArticlesList items={articles} blogId={blogId} />
</Col>
</Row>
</div>
);
}
IndexPage.propTypes = {
blogs : PropTypes.array,
articles: PropTypes.array,
};
export default connect(({dashboard}) => ({dashboard}))(IndexPage);
Для реализации фильтрации данных на сервере нам необходимо знать в каком виде необходимо отправить фильтр, чтобы получить необходимый список блогов или статей. В документации по Loopback говорится, что фильтр передается get параметром и представляет собой сериализованный json объект с различными параметрами для фильтрации, сортировки и постраничного вывода. Т.е. нам необходимы вот такие параметры для нашего случая:
# Для блогов
const filter = {
order: "id DESC",
limit: 5
}
# Для статей блога
const filter = {
order: "votes DESC",
limit: 5
}
На данном этапе все готово, чтобы поправить логику применения фильтров к mock данным. А затем приступим к реализации вызова api с Loopback.
В итоге у нас получилась вот такая заготовка страницы:
Итог
На данном этапе наше приложение:
- Умеет отображать на главной странице список 5 последних блогов и 5 лучших статей из выбранного блога;
- Имеет страницу со списком блогов, где, в виде карточек, представлены все блоки;
- Имеет страницу отдельного блога, где выводит все статьи данного блога;
- Имеет страницу отдельной статьи.
Все эти страницы связаны ссылками для перехода друг на друга. Данные на эти страницы загружаются с помощью встроенного сервера, который позволяет делать mock данные.
Скачать исходный код можно по ссылке:
git clone https://github.com/dionic-i/lb-blog-client.git
После загрузки переходим в папку проекта:
npm i
git checkout part2-1
npm run dev
В следующий раз мы подключим наш блог к API, разработанному на Loopback и разработаем авторизацию на JWT. Следите за обновлениями. Всем добра!