国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Redux 進階 - react 全家桶學習筆記(二)

Godtoy / 2120人閱讀

摘要:在函數式編程中,異步操作修改全局變量等與函數外部環境發生的交互叫做副作用通常認為這些操作是邪惡骯臟的,并且也是導致的源頭。

注:這篇是17年1月的文章,搬運自本人 blog...

https://github.com/BuptStEve/...

零、前言

在上一篇中介紹了 Redux 的各項基礎 api。接著一步一步地介紹如何與 React 進行結合,并從引入過程中遇到的各個痛點引出 react-redux 的作用和原理。

不過目前為止還都是紙上談兵,在日常的開發中最常見異步操作(如通過 ajax、jsonp 等方法 獲取數據),在學習完上一篇后你可能依然沒有頭緒。因此本文將深入淺出地對于 redux 的進階用法進行介紹。

一、中間件(MiddleWare)
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.  ————  by Dan Abramov

這是 redux 作者對 middleware 的描述,middleware 提供了一個分類處理 action 的機會,在 middleware 中你可以檢閱每一個流過的 action,挑選出特定類型的 action 進行相應操作,給你一次改變 action 的機會。

說得好像很吊...不過有啥用咧...?

1. 日志應用場景[[2]]

因為改變 store 的唯一方法就是 dispatch 一個 action,所以有時需要將每次 dispatch 操作都打印出來作為操作日志,這樣一來就可以很容易地看出是哪一次 dispatch 導致了異常。

1.1. 第一次嘗試:強行懟...
const action = addTodo("Use Redux");

console.log("dispatching", action);
store.dispatch(action);
console.log("next state", store.getState());

顯然這種在每一個 dispatch 操作的前后都手動加代碼的方法,簡直讓人不忍直視...

1.2. 第二次嘗試:封裝 dispatch

聰明的你一定馬上想到了,不如將上述代碼封裝成一個函數,然后直接調用該方法。

function dispatchAndLog(store, action) {
    console.log("dispatching", action);
    store.dispatch(action);
    console.log("next state", store.getState());
}

dispatchAndLog(store, addTodo("Use Redux"));

矮油,看起來不錯喲。

不過每次使用都需要導入這個額外的方法,一旦不想使用又要全部替換回去,好麻煩啊...

1.3. 第三次嘗試:猴子補丁(Monkey Patch)

在此暫不探究為啥叫猴子補丁而不是什么其他補丁。

簡單來說猴子補丁指的就是:以替換原函數的方式為其添加新特性或修復 bug。

let next = store.dispatch; // 暫存原方法

store.dispatch = function dispatchAndLog(action) {
    console.log("dispatching", action);
    let result = next(action); // 應用原方法
    console.log("next state", store.getState());

    return result;
};

這樣一來我們就“偷梁換柱”般的為原 dispatch 添加了輸出日志的功能。

1.4. 第四次嘗試:隱藏猴子補丁

目前看起來很不錯,然鵝假設我們又要添加別的一個中間件,那么代碼中將會有重復的 let next = store.dispatch; 代碼。

對于這個問題我們可以通過參數傳遞,返回新的 dispatch 來解決。

function logger(store) {
    const next = store.dispatch;

    return function dispatchAndLog(action) {
        console.log("dispatching", action);
        const result = next(action); // 應用原方法
        console.log("next state", store.getState());

        return result;
    }
}

store.dispatch = logger(store);
store.dispatch = anotherMiddleWare(store);

注意到最后應用中間件的代碼其實就是一個鏈式的過程,所以還可以更進一步優化綁定中間件的過程。

function applyMiddlewareByMonkeypatching(store, middlewares) {
    // 因為傳入的是原對象引用的值,slice 方法會生成一份拷貝,
    // 所以之后調用的 reverse 方法不會改變原數組
    middlewares = middlewares.slice();
    // 我們希望按照數組原本的先后順序觸發各個中間件,
    // 所以最后的中間件應當最接近原本的 dispatch,
    // 就像洋蔥一樣一層一層地包裹原 dispatch
    middlewares.reverse();

    // 在每一個 middleware 中變換 store.dispatch 方法。
    middlewares.forEach((middleware) =>
        store.dispatch = middleware(store);
    );
}

// 先觸發 logger,再觸發 anotherMiddleWare 中間件(類似于 koa 的中間件機制)
applyMiddlewareByMonkeypatching(store, [ logger, anotherMiddleWare ]);

so far so good~! 現在不僅隱藏了顯式地緩存原 dispatch 的代碼,而且調用起來也很優雅~,然鵝這樣就夠了么?

1.5. 第五次嘗試:移除猴子補丁

注意到,以上寫法仍然是通過 store.dispatch = middleware(store); 改寫原方法,并在中間件內部通過 const next = store.dispatch; 讀取當前最新的方法。

本質上其實還是 monkey patch,只不過將其封裝在了內部,不過若是將 dispatch 方法通過參數傳遞進來,這樣在 applyMiddleware 函數中就可以暫存 store.dispatch(而不是一次又一次的改寫),豈不美哉?

// 通過參數傳遞
function logger(store, next) {
    return function dispatchAndLog(action) {
        // ...
    }
}

function applyMiddleware(store, middlewares) {
    // ...

    // 暫存原方法
    let dispatch = store.dispatch;

    // middleware 中通過閉包獲取 dispatch,并且更新 dispatch
    middlewares.forEach((middleware) =>
        dispatch = middleware(store, dispatch);
    );
}

接著應用函數式編程的 curry 化(一種使用匿名單參數函數來實現多參數函數的方法。),還可以再進一步優化。(其實是為了使用 compose 將中間件函數先組合再綁定)

function logger(store) {
    return function(next) {
        return function(action) {
            console.log("dispatching", action);
            const result = next(action); // 應用原方法
            console.log("next state", store.getState());

            return result;
        }
    }
}

// -- 使用 es6 的箭頭函數可以讓代碼更加優雅更函數式... --
const logger = (store) => (next) => (action) => {
    console.log("dispatching", action);
    const result = next(action); // 應用原方法
    console.log("next state", store.getState());

    return result;
};

