還在找react例子? 記錄一下react練習當心得

react+reudx+router+material-ui+es六、7

初學者用來作練習很不錯,由於我就是。javascript

看見ShanaMaid寫了一個react讀書app, 本身借用API練習一下,記錄練習過程。https://github.com/fygethub/s...css

建立倉庫

經過create-react-app建立初始環境, 安裝material UI庫, 按照material官網描述修改webpack配置按需加載。詳細參照material-ui
效果圖java

效果圖

一、在src 文件下新建components文件夾在當前文件夾下面編寫組件。
二、在src 下新建source文件存放字體圖片等資源node

  1. 新建一個router文件配置路由跳轉。路由用的是react-route-dom 也就是react-router 的升級版本,路由在個人理解就是經過url來匹配組件的顯示這是下面是路由配置文件react

/* src/touter/router.config.js */
import Main from '../components/Main';
import Search from '../components/Search';
import About from '../components/About';
import BookIntro from '../components/BookIntro';
import Read from '../components/Read';
import ChangeOrigin from '../components/ChangeOrigin';
const routes = [
    {
        path: '/search',
        component: Search,
        exact: true,
    },
    {
        path: '/about',
        component: About,
        exact: true,
    },
    {
        path: '/bookIntro',
        component: BookIntro,
        exact: true,
    },
    {
        path: '/read/:id',
        component: Read
    },{
        path: '/changeOrigin',
        component: ChangeOrigin,
        exact: true,
    }
    ,{
        component: Main
    }
];

export  default routes;

/*src/router/Router.js*/
/**
 * Created by fydor on 2017/5/5.
 */
import React from 'react';
import {
    BrowserRouter as Router,
    Route,
    Switch,
} from 'react-router-dom';
import routes from './router.config';
const Routers = () => (
    <Router>
        <Switch>
            {
                routes.map((route,i)=> (
                   <Route key={i} path={route.path} exact={route.exact} component={route.component}/>
                ))
            }
        </Switch>
    </Router>
);

export default Routers;

這樣配置能夠直接在配置文件中添加路由,因爲只有一層路由因此對象中沒有繼續嵌套routes(嵌套的意思是在當前顯示的組件下面還有須要經過url匹配顯示的組件)路由嵌套能夠參照
react-router-dom route-configwebpack

目前位置目錄結構以下git

.
├── App.js
├── App.test.js
├── components
│   ├── About.js //用來顯示關於頁面
│   ├── BookIntro.js //介紹
│   ├── ChangeOrigin.js //換源
│   ├── Main.js //主頁顯示關注的圖書
│   ├── Read.js //閱讀界面
│   └── Search.js //搜索頁
├── index.js //渲染頁面
├── redux
│   ├── action.js  
│   └── reducer.js
├── router
│   ├── Routers.js
│   └── router.config.js
├── source
└── styles

在create-react-app 中經過 .eslintrc 配置文件配置 eslint

經過運行<font color=deepPink >npm run eject</font>使其暴露webpack等配置文件es6

自定義eslint 原文鏈接

上述步驟並無暴露react腳手架封裝的eslint操做,爲了使得項目統一規範化,添加jsx-eslint操做是很是不錯的選擇(關於js其餘的eslint操做,請參見官網,本文主要針對jsx限制規範配置)。github

  • 在項目根目錄下添加.eslintrc文件web

  • 在根目錄找到config文件夾,並找到文件夾下的webpack.config.dev.js文件

  • webpack.config.dev.js文件修改添加以下代碼

preLoaders: [
          {
            test: /\.(js|jsx)$/,
            loader: 'eslint',
              enforce: 'pre',
              use: [{
                  // @remove-on-eject-begin
                  // Point ESLint to our predefined config.
                  options: {
                      //configFile: path.join(__dirname, '../.eslintrc'),
                      useEslintrc: true
                  },
                  // @remove-on-eject-end
                  loader: 'eslint-loader'
              }],
            include: paths.appSrc,
          }
        ],

.運行npm start,此時,你編寫的jsx文件都是通過.eslintrc的配置限制
ps: 配置的value對應的值: 0 : off 1 : warning 2 : error
不知足如下的規範設置的,編譯代碼時將有黃色提示

