这几天女儿经历了跳舞海选通过初试然后又没通过复选的故事. 然后在顾村的新家也整理好了, 老婆还突然让我去买了一台NS, 结果又为ns配了一个手柄500多, 花钱真的快啊.

昨天早上上班的路上, 镇坪路地铁站里, 看到一个女生靠着柱子坐着, 脸埋在包里, 不知道她此刻的心情, 不过没有过不去的坎, 少女也请继续加油吧.

上个星期还去看了一次牙, 下周一又要再去补, 已经是今年坏的第四颗牙了, 是不是身体的零部件要迎来大保养的时刻了.

  1. setState的异步更新
  2. React的生命周期方法
  3. 新项目
  4. 路由相关组件
  5. 用路由来修改项目

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确实还是比较清晰的, 下边来看看具体的商品页面如何编制.