function applyMiddleware(store, middlewares) {
    // ...

    let dispatch = store.dispatch;

    middlewares.forEach((middleware) =>
        dispatch = middleware(store)(dispatch); // 注意調用了兩次
    );

    // ...
}

以上方法離 Redux 中最終的 applyMiddleware 實現已經很接近了,

1.6. 第六次嘗試:組合(compose,函數式方法)

在 Redux 的最終實現中,并沒有采用我們之前的 slice + reverse 的方法來倒著綁定中間件。而是采用了 map + compose + reduce 的方法。

先來說這個 compose 函數,在數學中以下等式十分的自然。

f(g(x)) = (f o g)(x)
f(g(h(x))) = (f o g o h)(x)

用代碼來表示這一過程就是這樣。

// 傳入參數為函數數組
function compose(...funcs) {
    // 返回一個閉包,
    // 將右邊的函數作為內層函數執行,并將執行結果作為外層函數再次執行
    return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

不了解 reduce 函數的人可能對于以上代碼會感到有些費解,舉個栗子來說,有函數數組 [f, g, h]傳入 compose 函數執行。

首次 reduce 執行的結果是返回一個函數 (...args) => f(g(...args))

接著該函數作為下一次 reduce 函數執行時的參數 a,而參數 b 是 h

再次執行時 h(...args) 作為參數傳入 a,即最后返回的還是一個函數 (...args) => f(g(h(...args)))

因此最終版 applyMiddleware 實現中并非依次執行綁定,而是采用函數式的思維,將作用于 dispatch 的函數首先進行組合,再進行綁定。(所以要中間件要 curry 化)

// 傳入中間件函數的數組
function applyMiddleware(...middlewares) {
  // 返回一個函數的原因在 createStore 部分再進行介紹
  return (createStore) => (reducer, preloadedState, enhancer) => {
    const store = createStore(reducer, preloadedState, enhancer)
    let dispatch = store.dispatch
    let chain = [] // 保存綁定了 middlewareAPI 后的函數數組

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 使用 compose 函數按照從右向左的順序綁定(執行順序是從左往右)
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

// store -> { getState } 從傳遞整個 store 改為傳遞部分 api
const logger = ({ getState }) => (next) => (action) => {
    console.log("dispatching", action);
    const result = next(action); // 應用原方法
    console.log("next state", getState());

    return result;
};

綜上如下圖所示整個中間件的執行順序是類似于洋蔥一樣首先按照從外到內的順序執行 dispatch 之前的中間件代碼,在 dispatch(洋蔥的心)執行后又反過來,按照從內到左外的順序執行 dispatch 之后的中間件代碼。

橋都麻袋!

你真的都理解了么?

在之前的實現中直接傳遞 store,為啥在最終實現中傳遞的是 middlewareAPI?

middlewareAPI 里的 dispatch 是為啥一個匿名函數而不直接傳遞 dispatch?

如下列代碼所示,如果在中間件里不用 next 而是調用 store.dispatch 會怎樣呢?

const logger = (store) => (next) => (action) => {
    console.log("dispatching", action);
    // 調用原始 dispatch,而不是上一個中間件傳進來的
    const result = store.dispatch(action); // <- 這里
    console.log("next state", store.getState());

    return result;
};
1.7. middleware 中調用 store.dispatch[[6]]

正常情況下,如圖左,當我們 dispatch 一個 action 時,middleware 通過 next(action) 一層一層處理和傳遞 action 直到 redux 原生的 dispatch。如果某個 middleware 使用 store.dispatch(action) 來分發 action,就發生了右圖的情況,相當于從外層重新來一遍,假如這個 middleware 一直簡單粗暴地調用 store.dispatch(action),就會形成無限循環了。(其實就相當于猴子補丁沒補上,不停地調用原來的函數)

因此最終版里不是直接傳遞 store,而是傳遞 getState 和 dispatch,傳遞 getState 的原因是可以通過 getState 獲取當前狀態。并且還將 dispatch 用一個匿名函數包裹 dispatch: (action) => dispatch(action),這樣不但可以防止 dispatch 被中間件修改,而且只要 dispatch 更新了,middlewareAPI 中的 dispatch 也會隨之發生變化。

1.8. createStore 進階

在上一篇中我們使用 createStore 方法只用到了它前兩個參數,即 reducer 和 preloadedState,然鵝其實它還擁有第三個參數 enhancer。

enhancer 參數可以實現中間件、時間旅行、持久化等功能,Redux 僅提供了 applyMiddleware 用于應用中間件(就是 1.6. 中的那個)。

在日常使用中,要應用中間件可以這么寫。

import {
    createStore,
    combineReducers,
    applyMiddleware,
} from "redux";

// 組合 reducer
const rootReducer = combineReducers({
    todos: todosReducer,
    filter: filterReducer,
});

// 中間件數組
const middlewares = [logger, anotherMiddleWare];

const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(...middlewares),
);

// 如果不需要 initialState 的話也可以忽略
const store = createStore(
    rootReducer,
    applyMiddleware(...middlewares),
);

在上文 applyMiddleware 的實現中留了個懸念,就是為什么返回的是一個函數,因為 enhancer 被定義為一個高階函數,接收 createStore 函數作為參數。

/**
 * 創建一個 redux store 用于保存狀態樹,
 * 唯一改變 store 中數據的方法就是對其調用 dispatch
 *
 * 在你的應用中應該只有一個 store,想要針對不同的部分狀態響應 action,
 * 你應該使用 combineReducers 將多個 reducer 合并。
 *
 * @param  {函數}  reducer 不多解釋了
 * @param  {對象}  preloadedState 主要用于前后端同構時的數據同步
 * @param  {函數}  enhancer 很牛逼,可以實現中間件、時間旅行,持久化等
 * ※ Redux 僅提供 applyMiddleware 這個 Store Enhancer ※
 * @return {Store}
 */
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }

    // enhancer 是一個高階函數,接收 createStore 函數作為參數
    return enhancer(createStore)(reducer, preloadedState)
  }

  // ...
  // 后續內容推薦看看參考資料部分的【Redux 莞式教程】
}

總的來說 Redux 有五個 API,分別是:

createStore(reducer, [initialState], enhancer)

combineReducers(reducers)

applyMiddleware(...middlewares)

