使用React Router管理应用位置信息

单页面应用

单页面应用可以有多种工作方式。单页面应用的一种加载方式是一次性下载整个网站的内容。这样,当你浏览网站时,一切内容已经出现在浏览器中,不需要刷新页面。单页面应用的另一种工作方式是下载渲染用户请求的页面所需的所有内容。当用户浏览到新页面时,仅对请求的内容发出异步 JavaScript 请求。

优质单页面应用的另一个关键因素是由 URL 控制页面内容。单页面应用互动性非常高,用户希望能够仅使用 URL 就能回到特定的状态。为何这一点很重要?Bookmarkability(书签功能,很确定的是这还不是一个单词)!当你将网站添加到书签中,该书签仅仅是 URL,并没有记录该页面的状态。

有没有注意到,你对应用执行的任何操作都不会更新页面的 URL?我们需要创建能够将页面添加到书签中的 React 应用!

React Router

React Router 会将 React 项目转变成单页面应用。它通过提供大量特殊的组件来实现这一点,这些组件能够管理链接的创建、管理应用的 URL、在不同的 URL 地点之间导航时提供转换,以及很多其他功能。

根据 React Router 网址:

React Router 是一系列导航式组件的集合,可以与应用一起以声明的方式编写代码。

如果你感兴趣的话,请参阅网站:https://reacttraining.com/react-router/

在下一部分,我们将根据项目的 this.state 对象中的值动态地向页面上渲染内容。我们将使用这个基本示例讲解 React Router 的工作原理,即通过状态控制正在显示的内容。然后,我们将学会如何使用 React Router。我们将逐步介绍如何安装 React Router,将其添加到项目中,并将一切连接到一起,使其能够管理链接和 URL。

动态的渲染页面

对于应用的当前功能,根本没法添加新的通讯录!很遗憾,因为我真的需要将 Richard 添加到我的通讯录里。我们创建一个表格,使我们能够新建通讯录并保存到服务器上。

我们不希望该表格一直显示,仅在启用某个设置后,才开始显示该表格。我们将此设置保存在 this.state 中。这样使我们能够知道 React Router 的工作原理。

在这个简短的视频中,我们创建了 CreateContact 组件,它将负责创建新通讯录的表格。根据 React 对组合功能的青睐,我们创建了这个独立的组件,并使用了组合功能,将其添加到 App 组件的 render() 方法中。

为了非常简单地重现 React Router 的工作原理,我们向 this.state 添加了 screen 属性, 并使用此属性控制什么内容应该显示在屏幕上。如果 this.state.screenlist,那么我们将显示所有现有通讯录的列表。如果 this.state.screencreate,那么我们将显示 CreateContact 组件。

短路求值语法

在此视频中,当创建了“当前显示”部分时,我们使用了一种看起来很奇怪的语法:

{this.state.screen === 'list' && (
  <ListContacts
    contacts={this.state.contacts}
    onDeleteContact={this.removeContact}
  />
)}

{this.state.screen === 'create' && (
  <CreateContact />
)}

看起来可能有点让人迷惑,既有组件的 JSX 代码,又有运行表达式的代码。但其实就是逻辑表达式 &&

expression && expression

这里使用的是一种叫做短路求值的 JavaScript 技巧。如果第一个表达式的值是 true,则运行第二个表达式。但是,如果第一个表达式的值是 false,则跳过第二个表达式。我们使用这种技巧来首先验证 this.state.screen 的值,然后显示正确的组件。

要深入了解这一技巧,请参阅 MDN 上的短路求值信息

代码示例:
ContactCreate.js

import React,{Component} from 'react'

class ContactCreate extends Component{


    render() {
        return (
            <div>contacts text!!!</div>
        )
    }
}

export default ContactCreate;

App.js 部分

......
  state = {
        screen:'create' ,//list,create
        contacts :[]
    }
.....

