这几天女儿经历了跳舞海选通过初试然后又没通过复选的故事. 然后在顾村的新家也整理好了, 老婆还突然让我去买了一台NS, 结果又为ns配了一个手柄500多, 花钱真的快啊.
昨天早上上班的路上, 镇坪路地铁站里, 看到一个女生靠着柱子坐着, 脸埋在包里, 不知道她此刻的心情, 不过没有过不去的坎, 少女也请继续加油吧.
上个星期还去看了一次牙, 下周一又要再去补, 已经是今年坏的第四颗牙了, 是不是身体的零部件要迎来大保养的时刻了.
setState的异步更新
之前使用了setState命令, 实际上这个命令是异步更新的, 并不是直接就能更新成功. 想要在更新成功后进行一些操作, 需要在第二个参数位置传入回调函数.
看一个最简单的, 默认创建的React App, 然后在state中添加一个couter变量, 用一个button来自每次将其增加1:
import React from 'react'; import logo from './logo.svg'; import './App.css'; class App extends React.Component { constructor() { super(); this.state ={ counter: 1 } } handleClick = ()=>{ this.setState({counter: this.state.counter + 1}) } render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo"/> <p> {this.state.counter} </p> <button onClick={this.handleClick}>increase</button> </header> </div> ); } } export default App;
这里如果修改一下, 在setState命令之后立刻打印出当前的state.counter:
handleClick = ()=>{ this.setState({counter: this.state.counter + 1}); console.log(this.state.counter) }
可以看到, 其打印出来的是原来的 state , 这是因为setState方法是一个异步方法, 并不是立刻就更新, 而是将更新权限交给react.
如果想要看到更新后的结果, 就必须传入回调函数:
handleClick = ()=>{
this.setState({counter: this.state.counter + 1}, () => console.log(this.state.counter));
}
不过, 这里的红字部分, 未必能够读取到更新的结果, 加入还有其他的state需要更新, 因此这里最好是使用一个函数:
handleClick = ()=>{
this.setState((prevState, prevProp) => {
return {counter: prevState.counter + 1}
}, () => console.log(this.state.counter));
}
有了这个函数, 就能保证每次更新完state之后, 获取, 再显示.
props也是类似, 可以传递一个prop给组件:
<App increment={1} />
然后可以修改如下:
handleClick = ()=>{
this.setState((prevState, prevProp) => {
return {counter: prevState.counter + prevProp.increment}
}, () => console.log(this.state.counter));
}
搞清楚这个异步更新, 以及如何使用更新之前的值, 就可以方便的确保state不出现问题.
React的生命周期方法
在这里有React生命周期方法的图表可供参考,
这个图表在git上也有.
生命周期可以分为三大块, 第一块是Mounting, 也就是创建组件并挂载, 这个时候会调用构造器, 之后不会再调用.
Mounting会先render(), 即生成元素, 然后更新DOM, 之后就会调用componentDidMount()方法.
第二块是更新, Mounting和Updating的整个过程里都会用到render()方法, render()完成之后调用componentDidUpdate()方法.
最后是Unmounting, 会调用componentWillUnmount()方法.
看一个例子, 用两个按钮控制一个组件显示或者不显示, 以及更新组件的内容:
import React from 'react'; import logo from './logo.svg'; import './App.css'; import {Lifecycles} from './lifecycles.component' class App extends React.Component { constructor() { super(); this.state ={ showChild: true, text: '' } } handleClick = () => { this.setState((prevState, prevProp) => { return {showChild: !prevState.showChild}; }) }; render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo"/> <button onClick={this.handleClick}>Toggle Lifecycles</button> <button onClick={() => { this.setState(state => ({text: state.text + "_hello"})) }}>Update Text </button> {this.state.showChild ? <Lifecycles text={this.state.text} /> : null} </header> </div> ); } } export default App;
关键是这个Lifecycles组件, 在其中可以覆盖各种生命周期方法:
import React from "react"; export class Lifecycles extends React.Component { constructor(props) { super(props); console.log("constructor") this.props = props; } componentDidMount() { console.log("componentDidMount"); } componentDidUpdate(prevProps, prevState, snapshot) { console.log("componentDidUpdate"); } componentWillUnmount() { console.log("componentWillUnmount"); } shouldComponentUpdate(nextProps, nextState, nextContext) { console.log("shouldComponentUpdate"); return true; } render() { return ( <h2>{this.props.text}</h2> ) } }
运行app之后, 由于showChild是true, 可以看到控制台打印出了:
constructor componentDidMount
然后点击Update Text, 可以发现每次更新后, 控制台打印出:
shouldComponentUpdate componentDidUpdate
如果切换showChild, 会发现触发unmount, 这是因为组件直接就被从DOM中删除了.
再切换, 构造器又会运行, 组件又会被渲染到DOM中.
shouldComponentUpdate()是一个判断函数, 用于判断这个组件是否需要更新, 有三个参数, 就是即将要被更新的props, state以及上下文.
所以可以使用一个判断, 返回要被更新的props是否等于原来的props之类.
新项目
课程接下来要写一个新的项目, 使用到SASS样式设置, 新创建一个项目, app.js中只返回一个div, 其他都删除掉, 这样就有了一个空项目. app.css中的样式也不要, div上的类名也去掉.
然后是主页组件, 菜单部分分成了各个, 这个就跟着视频做就可以了, 创建组件homepage.component.jsx, CSS方面则是要使用sass,
官方网站是https://sass-lang.com, 执行 npm add node-sass
即可将sass添加进来, 之后需要重新启动项目以获得sass支持.
重新启动之后, 就可以来使用sass了, 创建homepage.styles.css:
.homepage { display: flex; flex-direction: column; align-items: center; padding: 20px 80px; } .directory-menu { width: 100%; display: flex; flex-wrap: wrap; justify-content: space-between; } .menu-item { min-width: 30%; height: 240px; flex: 1 1 auto; display: flex; align-items: center; justify-content: center; border: 1px solid black; margin: 0 7.5px 15px; &:first-child { margin-right: 7.5px; } &:last-child { margin-left: 7.5px; } .content { height: 90px; padding: 0 25px; display: flex; flex-direction: column; align-items: center; justify-content: center; border: 1px solid black; .title { font-weight: bold; margin-bottom: 6px; font-size: 22px; color: #4a4a4a; } .subtitle { font-weight: lighter; font-size: 16px; } } }
scss中, &表示上一级选择器, SCSS即是SASS的新语法,是Sassy CSS的简写,是CSS3语法的超集. 选择器中套选择器比如第二个红字部分, 其实就相当于两个选择器加上空格.
这里还是要注意CSS的display:flex和grid的使用, 然后一层一层把组件拆分, 结构是index.js在#root上挂载app组件->HomePage组件->Directory组件->Menu-Item组件.
HomePage组件就使用display:flex让中间所有组件对齐并且居中, Directory组件也是flex, 最后是每个Item, 中间包含一个背景图片, 一个标题和一个小区域, 也是flex并居中.
CSS这玩意还真得好好看看, 每次用的是很都要现想, 没办法, 谁让这东西是黑魔法呢.
路由相关组件
单页应用最大的问题就是在切换URL的时候, 如何拦截并且渲染, 这个是最重要的一环.
路由其实是使用浏览器的History API来实现的. 对于前端来说, 其实就是将URL与组件绑定起来, 显示什么样的URL, 就使用什么样的组件来进行渲染.
React和React-dom本身不支持路由功能, 仅仅是一个UI组件, 不过像Vue一样, React家族还有react-router-dom来实现路由功能:
npm add react-router-dom
安装好之后, 在index.js, 也就是最外边的js文件处引入:
import {BrowserRouter} from 'react-router-dom'
这个东西是一个组件, 可以用来包裹其他的组件, 其中的组件就会获得路由功能. 这里就来包裹App组件:
ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter> , document.getElementById('root'));
然后在App.js中, 进行如下操作:
import {Route} from 'react-router-dom'; function App() { return ( <div> <Route exact path='/' component = {HomePage}/> </div> ); }
实际上, 就是有Route组件来取代了原来直接渲染的HomePage组件. Route组件的path就是匹配的路径, exact可以等于true(默认)或者false, 表示精确匹配与否.
现在重新启动服务器, 可以看到页面依然正常显示. 但是如果输入一些URL, 就会发现页面变空, 只有一个空白的div标签.
可以再添加一个东西来看, 就会明白多了. 在app.js中追加一个函数:
const HatsPage = ()=>( <div> <h1>HATS PAGE</h1> </div> )
然后为这个函数增加一条路径:
<Route exact path='/' component = {HomePage}/> <Route exact path='/' component = {HatsPage}/>
再到首页, 可以看到先后把两个都渲染出来了, 所以这里就可以来区分开了, 改成如下:
<Route exact path='/' component = {HomePage}/> <Route exact path='/hats' component = {HatsPage}/>
这样就可以访问根路径和/hats路径了. 详细的Router文档在https://reactrouter.com/web/guides/quick-start
然后是Switch组件, 说道Switch, 上周五(8月21号买了个NS)…老婆要招待朋友们家里玩, Hmmmm…..
Switch组件用来包裹Route组件, 假如Route组件是这样:
<Route path='/' component = {HomePage}/> <Route path='/hats' component = {HatsPage}/>
由于都不是精确匹配, 访问根路径的时候, 会将两个组件都渲染出来, 但是用Switch包裹之后, 就只会渲染匹配的到的第一个组件.
可以改成这样:
<Switch> <Route exact path='/' component = {HomePage}/> <Route path='/hats' component = {HatsPage}/> </Switch>
或者:
<Switch> <Route path='/hats' component = {HatsPage}/> <Route path='/' component = {HomePage}/> </Switch>
然后是传递参数:
<Route path='/hats/:hatsId' component = {HatsPage}/>
对于这种带参数的URL, 其实需要在函数中传入props, 然后打印出来可以发现其中的match.params, 就是参数名称和对应的值, 通过这个就可以进行处理了, 比如:
const HatsPage = (props)=>{ return( <div> <h1>HATS PAGE</h1> <p>This is No.{props.match.params.hatsId} hat.</p> </div> )}
这样进行访问网页其实也是可以的, 比较方便.
Link则是生成一个链接, 指向指定的路径: <Link to='/hats/11'>No.11</Link>
, 有了这个东西之后, 就可以动态拼接URL, 比如:
const HatsPage = (props) => { let path = props.match.path.split(':')[0]; const lastPage = parseInt(props.match.params.hatsId) return ( <div> <h1>HATS PAGE</h1> <p>This is No.{props.match.params.hatsId} hat.</p> <Link to={`${path}${lastPage+1}`} >Next</Link> </div> ) };
这个渲染就可以动态的拼接出下一个链接, 当然, 这还要看后端是否存在下一个链接, 中间还需要使用其他的东西.
链接还有一种方式是使用props.history, 使用.push()方法, 传入路径即可, 这个方法需要使用事件.
用路由来修改项目
其核心逻辑就是把每个 SHOPNOW 所在的地方都加上链接, 要分如下几步, 首先在app.js添加上:
function App() { return ( <div> <Route exact path='/' component = {HomePage}/> </div> ); }
之后比较方便的做法是, 在数据中给每个类别添加上url, 如下:
this.state = {
sections: [
{
title: 'hats',
imageUrl: 'https://i.ibb.co/cvpntL1/hats.png',
id: 1,
linkUrl: 'hats'
},
]
};
其他几个项目还没有制作, 不过可以以后再来添加, 因为要渲染的页面其实也是组件, 做好一个之后可以复用.
然后就是把linkUrl传给menu-item, 这里为了能直接使用history对象, 而不是浏览器的history对象, 需要添加一个导入, 然后在导出的地方用这个包裹导出对象即可 :
import {withRouter} from 'react-router-dom'; render() { return ( <div className='directory-menu'> {this.state.sections.map(({ title, imageUrl, id, size, linkUrl }) => ( <MenuItem key={id} title={title} imageUrl={imageUrl} size={size} linkUrl={linkUrl} /> ))} </div> ); } export default withRouter(MenuItem);
在menu-item组件中,添加一个事件给当中的小框, 使用history对象即可:
const MenuItem = ({ title, imageUrl, size, linkUrl, history, match }) => ( <div className={`${size} menu-item`} onClick={()=>history.push(`${linkUrl}`)}> <div className='background-image' style={{ backgroundImage: `url(${imageUrl})` }} /> <div className='content'> <h1 className='title'>{title.toUpperCase()}</h1> <span className='subtitle'>SHOP NOW</span> </div> </div> );
记得index.js中, 要用BrowserRouter组件来包裹App组件, 这样就做好了首页的链接功能.
看到这里, 我也明白了, 像url这种东西, 还是写在数据组件里, 一次设置好, 然后逐层向下分发就行了.
现在路由的基本用法掌握了, React确实还是比较清晰的, 下边来看看具体的商品页面如何编制.