react的SPA实践
上一篇关于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不做页面刷新,只做局部更新,也就是除了你第一次打开网站的时候需要加载整个页面之外,之后的一切站内跳转都是不重载页面的,而是在当前页面进行局部刷新,达到页面切换的效果。
想象一下,假设网站原本需要两个页面a
和b
,但现在我只做一个index
,然后把a
和b
两个页面的所有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会把a
和b
这两个模块以及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实践相关推荐
- Flutter React编程范式实践
作者:闲鱼技术-匠修 Flutter Widget的设计灵感来源于React,是一款原生就立足于响应式的UI框架.本文基于Flutter特点,试图结合闲鱼在Flutter的工程应用来谈下我们对Flut ...
- 使用Typescript和React的最佳实践
by Christopher Diggins 克里斯托弗·迪金斯(Christopher Diggins) 使用Typescript和React的最佳实践 (Best practices for us ...
- React组件设计实践总结05 - 状态管理
今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案. 前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 可能是文章篇幅太长了?掘金值太低了? 还是错别字太多了? 后面静 ...
- 基于 react, redux 最佳实践构建的 2048
前段时间 React license 的问题闹的沸沸扬扬,搞得 React 社区人心惶惶,好在最终 React 团队听取了社区意见把 license 换成了 MIT.不管 React license ...
- react技术栈实践(1)
本文来自网易云社区 作者:汪洋 背景 最近开发一个全新AB测试平台,思考了下正好可以使用react技术开发. 实践前技术准备 首先遇到一个概念,redux.这货还真不好理解,大体的理解:Store包含 ...
- react技术栈实践
背景 最近开发一个全新AB测试平台,思考了下正好可以使用react技术开发. 实践前技术准备 首先遇到一个概念,redux.这货还真不好理解,大体的理解:Store包含所有数据,视图触发一个Actio ...
- React+DVA开发实践
原文链接 文档概述 本文档在前面章节简单的介绍了React和其相关的一系列技术,最后章节介绍了React+Dva开发的整套过程和基本原理,也就是将一系列框架整合的结果. 文档结构 本文档划分为以下章节 ...
- React组件库实践:React + Typescript + Less + Rollup + Storybook
背景 原先在做低代码平台的时候,刚好有搭载React组件库的需求,所以就搞了一套通用的React组件库模版.目前通过这套模板也搭建过好几个组件库. 为了让这个模板更干净和通用,我把所有和低代码相关的代 ...
- Jest + Enzyme React 组件测试实践
≈ 最近把组件测试接入到日常开发,提高了项目代码健壮性,可维护性.本人也从0到1收获了组件测试的经验. 本文总结一下最近两周 组件测试 相关的研究,包括: Jest + Enzyme 的基本介绍 Je ...
最新文章
- List集合add使用过程中出现的错误
- 云计算:革新动力并不是一把万能钥匙
- Linux下搭建asp.net运行环境
- 周鸿祎:比情怀更重要的硬件创业三定律
- 【Python】如何选择赋值和拷贝
- 【点阵液晶编程连载四】MenuGUI 菜单应用
- linux 负载高ssh连不上,关于ssh连不上问题的解决方法(必看)
- 汇编语言中 编译 连接 构建时的一些错误以及错误的修正方法(不断积累中...)
- 手工清除severe.exe病毒
- Vmware15虚拟机安装win7镜像
- C语言栈的面试题,C语言面试编程题
- LoadRunner教程(2)-LoadRunner性能测试利器
- INA266电压电流模块驱动
- Alphapose_pytorch版本环境配置Win10
- 链家网爬取深圳租房分析
- php代码输出笑脸,利用HTML5中的Canvas绘制笑脸的代码
- 推荐七大写作利器,总有一款适合你
- 保持良好的人际关系,赢得好人缘的八大诀窍
- Linux CentOS删除或重命名文件夹和文件的办法
- Android指示器的使用总结
热门文章
- 尚硅谷2020最新版宋红康JVM教程更新至中篇(java虚拟机详解,jvm从入门到精通)
- Java并发编程之CountDownLatch
- JavaScript一_HTML
- 谷歌浏览器的使用方法
- 如何标题编号自动生成_【分享】实用word知识——章节标题与自动编号
- 2018hdu个人排位赛:Stadium
- 独角兽有泡沫?Absolutely!但绝非估值泡沫
- win10 vscode搭建go语言开发环境
- Sell-In, Sell-Through, Sell Out都神马意思?江湖黑话?
- 互联网早报:腾讯小微推新功能,支持在微信中进行硬件管理和音乐分享....