-----------
return (
            <div>
                {/*使用短路求值表达式 根据this.state.screen的状态来显示相应的组件 */}

                {/* 将removeContact函数通过onDeleteContact属性传递给ListContacts组件,并通过点击事件调用它,触发setState */}
                {this.state.screen === 'list' && (
                    <ListContacts onDeleteContact = {this.removeContact} contacts = {this.state.contacts} />
                )}

                {/*使用短路求值表达式 根据this.state.screen的状态来显示相应的组件 */}
                {this.state.screen == 'create' && (
                    <ContactCreate />
                )}

            </div>
        )
-----------

添加按钮

目前,我们需要手动更改状态以让应用显示不同的屏幕。我们希望用户能够在应用本身里控制这一点,我们添加个按钮吧!

App.js部分代码:

    render() {
        return (
            <div>
                {/*使用短路求值表达式 根据this.state.screen的状态来显示相应的组件 */}

                {/* 将removeContact函数通过onDeleteContact属性传递给ListContacts组件,并通过点击事件调用它,触发setState */}
                {this.state.screen === 'list' && (
                    <ListContacts
                        onDeleteContact = {this.removeContact}
                        contacts = {this.state.contacts}
                        //更改screen状态的匿名函数
                        onNavigateTo = {() => {this.setState({
                            screen:'create',
                        })}}/>
                )}

                {/*使用短路求值表达式 根据this.state.screen的状态来显示相应的组件 */}
                {this.state.screen == 'create' && (
                    <ContactCreate />
                )}

            </div>
        )
    }

ListContact.js

 <div className="list-contacts-top">
                    {/*{JSON.stringify(this.state)}*/}
                    <input
                        className="search-contacts"
                        type="text"
                        value={query}
                        //监听onChange事件,event的target的值传输给state更新状态
                        onChange={(event) => this.updateQuery(event.target.value)}
                    />

                    {/*添加加人按钮*/}
                    <a
                        href="#create"
                        onClick={this.props.onNavigateTo}
                        className="add-contact"

                    >Add Contact</a>
</div>

动态路由总结

在这部分添加的代码中,我们尝试使用状态来控制向用户显示什么内容。但是当我们使用后退按钮时,我们遇到了问题。

现在,我们改为使用 React Router 来管理应用的屏幕。

Browser Router组件

正如我们看到的,当用户点击浏览器中的“后退”按钮时,可能需要刷新页面才能看到该位置的适当内容。这对我们的用户来说体验并不佳!当我们更新位置时,我们可以使用 JavaScript 更新应用,这时候 React Router 就派上用场了。

安装 React Router

要在应用中使用 React Router,我们需要安装 react-router-dom

npm install --save react-router-dom

代码示例:最上层代码

import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import App from './App.js'
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(
    /*BrouserRouter作用是:监听URL的变化,确保URL变更时,通知其它组件*/
    <BrowserRouter><App /></BrowserRouter>,
    document.getElementById('root'));
registerServiceWorker();

React Router 的好处是一切都是组件。这样使用起来很方便,并且代码阅读起来也更轻松。我们看看 BrowserRouter 的代码原理。

以下是直接摘自 React Router 仓库的代码。

class BrowserRouter extends React.Component {
  static propTypes = {
    basename: PropTypes.string,
    forceRefresh: PropTypes.bool,
    getUserConfirmation: PropTypes.func,
    keyLength: PropTypes.number,
    children: PropTypes.node
  }

  history = createHistory(this.props)

  render() {
    return <Router history={this.history} children={this.props.children}  />
  }
}

当你使用 BrowserRouter 时,其实真正的是在渲染 Router 组件并将向其传递 history 属性。等等,什么是 history? history 来自 history 库(也是由 React Training 构建的)。该库的整个目标是抽象化不同环境中的区别,并提供最少的 API 来使你管理历史记录堆、导航、确认导航,并在会话之间保持状态。

简而言之,当你使用 BrowserRouter 时,你是在创建 history 对象,该对象将监听 URL 中的变化,并使你的应用知道这些变化。