bindActionCreators(actionCreators, dispatch)

compose(...functions)

createStore 生成的 store 有四個 API,分別是:

getState()

dispatch(action)

subscribe(listener)

replaceReducer(nextReducer)

以上 API 我們還沒介紹的應該就剩 bindActionCreators 了。這個 API 其實就是個語法糖起了方便地給 action creator 綁定 dispatch 的作用。

// 一般寫法
function mapDispatchToProps(dispatch) {
    return {
        onPlusClick: () => dispatch(increment()),
        onMinusClick: () => dispatch(decrement()),
    };
}

// 使用 bindActionCreators
import { bindActionCreators } from "redux";

function mapDispatchToProps(dispatch) {
    return bindActionCreators({
        onPlusClick: increment,
        onMinusClick: decrement,
        // 還可以綁定更多函數...
    }, dispatch);
}

// 甚至如果定義的函數輸入都相同的話還能更加簡潔
export default connect(
  mapStateToProps,
  // 直接傳一個對象,connect 自動幫你綁定 dispatch
  { onPlusClick: increment, onMinusClick: decrement },
)(App);
二、異步操作

下面讓我們告別干凈的同步世界,進入“骯臟”的異步世界~。

在函數式編程中,異步操作、修改全局變量等與函數外部環境發生的交互叫做副作用(Side Effect)
通常認為這些操作是邪惡(evil)骯臟(dirty)的,并且也是導致 bug 的源頭。
因為與之相對的是純函數(pure function),即對于同樣的輸入總是返回同樣的輸出的函數,使用這樣的函數很容易做組合、測試等操作,很容易驗證和保證其正確性。(它們就像數學公式一般準確)
2.1. 通知應用場景[[3]]

現在有這么一個顯示通知的應用場景,在通知顯示后5秒鐘隱藏該通知。

首先當然是編寫 action

顯示:SHOW_NOTIFICATION

隱藏:HIDE_NOTIFICATION

2.1.1. 最直觀的寫法

最直觀的寫法就是首先顯示通知,然后使用 setTimeout 在5秒后隱藏通知。

store.dispatch({ type: "SHOW_NOTIFICATION", text: "You logged in." });
setTimeout(() => {
  store.dispatch({ type: "HIDE_NOTIFICATION" });
}, 5000);

然鵝,一般在組件中尤其是展示組件中沒法也沒必要獲取 store,因此一般將其包裝成 action creator。

// actions.js
export function showNotification(text) {
  return { type: "SHOW_NOTIFICATION", text };
}
export function hideNotification() {
  return { type: "HIDE_NOTIFICATION" };
}

// component.js
import { showNotification, hideNotification } from "../actions";

this.props.dispatch(showNotification("You just logged in."));
setTimeout(() => {
  this.props.dispatch(hideNotification());
}, 5000);

或者更進一步地先使用 connect 方法包裝。

this.props.showNotification("You just logged in.");
setTimeout(() => {
  this.props.hideNotification();
}, 5000);

到目前為止,我們沒有用任何 middleware 或者別的概念。

2.1.2. 異步 action creator

上一種直觀寫法有一些問題

每當我們需要顯示一個通知就需要手動先顯示,然后再手動地讓其消失。其實我們更希望通知到時間后自動地消失。

通知目前沒有自己的 id,所以有些場景下存在競爭條件(race condition),即假如在第一個通知結束前觸發第二個通知,當第一個通知結束時,第二個通知也會被提前關閉。

所以為了解決以上問題,我們可以為通知加上 id,并將顯示和消失的代碼包起來。

// actions.js
const showNotification = (text, id) => ({
    type: "SHOW_NOTIFICATION",
    id,
    text,
});
const hideNotification = (id) => ({
    type: "HIDE_NOTIFICATION",
    id,
});

let nextNotificationId = 0;
export function showNotificationWithTimeout(dispatch, text) {
    const id = nextNotificationId++;
    dispatch(showNotification(id, text));

    setTimeout(() => {
        dispatch(hideNotification(id));
    }, 5000);
}

// component.js
showNotificationWithTimeout(this.props.dispatch, "You just logged in.");

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, "You just logged out.");

為啥 showNotificationWithTimeout 函數要接收 dispatch 作為第一個參數呢?
雖然通常一個組件都擁有觸發 dispatch 的權限,但是現在我們想讓一個外部函數(showNotificationWithTimeout)來觸發 dispatch,所以需要將 dispatch 作為參數傳入。

2.1.3. 單例 store

可能你會說如果有一個從其他模塊中導出的單例 store,那么是不是同樣也可以不傳遞 dispatch 以上代碼也可以這樣寫。

// store.js
export default createStore(reducer);

// actions.js
import store from "./store";

// ...

let nextNotificationId = 0;
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++;
  store.dispatch(showNotification(id, text));

  setTimeout(() => {
    store.dispatch(hideNotification(id));
  }, 5000);
}

// component.js
showNotificationWithTimeout("You just logged in.");

// otherComponent.js
showNotificationWithTimeout("You just logged out.");

這樣看起來似乎更簡單一些,不過墻裂不推薦這樣的寫法。主要的原因是這樣的寫法強制讓 store 成為一個單例。這樣一來要實現服務器端渲染(Server Rendering)將十分困難。因為在服務端,為了讓不同的用戶得到不同的預先獲取的數據,你需要讓每一個請求都有自己的 store。

并且單例 store 也將讓測試變得困難。當測試 action creator 時你將無法自己模擬一個 store,因為它們都引用了從外部導入的那個特定的 store,所以你甚至無法從外部重置狀態。

2.1.4. redux-thunk 中間件

首先聲明 redux-thunk 這種方案對于小型的應用來說足夠日常使用,然鵝對于大型應用來說,你可能會發現一些不方便的地方。(例如對于 action 需要組合、取消、競爭等復雜操作的場景)

首先來明確什么是 thunk...

A thunk is a function that wraps an expression to delay its evaluation.

簡單來說 thunk 就是封裝了表達式的函數,目的是延遲執行該表達式。不過有啥應用場景呢?

目前為止,在上文中的 2.1.2. 異步 action creator 部分,最后得出的方案有以下明顯的缺點

我們必須將 dispatch 作為參數傳入。

