(09)首页开发——① 如何在 React 中使用“路由” | React.js 项目实战:PC 端“简书”开发

转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥"前端一万小时"两大明星专栏------"从零基础到轻松就业"、"前端面试刷题",已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

1 路由

❓什么是路由?

答:路由可以直接理解为------根据网址的不同,返回不同的内容给用户(这也是路由的主要功能)。

如以下视频所演示的(在简书的首页时,它显示的是"首页",当我点击"公告"时,URL 发生了变化,其页面也跟着变了):

❓怎样将这个功能用在我们自己的项目中呢?

答: 1️⃣安装"路由 react-router-dom":

2️⃣打开 src 目录下的 App.js 文件,将"首页"、"详情页"的页面先做出来:

jsx 复制代码
import React, { Component } from "react";

import {GlobalStyle} from "./style";

import {GlobalIconStyle} from "./statics/iconfont/iconfont";

// 2️⃣-①:从 react-router-dom 中引入 BrowserRouter 和 Route;
import {BrowserRouter, Route} from "react-router-dom";

import Header from "./common/header";

import { Provider } from "react-redux";

import store from "./store";

class App extends Component  {  
  render() {  
    return (
      <div>
        <GlobalStyle />
        <GlobalIconStyle />
 
        <Provider store={store}>  
        	<BrowserRouter> {/*
          								 2️⃣-②:用 BrowserRouter 包裹一个 div 标签。
          								 ❗️BrowserRouter 表示"路由";
                            */}
      				
            <div>
              <Header />          
      
              {/*
               2️⃣-③:接着在 div 标签里,用 Route 包裹各个可以"跳转"的页面。
               ❗️Route 表示"一条条的路由规则"
                */}
              <Route path="/" exact render={() => <div>这是 home 页!</div>}></Route>
              
              {/*
               ❗️❗️❗️2️⃣-④:其中,path 指页面要"跳转"的"路径";
                              exact 指"路径"必须"精确"地匹配才"跳转";
                              render 指"跳转"后,页面会"渲染"出的内容;
                */}
                  
              <Route path="/detail" exact render={() => <div>这是 detail 页!</div>}></Route>
            </div>
          </BrowserRouter>
        </Provider>
      </div>
    );
  }
}

export default App; 

返回页面查看效果:

3️⃣既然"路由"被成功引入,接下来我们就可以在本项目中新建"首页"和"详情页"这两个组件,并让其得到正确地跳转。

3️⃣-①:在 src 目录下新建一个 pages 文件夹,并在这个文件夹下新建 homedetail 文件夹。用于表示各个可以跳转的"页面";

3️⃣-②:在 home 文件夹下,新建一个 index.js 文件,并写入以下代码;

jsx 复制代码
import React, {Component} from "react";

class Home extends Component {
  render() {
    return(
      <div>
        <div>home 页面!</div>
        

        <ol>
          <li>1. 点击这项时,它会跳转至对应的 detail 页面~</li>
          <li>2. 点击这项时,它会跳转至对应的 detail 页面~</li>
          <li>3. 点击这项时,它会跳转至对应的 detail 页面~</li>
        </ol>
      </div>
      
    )
  }
}

export default Home;

3️⃣-③:同理,在 detail 文件夹下,新建一个 index.js 文件,并写入以下代码;

jsx 复制代码
import React, {Component} from "react";

class Detail extends Component {
  render() {
    return(
      <div>detail 页面!</div>
    )
  }
}

export default Detail;

3️⃣-④:返回 src 目录下的 App.js 文件,替换相关代码;

jsx 复制代码
import React, { Component } from "react";

import {GlobalStyle} from "./style";

import {GlobalIconStyle} from "./statics/iconfont/iconfont";

import {BrowserRouter, Route} from "react-router-dom";

import Header from "./common/header";

// 3️⃣-⑤:引入组件 Home 和 Detail;
import Home from "./pages/home";
import Detail from "./pages/detail";

import { Provider } from "react-redux";

import store from "./store";

