介绍
是一种只需要将单个页面加载到服务器之中的web应用程序。
当浏览器向服务器发出第一个请求时,服务器会返回一个index.html文件,它所需的js,css等会在显示时统一加载,部分页面需要时加载。
优点
良好的交互式体验 | 用户无需刷新页面,获取数据通过异步ajax获取,页面显示流畅。 |
---|---|
良好的前后端分离模式(MVVM) | 减轻服务端压力。服务器只需要输出数据就可以,不用管逻辑和页面展示,吞吐能力会提高几倍。 |
开发效率高 | 共用同一套后端程序代码,不用修改就可用于web界面,手机和平板等客户端设备。 |
缺点
不利于SEO优化 | 用户无需刷新页面,获取数据通过异步ajax获取,页面显示流畅。 |
---|---|
需自行维护页面切换逻辑 | 由于单页应用在一个页面中显示,所以不可以使用浏览器自带的前进后退功能,想要实现页面切换需要自己进行管理。 |
首屏加载过慢(初次加载耗时多) | 为了实现单页web应用功能及展示效果,在页面初始化的时候就会将js,css等统一加载,部分页面在需要时加载,这样导致了首屏加载慢。 解决方法: ①使用路由懒加载 ②开启Gzip压缩 ③使用webpack的externals属性把不需要的库文件分离出去,减少打包后文件的大小 ④使用服务端渲染(SSR)方案 |
难点
SPA 是一组高度耦合的页面(页面耦合)
SPA 方案要求 App 内所有页面位于同一服务实例上, 也就是说处理 SPA 页面请求的每个实例都必须拥有 App 内所有页面的信息, 这一信息通常是页面组件的声明。
这是因为 SPA 要求页面切换不发生浏览器跳转。设想操作流程『打开页面A -> pushState 到页面 B -> 刷新 -> 返回』,这时浏览器不会重新加载 A,而只是触发 popstate 事件给 B。 因此对于任意页面 A,点出到的任意页面 B,B 页面反过来都需要 A 的信息,当然页面 A 也知道页面 B 的信息,因此任意两个有跳转关系的页面,都需要相互了解对方的信息,或引用对方组件。
这样相互耦合的一组页面,就构成了一个 SPA 方案的 Web App。 这样的 App 内所有页面都不再能够『独立部署』,因此也不能独立迭代演化。 这往往意味着它们的开发调试、前端编译、部署过程都是耦合在一起的, 这些都是 SPA 方案带来的成本:
- 开发依赖:因为要能够打开一个页面必须引用对应的组件,这些组件在开发和调试阶段一定需要绑在一起。如果两个页面涉及到业务会跨团队,无疑会增加很多成本。
- 编译依赖:考虑使用 MD5 戳的编译方法,相互引用的一组文件必须一起编译上线,这会降低协作效率因为它们本属于不同的业务或团队。当然也可以不使用 MD5 戳并分别上线,动态调整引用关系,这样的问题在于无法平衡 HTTP 缓存和快速生效的矛盾。
此外,由于浏览器的同源策略,一个 Web App 被限制共享一个域名。 否则在富交互的场景下跨域将会是一个非常复杂的问题, 当然如果你愿意使用 JSONP 这么不安全的接口另当别论。
强组件化容易陷入技术竖井(技术封闭)
SPA 方案伴随着强组件化方案,容易陷入封闭的技术竖井。 换句话说就是容易一条路走到黑,失去 Web 应有的架构优势。 这是因为异步页面拥有异步的天性。 浏览器重新渲染一个页面时, 全局变量、定时器、事件监听器都会初始化为全新的,这是『刷新』的含义。 而异步页面却不然:
- 异步页面间,全局变量、定时器是共享的,没有托管很容易乱掉。
- 异步页面的
<script>
之间,执行顺序是不保证的,没有托管极易出错。
因此绝大多数 SPA 方案都不会让你直接插入 <script>
来编写业务代码, 与此相反,会提供类似 模块、组件 之类的概念来托管一切。 你可能需要存储、需要网络、需要路由、需要通信,你需要把所有 Web API 都封装一遍。
这是各种 SPA 框架全家桶背后的逻辑。 最终业务的运行环境不再是浏览器,而是这套组件化方案。 而社区的组件化方案不会像 Web 标准一样去迭代,也不一定向下兼容,这在版本升级或框架迁移时会产生非常大的成本。
URL 不再能定位资源(URL 弱化,可访问性差)
对于原始 Web 页面,URL 不仅能定位资源的页面,甚至还能定位到页面种的具体浏览位置。 但是在 SPA 里页面由 SPA 框架渲染,经典的配置是对于所有 URL 都返回同一个资源, 浏览器端脚本通过 location.href
渲染不同的页面。所以这有啥问题?
- 首屏性能差。浏览器端渲染,在页面下载过程中是白屏的;浏览器直接渲染页面是流式的,下载多少渲染多少。
- 机器不可读。搜索引擎、CLI 用户代理等不支持脚本的用户代理无法解析页面,因为不同 URL 页面内容是一样的。
- 无法定位浏览位置。因为浏览器不再托管整页渲染也无法记录和恢复浏览位置。
可以看到不仅链接(URL)的概念被弱化,而且可访问性天生就很差。 比较先进的 SPA 框架会提供服务器端渲染(SSR)来补救,但对架构有额外的要求: 前后端都可以进行页面渲染,通常会要求前后端同构。
既然浏览器不再记录浏览位置,就需要 SPA 框架来实现。但由于 Web App 内可以局部地渲染任何一块内容。 页面的概念在 SPA 中就变得很模糊, 而树状 DOM 结构确实无法映射到线性的 URL 结构(除非你打算继续破坏 REST 把数据塞到 URL 里)。 因此即使花费大力气去做,也无法实现完美的浏览位置记录。
History API 不完备(体验不稳定)
History API 是指浏览器提供的浏览历史相关的 BOM API, 包括 pushState 方法,popstate 事件,history.state 属性等。 先不提在某些浏览器下 API 缺失的问题,在当前标准和主流浏览器如 Safari 和 Chrome 中的表现就有许多问题。 这些问题会导致非常不稳定的体验,例如前进后退无效,URL 与页面内容不对应、甚至出现交互没有响应的情况。 总之对于一个追求极致体验的 Web App 来讲是无法接受的。下面罗列一些笔者遇到过的:
- 同步渲染的页面资源 加载会延迟 popstate 事件。这使得页面未加载完时可以通过 pushState 点出但无法返回。
- PopStateEvent.state 总是等于
history.state
。因此当 popstate 事件发生时,谁都无法获取被 pop 出的 state,这让 state 几乎不可用。 - popstate 事件处理函数中无法区分是前进还是后退。考虑刷新页面的场景不能只存储为变量,只能存储在
sessionStorage
中,但这是同步调用会增加路由的延迟,而且需要维护配额不是一个简单可靠的方案。 - 有些高端浏览器(比如某些华为内置浏览器)
history.state
,但支持 pushState 和 popstate。 - iOS 下所有浏览器中,设置 scrollRestoration 为
manual
会使得手势返回时页面卡 1s,这让恢复浏览位置也不存在简单可靠的方案。 - 没有 URL 变化事件。在 pushState/replaceState 时不会触发 popstate 事件。因此没有统一的 URL 变化事件,通常需要一个路由工具来包装这些不一致。
- 手势前进/返回的行为在标准中没有定义。这意味着有些浏览器会做动画,有些不会。因为这些动画没有定义任何 API 所以 SPA 框架接管页面切换动画无法保证一致的体验。
Referer 的语义不再是来源(日志错误)
在 Web 时代,Referer HTTP Header 用来标识一个请求的来源,主要用于日志、统计和缓存优化。 典型的 SPA 框架会破坏 Referer 的语义。
SPA 中页面跳转分两种情况:一种是用户与 DOM 交互由脚本 pushState 来改变 URL; 另一种是用户与浏览器交互比如前进后退按钮或手势,此时浏览器触发 popstate 事件 来通知脚本。 对于后一种情况,popstate 事件发生时页面 URL 已经发生变化,此时才会通知到 SPA 框架载入下一页内容。 因此这时发出的请求 Referer 头的值一定 是当前页的 URL 而不是来源页的 URL。
参考链接 https://juejin.im/entry/5c9db118f265da60ee12f546 https://github.com/haizlin/fe-interview/issues/322