這樣一來任何使用了異步操作的組件都必須用 props 傳遞 dispatch(不管有多深...)。我們也沒法像之前各種同步操作一樣使用 connect 函數來綁定回調函數,因為 showNotificationWithTimeout 函數返回的不是一個 action。

此外,在日常使用時,我們還需要區分哪些函數是同步的 action creator,那些是異步的 action creator。(異步的需要傳 dispatch...)

同步的情況: store.dispatch(actionCreator(payload))

異步的情況: asyncActionCreator(store.dispatch, payload)

計將安出?

其實問題的本質在于 Redux “有眼不識 function”,目前為止 dispatch 函數接收的參數只能是 action creator 返回的普通的 action。所以如果我們讓 dispatch 對于 function 網開一面,走走后門潛規則一下不就行啦~

實現方式很簡單,想想第一節介紹的為 dispatch 添加日志功能的過程。

// redux-thunk 源碼
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === "function") {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

以上就是 redux-thunk 的源碼,就是這么簡單,判斷下如果傳入的 action 是函數的話,就執行這個函數...(withExtraArgument 是為了添加額外的參數,詳情見 redux-thunk 的 README.md)

這樣一來如果我們 dispatch 了一個函數,redux-thunk 會傳給它一個 dispatch 參數,我們就利用 thunk 解決了組件中不方便獲取 dispatch 的問題。

并且由于 redux-thunk 攔截了函數,也可以防止 reducer 接收到函數而出現異常。

添加了 redux-thunk 中間件后代碼可以這么寫。

// actions.js
// ...

let nextNotificationId = 0;
export function showNotificationWithTimeout(text) {
    // 返回一個函數
    return function(dispatch) {
        const id = nextNotificationId++;
        dispatch(showNotification(id, text));

        setTimeout(() => {
            dispatch(hideNotification(id));
        }, 5000);
    };
}

// component.js 像同步函數一樣的寫法
this.props.dispatch(showNotificationWithTimeout("You just logged in."));

// 或者 connect 后直接調用
this.props.showNotificationWithTimeout("You just logged in.");
2.2. 接口應用場景

目前我們對于簡單的延時異步操作的處理已經了然于胸了,現在讓我們來考慮一下通過 ajax 或 jsonp 等接口來獲取數據的異步場景。

很自然的,我們會發起一個請求,然后等待請求的響應(請求可能成功或是失敗)。

即有基本的三種狀態和與之對應的 action:

請求開始的 action:isFetching 為真,UI 顯示加載界面

{ type: "FETCH_POSTS_REQUEST" }

請求成功的 action:isFetching 為假,隱藏加載界面并顯示接收到的數據

{ type: "FETCH_POSTS_SUCCESS", response: { ... } }

請求失敗的 action:isFetching 為假,隱藏加載界面,可能保存失敗信息并在 UI 中顯示出來

{ type: "FETCH_POSTS_FAILURE", error: "Oops" }

按照這個思路,舉一個簡單的栗子。

// Constants
const FETCH_POSTS_REQUEST = "FETCH_POSTS_REQUEST";
const FETCH_POSTS_SUCCESS = "FETCH_POSTS_SUCCESS";
const FETCH_POSTS_FAILURE = "FETCH_POSTS_FAILURE";

// Actions
const requestPosts = (id) => ({
    type: FETCH_POSTS_REQUEST,
    payload: id,
});

const receivePosts = (res) => ({
    type: FETCH_POSTS_SUCCESS,
    payload: res,
});

const catchPosts = (err) => ({
    type: FETCH_POSTS_FAILURE,
    payload: err,
});

const fetchPosts = (id) => (dispatch, getState) => {
    dispatch(requestPosts(id));

    return api.getData(id)
        .then(res => dispatch(receivePosts(res)))
        .catch(error => dispatch(catchPosts(error)));
};

// reducer
const reducer = (oldState, action) => {
    switch (action.type) {
        case FETCH_POSTS_REQUEST:
            return requestState;

        case FETCH_POSTS_SUCCESS:
            return successState;

        case FETCH_POSTS_FAILURE:
            return errorState;

        default:
            return oldState;
    }
};

盡管這已經是最簡單的調用接口場景,我們甚至還沒寫一行業務邏輯代碼,但講道理的話代碼還是比較繁瑣的。

而且其實代碼是有一定的“套路”的,比如其實整個代碼都是針對請求、成功、失敗三部分來處理的,這讓我們自然聯想到 Promise,同樣也是分為 pending、fulfilled、rejected 三種狀態。

那么這兩者可以結合起來讓模版代碼精簡一下么?

2.2.1. redux-promise 中間件[[8]]

首先開門見山地使用 redux-promise 中間件來改寫之前的代碼看看效果。

// Constants
const FETCH_POSTS_REQUEST = "FETCH_POSTS_REQUEST";

// Actions
const fetchPosts = (id) => ({
    type: FETCH_POSTS_REQUEST,
    payload: api.getData(id), // payload 為 Promise 對象
});

// reducer
const reducer = (oldState, action) => {
    switch (action.type) {
        case FETCH_POSTS_REQUEST:
            // requestState 被“吃掉”了
            // 而成功、失敗的狀態通過 status 來判斷
            if (action.status === "success") {
                return successState;
            } else {
                return errorState;
            }

        default:
            return oldState;
    }
};

可以看出 redux-promise 中間件比較激進、比較原教旨。

不但將發起請求的初始狀態被攔截了(原因見下文源碼),而且使用 action.status 而不是 action.type 來區分兩個 action 這一做法也值得商榷(個人傾向使用 action.type 來判斷)。

// redux-promise 源碼
import { isFSA } from "flux-standard-action";

function isPromise(val) {
  return val && typeof val.then === "function";
}

export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }

    return isPromise(action.payload)
      // 直接調用 Promise.then(所以發不出請求開始的 action)
      ? action.payload.then(
          // 自動 dispatch
          result => dispatch({ ...action, payload: result }),
          // 自動 dispatch
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}

以上是 redux-promise 的源碼,十分簡單。主要邏輯是判斷如果是 Promise 就執行 then 方法。此外還根據是不是 FSA 決定調用的是 action 本身還是 action.payload 并且對于 FSA 會自動 dispatch 成功和失敗的 FSA。