class App extends Component  {  
  render() {  
    return (
      <div>
        <GlobalStyle />
        <GlobalIconStyle />
 
        <Provider store={store}>             
          <BrowserRouter>
            <div>
              <Header />
      
              {/* ❗️❗️❗️3️⃣-⑥:用 component={} 的形式,将"渲染"的内容替换为"组件"; */}
              <Route path="/" exact component={Home}></Route>
                  
              <Route path="/detail" exact component={Detail}></Route>
            </div>
          </BrowserRouter>
        </Provider>

      </div>
    );
  }
}

export default App; 

返回页面查看效果:

2 单页应用 🆚 多页应用

2.1 多页应用

❓什么是多页应用?

1️⃣随便打开一个网页(如"去哪儿网")进入检查 ,切换至 iPhone6/7/8 设备显示的模式:

2️⃣点开控制台里的 Network ,选择 Doc ,然后刷新页面。可以看到,当刷新网页重新访问首页的时候,会请求一个首页的 HTML 文件出来 touch/

3️⃣点击右上角的城市"深圳"访问一下城市列表页,返回了一个城市列表的 HTML toNewCityList.htm

4️⃣从城市列表页返回首页,又会重新请求一个首页的 HTML 的文件:

5️⃣再进入一个"世界之窗"的页面试试,请求到了一个 detail.htm 的文件:

🏆在这个网页中,我们每一次页面跳转的时候,后台服务器都会返回一个新的 HTML 文件。这种类型的网站,就把它称之为"多页网站",或者叫做"多页应用"。

❓多页应用有什么优缺点?

当我们使用一个多页面应用时,每次页面跳转,后端都会返回一个新的 HTML 文件。

这也就带来了它的几个优点

  1. 首屏时间快("首屏时间"即页面首个屏幕的内容展现出来的时间);
  2. 搜索引擎优化(SEO,Search Engine Optimization)效果好。
  • ❓为什么首屏时间快?

答:当访问一个页面时,服务器给我们返回一个 HTML 文件,然后页面就会被展示出来。这个过程只经历了一次 HTTP 请求,所以页面展示的速度会很快。当请求回来了,页面也就展示出来了。

  • ❓为什么它的 SEO 效果好?

答:因为搜素引擎在做网页排名的时候,它需要知道网页的内容,根据网页的内容它才能给网页权重,来进行排名。并且非谷歌浏览器搜索引擎(如"百度")的爬虫只认识 HTML 中的文本内容,不认识 JS 中的文本内容。而多页应用中,我们每个页面所有的内容都放在 HTML 中,所以多页应用的搜索引擎排名效果非常的好。

任何事情都是双面的,多页应用也有一个缺点:页面之间的切换有时会比较慢。

因为每一次跳转页面时,都需要发一个 HTTP 请求。当网络慢的时候,我们需要在页面之间来回跳转时,就会发现有明显的卡顿情况出现。

我们先在正常网速下切换页面,再把网速调为低速查看对比效果: 可以看到,正常网速下切换流畅,而当切换为低网速后,从"世界之窗"页面跳转到首页,再从首页跳转进"世界之窗"页面,页面加载完成的时间比网速正常时慢了很多。

2.2 单页应用

❓什么是单页应用?

在终端中进入到 qdywxs-jianshu 文件夹下,运行 npm run start 命令来启动服务器,访问 http://localhost:3000

此时访问的是 Home 首页,而在我们的项目中还有一个 detail 详情页,需要手动输入 /detail 进行访问。

❓如何可以不修改 URL,而是通过在首页点击某个链接就直接跳转至详情页(如,点击上图中红框内任意项)?

答:可以在首页增加链接。

提到链接,我们很容易就能想到 <a> 标签,但在 React 中,一般使用 <Link> 标签来进行页面跳转

1️⃣打开 home 目录下的 index.js 文件,对其代码进行一些调整:

jsx 复制代码
import React, {Component} from "react";

import {Link} from "react-router-dom"; // 1️⃣-①:引入 Link;

class Home extends Component {
  render() {
    return(
      <div>
        <div>home 页面!</div>
        

        <ol>
      		{/*
           1️⃣-②:将可点击的"项"用 Link 标签包裹!
           ❗️to 表示"将要跳转到的路径"
            */}
          <Link to="./detail"><li>1. 点击这项时,它会跳转至对应的 detail 页面~</li></Link>
          <Link to="./detail"><li>2. 点击这项时,它会跳转至对应的 detail 页面~</li></Link>
          <Link to="./detail"><li>3. 点击这项时,它会跳转至对应的 detail 页面~</li></Link>
        </ol>
      </div>
      
    )
  }
}

