上一篇关于react简介与入门的文章【写了个假react】,文章最后介绍了通过组件的嵌套来实现页面构建的思路,这是react组件化的基本实现方式,依靠这样“单纯”的嵌套关系,我们可以最终构建一个可用的页面。

但是,仅仅依靠这样的方式,在构建比较复杂的web应用的时候是不够的。假设你是开发一个多页面网站,如果你每个页面都这么从零开始搭一套react架构,一方面操作繁琐,一方面体积冗余,而且还得考虑很多其他的问题,比如如何改造原有的开发脚手架以使其适用多页面,不好玩。

大纲

针对这样的问题,我们提倡使用 SPA 架构来搭建你的应用,这一篇我们将了解以下内容:

  • SPA介绍
  • presentational & container components
  • react+react-router实现SPA
  • SPA带来的问题以及解决方案探索

SPA

单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。
—— 《百度百科》

SPA 的概念早已有之,简单说来就是:不管你这个网站有多少页面,我都给你整到一个页面里去。

SPA不做页面刷新,只做局部更新,也就是除了你第一次打开网站的时候需要加载整个页面之外,之后的一切站内跳转都是不重载页面的,而是在当前页面进行局部刷新,达到页面切换的效果。

想象一下,假设网站原本需要两个页面ab,但现在我只做一个index,然后把ab两个页面的所有html片段都写到index里去,显示的时候,通过js来判断当前的url,如果是/a,我就只显示原本属于a的html片段;同理,如果是/b,我就显示b的html片段。

a.html:

<!DOCTYPE html>
<html>
<head><title>a</title>
</head>
<body><div>a.html</div>
</body>
</html>

b.html:

<!DOCTYPE html>
<html>
<head><title>b</title>
</head>
<body><div>b.html</div>
</body>
</html>

index.html:

<!DOCTYPE html>
<html>
<head><title>index</title>
</head>
<body><div class="page a z-crt">a.html</div><div class="page b">b.html</div>
</body>
</html>

但是url的改变一般都是会引起页面跳转的,想要根据路由变化来展示不同的内容,同时页面又不跳转,这是怎么做到的?

我们知道,在url中,#号之后的内容是不会引起页面跳转的,所以我们利用这一点来实现,当url是#/a的时候,显示a的html片段,同理#/b则显示b的html片段,这样一来,前端就有了自己的路由控制,可以来实现前端自己的 多页面

这种方式我们称为 前端路由

presentational & container components

在使用前端路由之前,我们先来探讨一下,在react的开发中,什么是页面?

或者说,如果去界定一个页面?

我们知道,react是基于组件(component)来构建页面的,从这个定义上来讲,其实 页面只不过是一个更大的组件 ,它可以用来容纳和组织其他各个组件,从而编织出一个完整可用的页面。

但是这个所谓的 大组件 怎么看都与其他组件有那么些不一样的地方,它的目的不是被复用,而是负责为其他组件处理并注入数据,这样的组件我们称为 容器组件 (container component)。

但请注意,容器组件并不一定就是页面,它可以是各种粒度的组件组合。

相对应的,一个纯粹接收外部数据(不作处理)并只负责展示的组件,我们称为 展示组件 (presentational component)。

展示组件只负责展示而不对数据做任何处理,因此它更容易被各种业务场景集成和复用。

我们还是以上节内容当中的“时钟”例子来做演示:

var Clock = React.createClass({getInitialState: function() {return {time: new Date().toString()}},render: function() {var _t = thisreturn <div className="m-clock">{_t.props.city}: {_t.state.time}</div>},componentDidMount: function() {var _t = thisvar timer = setInterval(function () {if (_t.isMounted()) {_t.setState({time: new Date().toString()})} else {clearInterval(timer)}}, 1000)}
})

这里为了突出问题,我们把上一节里偷懒的部分补回来,把时间的展示格式给美化一下:

var Clock = React.createClass({getInitialState: function() {return {time: new Date()}},render: function() {var _t = thisvar time = _t.state.timereturn <div className="m-clock">{time.getHours()}:{time.getMinutes()}:{time.getSeconds()}</div>},componentDidMount: function() {var _t = thisvar timer = setInterval(function () {if (_t.isMounted()) {_t.setState({time: new Date()})} else {clearInterval(timer)}}, 1000)}
})

ok,好看多了(并没有)。

目前为止,这个组件是没有什么问题的,我们把组件的逻辑和界面绑定在了一个组件里。

产品经理看了之后,让你把时间格式补全,不足两位数的前面补零。

你吐槽了几句然后乖乖去改,随手写了个fix2方法,然后修改jsx{_t.fix2(time.getHours())}

到了下午,产品经理又走了过来,让你把时间格式显示为12小时制,不要24。

你又吐槽了几句然后乖乖去改,再一次修改jsx{_t.fix2(time.getHours() > 12 ? time.getHours() - 12 : time.getHours())}

jsx内心os:鬼知道我都经历了什么。。。

Clock内心os:你为什么不直接把时分秒三个值计算完再给我?这样一点都不酷!

是啊,这样的jsx很丑。事实上,当只有逻辑改变的时候,我们应该尽量不要牵涉到界面;同理,当只有界面改变的时候,我们也不该去影响逻辑。但是当组件集成了这两者的时候,我们就很难避免这类情况发生,比如不经意间就在jsx里写计算和装饰的逻辑了。

为了更好地实践上面的理论,我们需要把逻辑和视图分开,写成两个部分,一部分负责逻辑计算(container component),一部分负责展示(presentational component),最后,逻辑计算部分会引入展示部分,并且将展示部分需要的参数注入:

// /Clock/Clock.jsx
import React from 'react'var Clock = function(props) {return <div className="m-clock">{props.hours}:{props.minutes}:{props.seconds}</div>
}export default Clock
// /Clock/Index.jsx
import React from 'react'
import Clock from './Clock'var ClockContainer = React.createClass({getInitialState: function() {return {time: new Date()}},render: function() {var _t = thisvar time = _t.state.timevar hours = time.getHours()var minutes = time.getMinutes()var seconds = time.getSeconds()hours = _t.fix2(_t.limit(hours))minutes = _t.fix2(minutes)seconds = _t.fix2(seconds)return <Clock hours={hours} minutes={minutes} seconds={seconds} />},componentDidMount: function() {var _t = thisvar timer = setInterval(function () {if (_t.isMounted()) {_t.setState({time: new Date()})} else {clearInterval(timer)}}, 1000)},limit: function(num) {if (num > 12) {return num - 12}return num},fix2: function(num) {if (num < 10) {return '0' + num}return num}
})export default ClockContainer

以上,我们便实现了关注点分离,可以看到展示组件非常简洁,而容器组件也只关注逻辑计算。

当然,这个例子还是看不出分离的必要性,因为目前的展示组件结构非常简单,而当这两者都比较复杂的时候,分离的作用就突显出来了。

注意,我们使用Clock组件的时候,是引入容器组件,而不是直接引入展示组件。这里有一点小技巧,为了避免组件太零碎,我们把两个组件都放在一个同名的文件夹Clock中,展示组件用Clock.jsx命名,而容器组件则以Index.jsx命名,这样一来,在页面中引用Clock的时候,我们就可以直接使用import Clock from './components/Clock',看着像引用了Clock.jsx,其实是默认引用./components/Clock/Index.jsx

以上,就是关于 容器组件展示组件 概念的介绍。我们回到一开始的问题,什么是页面?

没错,页面就是一个炒鸡大的 容器组件

理论上来说,你大可以各种拼装 容器组件,比如页面Index可以只有:

import React from 'react'require('../../../style/index')var Index = function(props) {return <div className="g-index"><Header /><Content /><Footer /></div>
}export default Index

然后再分别去构建<Header /><Content /><Footer />三部分,这样一步步细分下去。