2.2.2. redux-promise-middleware 中間件

盡管 redux-promise 中間件節省了大量代碼,然鵝它的缺點除了攔截請求開始的 action,以及使用 action.status 來判斷成功失敗狀態以外,還有就是由此引申出的一個無法實現的場景————樂觀更新(Optimistic Update)。

樂觀更新比較直觀的栗子就是在微信、QQ等通訊軟件中,發送的消息立即在對話窗口中展示,如果發送失敗了,在消息旁邊展示提示即可。由于在這種交互方式中“樂觀”地相信操作會成功,因此稱作樂觀更新。

因為樂觀更新發生在用戶發起操作時,所以要實現它,意味著必須有表示用戶初始動作的 action。

因此為了解決這些問題,相對于比較原教旨的 redux-promise 來說,更加溫和派一點的 redux-promise-middleware 中間件應運而生。先看看代碼怎么說。

// Constants
const FETCH_POSTS = "FETCH_POSTS"; // 前綴

// Actions
const fetchPosts = (id) => ({
    type: FETCH_POSTS, // 傳遞的是前綴,中間件會自動生成中間狀態
    payload: {
        promise: api.getData(id),
        data: id,
    },
});

// reducer
const reducer = (oldState, action) => {
    switch (action.type) {
        case `${FETCH_POSTS}_PENDING`:
            return requestState; // 可通過 action.payload.data 獲取 id

        case `${FETCH_POSTS}_FULFILLED`:
            return successState;

        case `${FETCH_POSTS}_REJECTED`:
            return errorState;

        default:
            return oldState;
    }
};

如果不需要樂觀更新,fetchPosts 函數可以更加簡潔。

// 此時初始 actionGET_DATA_PENDING 仍然會觸發,但是 payload 為空。
const fetchPosts = (id) => ({
    type: FETCH_POSTS, // 傳遞的是前綴
    payload: api.getData(id), // 等價于 payload: { promise: api.getData(id) },
});

相對于 redux-promise 簡單粗暴地直接過濾初始 action,從 reducer 可以看出,redux-promise-middleware 會首先自動觸發一個 FETCH_POSTS_PENDING 的 action,以此保留樂觀更新的能力。

并且,在狀態的區分上,回歸了通過 action.type 來判斷狀態的“正途”,其中 _PENDING_FULFILLED_REJECTED 后綴借用了 Promise 規范 (當然它們是可配置的) 。

后綴可以配置全局或局部生效,例如全局配置可以這么寫。
applyMiddleware(
  promiseMiddleware({
    promiseTypeSuffixes: ["LOADING", "SUCCESS", "ERROR"]
  })
)

源碼地址點我,類似 redux-promise 也是在中間件中攔截了 payload 中有 Promise 的 action,并主動 dispatch 三種狀態的 action,注釋也很詳細在此就不贅述了。

注意:redux-promise、redux-promise-middleware 與 redux-thunk 之間并不是互相替代的關系,而更像一種補充優化。
2.3. redux-loop 中間件

簡單小結一下,Redux 的數據流如下所示:

UI => action => action creator => reducer => store => react => v-dom => UI

redux-thunk 的思路是保持 action 和 reducer 簡單純粹,然鵝副作用操作(在前端主要體現在異步操作上)的復雜度是不可避免的,因此它將其放在了 action creator 步驟,通過 thunk 函數手動控制每一次的 dispatch。

redux-promise 和 redux-promise-middleware 只是在其基礎上做一些輔助性的增強,處理異步的邏輯本質上是相同的,即將維護復雜異步操作的責任推到了用戶的身上。

這種實現方式固然很好理解,而且理論上可以應付所有異步場景,但是由此帶來的問題就是模版代碼太多,一旦流程復雜那么異步代碼就會到處都是,很容易導致出現 bug。

因此有一些其他的中間件,例如 redux-loop 就將異步處理邏輯放在 reducer 中。(Redux 的思想借鑒了 Elm,注意并不是“餓了么”,而 Elm 就是將異步處理放在 update(reducer) 層中)。

Synchronous state transitions caused by returning a new state from the reducer in response to an action are just one of all possible effects an action can have on application state.
這種通過響應一個 action,在 reducer 中返回一個新 state,從而引起同步狀態轉換的方式,只是在應用狀態中一個 action 能擁有的所有可能影響的一種。(可能沒翻好~歡迎勘誤~)

redux-loop 認為許多其他的處理異步的中間件,尤其是通過 action creator 方式實現的中間件,錯誤地讓用戶認為異步操作從根本上與同步操作并不相同。這樣一來無形中鼓勵了中間件以許多特殊的方式來處理異步狀態。

與之相反,redux-loop 專注于讓 reducer 變得足夠強大以便處理同步和異步操作。在具體實現上 reducer 不僅能夠根據特定的 action 決定當前的轉換狀態,而且還能決定接著發生的操作。

應用中所有行為都可以在一個地方(reducer)中被追蹤,并且這些行為可以輕易地分割和組合。(redux 作者 Dan 開了個至今依然 open 的 issue:Reducer Composition with Effects in JavaScript,討論關于對 reducer 進行分割組合的問題。)

redux-loop 模仿 Elm 的模式,引入了 Effect 的概念,在 reducer 中對于異步等操作使用 Effect 來處理。如下官方示例所示:

import { Effects, loop } from "redux-loop";

function fetchData(id) {
  return fetch(`endpoint/${id}`)
    .then((r) => r.json())
    .then((data) => ({ type: "FETCH_SUCCESS", payload: data }))
    .catch((error) => ({ type: "FETCH_FAILURE", payload: error.message }));
}

function reducer(state, action) {
  switch(action.type) {
    case "FETCH_START":
      return loop( // <- 并沒有直接返回 state,實際上了返回數組 [state, effect]
        { ...state, loading: true },
        Effects.promise(fetchData, action.payload.id)
      );

    case "FETCH_SUCCESS":
      return { ...state, loading: false, data: action.payload };

    case "FETCH_FAILURE":
      return { ...state, loading: false, errorMessage: action.payload };
  }
}

