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

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

在線地址 手機模式

源碼node

主要功能

  • 爬取豆瓣電影信息並錄入MongoDBreact

  • 電影列表展現,分類、搜索git

  • 電影詳情展現及附件管理github

  • 註冊、登陸web

  • 權限控制,普通用戶能夠錄入、收藏,administrator錄入、修改、刪除redis

  • 用戶中心,個人收藏列表mongodb

一些總結

前端

前端使用了react,redux加redux-saga,對redux簡單總結一下,同時記錄一個先後接口調用有依賴關係的問題express

  • redux

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

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斷言庫,可讀性很高

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

最後

喜歡能夠關注下,萬一有福利呢。。。。。

相關文章
相關標籤/搜索