export default Home;

🏆以上,我们使用 React 写的这个项目,它就是一个单页应用。

❓如何理解单页应用?

打开检查,还是点到 Network 下的 Doc

刷新页面后,先清除一下 Doc 下的内容,再重新点开详情页:

可以看到,当刷新页面第一次进入页面时,请求到了一个 HTML 文件。把它清除后进入详情页,我们并没有再去请求一个 HTML 文件。当再次返回首页,依然不会请求 HTML 文件。

❓为什么不请求 HTML 文件,但是页面却依然会变呢?

答:因为 JS 会感知到 URL 的变化。

通过 JS 感知到 URL 的变化之后,我们可以用 JS 动态地把当前页面的内容清除掉,再把下一个页面的内容挂载到页面上。

所以,这时的路由不是由后端来处理,而是由我们前端来做的。我们判断页面到底是显示哪一个组件,然后把前一个组件先清除掉再去显示新的组件。单页应用这种处理过程之中,就不会每次跳转都需要请求 HTML 文件了。

❓单页应用有什么优缺点?

当我们使用单页应用进行页面跳转时,页面上每次跳转并不用去加载一个 HTML 文件,而是通过 JS 动态的把当前页面的内容清除掉,再去把新页面内容的 DOM 结构渲染出来。

  • 单页应用这种做法的优点是:页面切换快。当页面之间进行跳转时,不需要去请求 HTML 文件,这就节约了很多 HTTP 请求发送时延。

  • 单页应用的缺点是:

    • 首屏时间相比多页应用会稍微慢点;
    • 搜索引擎优化(SEO)效果比较差。

❓什么导致单页应用首屏时间相比多页应用会慢点?

答:在后面随着项目的深入,我们会发现单页应用首屏展示出来需要请求一次 HTML,同时还需要发一个 JS 的请求。当两个请求都回来后,首屏才会展示出来。

❓为什么 SEO 效果较差?

答:因为非谷歌浏览器搜索引擎(如"百度")的爬虫只认识 HTML 中的文本内容,不认识 JS 中的文本内容。而单页应用中,所有页面内容都是依赖 JS 渲染生成的。所以非谷歌浏览器搜索引擎的爬虫就不识别这块的内容,就不会给网页一个好的排名。这样就会导致单页应用做出来的网页在"百度"这样的搜索引擎之中,排名效果会比较差。

❓既然单页应用也有一些缺点,我们为什么要使用 React 开发 qdywxs-jianshu 这样的单页应用呢?

答:因为 React 中还提供了一些其他的技术,比如服务器端渲染等。通过这些技术,我们可以完美的解决单页应用之中的问题。而解决了这些问题后,单页应用实际上对我们前端来说是一个非常完美的页面开发解决方案。

❓【拓展】什么是"服务器端渲染",什么是"客户端渲染"?

答: ① 服务端渲染(SSR):页面上的内容是由服务器上的代码决定的。

即,页面上的内容在服务器上已经生成好了,服务器把这个内容给到浏览器,浏览器拿到这个内容直接显示在页面上即可。

② 客户端渲染(CSR):一个网页是由 JS 文件渲染出来的,而不是服务器直接返回回来的------我们传统的 React 和 Vue 单页应用都是"客户端渲染(CSR)"

❓【拓展】当前主流"服务器渲染(SSR)"框架有哪些?

答:

  • NEXT.js------对应 React.js 应用;
  • Nuxt.js------对应 Vue.js 应用。

在实际项目工作中,若真的有需要,我们仅需照着 NEXT.jsNuxt.js 的文档,创建目录结构,使用对应的 API,就可以相对轻松地构建一个"服务端渲染(SSR)"的项目。

❗️❗️❗️其实还是比较复杂,一旦用了"服务端渲染",虽然解决了"首屏展示"和"SEO"的问题,但缺点也暴露出来了:

① 使用 SSR 后,整个项目架构会变得复杂------由于会在前端和后端之间引入一个 Node 中间层,维护时,就需要一个好的前端工程师 + 一个好的 Node 工程师 + 一个好的后端工程师。