雖然這個想法很 Elm 很函數式,不過由于修改了 reducer 的返回類型,這樣一來會導致許多已有的 Api 和第三方庫無法使用,甚至連 redux 庫中的 combineReducers 方法都需要使用 redux-loop 提供的定制版本。因此這也是 redux-loop 最終無法轉正的原因:

"If a solution doesn’t work with vanilla combineReducers(), it won’t get into Redux core."
三、復雜異步操作 3.1. 更復雜的通知場景[[9]]

讓我們的思路重新回到通知的場景,之前的代碼實現了:

展示一個通知并在數秒后消失

可以同時展示多個通知。

現在假設可親可愛的產品又提出了新需求:

同時不展示多于3個的通知

如果已有3個通知正在展示,此時的新通知請求將排隊延遲展示。

“這個實現不了...”(全文完)

這個當然可以實現,只不過如果只用之前的 redux-thunk 實現起來會很麻煩。例如可以在 store 中增加兩個數組分別表示當前展示列表和等待隊列,然后在 reducer 中手動控制各個狀態時這倆數組的變化。

3.2. redux-saga 中間件

首先來看看使用了 redux-saga 后代碼會變成怎樣~(代碼來自生產環境的某 app)

function* toastSaga() {
    const MaxToasts = 3;
    const ToastDisplayTime = 4000;

    let pendingToasts = []; // 等待隊列
    let activeToasts = [];  // 展示列表

    function* displayToast(toast) {
        if ( activeToasts >= MaxToasts ) {
            throw new Error("can"t display more than " + MaxToasts + " at the same time");
        }

        activeToasts = [...activeToasts, toast];      // 新增通知到展示列表
        yield put(events.toastDisplayed(toast));      // 展示通知
        yield call(delay, ToastDisplayTime);          // 通知的展示時間
        yield put(events.toastHidden(toast));         // 隱藏通知
        activeToasts = _.without(activeToasts,toast); // 從展示列表中刪除
    }

    function* toastRequestsWatcher() {
        while (true) {
            const event = yield take(Names.TOAST_DISPLAY_REQUESTED); // 監聽通知展示請求
            const newToast = event.data.toastData;
            pendingToasts = [...pendingToasts, newToast]; // 將新通知放入等待隊列
        }
    }

    function* toastScheduler() {
        while (true) {
            if (activeToasts.length < MaxToasts && pendingToasts.length > 0) {
                const [firstToast,...remainingToasts] = pendingToasts;
                pendingToasts = remainingToasts;
                yield fork(displayToast, firstToast); // 取出隊頭的通知進行展示

                // 增加一點延遲,這樣一來兩個并發的通知請求不會同時展示
                yield call(delay, 300);
            }
            else {
                yield call(delay, 50);
            }
        }
    }

    yield [
        call(toastRequestsWatcher),
        call(toastScheduler)
    ]
}

// reducer
const reducer = (state = {toasts: []}, event) => {
    switch (event.name) {
        case Names.TOAST_DISPLAYED:
            return {
                ...state,
                toasts: [...state.toasts, event.data.toastData]
            };

        case Names.TOAST_HIDDEN:
            return {
                ...state,
                toasts: _.without(state.toasts, event.data.toastData)
            };

        default:
            return state;
    }
};

先不要在意代碼的細節,簡單分析一下上述代碼的邏輯:

store 上只有一個 toasts 節點,且 reducer 十分干凈

排隊等具體的業務邏輯都放到了 toastSaga 函數中

displayToast 函數負責單個通知的展示和消失邏輯

toastRequestsWatcher 函數負責監聽請求,將其加入等待隊列

toastScheduler 函數負責將等待隊列中的元素加入展示列表

基于這樣邏輯分離的寫法,還可以繼續滿足更加復雜的需求:

如果在等待隊列中有太多通知,動態減少通知的展示時間

根據窗口大小的變化,改變最多展示的通知數量

...

redux-saga V.S. redux-thunk[[11]]
redux-saga 的優點:

易于測試,因為 redux-saga 中所有操作都 yield 簡單對象,所以測試只要判斷返回的對象是否正確即可,而測試 thunk 通常需要你在測試中引入一個 mockStore

redux-saga 提供了一些方便的輔助方法。(takeLatest、cancel、race 等)

在 saga 函數中處理業務邏輯和異步操作,這樣一來通常代碼更加清晰,更容易增加和更改功能

使用 ES6 的 generator,以同步的方式寫異步代碼

redux-saga 的缺點:

generator 的語法("又是 * 又是 yield 的,很難理解誒~")

學習曲線陡峭,有許多概念需要學習("fork、join 這不是進程的概念么?這些 yield 是以什么順序執行的?")

API 的穩定性,例如新增了 channel 特性,并且社區也不是很大。

通知場景各種中間件寫法的完整代碼可以看這里
3.3. 理解 Saga Pattern[[14]] 3.3.1. Saga 是什么

Sagas 的概念來源于這篇論文,該論文從數據庫的角度談了 Saga Pattern。

Saga 就是能夠滿足特定條件的長事務(Long Lived Transaction)

暫且不提這個特定條件是什么,首先一般學過數據庫的都知道事務(Transaction)是啥~

如果不知道的話可以用轉賬來理解,A 轉給 B 100 塊錢的操作需要保證完成 A 先減 100 塊錢然后 B 加 100 塊錢這兩個操作,這樣才能保證轉賬前后 A 和 B 的存款總額不變。
如果在給 B 加 100 塊錢的過程中發生了異常,那么就要返回轉賬前的狀態,即給 A 再加上之前減的 100 塊錢(不然錢就不翼而飛了),這樣的一次轉賬(要么轉成功,要么失敗返回轉賬前的狀態)就是一個事務。
3.3.2. 長事務的問題
長事務顧名思義就是一個長時間的事務。

一般來說是通過給正在進行事務操作的對象加鎖,來保證事務并發時不會出錯。

例如 A 和 B 都給 C 轉 100 塊錢。

如果不加鎖,極端情況下 A 先轉給 C 100 塊,而 B 讀取到了 C 轉賬前的數值,這時 B 的轉賬會覆蓋 A 的轉賬,C 只加了 100 塊錢,另 100 塊不翼而飛了。

如果加了鎖,這時 B 的轉賬會等待 A 的轉賬完成后再進行。所以 C 能正確地收到 200 塊錢。

以押尾光太郎的指彈演奏會售票舉例,在一個售票的時間段后,最終舉辦方需要確定售票數量,這就是一個長事務。

然鵝,對于長事務來說總不能一直鎖住對應數據吧?

為了解決這個問題,假設一個長事務:T,

可以被拆分成許多相互獨立的子事務(subtransaction):t_1 ~ t_n。

以上述押尾桑的表演為例,每個 t 就是一筆售票記錄。

假如每次購票都一次成功,且沒有退票的話,整個流程就如下圖一般被正常地執行。

那假如有某次購票失敗了怎么辦?

3.3.3. Saga 的特殊條件
A LLT is a saga if it can be written as a sequence of transactions that can be interleaved with other transactions.
Saga 就是能夠被寫成事務的序列,并且能夠在執行過程中被其他事務插入執行的長事務。

Saga 通過引入補償事務(Compensating Transaction)的概念,解決事務失敗的問題。

即任何一個 saga 中的子事務 t_i,都有一個補償事務 c_i 負責將其撤銷(undo)。

注意是撤銷該子事務,而不是回到子事務發生前的時間點。

根據以上邏輯,可以推出很簡單的公式:

Saga 如果全部執行成功那么子事務序列看起來像這樣:t_1, t_2, t_3, ..., t_n

Saga 如果執行全部失敗那么子事務序列看起來像這樣:t_1, t_2, t_3, ..., t_n, c_n, ..., c_1

注意到圖中的 c_4 其實并沒有必要,不過因為每次撤銷執行都應該是冪等(Idempotent)的,所以也不會出錯。

篇幅有限在此就不繼續深入介紹...

推薦看看從分布式系統方面講 Saga Pattern 的視頻:GOTO 2015 ? Applying the Saga Pattern ? Caitie McCaffrey

MSDN 的文章:A Saga on Sagas

3.4. 響應式編程(Reactive Programming)[[15]]

redux-saga 中間件基于 Sagas 的理論,通過監聽 action,生成對應的各種子 saga(子事務)解決了復雜異步問題。

而接下來要介紹的 redux-observable 中間件背后的理論是響應式編程(Reactive Programming)。

In computing, reactive programming is a programming paradigm oriented around data flows and the propagation of change.

簡單來說,響應式編程是針對異步數據流的編程并且認為:萬物皆流(Everything is Stream)。

流(Stream)就是隨著時間的流逝而發生的一系列事件。

例如點擊事件的示意圖就是這樣。

用字符表示【上上下下左右左右BABA】可以像這樣。(注意順序是從左往右)

--上----上-下---下----左---右-B--A-B--A---X-|->

上, 下, 左, 右, B, A 是數據流發射的值
X 是數據流發射的錯誤
| 是完成信號
---> 是時間線

那么我們要根據一個點擊流來計算點擊次數的話可以這樣。(一般響應式編程庫都會提供許多輔助方法如 map、filter、scan 等)

  clickStream: ---c----c--c----c------c-->
                    map(c becomes 1)
               ---1----1--1----1------1-->
                         scan(+)
counterStream: ---1----2--3----4------5-->

如上所示,原始的 clickStream 經過 map 后產生了一個新的流(注意原始流不變),再對該流進行 scan(+) 的操作就生成了最終的 counterStream。

再來個栗子~,假設我們需要從點擊流中得到關于雙擊的流(250ms 以內),并且對于大于兩次的點擊也認為是雙擊。先想一想應該怎么用傳統的命令式、狀態式的方式來寫,然后再想想用流的思考方式又會是怎么樣的~。

這里我們用了以下輔助方法:

節流:throttle(250ms),將原始流在 250ms 內的所有數據當作一次事件發射

緩沖(不造翻譯成啥比較好):buffer,將 250ms 內收集的數據放入一個數據包裹中,然后發射這些包裹

映射:map,這個不解釋

過濾:filter,這個也不解釋

更多內容請繼續學習 RxJS。

3.5. redux-observable 中間件[[16]]

redux-observable 就是一個使用 RxJS 監聽每個 action 并將其變成可觀測流(observable stream)的中間件。

其中最核心的概念叫做 epic,就是一個監聽流上 action 的函數,這個函數在接收 action 并進行一些操作后可以再返回新的 action。

At the highest level, epics are “actions in, actions out”

redux-observable 通過在后臺執行 .subscribe(store.dispatch) 實現監聽。

Epic 像 Saga 一樣也是 Long Lived,即在應用初始化時啟動,持續運行到應用關閉。雖然 redux-observable 是一個中間件,但是類似于 redux-saga,可以想象它就像新開的進/線程,監聽著 action。

在這個運行流程中,epic 不像 thunk 一樣攔截 action,或阻止、改變任何原本 redux 的生命周期的其他東西。這意味著每個 dispatch 的 action 總會經過 reducer 處理,實際上在 epic 監聽到 action 前,action 已經被 reducer 處理過了。

所以 epic 的功能就是監聽所有的 action,過濾出需要被監聽的部分,對其執行一些帶副作用的異步操作,然后根據你的需要可以再發射一些新的 action。

舉個自動保存的栗子,界面上有一個輸入框,每次用戶輸入了數據后,去抖動后進行自動保存,并在向服務器發送請求的過程中顯示正在保存的 UI,最后顯示成功或失敗的 UI。

使用 redux-observable 中間件編寫代碼,可以僅用十幾行關鍵代碼就實現上述功能。

import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/dom/ajax";
import "rxjs/add/observable/of";
import "rxjs/add/operator/catch";
import "rxjs/add/operator/debounceTime";
import "rxjs/add/operator/map";
import "rxjs/add/operator/mergeMap";
import "rxjs/add/operator/startWith";

import {
    isSaving, savingSuccess, savingError,
} from "../actions/autosave-actions.js";

const saveField = (action$) => // 一般在變量后面加 $ 表示是個 stream
    action$
        .ofType("SAVE_FIELD")  // 使用 ofType 監聽 "SAVE_FIELD" action
        .debounceTime(500)     // 防抖動
        // 即 map + mergeAll 因為異步導致 map 后有多個流需要 merge
        .mergeMap(({ payload }) =>
            Observable.ajax({  // 發起請求
                method: "PATCH",
                url: payload.url,
                body: JSON.stringify(payload),
            })
            .map(res => savingSuccess(res))                 // 發出成功的 action
            .catch(err => Observable.of(savingError(err)))  // 捕捉錯誤并發出 action
            .startWith(isSaving())                          // 發出請求開始的 action
        );

export default saveField;

篇幅有限在此就不繼續深入介紹...

關于 redux-observable 的前世今生推薦看看 Netfix 工程師的這個視頻:Netflix JavaScript Talks - RxJS + Redux + React = Amazing!

如果覺得看視頻聽英語麻煩的話知乎有人翻譯了...

RxJS + Redux + React = Amazing!(譯一)

RxJS + Redux + React = Amazing!(譯二)

四、總結

本文從為 Redux 應用添加日志功能(記錄每一次的 dispatch)入手,引出 redux 的中間件(middleware)的概念和實現方法。

接著從最簡單的 setTimeout 的異步操作開始,通過對比各種實現方法引出 redux 最基礎的異步中間件 redux-thunk。

針對 redux-thunk 使用時模版代碼過多的問題,又介紹了用于優化的 redux-promise 和 redux-promise-middleware 兩款中間件。

由于本質上以上中間件都是基于 thunk 的機制來解決異步問題,所以不可避免地將維護異步狀態的責任推給了開發者,并且也因為難以測試的原因。在復雜的異步場景下使用起來難免力不從心,容易出現 bug。

所以還簡單介紹了一下將處理副作用的步驟放到 reducer 中并通過 Effect 進行解決的 redux-loop 中間件。然鵝因為其無法使用官方 combineReducers 的原因而無法被納入 redux 核心代碼中。

此外社區根據 Saga 的概念,利用 ES6 的 generator 實現了 redux-saga 中間件。雖然通過 saga 函數將業務代碼分離,并且可以用同步的方式流程清晰地編寫異步代碼,但是較多的新概念和 generator 的語法可能讓部分開發者望而卻步。

同樣是基于觀察者模式,通過監聽 action 來處理異步操作的 redux-observable 中間件,背后的思想是響應式編程(Reactive Programming)。類似于 saga,該中間件提出了 epic 的概念來處理副作用。即監聽 action 流,一旦監聽到目標 action,就處理相關副作用,并且還可以在處理后再發射新的 action,繼續進行處理。盡管在處理異步流程時同樣十分方便,但對于開發者的要求同樣很高,需要開發者學習關于函數式的相關理論。

五、參考資料

Redux 英文原版文檔

Redux 中文文檔

Dan Abramov - how to dispatch a redux action with a timeout

阮一峰 - Redux 入門教程(二):中間件與異步操作

Redux 莞式教程

redux middleware 詳解

Thunk 函數的含義和用法

Redux異步方案選型

Sebastien Lorber - how to dispatch a redux action with a timeout

Sagas 論文

Pros/cons of using redux-saga with ES6 generators vs redux-thunk with ES7 async/await

Redux-saga 英文文檔

Redux-saga 中文文檔

Saga Pattern 在前端的應用

The introduction to Reactive Programming you"ve been missing

Epic Middleware in Redux

以上 to be continued...

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96875.html

相關文章

  • react全家+koa2實現一個多人博客~

    摘要:不斷更新筆記效果有待進一步完善搭建一個基于的多人功能登錄注冊上傳頭像發表博文發表留言參考自前端部分以的腳手架搭起的全家桶后端采用開發環境開發環境要求以上目錄結構如何運行后端默認配置在中請確保本地端口默認可用發布到目錄中默 Full-stack-blog(不斷更新筆記) 效果Demo(有待進一步完善)搭建一個基于Koa2的多人blog功能(登錄注冊上傳頭像,發表博文,發表留言)參考自ht...

    mdluo 評論0 收藏0
  • 好程序員React精品項目全集:商城管理后臺(視頻+源碼+筆記

    摘要:今天給大家帶來了好程序員實戰項目商城管理后臺。配合項目學習會讓你更快掌握它的使用方法下面就來看看好程序員這套實戰項目課程介紹好程序員項目本項目是一個使用開發的商城系統的管理后臺,里面登錄判斷,接口調用,數據展示和編輯,文件上傳等后臺功能。 眾所周知,項目經驗對于一個程序員變得越來越重要。在面...

    李世贊 評論0 收藏0
  • Redux 基礎 - react 全家學習筆記(一)

    摘要:二基礎就是一個普通的。其他屬性用來傳遞此次操作所需傳遞的數據,對此不作限制,但是在設計時可以參照標準。對于異步操作則將其放到了這個步驟為添加一個變化監聽器,每當的時候就會執行,你可以在回調函數中使用來得到當前的。 注:這篇是16年10月的文章,搬運自本人 blog...https://github.com/BuptStEve/... 零、環境搭建 參考資料 英文原版文檔 中文文檔 墻...

    aaron 評論0 收藏0
  • 2017-06-23 前端日報

    摘要:前端日報精選大前端公共知識梳理這些知識你都掌握了嗎以及在項目中的實踐深入貫徹閉包思想,全面理解閉包形成過程重溫核心概念和基本用法前端學習筆記自定義元素教程阮一峰的網絡日志中文譯回調是什么鬼掘金譯年,一個開發者的好習慣知乎專 2017-06-23 前端日報 精選 大前端公共知識梳理:這些知識你都掌握了嗎?Immutable.js 以及在 react+redux 項目中的實踐深入貫徹閉包思...

    Vixb 評論0 收藏0
  • React-全家仿簡書部分功能

    摘要:全家桶仿簡書部分功能前言前段時間接觸了下,一直想要自己寫一個小練手。在眾多應用中,考慮之后選擇了簡書來模仿,這段時間就利用了工作之余的時間進行開發。在這里簡單敘述一下我仿簡書部分布局以及功能實現的過程,僅做學習用途。 React-全家桶仿簡書部分功能 前言 前段時間接觸了下React,一直想要自己寫一個小Demo練手。在眾多應用中,考慮之后選擇了簡書來模仿,這段時間就利用了工作之余的時...

    Jinkey 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<