<pre>

"extends": "react-app",
       "rules": {
         "no-multi-spaces": 1,
         "react/jsx-space-before-closing": 1,        // 老是在自動關閉的標籤前加一個空格,正常狀況下也不須要換行
         "jsx-quotes": 1,
         "react/jsx-closing-bracket-location": 1,    // 遵循JSX語法縮進/格式
         "react/jsx-boolean-value": 1,               // 若是屬性值爲 true, 能夠直接省略
         "react/no-string-refs": 1,      // 老是在Refs裏使用回調函數
         "react/self-closing-comp": 1,    // 對於沒有子元素的標籤來講老是本身關閉標籤
         "react/jsx-no-bind": 1,          // 當在 render() 裏使用事件處理方法時,提早在構造函數裏把 this 綁定上去
         "react/sort-comp": 1,            // 按照具體規範的React.createClass 的生命週期函數書寫代碼
         "react/jsx-pascal-case": 1        // React模塊名使用帕斯卡命名,實例使用駱駝式命名
       }
     }

</pre>

經過material UI 去對頁面佈局

  • 書籍詳情頁

  • 查詢列表頁

頁面寫好了之後確定就是寫功能了,功能咱們不一次性去寫完而是用到什麼添加什麼

目前書籍搜索頁面佈局好了之後開始添加功能,不知不覺本身的文件就變得多了。

這裏普及一下生成圖形目錄的工具 用的是tree 工具

直接tree -I "node_modules|dist" 就出來了 ? 固然須要安裝 這裏連接一篇mac上使用tree命令生成樹狀目錄

├── README.md
├── config // 配置文件 create-react-app配置 缺乏本身想要的功能就在上面添加
│   ├── env.js
│   ├── jest
│   ├── paths.js
│   ├── polyfills.js
│   ├── webpack.config.dev.js 
│   └── webpack.config.prod.js
├── package.json
├── scripts  // node 啓動文件
│   ├── build.js
│   ├── start.js // 啓動文件 配置本身的轉發能夠在這裏配置 如devserver的proxy
│   └── test.js
├── src
│   ├── App.js
│   ├── App.test.js
│   ├── components
│   │   ├── About.js
│   │   ├── BookIntro.js
│   │   ├── ChangeOrigin.js
│   │   ├── Main.js
│   │   ├── Read.js
│   │   ├── Search.js //只是一個簡單的搜索頁面返回按鈕
│   │   └── commont
│   │       ├── Loading.js
│   │       ├── ReturnButton.js   //只是一個簡單的返回按鈕
│   │       └── Share.js  //只是一個簡單的分享按鈕
│   ├── index.js
│   ├── redux
│   │   ├── action.js
│   │   ├── middleware   // 這裏是redux middleware 寫的logmiddle 和 thunk ,固然也有人家寫好了的自行github
│   │   │   └── middleware.js
│   │   ├── reducer.js
│   │   └── store.js 
│   ├── router
│   │   ├── Routers.js
│   │   └── router.config.js
│   ├── source
│   ├── styles
│   │   ├── animate.css
│   │   ├── bookIntro.css
│   │   ├── font     // 配置iconfont 這裏使用的阿里 ?
│   │   │   └── font.css
│   │   ├── loading.css
│   │   ├── reset.css  
│   │   ├── search.css
│   │   ├── share.css
│   │   └── variables.css
│   └── tools
│       └── index.js
└── yarn.lock

編寫須要用到的action

這裏目前用到的action有獲取書籍列表receiveBookList 是否顯示加載框 isShowLoading

自動不全 autoComplete 以上都是同步action

import 'whatwg-fetch';
import { urlChange } from '../tools';

export const IS_LOADING = 'IS_LOADING';
export const GET_BOOK_LIST = 'GET_BOOK_LIST';
export const AUTO_COMPLETE = 'AUTO_COMPLETE';

const receiveBookList = (data, name) => ({
    type: GET_BOOK_LIST,
    searchData: data,
    name: name
});

export const isShowLoading = (isloading) => ({
    type: IS_LOADING,
    isloading
});

export const autoComplete = (name, completeList) => ({
    type: AUTO_COMPLETE,
    name,
    completeList
});