② 使用 SSR 后,我们的 JS 代码不仅需要在客户端执行一次,还需要在服务器端(Node 端)执行一次------"同构"。这就意味着,服务器端(Node 端)需要进行大量的计算,这种计算是非常损耗性能的。很多时候,就需要在 Node 中间层增加很多台服务器,这就大大增加了费用成本以及维护负担。

③ 使用 SSR 后,调错上也增加了很多难度。以往前后端进行联调或测试的时候,或线上出现 BUG 的时候,不是前端就是后端出的错;现在不一样了,多了一个 Node 中间层,定位错误的成本又增加一层。

❗️❗️❗️因此:

① 如果你所做的项目并不特别在意那 300ms 左右的首屏展示快慢(SSR 首屏展示要比 CSR 平均快 300ms 以上),且 SEO 需求不大的话,完全可以不用去管"服务端渲染",直接用 React.js、Vue.js 或原生 JS 完成你的项目即可。并且,随着 5G 商用的普及,网速得到了进一步提高,这个首屏展示时间差异基本可以忽略。

② 但,若你不在意首屏展示快慢,却对 SEO 需求很大的话,除了直接在"百度"买广告排名,你依然可以不用进行"服务端渲染"!用什么呢?一个很好且简单的方式就是"预渲染"。

一句话概括"预渲染"干的事情:这个工具会先去访问你的网页(React.js、Vue.js 等框架做的单页面应用),然后将网页渲染出来的内容全部自己拿过来,重新生成一个新的、有文本内容的 HTML,最后返回给搜索引擎的爬虫。

"预渲染 "这个知识点,大家后续有需要可以自行去拓展,GitHub 上也有高 star 的"预渲染"工具------prerender,照着文档使用即可。

若对 prerender 很感兴趣,推荐仔细研读以下网站,其对 prerender 有详细的介绍:

arduino 复制代码
https://prerender.io

上文中,我们见识到了 Link 的优势和用处所在,接下来我们需要把之前代码中的 a 标签用 Link 替换!

❗️❗️❗️但要注意:页面上很多地方都是可以点击且会跳转至相应页面的,可由于我们并不会把"简书"所有页面都写完,故我们先约定暂都跳转至 / (有明确"跳转路径"的除外),待后续编写具体页面时,我们再作相应地改动!

3️⃣-①:打开 header 目录下的 style.js 文件(有"❗️"标注的地方都是改动了的);

javascript 复制代码
import styled, {keyframes} from "styled-components";  

export const HeaderWrapper = styled.header`
  box-sizing: content-box;
  padding: 0 20px;
  height: 56px;
  
  line-height: 56px;
  
  border-bottom: 1px solid #eee;
  
  &:after {
    content: "";
    display: block;
    clear: both;
  }
`;

export const Logo = styled.div` /* ❗️a 改为 div! */
  float: left;
  height: 56px;
  & > img {
    height: 50px;  
  }
`;


// 🚀Navbar 相关~
export const Navbar = styled.ul`
  float: left;

`;

export const ItemList = styled.li`
  float: left;
  padding: 0 4px;

  &.active div{ /* ❗️选择器需要改变! */
    color: #e86f5e;  
  }

  &.active div:hover { /* ❗️选择器需要改变! */
    background-color: #fff;
  }
`;

export const LinkList = styled.div`
  /* ❗️去掉 display: block; */

  padding: 0 10px;
  
  font-size: 17px;
  line-height: 56px;
  color: #333;
  

  &:hover {
    background-color: #eee;
  }

`;


// 🚀SearchArea 相关~
export const SearchArea = styled.div`
  position: relative;

  float: left;  
  margin-left: 30px;

  .icon-search {
    position: absolute;
    top: 10px;
    right: 10px;

    width: 32px;
    height: 32px;  

    color: #aaa;
    line-height: 32px;  
    text-align: center;  

    border-radius: 50%;
  }
`;

