Вот как я смог сделать это для микросервиса аватара, который я создал для работы с моим основным приложением.
ПРЕДУПРЕЖДЕНИЕ : Это объяснение распространяется на весь поток, поэтому оно может быть длинным и избыточным, если вы его уже понимаете.
Создать конфигурацию axios.
Сначала необходимо создать конфигурацию axios
.По умолчанию axios
не будет отображать err
, возвращаемый сервером, вместо этого он будет просто отображать общий Error
объект.Вам нужно настроить interceptor
.
utils / axiosConfig.js
import get from 'lodash/get';
import axios from 'axios';
export const avatarAPI = axios.create({
baseURL: 'http://localhost:4000/api/', // this makes it easier so that any request will be prepended with this baseURL
});
avatarAPI.interceptors.response.use(
response => response, // returns the server response
error => {
const err = get(error, ['response', 'data', 'err']); // this checks if "error.response.data.err" is present (this is the error returned from the server); VERY IMPORTANT: this "err" property is specified in our express middlewares/controllers, so please pay attention to the naming convention.
return err ? Promise.reject(err) : Promise.reject(error.message); // if the above is present, return the server error, else return a generic Error object
},
);
Поток изклиент к серверу обратно к клиенту.
Клиент
Пользователь отправляет форму с formData
, и это вызывает action
создатель:
thunk action uploadAvatarсоздатель (обещание, ожидающее response
или error
с нашего сервера) :
import { avatarAPI } from '../utils/axiosConfig'; // import the custom axios configuration that was created above
import * as types from 'types';
const uploadAvatar = formData => dispatch =>
avatarAPI
.post(`avatar/create`, formData) // this makes a POST request to our server -- this also uses the baseURL from the custom axios configuration, which is the same as "http://localhost:4000/api/avatar/create"
.then(({ data }) => {
dispatch({ type: types.SET_CURRENT_AVATAR, payload: data.avatarurl });
})
.catch(err => // this will return our server "err" string if present, otherwise it'll return a generic Error object. IMPORTANT: Just in case we get a generic Error object, we'll want to convert it to a string (otherwise, if it passes the generic Error object to our reducer, stores it to redux state, passes it to our connected component, which then tries to display it... it'll cause our app to crash, as React can't display objects)
dispatch({ type: types.SERVER_ERROR, payload: err.toString() }),
);
Сервер
Запрос POST
выбранвверх по нашему express
маршруту :
app.post('/api/avatar/create', saveImage, create);
Запрос попадает на этот маршрут: '/api/avatar/create'
, проходит через функцию промежуточного программного обеспечения (см. ниже) до прохождения через другую функцию saveImage
промежуточного программного обеспечения, донаконец, проходя к контроллеру create
.
Клиент
Сервер отправляет ответ клиенту.Ответ от нашего сервера проходит через axios
конфигурацию interceptor
, которая определяет, как обрабатывать response
или error
, которые были возвращены с нашего сервера.Затем он передает response
или error
.then()
или .catch()
создателя action
.Создатель action
передает его в reducer
, который обновляет состояние redux
, которое затем обновляет connect
ed компонент.
Настройка сервера (микросервис).
Где бы вы ни определяли ваше промежуточное программное обеспечение express
(например: bodyParser
, cors
или passport
и т. Д.), Вам нужно будет создать функцию промежуточного программного обеспечения multer
(каждый раз, когда файл загружается,она проходит через эту функцию first ):
middlewares / index.js
app.use(cors({ origin: "http://localhost:3000" }));
app.use(bodyParser.json());
app.use(
multer({
limits: {
fileSize: 10240000,
files: 1,
fields: 1
},
fileFilter: (req, file, next) => {
if (!/\.(jpe?g|png|gif|bmp)$/i.test(file.originalname)) {
req.err = "That file extension is not accepted!"; // this part is important, I'm attaching the err to req (which gets passed to the next middleware function => saveImage)
next(null, false);
}
next(null, true);
}
}).single("file")
);
...etc
services / saveImage.js (после прохождения вышеупомянутой функции промежуточного программного обеспечения результат передается этой функции промежуточного программного обеспечения saveImage
)
const fs = require("fs");
const sharp = require("sharp");
const { createRandomString } = require('../../utils/helpers');
module.exports = (req, res, next) => {
// if the file failed to pass the middleware function above, we'll return the "req.err" as "err" or return a string if "req.file" is undefined. In short, this returns an "error.response.data.err" to the client.
if (req.err || !req.file) {
return res.status(400).json({ err: req.err || "Unable to process file." });
}
const randomString = createRandomString();
const filename = `${Date.now()}-${randomString}-${req.file.originalname}`;
const filepath = `uploads/${filename}`;
const setFile = () => {
req.file.path = filepath;
return next();
};
/\.(gif|bmp)$/i.test(req.file.originalname)
? fs.writeFile(filepath, req.file.buffer, (err) => {
if (err) return res.status(400).json({ "Unable to process file." });
setFile();
})
: sharp(req.file.buffer)
.resize(256, 256)
.max()
.withoutEnlargement()
.toFile(filepath)
.then(() => setFile());
};
Если вышеуказанное проходит, то передается req
(который содержит req.file
и всеего свойства) на контроллер create
, который в моем случае хранит путь к файлу (/uploads/name-of-file.ext) и строку для извлечения изображения (http://localhost:4000/uploads/name-of-file.ext) на мойВ моем случае эта строка затем отправляется обратно клиенту для сохранения в состояние избыточности и затем обновляется как аватара пользователя.ar (при передаче строки в <img src={avatarurl} alt="avatarurl.png" />
он отправляет запрос GET
обратно в микросервис).
Проверка не пройдена.
Допустим, пользователь пыталсязагрузить .tiff
изображение.Он проходит через нашу многопользовательскую функцию express
, которая вызывает ошибку "That file extension is not accepted!"
, эта ошибка возвращается через req.err
в saveImage
, которая возвращает req.err
как: return res.status(400).json({ err: req.err });
Вкл.на нашей клиентской стороне, что err
протекает через наши оси interceptor
:
avatarAPI.interceptors.response.use(
response => response,
error => {
const err = get(error, ['response', 'data', 'err']); // this checks if "error.response.data.err" is present; which it is, and is now "That file extension is not accepted!"
return err ? Promise.reject(err) : Promise.reject(error.message); // that err string gets returned to our uploadAvatar action creator's "catch" block
},
);
Блок uploadAvatar
создателя действий catch
запускается:
.catch(err => // our server "err" is passed to here from the interceptor
dispatch({ type: types.SERVER_ERROR, payload: err.toString() }), // then that "err" is passed to a reducer
);
The reducer
берет сервер err
и сохраняет его в состоянии:
import * as types from 'types';
const serverInitialState = {
error: '',
message: '',
};
const ServerReducer = (state = serverInitialState, { payload, type }) => {
switch (type) {
case types.RESET_SERVER_MESSAGES:
return { ...state, error: '' };
case types.SERVER_ERROR:
return { ...state, error: payload }; // the server err is stored to redux state as "state.server.error"
case types.SERVER_MESSAGE:
return { ...state, message: payload };
default:
return state;
}
};
export default ServerReducer;
Компонент connect
ed извлекает этот state.server.error
и отображает его (не слишком беспокоитесь о логике здесь,просто это подключенный компонент, отображающий state.server.error
как serverError
):
class RenderMessages extends Component {
shouldComponentUpdate = nextProps =>
this.props.serverError !== '' ||
nextProps.serverError !== '' ||
this.props.serverMessage !== '' ||
nextProps.serverMessage !== '';
componentDidUpdate = () => {
const { serverError, serverMessage } = this.props;
if (serverError || serverMessage) {
const notification = serverError
? serverErrorMessage(serverError)
: serverSuccessMessage(serverMessage);
this.renderNotification(...notification);
}
};
renderNotification = ({ noteType, description }) => {
notification[noteType]({
message: noteType === 'error' ? 'Error' : 'Update',
description,
icon: descriptionLayout(noteType),
});
setTimeout(() => this.props.resetServerMessages(), 3000);
};
render = () => null;
}
export default connect(
state => ({
serverError: state.server.error, // retrieving the error from redux state
serverMessage: state.server.message,
}),
{ resetServerMessages },
)(RenderMessages);
Окончательный результат - That file extension is not accepted!
Ошибка отображается для пользователя: