这部分学习多页面之间的路径导航配置和组件的生命周期。
创建程序
$ 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
还有 HashRouter
和 MemoryRouter
前者在 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