但是,深层次的嵌套可能会造成通讯的困难,我们知道子组件之间通讯是需要依赖父组件环境的,也就是说,如果一个子组件处在很深层的位置上,那么它与另外一个处于其他层次的组件通讯就十分的困难了,需要找到他们两者之间的公共父组件,然后再从父组件开始一层层把handler通过props传递下来。

此刻的你:强颜欢笑.jpg

所以,我们在设计页面的时候,应该尽量避免深层次的嵌套关系,尽量保持扁平。

这样对大家都好 :)

当然,这种问题也是有解决方法的,比如可以使用一个event bus(事件总线)来实现,这也是vue采用的做法,可以参考;另外,也可以借助redux来实现,这是后话了。

react-router

好吧,话题好像跑到了一个很远的地方,回到我们的SPA。

讲页面之前我们讲到了前端路由,react搭配react-router可以很轻易地实现。

使用react集成router很简单,直接installreact-router即可:

import React, { Component, PropTypes } from 'react'
import ReactDom from 'react-dom'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'import Index from './containers/Index'
import About from './containers/About'ReactDom.render(<Router history={hashHistory}><Route path="/" component={Index}/><Route path="/about" component={About}/></Router>,document.getElementById('root')
)

每个Route就是一个路由规则,path对应路径,component即对应要显示的容器组件,也就是我们前面说的页面。所以现在我们访问/,可以看到链接自动变成了访问/#/,同时显示Index的内容,然后我们访问/#/about,会发现页面变成了About的内容。

而且页面并没有跳转!

(装作很惊讶的样子)

好吧,如果你已经开始使用react,那么你早该用上router了。

我当前使用的版本是3.0.2,它会直接在匹配的component的props中注入一个router对象,所以我们可以在component中直接调用:

const Index = React.createClass({render: function() {return <div className="g-index">...</div>},componentDidMount: function() {var _t = thisvar router = _t.props.routersetTimeout(function () {router.push({pathname: '/about'})}, 2000)}
})

上面实现的是,在进入index页面2s后,页面自动跳转到about页面,是不是很简单?

注意:react-router的api设计有变动,在之前的版本,可能还需要配合withRouter来使用,如果你发现本教程使用的方法不起作用,请根据自己当前使用的版本,对应查阅官方文档。

除了使用push之外,replace也是比较常用的方法,在这里我们不多做介绍,一切用法,看文档即可,因为很简单。我们接下来要着重讲的是,使用SPA要注意的事项。

SPA带来的问题以及解决方案探索

我们知道,SPA最终是把所有页面集合到了一个页面当中,那么,就css而言,我们如何做到页面Index与页面About的样式不会互相干扰?首页体积会不会变得很大?首屏的加载时间是不是会变得很长?SEO怎么做?

我们总结一下,问题大概有以下几点:

  • 全局污染
  • SEO
  • 体积过大,加载缓慢

我们探讨一下怎么解决。

css规划

我们知道,使用webpack打包后的代码,如果没有使用插件分离的话,我们的css是会打包到js当中的,这样会使得js体积非常庞大,所以出于性能考虑,我们应该把css分离出来,通过使用extract-text-webpack-plugin可以实现。

这个webpack插件会把所有css都打包到一个独立的文件当中。

注意,是一个!一个!!!

但是我们明明有那么多个页面,你这样全塞进去我怎么放心?

所以我们需要一些小技巧,来规避css互相影响的问题。

如果你认真在看我上面的演示代码,在看到页面容器组件的时候,你一定会留意到,我在每个组件的最外层元素,都会定义一个根className,再看一遍:

import React from 'react'require('../../../style/index')var Index = function(props) {return <div className="g-index"><Header /><Content /><Footer /></div>
}export default Index

g-index是这个页面的根class,也就是说,凡是属于这个页面的css内容,都必须包裹在它内部,这里粗略贴一下我的index.scss

.g-index {.g-hd {.m-nav {...}}.g-bd {.list {...}}.g-ft {.m-copy_right {...}}
}

是不是很粗略?嗯,能理解就好。

