react-bookstore
线上地址:https://react-bookstore.herokuapp.com
github地址:https://github.com/yuwanlin/react-bookstore
学习了react相关技术,需要贯通一下。所以有了这个。会持续更新。项目不复杂,但我本来就是来练手的。我觉得达到了练手的效果。包括redux/react-redux的使用,以及使用redux-thunk处理异步请求,并且异步中还尝试使用了async/await。当然最重要的是实践了react-router。由于起步较晚,之前并没有使用了2.x,3.x的版本,所以看了v4的文档和官方demo后直接上手的。在浏览器中主要使用的是react-router-dom,很好的实践了其中的组件。包括其中注册登录的弹出框(而不是在新的页面)就是参照官方文档的。我看的是中文翻译后的文档,再次感谢翻译人员。除此之外,我还结合了github上react-router项目的文档这里关于某些部分的解释更为详细。
- 使用create-react-app创建的react程序。省去了相对繁琐的webpack配置。
- 技术栈包括react + redux + react-redux + redux-thunk + react-router(v4,在浏览器端主要使用的是react-router-dom)
React.PropTypes
已经弃用了,现在使用的库是prop-types
(import PropTypes from 'prop-types';
),当然这个小项目中没有用到属性验证(懒)。
使用方法
dev为开发分支,master为主分支(用于功能展示)。
git clone https://github.com/yuwanlin/react-bookstore.git
cd react-bookstore
yarn # or npm install
浏览器地址栏:
localhost:3000
react-router-dom
中文文档
在这个项目中使用了:
组件:BrowserRouter
、Link
、Redirect
、Route
、Switch
、 withRouter
API:history
、match
、location
。
在浏览器环境下,这些知识足够了。
BrowserRouter
<Router>
使用 HTML5 提供的 history API (pushState,replaceState 和 popstate 事件) 来保持 UI 和 URL 的同步。现代浏览器都支持H5的history API。在传统的浏览器中才是用HashRouter。
import { BrowserRouter } from 'react-router-dom'
<BrowserRouter basename={optionalString} forceRefresh={optionalBool} getUserConfirmation={optionalFunc} keyLength={optionalNumber} >
<App/>
</BrowserRouter>
在我的项目中src/index.js
,我使用的是
<Route component={App}/>
而不是直接的App组件。相同点是因为无论什么location,都会匹配到App组件,因为这里的Route没有明确的path。不同点是因为Route导航的组件的props中会默认有match、location、history参数。而我在App组件中就使用了location来判断模态框。
Link
Link是一个链接(实际上一个a标签),用来跳转到相应的路由。其中的to
属性可以是string或者object。
<Link to="/courses"/> <Link to={{ pathname: '/courses',search: '?sort=name',hash: '#the-hash',state: { fromDashboard: true } }}/>
其中pathname可以通过location.pathname或者match.URL得到。search可以通过location.search获取到。hash通过location.hash获取到。
Link还有个replace
属性,表示使用Link的页面代替现有页面而不是将新页面添加到history栈(即history.replace而不是history.push)。
Redirect
表示重定向到一个新的页面。默认是使用新页面代替旧页面。如果加上push
属性表示将新页面添加到history栈。
<Redirect path to='/courses'/>
其中to
属性和Link的to属性是一样的。Redirect还有一个from
属性,用来Switch组件中重定向。
Route
Route或许是最重要的组件了。它定义了路由对应的组件。它的path
属性和Link组件的to属性是相对应的。
import { BrowserRouter as Router,Route } from 'react-router-dom'
<Router>
<div>
<Route exact path="/" component={Home}/>
<Route path="/news" component={NewsFeed}/>
</div>
</Router>
exact
属性表示Route值匹配和path一样的URL。而不包括其二级目录。比如/book
可以匹配到/book/abc
。
path | location.pathname | matches? |
---|---|---|
/one/ | /one | no |
/one/ | /one/ | yes |
/one/ | /one/two | yes |
path | location.pathname | matches? |
---|---|---|
/one | /one | yes |
/one | /one/ | no |
/one | /one/two | no |
然后就是渲染对应组件的三种方式:component
,render
,children
。这三种渲染方法都会获得相同的三个属性。分别是match、location、history。
component
: 如果你使用component(而不是像下面这样使用render),路由会根据指定的组件使用React.createElement来创建一个新的React element。这就意味着如果你提供的是一个内联的函数的话会带来很多意料之外的重新挂载。所以,对于内联渲染,要使用render属性(如下所示)。
<Route path="/user/:username" component={User}/>
const User = ({ match }) => {
return <h1>Hello {match.params.username}!</h1>
}
render
: 使用render属性,你可以选择传一个在地址匹配时被调用的函数,而不是像使用component属性那样得到一个新创建的React element。使用render属性会获得跟使用component属性一样的route props。
// 便捷的行内渲染
<Route path="/home" render={() => <div>Home</div>}/>
// 包装/合成
const FadingRoute = ({ component: Component,...rest }) => (
<Route {...rest} render={props => (
<FadeIn>
<Component {...props}/>
</FadeIn>
)}/>
)
<FadingRoute path="/cool" component={Something}/>
警告: <Route component>
的优先级要比<Route render>
高,所以不要在同一个 <Route>
中同时使用这两个属性。
children
: 有时候你可能想不管地址是否匹配都渲染一些内容,这种情况你可以使用children属性。它与render属性的工作方式基本一样,除了它是不管地址匹配与否都会被调用。
除了在路径不匹配URL时match的值为null之外,children渲染属性会获得与component和render一样的route props。这就允许你根据是否匹配路由来动态地调整UI了,来看这个例子,如果理由匹配的话就添加一个active类:
<Route children={({ match,...rest }) => (
{/* Animate总会被渲染,所以你可以使用生命周期来使它的子组件出现
或者隐藏
*/}
<Animate>
{match && <Something {...rest}/>}
</Animate>
)}/>
警告: <Route component>
和<Route render>
的优先级都比<Route children>
高,所以在同一个<Route>
中不要同时使用一个以上的属性.
Switch
无论如何,它最多只会渲染一个路由。或者是Route的,或者是Redirect的。考虑以下代码:
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
如果Route不在Switch组件中,那么当URL是/about时,这三个路由组件都会渲染。其中第二个Route中,通过match.params.user可以获取URL–about。这种设计,允许我们以多种方式将多个 组合到我们的应用程序中,例如侧栏(sidebars),面包屑(breadcrumbs),bootstrap tabs等等。 然而,偶尔我们只想选择一个<Route>
来渲染。如果我们现在处于 /about ,我们也不希望匹配 /:user (或者显示我们的 “404” 页面 )。以下是使用 Switch 的方法来实现:
<Switch>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
</Switch>
现在,只有第一个路由组件会渲染了。其缘由是Switch仅仅渲染一个路由。
对于当前地址(location),Route组件使用path匹配,Redirect组件使用from匹配。所以对于没有path的Route组件或者没有from的Redirect组件,总是可以匹配所有的地址。这一重大的应用当然就是404了。。
withRouter
前面提到,使用Route组件匹配到的组件总是可以获得match、location、history属性。然后,对于一个普通的组件,有时也需要相关数据。这时,就可以使用到withRouter了。
const CommonComponent = ({match,location,history}) => null
const CommonComponent2 = withRouter(CommonComponent);
另外,有时候URL改变可能组件没有重载,这时因为组件可能检查到它的属性没有变。同理,使用withRoute,当URL改变,组件的this.props.location一定会改变,这样就可以使组件重载了。
match
- params -( object 类型)即路径参数,通过解析URL中动态的部分获得的键值对。
- isExact - 当为 true 时,整个URL都需要匹配。
- path -( string 类型)用来做匹配的路径格式。在需要嵌套 的时候用到。
- url -( string 类型)URL匹配的部分,在需要嵌套 的时候会用到。
地址栏: /user/real
<Route path="/user/:user">
此时:match如下:
{
params: { user: "real"}
isExact: true,path: "/user/:user",url: "user/real"
}
location
location 是指你当前的位置,下一步打算去的位置,或是你之前所在的位置,形式大概就像这样:
地址栏:/user/real?q=abc#sunny
{
key: 'ac3df4',// 在使用 hashHistory 时,没有key。值不一定
pathname: '/user/real'
search: '?q=abc',hash: '#sunny',state: undefined
}
在react-router中,可以在下列环境使用location。
- Web Link to
- Native Link to
- Redirect to
- history.push
- history.replace
通常,我们只需要使用字符串表示location,如下:
<Link to="/user/:user">
使用对象形式可以表达更多的信息。如果需要从一个页面传递数据到另一个页面(除了URL相关数据),使用state是一个好方法。
<Link to={{ pathname: '/user/real',state: { data: 'your data'} }}>
我们也可以通过history.location获取location对象,但是不要这样做,因为history是可变的。而location是不可变的(URL发生变化location一定变化)。
class Comp extends React.Component {
componentWillReceiveProps(nextProps) {
// locationChanged 变量为 true
const locationChanged = nextProps.location !== this.props.location
// 不正确,locationChanged 变量会 *永远* 为 false ,因为 history 是可变的(mutable)。
const locationChanged = nextProps.history.location !== this.props.history.location
}
}
history
- 「browser history」 - history 在 DOM 上的实现,经常使用于支持 HTML5 history API 的浏览器端。
- 「hash history」 - history 在 DOM 上的实现,经常使用于旧版本浏览器端。
- 「memory history」 - 一种存储于内存的 history 实现,经常用于测试或是非 DOM 环境(例如 React Native)。
length -( number 类型)指的是 history 堆栈的数量。
action -( string 类型)指的是当前的动作(action),例如 PUSH,REPLACE 以及 POP 。
location -( object类型)是指当前的位置(location),location 会具有如下属性:
- pathname -( string 类型)URL路径。
- search -( string 类型)URL中的查询字符串(query string)。
- hash -( string 类型)URL的 hash 分段。
- state -( string 类型)是指 location 中的状态,例如在 push(path,state) 时,state会描述什么时候 location 被放置到堆栈中等信息。这个 state 只会出现在 browser history 和 memory history 的环境里。push(path,[state]) -( function 类型)在 hisotry 堆栈顶加入一个新的条目。
replace(path,[state]) -( function 类型)替换在 history 堆栈中的当前条目。
go(n) -( function 类型)将 history 对战中的指针向前移动 n 。
goBack() -( function 类型)等同于 go(-1) 。
goForward() -( function 类型)等同于 go(1) 。
block(prompt) -( function 类型)阻止跳转,(请参照 history 文档)。
react
react组件的生命周期中主要用到的有componentDidMount
,componentWillReceiveProps
、componentWillUpdate
、render
。
render
render方法自然不必多说,当组件的state或者props改变的时候组件会重新渲染。
componentDidMount
这个方法在组件的声明周期中只会执行一次,这代表组件已经挂载了。所以在此方法中可以进行dom操作,异步请求(比如我用的dispatch,action中使用redux-thunk处理的异步请求)等。
componentWillReceiveProps
这个方法也是常用的,它接受一个参数nextProps
。在一些组件中我使用了react-redux的connect方法,并获取state中的某些数据映射到组件的属性。对于一些数据,比如在BookDetail组件中,const { bookDetail,history } = nextProps;
,bookDetail一开始是没有的,我在componentDidMount
方法中dispatch(getSomeBook(bookId));
,这是一个异步请求,然后请求豆瓣数据,改变state,再映射到组件的属性(mapStateToProps
),这样组件会再次渲染,并且bookDetail属性也有了实际的内容。通常使用nextProps和当前的this.props某些属性做对比,然后决定下一步该怎么做。
componentWillUpdate
这个方法接受两个参数,分别是nextProps
,nextState
。这是在组件确认需要更新之后执行的方法。在App组件中,为了记住组件的上一个location,就是在这个声明周期这种进行的。这个函数中不可以更新props或者state。如果需要更新,应该在componentWillReceiveProps
方法中进行更新。
redux
redux提供的api主要有applyMiddleware
,bindActionCreators
,compose
,combineReducers
,createStore
。
applyMiddleware
接受中间件。对于多个中间价,可以使用...
扩展运算符。
const middlewares = []; applyMiddleware(...middlewares)
bindActionCreators
这个函数自带dispatch。可能在mapDispatchToProps总会用到。
import actions as * from '../actions/index.js';
const mapDispatchToProps = (dispatch) => bindActionCreators(actions,dispatch);
compose
有时候,项目中已经引入了一些middleware或别的store enhancer(applyMiddleware的结果本身就是store enhancer),如下:
const store = createStore( reducer,preloadState,applyMiddleware(...middleware) )
这时候,需要将现有的enhancer与window.devToolsExtension()组合后传入,组合可以使用redux提供的辅助方法compose。
import { createStore,compose,applyMiddleware } from 'redux';
const store = createStore(
reducer,compose(
applyMiddleware(...middleware),window.devToolsExtension ? window.devToolsExtension() : f => f
)
)
compose的效果很简单:compose(a,b)的行为等价于(…args) => a(b(…args))。即从右向左执行,并将右边函数的返回值作为它左边函数的参数。如果window.devToolsExtension
不存在,其行为等价于compose(a,f=>f),等价于a(f=>f输入什么参数就返回什么参数)。
combineReducers
将多个小的reducer合并成一个。由于redux中state只有一个,所以每个小的reducer都是state的一个属性。
createStore
上面已经介绍过。
react-redux
react-redux主要提供两个接口。Provider
和connect
。
Provider
顾名思义,Provider的主要作用是“provide”。Provider的角色是store的提供者。一般情况下,把原有组件树根节点包裹在Provider中,这样整个组件树上的节点都可以通过connect获取store。
<Provider store={store}>
<App />
</Provider>
connect
connect用来“连接”组件与store。它的形式如下:
connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])
-
[mapStateToProps(state,[ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件接收到新的 props,mapStateToProps 也会被调用。
-
[mapDispatchToProps(dispatch,[ownProps]): dispatchProps] (Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,而且这个对象会与 Redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。如果传递的是一个函数,该函数将接收一个 dispatch 函数,然后由你来决定如何返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起(提示:你也许会用到 Redux 的辅助函数 bindActionCreators())。如果你省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到你的组件 props 中。如果指定了该回调函数中第二个参数 ownProps,该参数的值为传递到组件的 props,而且只要组件接收到新 props,mapDispatchToProps 也会被调用。
-
[mergeProps(stateProps,dispatchProps,ownProps): props] (Function): 如果指定了这个参数,mapStateToProps() 与 mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中。该回调函数返回的对象将作为 props 传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props 来筛选部分的 state 数据,或者把 props 中的某个特定变量与 action creator 绑定在一起。如果你省略这个参数,默认情况下返回 Object.assign({},ownProps,stateProps,dispatchProps) 的结果。
-
[options] (Object) 如果指定这个参数,可以定制 connector 的行为。
- [pure = true] (Boolean): 如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。
- [withRef = false] (Boolean): 如果为 true,connector 会保存一个对被包装组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false
部署
项目地址:https://react-bookstore.herokuapp.com
- 首先下载heroku cli
https://devcenter.heroku.com/articles/getting-started-with-nodejs#set-up - heroku login
- heroku config:set NPM_CONFIG_PRODUCTION=false
- git push heroku master
- heroku open
目前已完成的功能
1.路由和搜索
2.分页
5月9日更新(接下来这个项目不知道往哪写了,有没有老铁们star一下)
注册:在reduces中使用state.user.users保存用户的用户名。注册时做了验证。
登录:查看state.user.users查看是否匹配。
5月14日更新
4.商品详情页
商品详情:这些数据不是从路由传过来的(当然从路由传过来也可以,通过history.push(URL[,state])的第二个参数),而是请求豆瓣接口的。所以再次打开页面还是可以看到数据的。
6.查看购物车
感觉达到了练手的效果,剩下的就没写了。