### 異步action 這裏分發異步action須要用到 middleware 做用是dispatch的時候能夠傳除對象外還能夠是函數
下面是middleware src/redux/middleware/middleware.js

export const thunk = (store) => next => action =>
        typeof action === 'function' ?
            action(store.dispatch, store.getState) :
            next(action);

export const logger = (store) => next => action => {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd(action.type);
      return result;
}

有了上面的middleware 就能夠編寫異步action了一樣在 src/redux/action.js中添加

export const receiveAutoComplete = name => dispatch =>
    fetch(`book/auto-complete?query=${name}`)
        .then(res=>res.json())
        .then(data => dispatch(autoComplete(name,data.keywords)))
        .catch(error => new Error(error));



export const getBookList = (name) => dispatch => {
        dispatch(isShowLoading(true));
        return fetch(`/api/book/fuzzy-search?query=${name}&start=0`)
            .then(res => res.json())
            .then(data => data.books.map((book) => urlChange(book.cover)))
            .then(data => {
                let action = dispatch(receiveBookList(data,name));
                dispatch(isShowLoading(false));
                return action;
            })
            .catch(error => {
                new Error(error);
            })
        };

action編寫完畢 接下來就應該編寫reducer ,reducer意思是經過action計算出下次的state因爲咱們會用到conbinereducer因此

能夠向下面的方式編寫
src/redux/reducer.js

import { IS_LOADING, GET_BOOK_LIST, AUTO_COMPLETE } from './action';

export const bookList = (state = {books:[], name: ''},action={}) => {
    switch (action.type){
        case GET_BOOK_LIST:
            let { books, name } = action;
            return {name,books}
        default:
            return state;
    }
}

export const autoBookList = (state = {lists : [],name : '' }, action) => {
   switch (action.type){
       case AUTO_COMPLETE:
            let { completeList, name} = action;
            return {lists:completeList, name};
       default: return state;
   }

}

export const isLoading = (state = false,action) => {
    switch(action.type){
        case IS_LOADING:
            return action.isloading;
        default:
            return state;
    }
}

生成store底層步驟寫完後下面就開始建立出咱們須要的store了,建立store須要redux 裏面的方法

//src/redux/store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import * as reducer from './reducer';
import { thunk, logger} from './middleware/middleware';

let store = createStore(
    combineReducers(reducer),
    applyMiddleware(thunk,logger));

export default store;

好了該有的方法咱們都建立完畢在App文件中來測試一下❤先 , 跟着我默唸一遍咒語
神獸保佑?代碼一次過

import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
import { Provider } from 'react-redux';
import Routes from './router/Routers'
import darkBaseTheme from "material-ui/styles/baseThemes/lightBaseTheme";
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import injectTapEventPlugin from 'react-tap-event-plugin';
import './styles/reset.css';
import { receiveAutoComplete, getBookList} from './redux/action';
import Loading from './components/commont/Loading';
import store from './redux/store';

store.subscribe(() =>
    console.log(store.getState())
)

store.dispatch(receiveAutoComplete('he'));

setTimeout(function () {
    store.dispatch(receiveAutoComplete('大'));
},1000)

setTimeout(function () {
    store.dispatch(getBookList('hello world'));
},2000)


/*引用tap事件適配移動端*/
injectTapEventPlugin();

class App extends Component {

  /*material-ui 須要配置主題纔可使用*/
  getChildContext() {
      return { muiTheme: getMuiTheme(darkBaseTheme) };
  }

  render() {
    return (
        <Provider store={store}>
          <div className="App">
                <Loading/>
                <Routes />
          </div>
        </Provider>
    );
  }
}

App.childContextTypes = {
    muiTheme: PropTypes.object.isRequired,
};

export default App;

代碼跑起來 npm start

看到咱們的控制檯發現有個小警告說閉合標籤前面須要有一個空格 果斷跑去加一個 pic;

在看一次咱們的請求都發出去了,reducer也接收到action後爲咱們處理了。
;