关于css模块化的内容,有兴趣的可以看一下我的另一篇文章【从css谈模块化】。

简而言之,就是通过命名空间的方式来隔离这些样式,使得各个页面之间不会互相造成影响。

SEO

这是个比较专业的工作。

由于所有的内容都集中在了一个页面里,百度爬虫能抓取到的页面就变少了;由于页面是由js动态构建的,数据是通过api异步获取的,而百度搜索引擎目前还不支持解析js,所以页面在爬虫眼里是没有内容的。

……

以上一切决定了SEO优化工作的困难重重。

讲道理的话,这个锅也不该是SPA来背,这是由前后端的通讯方式决定的,一旦你决定走api的方式来跟后端打交道(前后端分离的一步),你的页面就不会有太多的静态内容输出。

所以解决这个问题,关键还是在静态内容输出上。

最简单的方式,就是通过后端模板引擎在首页做静态内容输出,其他页面都按照原来的方式走api;比较复杂的,可以考虑使用 同构应用,让js构建的页面也可以在后端完成渲染,最后输出静态页面。

但是以上方案实现起来都比较麻烦,特别是后者,对开发者能力要求比较高。

所以,我们建议,在采用SPA方式之前,先问过你老板意见。。。

如果老板不答应的话,建议换老板。

……

好吧,开玩笑的,自己权衡吧。

code splitting

当我们决定使用SPA的方式来承载一整个网站的时候,意味着整个网站的体积都集中在了一个页面上,那么,我们的首屏加载时间不可避免的会被拖慢,原本秒开的首页,现在需要2~5s不等。

要是被老板发现,这个季度的kpi怕是要狗带。

心里已经开始怀念起以前的多页面了。

不怕,webpack的 code splitting 可以解决这个问题!

code splitting 即代码分割,webpack支持将文件分割成若干份,然后异步地加载到页面上来。

require.ensure(['a', 'b'], function() {var a = require('a')var b = require('b')
})

通过使用require.ensure的方式,webpack会把ab这两个模块以及callback里面的代码,单独打包成独立的资源文件,然后在运行的时候通过jsonp的方式加载回来。

回头看我们的路由:

import React, { Component, PropTypes } from 'react'
import ReactDom from 'react-dom'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'import Index from './containers/Index'
import About from './containers/About'ReactDom.render(<Router history={hashHistory}><Route path="/" component={Index}/><Route path="/about" component={About}/></Router>,document.getElementById('root')
)

用户进入页面的时候,除了首页Index需要快速呈现之外,其他的路由都不需要第一时间加载,幸运的是,react-router允许我们使用回调的方式来载入页面!

那么我们可以稍微改造一下:

import React, { Component, PropTypes } from 'react'
import ReactDom from 'react-dom'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'import Index from './containers/Index'ReactDom.render(<Router history={hashHistory}><Route path="/" component={Index}/><Route path="/index" getComponent={(nextState, cb) => {require.ensure(['./containers/About'], function(require) {var About = require('./containers/About').defaultcb(null, About)})}}/></Router>,document.getElementById('root')
)

这样一组合,就实现了我们所期待的按需加载,同时有效减少了首屏应用的体积,达到加速首屏的目的。

完美!

……

……

……

了吗?

不知道,也许还有其他问题,欢迎补充。

总结

使用SPA可以提供一种更好的浏览体验,由于它是无跳转刷新的,所以我们可以更好来做全站的数据共享,比如跨页面的数据共享和通讯;还可以实现页面切换时候的过渡效果,比如淡入淡出;

……

这些在传统的多页面里都是很难实现的。