BrowserRouter 组件总结

总之,要使 React Router 正常工作,你需要将整个应用封装在 BrowserRouter 组件中,此外,BrowserRouter 还会封装 history 库,使你的应用能够知道 URL 中的变化。

深入研究

Link 组件

使用示例:

import {Link} from 'react-router-dom'
...
<Link
         to="/create"
        className="add-contact"
    >Add Contact</Link>

正如你看到的,Link 是提供声明式、可访问的应用导航的简单方式。通过向 Link 组件传递 to 属性,可以告诉应用要路由到哪个部分。

<Link to="/about">About</Link>

如果你对网络上的路由熟悉,那么你将知道链接有时候需要稍微复杂下,而不仅仅只是个字符串。例如,可以传递查询参数或链接到页面的特定部分。如果要将状态传递给新的路由,该怎么办?要考虑这些情形,你可以向 Linkto 属性传递对象,而不是字符串,如下所示:

<Link to={{
  pathname: '/courses',
  search: '?sort=name',
  hash: '#the-hash',
  state: { fromDashboard: true }
}}>
  Courses
</Link>

你不需要时刻使用该功能,但是有必要知道存在这一功能。要详细了解 Link,请参阅官方文档

Link 总结

React Router 提供了一个 Link 组件,使你能够添加声明式、可访问的应用导航功能。你将在锚点标记 (a) 中使用它,就像通常使用的那样。React Router 的 <Link> 组件是让用户能够导航应用的很好方式。例如,向链接传递 to 属性可以将用户指向绝对路径(例如 /about):

<Link to="/about">About</Link>

因为 <Link> 组件会渲染成拥有相应的 href 的锚点标签 (<a>),因此,它的行为和网络上的普通链接的行为一样。

课外资料

Route组件

  • 用来接收URL,匹配到就更新UI。匹配不到就不更新UI。
    修改App.js代码示例:
import {Route} from 'react-router-dom'
...
return (
            <div>

                {/*  () => () 箭头函数后面的括号代表return  ,exact属性代表完全匹配  */}
                <Route exact path='/' render={() => (
                    <ListContacts
                        onDeleteContact={this.removeContact}
                        contacts={this.state.contacts}
                        />
                )}>

                </Route>
                {/*Route 组件的第二种写法*/}
                <Route path="/create" component={ContactCreate}>
                    <ContactCreate />
                </Route>

            </div>
        )

Route 组件总结

这节课的主要内容是,对于 Route 组件,如果你希望向 router 将渲染的特定组件传递属性,你需要使用 Route 的 render 属性。正如你所看到的,render 使你能够控制渲染组件,进而使你能够向要渲染的组件传递任何属性。

总之,Route 组件是使用 React Router 构建应用的关键部分,因为该组件将根据当前的 URL 路径决定要渲染哪些组件。

创建通讯录表格

目前,创建通讯录的页面是空的!我们在该页面上构建一个表单,以便开始添加自定义通讯录。

⚠️ 必需的文件 ⚠️

在这门课程的开始阶段,我们给了你两个选择:克隆我们的起始项目或使用 create-react-app 从头开始。如果你尚未添加该项目,则对于下面的视频,你需要 ImageInput.js 文件

ImageInput 组件是自定义 <input>,该组件会动态地读取图片文件并且在将图片当做数据 URL 提交到服务器之前重新调整其大小,它还会显示图片预览。我们选择直接将该组件提供给你,而不是让你来构建,因为它包含了与网络文件和图片相关的功能,而这些功能对这门课程来说并不重要。如果你感兴趣的话,可以阅读相关代码,但并不是必修要求。

序列化表格数据

现在,我们的表格将序列化用户输入的值(即 nameemail),将它们当做查询字符串添加到 URL 中。我们可以添加额外的功能,让应用自己序列化这些表格字段。毕竟,我们希望应用能够最终自己创建通讯录并将其保存到状态中。

