React App – Router and LC

这部分学习多页面之间的路径导航配置和组件的生命周期。

创建程序
$ mkdir 02_router
$ cd 02_router
$ create-react-app .
$ npm install react-router-dom –save
删除 index.js 和 index.html 外 public 和 src 中其他文件。
其中,index.js 的文件内容也需要清空,并更新为如下:

import React from 'react';
import ReactDOM from 'react-dom';

 const App = () => {
   return (
     <div>Home</div>
   )
 }

 ReactDOM.render(
  <App/>,
  document.querySelector('#root')
)

以上只是创建了组件的占位符和基本结构代码。

组件结构
创建组件目录src\components ,添加如下三个组件文件:
home.js posts.js profiles.js 使用如下的初始代码:

import React from 'react';

const Profiles = () => {
    return (
        <div>
            Profiles
        </div>
    )
}

export default Profiles;

以上是 Profile 的代码,Home 和 Posts 修改相应文字。

加入导航
更新 index.js 文件,导入 router 模块和其他组件:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';

// COMPONENTS
import Home from './components/home';
import Posts from './components/posts';
import Profile from './components/profile';

 const App = () => {
   return (
     <BrowserRouter>
      <div>
        <header>
          Header
        </header>
        <Route path="/" exact component={Home}/>
        <Route path="/posts" component={Posts}/>
        <Route path="/profile" component={Profile}/>
      </div>
     </BrowserRouter>
   )
 }

 ReactDOM.render(
  <App/>,
  document.querySelector('#root')
)

组件 BrowserRouter 和 Route 分别作容器和导航之用。
类似 return 方法,容器只能包含一个 HTML tag 对象。
导航组件 exact 属性强制路径匹配,可删掉查看效果。
在导航容器中加入的 header 组件会出现在所有页面上。

除了 BrowserRouter 还有 HashRouterMemoryRouter
前者在 URL 中添加 # 标记,后者则除根路径不显示具体路径。
可以直接替换以上代码中的 BrowserRouter 查看结果。

最后,在浏览器地址栏输入以下地址查看结果:

  • http://localhost:3000/
  • http://localhost:3000/posts
  • http://localhost:3000/profile

路径选择
以上 Route 代码中,添加 exact 是为实现精确的路径匹配。
否则 path="/posts" 也匹配 path="/",组件将重复显示。
这里可用的解决方案除了 exact 属性还有 Switch 组件:

…
import { BrowserRouter, Route, 
  NavLink, Switch } from 'react-router-dom';
…
        <Switch>
          <Route path="/posts/:id/:username" component={PostItem}/>
          <Route path="/profile" component={Profile}/>
          <Route path="/posts" component={Posts}/>
          <Route path="/" component={Home}/>
        </Switch>
…

注意 Switch 组件中的 Route 顺序,精细靠前模糊靠后。
因为 Switch 只返回第一个匹配的 Route,选项顺序重要。

创建链接
通过 react-router-dom 的 Link 模块,可轻松创建链接。
更新 index.js 中的如下语句:

…
import { BrowserRouter, Route, 
  Link } from 'react-router-dom';
…
        <header>
          <Link to="/">Home</Link><br/>
          <Link to="/posts">Posts</Link><br/>
          <Link to={{
            pathname:'/profile',
            hash: '#thehash',
            search: '?profile=true'
          }}>Profile</Link><br/>
          <hr/>
        </header>
…

Link 的语法很简单,有趣的是可添加 URL 参数。
分别打开链接查看结果,注意 profile 页面路径的参数。

除了 Link 还有 NavLink 可用,提供了更多功能,如:

          <NavLink 
            to="/posts"
            activeStyle={{color:'red'}}
            activeClassName="selected"
          >Posts</NavLink><br/>

以上为到 Posts 的链接设定选中时的 CSS 类和颜色样式。

动态参数
一般 URL 路径中存在 id 等动态参数,这里演示如下。
创建 post_item.js 组件文件,用于单个日志:

import React from 'react';

const PostItem = (props) => {
    console.log(props);
    let params = props.match.params;
    return (
        <div>
            {params.id} - {params.username}
        </div>
    )
}

export default PostItem;

以上接收来自 index 的 props,显示 id 和 username
控制台日志显示了 props 中传递的可用参数信息。

更新 index.js 代码的如下有关 PostItem 的部分:

…
// COMPONENTS
import Home from './components/home';
import Posts from './components/posts';
import Profile from './components/profile';
import PostItem from './components/post_item';
…
        </header>
        <Route path="/" exact component={Home}/>
        <Route path="/posts" exact component={Posts}/>
        <Route path="/posts/:id/:username" component={PostItem}/>
        <Route path="/profile" component={Profile}/>
      </div>

注意 Router 中对 id 和 username 的声明传递了 props

作为从 props 提取路径的另一个样例,更新 profile.js

import React from 'react';
import { Link } from 'react-router-dom';

const Profile = (props) => {
    return (
        <div>
            <Link to={{pathname: `${props.match.url}/posts`}}>
                take me to /profile/posts
            </Link>
        </div>
    )
}

export default Profile;

以上代码调用了 props 中的部分信息构造了一个 URL 链接。
其中 props.match.url 中保存的就是当前 Profile 页面地址。
构造的 /profile/posts 地址并不存在,点击也没有反应。

重定向页
继续上面的话题,有时候可能需要对页面做重定向处理。
例如,将上节中无效的 URL 地址重定向到有效的地址。
更新 index.js

…
import { BrowserRouter, Route, NavLink, 
  Switch, Redirect } from 'react-router-dom';
…
        <Switch>
          <Redirect from="/profile/posts" to="/posts"/>
          <Route path="/posts/:id/:username" exact component={PostItem}/>
          <Route path="/profile" component={Profile}/>
          <Route path="/posts" component={Posts}/>
          <Route path="/" exact component={Home}/>
          <Route render={()=> <h3> Page not found - 404</h3>}/>
          {/* <Route component={notFound}/> */}
        </Switch>
…

以上更新导入了 Redirect 模块功能,在 Swtich 中添加:
从无效 /profile/posts 到有效 /posts 的重定向
此外,所有根目录以上的无效 URL 会导航到 404 页面。
而更友好的备选方式是创建一个 notFound 组件。

注意,以上更新重新为根页面添加了 exact 属性。
其他没有添加 exact 属性的无效页面将默认匹配成功。

  • 也就是说,/asdf 将触发 404 页面。
  • /posts/asdf 将不会触发 404 页面。

另一种重定向的方式是直接修改 props 中的 history 值。
更新 profile.js,实现从 Profile 到 Home 的默认跳转:

import React from 'react';
import { Link } from 'react-router-dom';

const Profile = (props) => {
    //console.log(props);
    const redir = () => {
        props.history.push('/')
    }
    return (
        <div>
            <Link to={{pathname: `${props.match.url}/posts`}}>
                take me to /profile/posts
            </Link>
            {redir()}
        </div>
    )
}
export default Profile;

以上定义的 redir() 方法将 ‘/’ 添加到 props 的 history
该 Profile 页面打开时,redir() 方法自动定向页面到 /

生命周期
创建 lifecycle.js 组件初始文件:

import React, { Component } from 'react';

class Life extends Component {
    render() {
        return (
            <div>
                <h3>Life Cycles</h3>
            </div>
        )
    }
}

export default Life;

更新 index.js 文件,导入并建立 Life 的链接和导航:

// COMPONENTS
import Home from './components/home';
import Posts from './components/posts';
import Profile from './components/profile';
import PostItem from './components/post_item';
import Life from './components/lifecycle';

 const App = () => {
   return (
     <BrowserRouter>
      <div>
        <header>
          <NavLink to="/">Home</NavLink><br/>
          <NavLink 
            to="/posts"
            activeStyle={{color:'red'}}
            activeClassName="selected"
          >Posts</NavLink><br/>
          <NavLink to={{
            pathname:'/profile',
          }}>Profile</NavLink><br/>
          <NavLink to="/life">Life</NavLink>
          <hr/>
        </header>
        <Switch>
          <Redirect from="/profile/posts" to="/posts"/>
          <Route path="/posts/:id/:username" exact component={PostItem}/>
          <Route path="/profile" component={Profile}/>
          <Route path="/posts" component={Posts}/>
          <Route path="/life" component={Life}/>
          <Route path="/" exact component={Home}/>
          <Route render={()=> <h3> Page not found - 404</h3>}/>
          {/* <Route component={notFound}/> */}
        </Switch>
      </div>
     </BrowserRouter>
   )
 }

注意其中添加的有关 Life 的 import,NavLink 和 Route 语句。

更新 lifecycle.js 文件:

import React, { Component } from 'react';

class Life extends Component {
    // 1st step: get default props

    // 2nd step: get default state
    state = {
        title: 'Life Cycle'
    }

    // 3rd step: before render
    UNSAFE_componentWillMount() {
        console.log('before render');
        // document.querySelector('h3').style.color = 'red'
    }

    UNSAFE_componentWillUpdate() {
        console.log('before update')
    }

    componentDidUpdate() {
        console.log('after update')
    }

    shouldComponentUpdate(nexProps, nextState) {
        console.log(this.state)
        console.log(nextState);
        if(nextState.title === 'something else') {
            return false
        }
        return true;
    }

    UNSAFE_componentWillReceiveProps() {
        console.log('before receive props')
    }

    componentWillUnmount() {
        console.log('component unmount')
    }

    // 4th step: render jsx
    render() {
        console.log('RENDER')
        return (
            <div>
                <h3>{this.state.title}</h3>
                <div onClick={
                    // ()=> this.setState({title:'something else'})
                    ()=> this.setState({title:'title is changed'})
                }>Click to change the title!</div>
            </div>
        )
    }

    // 5th step: after render
    componentDidMount() {
        console.log('after render')
        document.querySelector('h3').style.color = 'red'
    }
}

export default Life;

注意,其中注释部分的说明,前后顺序不影响实际执行顺序。
第三步中的样式修改不可用,因为发生在 HTML render 之前。
有些方法将由于安全问题在未来被弃用,名称为 UNSAFE_xxx

关于更新和接收属性的方法都只以当前组件内部为作用域。
也就是说组件获得初始状态和属性不会触发相关方法。
点击 Safe 链接测试 UNSAFE_componentWillReceiveProps()
点击其他链接测试 componentWillUnmount()

遗留问题
如前述,UNSAFE_componentWillUpdate() 会在触发时更新组件。
触发条件有两种,在发生更新或从当前页重新加载当前组件时。
后一种情况实际没有发生任何组件内容更新,是一个问题。

这里可以更新 UNSAFE_componentWillUpdate() 方法:

    shouldComponentUpdate(nexProps, nextState) {
        if(nextState.title === this.state.title) {
            return false
        }
        return true;
    }

以上判断新旧属性是否有变化,有变化才更新组件内容。
但如果需要判断的属性过多也会带来问题…
所以 React 提供了 PureComponent 来解决这个问题。
导入时选用 PureComponent 而非 Component 即可。

用 React 的 Pure

条件渲染
有时需要按照条件生成组件,JSX 支持 value? case_t : case_f
类似以上 lifecycle.js,创建 conditional.js及其路径和导航。
更新 index.js 部分略过,conditional.js 使用如下内容:

import React, { Component } from 'react';

const showIt = (value) => {
    return ( value ?
        <div>Hello, it's true!</div>
        :
        <div>Sorry, it's false!</div>
    )
}

class Conditional extends Component {
    state = {
        value: true
    }

