react技術棧實踐(從前到後擼一個電影蒐集應用)

對對對就是這發表於2019-02-16

之前做了個電影蒐集的小應用,前端採用react,後端採用express+mongodb,最近又將元件間的狀態管理改成了redux,並加入了redux-saga來管理非同步操作,記錄一些總結

線上地址 手機模式

原始碼

主要功能

  • 爬取豆瓣電影資訊並錄入MongoDB

  • 電影列表展示,分類、搜尋

  • 電影詳情展示及附件管理

  • 註冊、登入

  • 許可權控制,普通使用者可以錄入、收藏,administrator錄入、修改、刪除

  • 使用者中心,我的收藏列表

react技術棧實踐(從前到後擼一個電影蒐集應用)

一些總結

前端

前端使用了react,redux加redux-saga,對redux簡單總結一下,同時記錄一個前後介面呼叫有依賴關係的問題

  • redux

一句話總結redux,我覺的就是將元件之間的縱向的props傳遞和層級元件間狀態關係給打平了,將一種縱向關係轉變成多個元件和一個獨立出來的狀態物件直接互動,這樣之後,程式碼結構確實看上去更加清晰了。

redux的核心概念,action,reducer,和store

action就是說明我要操作一個狀態了,怎麼操作是reducer的事,而所有狀態儲存在store中,store發出動作並交由指定的reducer來處理

redux強制規範了我們對狀態的操作,只能在action和reducer這些東西中,這樣,原本錯綜複雜的業務邏輯處理就換了個地,限制在了action和reducer中,元件看上去就很乾淨了。其實,該複雜的東西在哪放都複雜,只不過現在更清晰一點

使用redux不好的地方就是太繁瑣了,定義各種action,connect各種元件。。。。。現在又出來一個Mobx,不明覺厲,反正大家都說好~

  • redux-saga

redux-saga用來處理非同步呼叫啥的,藉助於generator,讓非同步程式碼看起來更簡潔,常用的有take,takeLatest,takeEvery,put,call,fork,select,使用過程中遇到一個介面呼叫有前後依賴關係的問題,比較有意思

描述一下:

  1. 有一個介面*/api/user/checkLogin*,用來判斷是否登入,在最外層的元件的componentDidMount中觸發action來發起這個請求,並且介面返回狀態是登入的話,還要發一個獲取使用者資訊的
function* checkLogin() {
    const res = yield Util.fetch('/api/user/checkLogin')
    yield put(recieveCheckLogin(!res.code))
    if (!res.code) {
        //已登入
        yield put(fetchUinfo())
    }
}
export function* watchCheckLogin() {
    yield takeLatest(CHECK_LOAGIN, checkLogin)
}
複製程式碼
  1. 然後我有一個電影詳情頁元件,在這個元件的componentDidMount中會發起*/api/movies/${id}介面獲取電影資訊,如果使用者是登入狀態的話,還會發起一個獲取電影附件資訊的介面/api/movies/${id}/attach*,整個步驟寫在一個generator中
function* getItemMovie(id) {
    return yield Util.fetch(`/api/movies/${id}`)
}

function* getMovieAttach(id) {
    return yield Util.fetch(`/api/movies/${id}/attach`)
}

function* getMovieInfo(action) {
    const { movieId } = action
    let { login } = yield select(state => state.loginStatus)
    const res = yield call(getItemMovie, movieId)
    yield put(recieveItemMovieInfo(res.data[0]))
    if (res.data[0].attachId && login) {
        const attach = yield call(getMovieAttach, movieId)
        yield put(recieveMovieAttach(attach.data[0]))
    }
}

export function* watchLoadItemMovie() {
    yield takeLatest(LOAD_ITEM_MOVIE, getMovieInfo)
}
複製程式碼
  1. 使用者登入了,進到詳情,流程正常,但如果在詳情頁重新整理了頁面,獲取附件的介面沒觸發,原因是此時checkLogin介面還沒返回結果,state.loginStatus狀態還是false,上面就沒走到if中

  2. 一開始想著怎麼控制一些generator中yield的先後順序來解決(如果使用者沒有登入的話,再發一個CHECK_LOAGIN,結果返回了流程再繼續),但存在CHECK_LOAGIN呼叫兩次,如果登入了,還會再多一次獲取使用者資訊的介面呼叫的情況,肯定不行