繼續編寫搜索頁面並優化

  • 點擊搜索發送一個搜索action reducer處理後search組件獲取到書籍數據顯示到列表

  • 優化書籍自動補全時候輸入框每輸入一個字符都要發送action 增長一個延時發送效果<font color=deepPink>主要方法:當輸入中止後350毫秒搜索,每當輸入時都清除定時器而後在添加一個定時器</font>

constructor(props){
            super(props);
            this.state = {
                searchText:''
            }
            this.inputTimer = 0;
        }
    handleSearchAutoComplete = () => {
        const { dispatch } = this.props;
        dispatch(getBookList(this.state.searchText));
    }

    /*輸入框延處理*/
    handleAutoSearchDelay = (time) => {
        const { dispatch } = this.props;
        this.inputTimer = setTimeout( () => {
            dispatch(receiveAutoComplete(this.state.searchText));
        },time);
    }
  • 爲了避免讓每次刷新時候都render 頁面這裏用了<font color=deepPink> decorator 至關於java的註解 AOP</font> ES7 的提議方法你們能夠自行google一下

//只須要在類上面添加 @PureRender 就能夠自動注入方法
@PureRender
class AutoCompleteClass extends Component {}

高階函數Higher Order Components 編寫PureRender

//src/tools/decorators.js
function shalloEqual(next,prev) {
    if(prev === next) return true;
    const prevKes = Object.keys(prev);
    const nextKes = Object.keys(next);
    if(prevKes.length !== nextKes.length) return false;
    return prevKes.every((key)=>prev.hasOwnProperty(key) && prev[key] === next[key]);
}


function PureRender(Component) {
    if(!Component.prototype.shouldComponentUpdate){
        Component.prototype.shouldComponentUpdate = function (nextProps, nextState) {
            console.group('start equal component props and state');
            let isRender = PureRender.prototype.shouldComponentUpdate(nextProps,nextState,this.props,this.state);
            console.info('the equal result is  :' + isRender);
            console.groupEnd();
            return isRender
        }
    }
}


PureRender.prototype.shouldComponentUpdate = function(nextProps,nextState,prevProp,prevState){
    return !shalloEqual(nextProps,prevProp) || !shalloEqual(nextState,prevState);
}


export default PureRender;

搜索頁面、添加歷史搜索頁和推薦列表

  • 歷史搜索頁面佈局

  • 新增歷史搜索的action 和 reducer

歷史搜索頁和推薦列表

若是state中沒有搜索列表就顯示推薦列表和歷史記錄,歷史記錄還沒添加本地緩存功能。

添加歷史記錄功能後search組件中佈局內容多了起來,所以把歷史和列表顯示拆分紅兩個不通的組件,這也符合漸進式推動本身的項目。


搜索列表跳轉書籍詳情頁

  • 準備用Link 標籤跳轉到詳情頁,點擊的同時發送一個請求書籍詳情的action 而後顯示在詳情頁。佈局以下並添加action與reducer函數
    書籍詳情

// src/redux/action.js 新增
export const ADD_BOOK_LONG_INTRO = 'ADD_BOOK_LONG_INTRO';

export const addBookLongIntro = (bookIntro = {}) => ({
    type: ADD_BOOK_LONG_INTRO,
    bookIntro
})

export const receiveBookLongIntro = (bookId) => dispatch => {
    dispatch(isShowLoading(true));
    fetch(`/book/${bookId}`).then(res => res.json())
        .then(data => {
            dispatch(addBookLongIntro(data));
            dispatch(isShowLoading(false));
        })
        .catch(err => {
            console.error(Error(err));
        })
}
  • 在reducer中添加處理函數

//src/redux/reducer.js

export const bookLongIntro = (state = {}, action) =>{
    switch (action.type){
        case ADD_BOOK_LONG_INTRO:
            let {bookIntro } = action;
            return { bookIntro }
        default:
            return state;
    }
}

//App.js 測試一下
store.dispatch(receiveBookLongIntro('57206c3539a913ad65d35c7b'));
//而後看打印日誌

測試詳細介紹

測試

  • 接下來要作的就是往本身寫的詳情頁面塞數據,相信你們都能作到。

bug 遇到一個點擊穿透的問題,當點擊自動補全的列表時,實際上會點到下面介紹列表。

