初學者用來作練習很不錯,由於我就是。javascript
看見ShanaMaid寫了一個react讀書app, 本身借用API練習一下,記錄練習過程。https://github.com/fygethub/s...css
經過create-react-app建立初始環境, 安裝material UI庫, 按照material官網描述修改webpack配置按需加載。詳細參照material-uijava
一、在src 文件下新建components文件夾在當前文件夾下面編寫組件。
二、在src 下新建source文件存放字體圖片等資源node
新建一個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
.eslintrc
配置文件配置 eslint經過運行<font color=deepPink >npm run eject
</font>使其暴露webpack等配置文件es6
上述步驟並無暴露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>
書籍詳情頁
查詢列表頁
目前書籍搜索頁面佈局好了之後開始添加功能,不知不覺本身的文件就變得多了。
這裏普及一下生成圖形目錄的工具 用的是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有獲取書籍列表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;
看到咱們的控制檯發現有個小警告說閉合標籤前面須要有一個空格 果斷跑去加一個 ;
在看一次咱們的請求都發出去了,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 {}
//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')); //而後看打印日誌
接下來要作的就是往本身寫的詳情頁面塞數據,相信你們都能作到。
猜想是由於選擇補全列表後移動設備有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,擼生中沒有改需求。