export const SearchInput = styled.input.attrs({
  placeholder: "搜索"
})`
  width: 200px;
  padding: 0 20px;
  
  font-size: 15px;
  line-height: 36px;

  border: none;  
  background-color: #eee;
  border-radius: 18px;
  outline: none;

  transition: all .3s;  

  &:focus {
    width: 240px;
  }

  &:focus~div {
    display: block;
  }

  &:focus + .icon-search {
    color: #fff;
    background-color: #969696;
  }
`;

export const SearchPanel = styled.div`
  position: absolute;
  z-index: 1;

  width: 250px;
  padding: 16px;

  line-height: 1;  

  background-color: #fff;
  border-radius: 6px;
  box-shadow: 0 1px 4px 2px rgba(0,0,0,0.1);  

  display: none;

  &::before {  
    content: "";
    display: block;
    position: absolute;
    top: -5px;
    left: 30px;
    width: 14px;
    height: 14px;
    background-color: #fff;
    transform: rotateZ(45deg);
    box-shadow: -2px -2px 2px -2px rgba(0,0,0,0.1);
  }

  &:hover {
    display: block;
  }


`;

export const PanelTitle = styled.h3`
  color: #888;
  font-weight: normal;
`;


const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

export const PanelChange = styled.div`
  float: right;
  
  color: #888;
  font-size: 13px;
  font-weight: normal;

  cursor: pointer;  

  &:hover {
    color: #333;
  }
  
  .refresh {
    display: inline-block;
    animation: ${rotate}  linear 0.3s infinite;
  }
`;






export const PanelLabels = styled.div`
  margin-top: 10px;
`;

export const LabelLink = styled.div` /* ❗️a 改为 div! */
  float: left;
  
  padding: 2px 4px;
  margin: 10px 10px 0 0;

  color: #888;

  border: 1px solid #ccc;
  border-radius: 4px;
  

  &:hover {
    border-color: #888;
  }
`;



// 🚀Extra 相关~
export const Extra = styled.div`
  float: right;

  .icon-textsize {
    float: left;
    font-size: 25px;
    color: #888;
    
    cursor: pointer;
  }

`;

export const ExtraLink = styled.div` /* ❗️a 改为 div! */
  float: left;
  padding: 10px 20px;
  margin: 10px 0 0 20px;
  font-size: 15px;
  line-height: 1;


  &.login {
    color: #888;
    background-color: #fff;
  }

  &.register {
    color: #e56e5d
    border: 1px solid #e56e5d
    border-radius: 30px;
  }

  &.writing {
    color: #fff;
    background-color: #e56e5d;
    border-radius: 30px;
    
  }
`;

3️⃣-①:打开 header 目录下的 index.js 文件(有"❗️"标注的地方都是改动了的);

jsx 复制代码
import React, {Component} from "react";

import {Link} from "react-router-dom"; // ❗️引入 Link!

import {
  HeaderWrapper,
  Logo,
  
  Navbar,
  ItemList,
  LinkList,
  
  SearchArea,
  SearchInput,
  SearchPanel,
  PanelTitle,
  PanelChange,
  PanelLabels,
  LabelLink,
  
  Extra,
  ExtraLink
  
} from "./style";

import { connect } from "react-redux";

import {actionCreators} from "./store";


class Header extends Component {
  
  getPanels() {
    const newList = this.props.list.toJS();
    const pageLabels = [];  
    
    if(newList.length) {  
      for(let i=(this.props.page - 1)*10; i<this.props.page*10; i++) { 
        pageLabels.push(  
          <Link to="/"  key={newList[i]}> {/* ❗️加 Link! */}
            <LabelLink>  
              {newList[i]} 
            </LabelLink>
          </Link>
        )
      }
      return pageLabels; 
    }
  } 
  