bug

  • 猜想是由於選擇補全列表後移動設備有300ms延遲,在300ms內補全列表隱藏了因此就點擊到查詢列表項。
    試了好幾種解決辦法 發現不是什麼300ms的問題。由於經過router 的 history.push() 方法延遲跳轉後仍是會跳轉,感受就是直接點擊到上面的。

  • 不解決本身無法往下作了,耗時快兩天(內心惦記着她) 。無奈之下在input輸入框onfous的時候用一個加載層遮住下面的list使其不能點擊?

  • 若有其餘良策或者什麼緣由請必定告訴我,感激涕零。

給歷史記錄添加緩存

經過storejs (給localStorage 添加幾個操做方法,少了一次字符串和json轉換)添加緩存,並在App.js中啓動時候調用一次讀取上次的緩存。

修改了一下文件的存放

文件安裝模塊簡歷文件夾存放文件

添加章節列表

  • 添加action

  • 添加reducer

  • 編寫組件

//action
// 章節列表,須要先獲取書源信息
export const getChpters = id => dispatch => {
    dispatch(isShowLoading(true));
    let chapters = {};
    fetch(`/api/toc?view=summary&book=${id}`)
        .then(res => res.json())
        .then(data => {
           let sourceId =data[0]._id;
           for(let item of data){
               // 爲何要用他的 我也不知道 多是比較好拿
               if(item.source === 'shuhaha'){
                   sourceId = item._id;
               }
           }
            chapters.sourceId = sourceId;
            return fetch(`/api/toc/${sourceId}?view=chapters`)
        })
        .then(res => res.json())
        .then(data => {
            chapters.chapters = data;
            let action = dispatch(addChapters(chapters));
            dispatch(isShowLoading(false));
            return action;
        })
        .catch(error => {
            dispatch(isShowLoading(false));
            new Error(error);
        })

}
//redcer

//書籍章節列表
export const chaptersList = (state = {}, action) => {
    switch (action.type){
        case ADD_CHAPTERS_LIST:
            return action.chapters;
        default:
            return state;
    }
}

編寫功能模塊以前前都應該先寫好action和reducer 這樣能夠寫組件的時候肯定好方向。

閱讀頁面

  • 老規矩先編寫action,reducer

/*詳細閱讀*/
export const getReadDetail = url => dispatch => {
    if(url === '') return ;
    dispatch(isShowLoading(true));
    return fetch(`/chapter/${url}?k=2124b73d7e2e1945&t=1468223717`)
        .then(res => res.json())
        .then(data => {
            let action = dispatch(readDetail(data));
            dispatch(isShowLoading(false));
            return action;
        })
        .catch(err=> {
            dispatch(isShowLoading(false));
            new Error(err)
        });

}

經過connect 傳入的dispatch 方法觸發一個getReadDetailaction <font color=deepPink>dispatch(getReadDetailaction(url)) </font> redux會直接調用reducer函數改變state

//詳細閱讀
export const readDetail = (state = {}, action) => {
    switch (action.type){
        case ADD_READ_DETAIL:
            action.readObj && storejs.set('readDetail', action.readObj);
            return action.readObj.chapter;
        default:
            return state;
    }
}

觸發action後reduer函數改變state咱們的state就會增長一個和reduer處理函數名字同樣的屬性readDetail(這主要是combineReducers幫咱們簡化了一小部分,不懂須要去看看redux文檔)state下圖:

圖

  • 這圖中的readDetail 是action 請求到數據給reducer處理後的state

而後包裝咱們的組件

class ReadDetail extends Component {
    // ...
}

const mapStateToProps = (state) => ({
    readDetail:state.readDetail,
})

const mapDispatchToProps = (dispatch) => ({
    getReadDetail:(id)=> dispatch(getReadDetail(id))
})

export default connect(mapStateToProps,mapDispatchToProps)(ReadDetail);
  • 在須要的頁面直接調用便可 當state改變時候組件就會自動更新

目前界面爲下面的效果須要有修改背景顏色,改變字體大小等功能能夠考慮一下怎麼實現。

圖

涉及到的知識點

你們看到這裏改給個小星星了? ㊗️你們的代碼沒有bug,擼生中沒有改需求。

相關文章
相關標籤/搜索