react的SPA实践相关推荐

  1. Flutter React编程范式实践

    作者:闲鱼技术-匠修 Flutter Widget的设计灵感来源于React,是一款原生就立足于响应式的UI框架.本文基于Flutter特点,试图结合闲鱼在Flutter的工程应用来谈下我们对Flut ...

  2. 使用Typescript和React的最佳实践

    by Christopher Diggins 克里斯托弗·迪金斯(Christopher Diggins) 使用Typescript和React的最佳实践 (Best practices for us ...

  3. React组件设计实践总结05 - 状态管理

    今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案. 前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 可能是文章篇幅太长了?掘金值太低了? 还是错别字太多了? 后面静 ...

  4. 基于 react, redux 最佳实践构建的 2048

    前段时间 React license 的问题闹的沸沸扬扬,搞得 React 社区人心惶惶,好在最终 React 团队听取了社区意见把 license 换成了 MIT.不管 React license ...

  5. react技术栈实践(1)

    本文来自网易云社区 作者:汪洋 背景 最近开发一个全新AB测试平台,思考了下正好可以使用react技术开发. 实践前技术准备 首先遇到一个概念,redux.这货还真不好理解,大体的理解:Store包含 ...

  6. react技术栈实践

    背景 最近开发一个全新AB测试平台,思考了下正好可以使用react技术开发. 实践前技术准备 首先遇到一个概念,redux.这货还真不好理解,大体的理解:Store包含所有数据,视图触发一个Actio ...

  7. React+DVA开发实践

    原文链接 文档概述 本文档在前面章节简单的介绍了React和其相关的一系列技术,最后章节介绍了React+Dva开发的整套过程和基本原理,也就是将一系列框架整合的结果. 文档结构 本文档划分为以下章节 ...

  8. React组件库实践:React + Typescript + Less + Rollup + Storybook

    背景 原先在做低代码平台的时候,刚好有搭载React组件库的需求,所以就搞了一套通用的React组件库模版.目前通过这套模板也搭建过好几个组件库. 为了让这个模板更干净和通用,我把所有和低代码相关的代 ...

  9. Jest + Enzyme React 组件测试实践

    ≈ 最近把组件测试接入到日常开发,提高了项目代码健壮性,可维护性.本人也从0到1收获了组件测试的经验. 本文总结一下最近两周 组件测试 相关的研究,包括: Jest + Enzyme 的基本介绍 Je ...

最新文章

  1. List集合add使用过程中出现的错误
  2. 云计算:革新动力并不是一把万能钥匙
  3. Linux下搭建asp.net运行环境
  4. 周鸿祎:比情怀更重要的硬件创业三定律
  5. 【Python】如何选择赋值和拷贝
  6. 【点阵液晶编程连载四】MenuGUI 菜单应用
  7. linux 负载高ssh连不上,关于ssh连不上问题的解决方法(必看)
  8. 汇编语言中 编译 连接 构建时的一些错误以及错误的修正方法(不断积累中...)
  9. 手工清除severe.exe病毒
  10. Vmware15虚拟机安装win7镜像
  11. C语言栈的面试题,C语言面试编程题
  12. LoadRunner教程(2)-LoadRunner性能测试利器
  13. INA266电压电流模块驱动
  14. Alphapose_pytorch版本环境配置Win10
  15. 链家网爬取深圳租房分析
  16. php代码输出笑脸,利用HTML5中的Canvas绘制笑脸的代码
  17. 推荐七大写作利器,总有一款适合你
  18. 保持良好的人际关系,赢得好人缘的八大诀窍
  19. Linux CentOS删除或重命名文件夹和文件的办法
  20. Android指示器的使用总结

热门文章

  1. 尚硅谷2020最新版宋红康JVM教程更新至中篇(java虚拟机详解,jvm从入门到精通)
  2. Java并发编程之CountDownLatch
  3. JavaScript一_HTML
  4. 谷歌浏览器的使用方法
  5. 如何标题编号自动生成_【分享】实用word知识——章节标题与自动编号
  6. 2018hdu个人排位赛:Stadium
  7. 独角兽有泡沫?Absolutely!但绝非估值泡沫
  8. win10 vscode搭建go语言开发环境
  9. Sell-In, Sell-Through, Sell Out都神马意思?江湖黑话?
  10. 互联网早报:腾讯小微推新功能,支持在微信中进行硬件管理和音乐分享....