    render() {
        return (
            <div>
                <div>{showIt(this.state.value)}</div>
                <div onClick={
                    // ()=> this.setState({title:'something else'})
                    ()=> this.setState({value: !this.state.value})
                    }>Click to toggle the value!</div>
            </div>
        )
    }
}

export default Conditional;

以上设定一个逻辑值状态,根据值返回不同刷新组件内容。
使用前一节中介绍的类似方法,点击切换其值和组件内容。

返回数组
为了了解可以返回的数组是怎样,更新 posts.js 如下:

import React from 'react';
import { Link } from 'react-router-dom';

const Posts = () => {
    const data = [
        {id: '1', name: 'Post 1'},
        {id: '2', name: 'Post 2'},
        {id: '3', name: 'Post 3'},
    ]

    const list = data.map(item => {
        return (
            <span key={item.id}>
                <Link to={item.id}>{item.name}</Link><br/>
            </span>
        )
    })

    return [
        <div key="aList">
            {list}
        </div>,
        <div key="item1">Item 1</div>,
        <div key="item2">Item 2</div>
    ]
}

export default Posts;

以上返回一个数组,且第一项内嵌映射生成的数组。

高阶组件
全称 High Order Component,输入和输出组件。
船舰 Card 以嵌入其他组件,文件 src/hoc/card. is
这里需要重复嵌入的仅仅是一种灰色背景的格式。

import React from 'react';

const Card = (props) => {
    const style = {
        background: 'lightgrey'
    }

    return (
        <div style={style}>
            {props.children}
        </div>
    )
}

export default Card;

以上对传入的 children 属性添加灰色背景。
更新 Posts,为其返回数组应用 Card 组件:

import React from 'react';
import { Link } from 'react-router-dom';

import Card from '../hoc/card';

const Posts = () => {
…
    return [
        <Card key="aList">
            {list}
        </Card>,
        <div key="item1">Item 1</div>,
        <div key="item2">Item 2</div>
    ]
}

export default Posts;

如上,返回的第一个数组对象有灰色背景。

再看一个用 HOC 实现用户认证的场景。
创建文件 src/hoc/auth. is

import React from 'react';

const Auth = (props) => {
    const pass = 'P@ssw0rd';

    return ( 
        pass !=='P@ssw0rd' ?
        <h3>Not authorizded</h3>
        : props.children
    )
}

export default Auth;

以上模拟认证过程,对比定义变量和预设值。
相同则显示组件子属性,否则警告未认证。

这次把它用到 Home 组件中:

import React from 'react';

import Auth from '../hoc/auth';

const Home = () => {
    return (
        <Auth>
            Home
        </Auth>
    )
}

export default Home;

以上导入和应用自不待言。

再看一个例子,创建 src/component/user.js

import React from 'react';

import User from '../hoc/user';

const User1 = (props) => {
    console.log('User1:', props)
    return (
        <div>
            User 1
        </div>
    )
}

const User2 = (props) => {
    console.log('User2:', props)
    return (
        <div>
            User 2
        </div>
    )
}

export default User(User1, User2, 'Hello', 'Bonjour');

index.js 导入和创建导航与路径的操作略过。
以上创建两个用户连同两个参数传给 HOC对象。

以下为 src/hoc/user.js

import React from 'react';

const User = (User1, User2, arg1, arg2) => {
    return (props) => (
        <div>
            {arg1} <User1 {...props}/>
            {arg2} <User2 {...props} />
        </div>
    )
}

export default User;

以上读入两个用户和参数,按指定方式返回。
注意在用户对象中指定 props 以保留属性。

为什么需要做以上看似多余的 HOC 操作。
为了实现 Separate Of Concern (SOC)

安装错误
运行 npm install 安装库,可能遇到错误:
npm ERR! code ETIMEDOUT
解决办法是删除可能存在的代理:
npm config delete proxy
npm config delete https-proxy

Leave a comment