|
@@ -0,0 +1,217 @@
|
|
|
+<header>reducers</header>
|
|
|
+
|
|
|
+<body>
|
|
|
+ <div id="testDiv">
|
|
|
+ Test
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ function jwtDecode(token) {
|
|
|
+ if (!token || typeof token != "string")
|
|
|
+ return undefined;
|
|
|
+ let tokenArr = token.split(".");
|
|
|
+ if (tokenArr.length != 3)
|
|
|
+ return undefined;
|
|
|
+ try {
|
|
|
+ let tokenJsonStr = atob(tokenArr[1]);
|
|
|
+ let tokenJson = JSON.parse(tokenJsonStr);
|
|
|
+ return tokenJson;
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ function gql(url, query, vars) {
|
|
|
+ let fetchSettings =
|
|
|
+ {
|
|
|
+ method: "POST",
|
|
|
+ headers:
|
|
|
+ {
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ "Accept": "application/json"
|
|
|
+ },
|
|
|
+ body: JSON.stringify(
|
|
|
+ {
|
|
|
+ query: query,
|
|
|
+ variables: vars
|
|
|
+
|
|
|
+ })
|
|
|
+ };
|
|
|
+ return fetch(url, fetchSettings).then(res => res.json());
|
|
|
+ }
|
|
|
+ function signIn(login, password, url) {
|
|
|
+ const loginQuery =
|
|
|
+ `query login($login:String, $password:String){
|
|
|
+ login(login:$login, password:$password)
|
|
|
+ }`;
|
|
|
+ return gql(
|
|
|
+ url,
|
|
|
+ loginQuery,
|
|
|
+ { login: login, password: password });
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ function TreatAuth({ payload }, state) {
|
|
|
+ let newState = { ...state };
|
|
|
+ newState.auth = { ...state.auth };
|
|
|
+ newState.auth.state = { token: payload.data.login, payload: jwtDecode(payload.data.login) };
|
|
|
+ return newState;
|
|
|
+ }
|
|
|
+
|
|
|
+ function TreatBuy({ status, payload }) {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
|
|
|
+ function promiseReducer(state = {}, action) {
|
|
|
+ if (action) {
|
|
|
+ if (action.type === 'PROMISE') {
|
|
|
+ let newState = TreatPayload(action, "buy", state, TreatBuy);
|
|
|
+ newState = TreatPayload(action, "auth", newState, TreatAuth);
|
|
|
+ return newState;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return state;
|
|
|
+ }
|
|
|
+
|
|
|
+ function TreatPayload(action, key, state, onPayloadFunc) {
|
|
|
+ let result = state;
|
|
|
+ let keyAction = action[key];
|
|
|
+ if (keyAction) {
|
|
|
+ result = changeStatus(key, state, action);
|
|
|
+ if (state !== result && keyAction.status == "FULLFILLED")
|
|
|
+ onPayloadFunc(keyAction, result)
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ function changeStatus(key, state, action) {
|
|
|
+ let result = state;
|
|
|
+ let actionData = action[key];
|
|
|
+ if (actionData) {
|
|
|
+ let stateData = state[key];
|
|
|
+ let status = actionData.status;
|
|
|
+ if (!stateData || stateData.status !== status) {
|
|
|
+ let newStateForKey = { ...stateData, ...{ status: status } };
|
|
|
+ result = { ...state };
|
|
|
+ result[key] = newStateForKey;
|
|
|
+ if (status == "REJECTED")
|
|
|
+ newStateForKey.error = actionData.error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ function createStore(reducer) {
|
|
|
+ let state = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
|
|
|
+ let cbs = [] //массив подписчиков
|
|
|
+
|
|
|
+ const getState = () => state //функция, возвращающая переменную из замыкания
|
|
|
+ const subscribe = cb => (cbs.push(cb), //запоминаем подписчиков в массиве
|
|
|
+ () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
|
|
|
+
|
|
|
+ function dispatch(action) {
|
|
|
+ if (typeof action === 'function') { //если action - не объект, а функция
|
|
|
+ return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
|
|
|
+ }
|
|
|
+ const newState = reducer(state, action) //пробуем запустить редьюсер
|
|
|
+ if (newState !== state) { //проверяем, смог ли редьюсер обработать action
|
|
|
+ state = newState //если смог, то обновляем state
|
|
|
+ for (let cb of cbs) cb() //и запускаем подписчиков
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ getState, //добавление функции getState в результирующий объект
|
|
|
+ dispatch,
|
|
|
+ subscribe //добавление subscribe в объект
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function actionPromise({ name, promise }) {
|
|
|
+ return async function Exec(dispatch) {
|
|
|
+ let action = { type: "PROMISE" };
|
|
|
+ action[name] = { status: "PENDING" };
|
|
|
+ dispatch(action) //сигнализируем redux, что промис начался
|
|
|
+
|
|
|
+ try {
|
|
|
+ const payload = await promise //ожидаем промиса;
|
|
|
+ action = { type: "PROMISE" };
|
|
|
+ action[name] = { status: "FULLFILLED", payload: payload };
|
|
|
+ dispatch(action) //сигнализируем redux, что промис успешно выполнен
|
|
|
+ return payload //в месте запуска store.dispatch с этим thunk можно так же получить результат промиса
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ action = { type: "PROMISE" };
|
|
|
+ action[name] = { status: "REJECTED", error: error };
|
|
|
+ dispatch(action) //в случае ошибки - сигнализируем redux, что промис несложился
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const store = createStore(promiseReducer)
|
|
|
+
|
|
|
+ store.subscribe(() => {
|
|
|
+ let state = store.getState();
|
|
|
+ if (state.buy) {
|
|
|
+ if (state.buy.status == "PENDING")
|
|
|
+ testDiv.style.color = "orange";
|
|
|
+ else if (state.buy.status == "FULLFILLED")
|
|
|
+ testDiv.style.color = "green";
|
|
|
+ else if (state.buy.status == "REJECTED") {
|
|
|
+ testDiv.style.color = "red";
|
|
|
+ testDiv.innerText = state.buy.errorMessage;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ let execFunc = actionPromise({ name: "auth", promise: signIn("test457", "123123", "http://shop-roles.node.ed.asmer.org.ua/graphql") });
|
|
|
+ store.dispatch(execFunc);
|
|
|
+
|
|
|
+ /*
|
|
|
+ const actionPending = () => ({ type: 'PROMISE', status: 'PENDING' })
|
|
|
+ const actionFulfilled = payload => ({ type: 'PROMISE', status: 'FULFILLED', payload })
|
|
|
+ const actionRejected = error => ({ type: 'PROMISE', status: 'REJECTED', error })
|
|
|
+
|
|
|
+
|
|
|
+ store.subscribe(() => console.log(store.getState()))
|
|
|
+
|
|
|
+ store.dispatch({ type: 'COUNTER_INC' })
|
|
|
+ store.dispatch({ type: 'BOOLEAN_SET' })
|
|
|
+ store.dispatch({ type: 'COUNTER_INC' })
|
|
|
+ store.dispatch({ type: 'BOOLEAN_TOGGLE' })
|
|
|
+ store.dispatch({ type: 'COUNTER_DEC' })
|
|
|
+ store.dispatch({ type: 'ДИЧЬ' }) //не вызывает подписчика
|
|
|
+ */
|
|
|
+
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+
|
|
|
+function combineReducers(reducers) {
|
|
|
+function totalReducer(totalState = {}, action) {
|
|
|
+const newTotalState = {} //объект, который будет хранить только новые состояния дочерних редьюсеров
|
|
|
+
|
|
|
+//цикл + квадратные скобочки позволяют написать код, который будет работать с любыми количеством дочерных редьюсеров
|
|
|
+for (const [reducerName, childReducer] of Object.entries(reducers)) {
|
|
|
+const newState = childReducer(totalState[reducerName], action) //запуск дочернего редьюсера
|
|
|
+if (newState !== totalState[reducerName]) { //если он отреагировал на action
|
|
|
+newTotalState[reducerName] = newState //добавляем его в newTotalState
|
|
|
+}
|
|
|
+}
|
|
|
+
|
|
|
+//Универсальная проверка на то, что хотя бы один дочерний редьюсер создал новый стейт:
|
|
|
+if (Object.values(newTotalState).length) {
|
|
|
+return { ...totalState, ...newTotalState } //создаем новый общий стейт, накладывая новый стейты дочерних редьюсеров на
|
|
|
+старые
|
|
|
+}
|
|
|
+
|
|
|
+return totalState //если экшен не был понят ни одним из дочерних редьюсеров, возвращаем общий стейт как был.
|
|
|
+}
|
|
|
+
|
|
|
+return totalReducer
|
|
|
+}
|
|
|
+
|
|
|
+//////
|
|
|
+
|
|
|
+
|
|
|
+/////
|