  render() {
    return (
      <HeaderWrapper>

        <Link to="/"> {/* ❗️加 Link! */}
          <Logo>
            <img src="https://qdywxs.github.io/jianshu-images/logo.png" alt="logo" />
          </Logo>
        </Link>

        <Navbar className="clearfix">
          <ItemList className="active">
            <Link to="/"> {/* ❗️加 Link! */}
              <LinkList href="/">
                首页
              </LinkList>
            </Link>
          </ItemList>

          <ItemList>
            <Link to="/"> {/* ❗️加 Link! */}
              <LinkList>
                下载APP
              </LinkList>  
            </Link>         
          </ItemList>
        </Navbar>
      
        <SearchArea>

          <SearchInput
            onFocus={() => this.props.handleInputFocus(this.props.list)}
          />
      
          <span className="iconfont icon-search">&#xe63e;</span>
      
          <SearchPanel>
            <PanelTitle>
              热门搜索
      
              <PanelChange
                onMouseDown={this.props.handleMouseDown}
                onMouseUp={this.props.handleMouseUp}

                onClick={() => this.props.handleChangePage(this.props.page, this.props.totalPage)}
              >  
                  
                <span className={this.props.refresh ? "iconfont refresh" : "iconfont"}>&#xe65f;</span>
                换一批
              </PanelChange>
            </PanelTitle>
      
            <PanelLabels className="clearfix">
              {this.getPanels()}
            </PanelLabels>
          </SearchPanel>
        </SearchArea>
      
      
        <Extra>
          <span className="iconfont icon-textsize" >&#xe739;</span>

          <Link to="/"> {/* ❗️加 Link! */}
            <ExtraLink className="login">
              登录
            </ExtraLink>
          </Link>

          <Link to="/"> {/* ❗️加 Link! */}
            <ExtraLink className="register">
              注册
            </ExtraLink> 
          </Link>

          <Link to="/"> {/* ❗️加 Link! */}
            <ExtraLink className="writing">
              <span className="iconfont icon-pen">&#xe600;</span>
              写文章
            </ExtraLink>  
          </Link>  

        </Extra>
      </HeaderWrapper>
    )
  }
}

const mapStateToProps = (state) => { 
  return { 
    refresh: state.getIn(["header", "refresh"]),
    list: state.getIn(["header", "list"]),
    
    page: state.getIn(["header", "page"]),
    totalPage: state.getIn(["header", "totalPage"])
  }
}

const mapDispatchToProps = (dispatch) => {  
  return {
    handleMouseDown() { 
      const action = actionCreators.changeClassNameAction(); 
      dispatch(action)
    
    },

    handleMouseUp() {
      const action = actionCreators.resumeClassNameAction();
      dispatch(action)
    },
    
    
    handleInputFocus(list) {  
      if(list.size === 0) { 
        const action = actionCreators.initLabelAction();
        dispatch(action)
      }
    },
    
    handleChangePage(page, totalPage) {
      if(page < totalPage) {
        dispatch(actionCreators.changePageAction(page + 1))
      }else {
        dispatch(actionCreators.changePageAction(1))
      }
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Header); 

返回页面查看(可以很明显地看出"单页应用"的优势):

当然,与"路由"相关的知识点还有很多,比如怎样编写代码才能实现------点击 home 页中 1. 点击这项时,它会跳转至对应的 detail 页面~2. 点击这项时,它会跳转至对应的 detail 页面3. 点击这项时,它会跳转至对应的 detail 页面 会跳到不同的 details 页面?

随着项目的推进,我们会在适当的时机讲解与"路由"相关的所有知识。

祝好,qdywxs ♥ you!

相关推荐
啧不应该啊8 分钟前
vue配置axios
前端·javascript·vue.js
__fuys__12 分钟前
【HTML样式】加载动画专题 每周更新
前端·javascript·html
Want59515 分钟前
HTML粉色烟花秀
前端·css·html
让开,我要吃人了20 分钟前
HarmonyOS鸿蒙开发实战(5.0)自定义全局弹窗实践
前端·华为·移动开发·harmonyos·鸿蒙·鸿蒙系统·鸿蒙开发
一条晒干的咸魚38 分钟前
响应式CSS 媒体查询——WEB开发系列39
前端·css·html·css3·响应式设计·媒体查询
凌晨五点的星1 小时前
网络安全-webshell绕过,hash碰撞,webshell绕过原理
开发语言·前端·javascript
天心天地生1 小时前
【bugfix】-洽谈回填的图片消息无法显示
开发语言·前端·javascript
啧不应该啊1 小时前
element plus 按需导入vue
前端·javascript·vue.js
Gungnirss1 小时前
vue中提示Parsing error: No Babel config file detected
前端·vue.js·ubuntu
梅秃头2 小时前
vue2+elementUI实现handleSelectionChange批量删除-前后端
前端·javascript·elementui