为了实现这一点,我们将使用 form-serialize 包将此信息当做普通的 JavaScript 对象输出给应用使用。

npm install --save form-serialize

使用新的通讯录更新服务器
我们创建了通讯录表格。我们序列化了数据并将其传递给父组件。为了拥有一个功能完整的应用,我们需要将通讯录保存到服务器上。

ContactCreate.js代码:


import React,{Component} from 'react'
import serializeForm from 'form-serialize'
import {Link} from 'react-router-dom'
import ImageInput from './ImageInput'


class ContactCreate extends Component{

    //定义提交函数
    handleSubmit = (e)=>{
        const values = serializeForm(e.target,{hash:true})
        if(this.props.onCreateContact){
            this.props.onCreateContact(values)
        }

    }

    render() {
        return (
            <div>
                <Link className="close-create-contact" to="/">close</Link>
                <form onSubmit={this.handleSubmit} className="create-contact-form">
                    <ImageInput
                        className="create-contact-avatar-input"
                        maxHeight={64}
                        name="avatarURL"

                    />
                    <div className="create-contact-details">
                        <input type="text" name="name" placeholder="Name"/>
                        <input type="text" name="email" placeholder="Email"/>
                        <button>Add Contact</button>
                    </div>
                </form>
            </div>
        )
    }
}

export default ContactCreate;

App.js代码:

import React, {Component} from 'react';

import ListContacts from './ListContacts'
import "./index.css"
import {Route} from 'react-router-dom'
import {history} from 'react-router-dom'


import ContactCreate from './ContactCreate'
import * as contactsAPI from './utils/ContactsAPI'

export default class App extends Component {
    state = {
        contacts: []
    }

    componentDidMount() {
        contactsAPI.getAll().then(
            (contacts) => {
                this.setState({
                    contacts: contacts
                })
            }
        );
    }

    removeContact = (contact) => {
        //=>({}) 加括号的用法是因为是return 这个对象
        this.setState((state) => ({
            // c是contacts的匿名函数的变量,代表当前state状态的contacts数组,contact是被传递过来的被点击的迭代联系人
            contacts: state.contacts.filter((c) => c.id !== contact.id)
        }));


        //同时删除数据库中的contact
        contactsAPI.remove(contact);
    }


    createContact = (contact) => {(
        contactsAPI.create(contact).then((contact)=>{
            this.setState((state) => {
                contacts: state.contacts.concat([ contact ])
        })})

    )}

    render() {
        return (
            <div>

                {/*  () => () 箭头函数后面的括号代表return  ,exact属性代表完全匹配  */}
                <Route exact path='/' render={() => (
                    <ListContacts
                        onDeleteContact={this.removeContact}
                        contacts={this.state.contacts}
                        />
                )}>

                </Route>
                {/*Route 组件的第二种写法*/}
                <Route path="/create" render={({history})=>(
                    <ContactCreate
                        onCreateContact = {(contact) => {
                            this.createContact(contact)
                            history.push('/')
                        }}

                    />
                )}>
                </Route>

            </div>
        )
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生x阅读 15,967评论 3 119
  • 文/忆素 说了千百遍 不曾厌烦 你好 这未来的岁月 你是否要请我 好好关照 虽然你并不友好 见第一面 就嚎啕大哭 ...
    忆素说阅读 227评论 5 1
  • 感谢这一生。我爱你。-----《你是我一切的一切》 亲爱的奥利: 我突然很想给你写一封很长很长的信,但是,我...
    简简单单的小易阅读 618评论 0 0
  • 目标:种下理想的伴侣 感恩冥想: 1、感恩今天我的数学老师张老师,在我不知情的情况下在工作上给我帮助,谢谢张老师。...
    小兔兔姐姐爱吃胡萝卜阅读 109评论 0 0
  • 从小见过无数雪花飘落,这次你来的更早一些,在寒夜里零零散散飘落。 冬 注定 她要来 他就要走 初醒的暖冬 透着丝丝...
    心愿菩提阅读 198评论 0 1