function* getMovieInfo(action) {
    const { movieId } = action
    let { login } = yield select(state => state.loginStatus)
    const res = yield call(getItemMovie, movieId)
    yield put(recieveItemMovieInfo(res.data[0]))
    // if (!login) {
    //     //重新整理頁面的時候,如果此時checklogin介面還沒返回資料或還沒發出,應觸發一個checklogin
    //     //checklogin返回後才能得到login狀態
    //     yield put({
    //         type: CHECK_LOAGIN
    //     })
    //     const ret = yield take(RECIEVE_CHECK_LOAGIN)
    //     login = ret.loginStatus
    // }
    if (res.data[0].attachId && login) {
        const attach = yield call(getMovieAttach, movieId)
        yield put(recieveMovieAttach(attach.data[0]))
    }
}
複製程式碼
  1. 最終的辦法,分解generator的職責,componentWillUpdate中合適的觸發獲取附件的動作
//將獲取附件的動作從 getMovieInfo這個generator中分離出來
function* getMovieInfo(action) {
    const { movieId } = action
    const res = yield call(getItemMovie, movieId)
    yield put(recieveItemMovieInfo(res.data[0]))
}
function* watchLoadItemMovie() {
    yield takeLatest(LOAD_ITEM_MOVIE, getMovieInfo)
}
function* watchLoadAttach() {
    while (true) {
        const { movieId } = yield take(LOAD_MOVIE_ATTACH)
        const { attachId } = yield select(state => state.detail.movieInfo)
        const attach = yield call(getMovieAttach, movieId)
        yield put(recieveMovieAttach(attach.data[0]))
    }
}

//元件中
componentWillUpdate(nextProps) {
        if (nextProps.loginStatus && (nextProps.movieInfo!==this.props.movieInfo)) {
            //是登入狀態,並且movieInfo已經返回時
            const { id } = this.props.match.params
            this.props.loadMovieAttach(id)
        }
}
複製程式碼
  1. 總結,合理使用元件的鉤子函式,generator中不要處理太多操作,增加靈活性

後端

後端採用express和mongodb,也用到了redis,主要技術點有使用pm2來管理node應用及部署程式碼mongodb中開啟身份認證,使用token+redis來做身份認證、在node中寫了寫單元測試,還是值得記錄一下的

  • 使用 jwt + redis 來做基於token的使用者身份認證

基於token的認證流程

  1. 客戶端發起登入請求

  2. 服務端驗證使用者名稱密碼

  3. 驗證成功服務端生成一個token,響應給客戶端

  4. 客戶端之後的每次請求header中都帶上這個token

  5. 服務端對需要認證的介面要驗證token,驗證成功接收請求

這裡採用jsonwebtoken來生成token,

jwt.sign(payload, secretOrPrivateKey, [options, callback])
複製程式碼

使用express-jwt驗證token(驗證成功會把token資訊放在request.user中)

express_jwt({
        secret: SECRET,
        getToken: (req)=> {
        if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
            return req.headers.authorization.split(' ')[1];
        } else if (req.query && req.query.token) {
            return req.query.token;
        }
        return null;
    }
    }
複製程式碼

為什麼使用redis

採用jsonwebtoken生成token時可以指定token的有效期,並且jsonwebtoken的verify方法也提供了選項來更新token的有效期, 但這裡使用了express_jwt中介軟體,而express_jwt不提供方法來重新整理token

思路:

  1. 客戶端請求登入成功,生成token

  2. 將此token儲存在redis中,設定redis的有效期(例如1h)

  3. 新的請求過來,先express_jwt驗證token,驗證成功, 再驗證token是否在redis中存在,存在說明有效

  4. 有效期內客戶端新的請求過來,提取token,更新此token在redis中的有效期

  5. 客戶端退出登入請求,刪除redis中此token

具體程式碼

  • 使用 mocha + supertest + should 來寫單元測試

測試覆蓋了所有介面,在開發中,因為沒什麼進度要求就慢慢寫了,寫完一個介面就去寫一個測試,測試寫也還算詳細,等測試通過了再前端調介面,整個過程還是挺有意思的

mocha 是一個node單元測試框架,類似於前端的jasmine,語法也相近

supertest 用來測試node介面的庫

should nodejs斷言庫,可讀性很高

測試的一個例子,篇幅太長,就不放在這

最後

喜歡可以關注下,萬一有福利呢。。。。。

相關文章