SPA 与 MPA 的比较与优化
什么是 SPA
SPA
即为 single page application 的缩写,意为单页面应用,其作为一种网页应用模型,它的主要特点有:
-
优势
-
SPA
路由跳转是基于特定的实现(如vue-router
,react-router
等前端路由),而非原生浏览器的文档跳转(navigating across documents
)。那么即可实现按需进行页面中的必要的组件级更新,而非无差别式
页面级更新。 -
基于 1 的特点,相较于
MPA
避免了不必要的整个页面重载,那么在页面切换之间的间隙更短,更能体现出web
应用的流畅
特点,因而更具有接近原生应用的 性能优势 与体验。 -
因为组件级更新的特点,那么页面中的代码复用性高于
MPA
。正是基于组件复用的特性,那么SPA
更加适应需要快速迭代的产品。 -
基于
SPA
的前端路由,使得SPA
与应用后端解耦,使得前端不再依赖于后端的路由分配。即前后端分离。
-
-
弊端
-
SPA
应用在初始时是从无状态
空白页面进入到有状态
内容页面。而搜索引擎算法的抓取结果仅限初次请求时返回页面,搜索引擎是不会等待当前SPA
进行状态
填充。那么纯粹的SPA
是不利于搜索引擎优化(SEO
)。 -
父子组件必形成耦合,有
父
才有子
。在原则上,对比开闭原则
,每一次页面迭代,都需要修改组件内部代码,有引入BUG
风险。 -
SPA
是整个应用页面,那么在未优化前端路由加载时,应用初始首屏即需要下载整个应用。这其中包含了一些用户根本不会在会话中访问的页面(但这些页面对于应用来说又是不可或缺的。)。这一点,相对于单个MPA
组件来说,SPA
更重
一点。
-
SPA 优化
SSR
基于以上特点,SPA
最大的优势就是基于 前端路由
实现组件级更新所带来的性能优势,在体验上 SPA
更为接近原生应用。但纯粹的 SPA
是从空白页面进行应用初始化。基于一般搜索引擎算法,从空白页面进行应用初始化是不利于 SEO
的。一个适应 SEO
的 SPA
必是需要通过 SSR
(即 Server-Side Rendering
)来进行应用优化,而 SSR
亦会增加了服务器的压力。基于此,SPA
要做到符合 SEO
的应用,必须要付出一些服务端的代价来换取良好的 SEO
。
这里也存在一个 SPA
特例,即 静态 页面内容(如 产品介绍页
)的 SPA
。因为静态内容的不可变性特点,那么我们可以在服务端进行静态内容预渲染(pre-render
),以此来减轻服务端不必要的即时页面服务端渲染需求的性能压力。预渲染的一种实现是使用 Chrome
浏览器的无界面 API
puppeteer 并配合 prerender-spa-plugin 来实现静态内容的预渲染。在完成预渲染之后,即可得到 有状态 的 SPA
,之后再将生成的页面部署在静态服务器上即可。此时这部分静态内容就避免了即时的 SSR
。
因为动态内容(如 用户信息页
)具有不确定性,那么这部分内容页面为了保证良好的 SEO
还是不可避免的需要 SSR
。
动态加载路由组件
SPA
是基于前端路由来实现各级组件路由,那么在未优化 SPA
应用时,应用初始化就需要加载完成所有子路由基础组件。在这一点上,未优化的 SPA
相较于 MPA
首屏需要加载更多的基础内容。不论后期,用户是否会访问一些组件页面,在首屏加载时都会进行下载。那么此处无形中增加了应用初始化成本。那么 未优化的 SPA
相较于一般的 MPA
具有更高的初始启动成本。
我们常用的 SPA
首屏优化策略是使用 动态加载路由组件。那么即在应用初始,并不加载所有的子路由组件,而是在用户访问时再下载相应的子路由组件。本质上,我们将下载子路由组件的时间均摊到各个子路由组件自身,而不是在首屏集中处理。
以下以 vue-router 为例:
常用的一种实现是 vue
下的 异步组件 配合 webpack
的 代码分割 以及 babel
转义(syntax-dynamic-import)草案的动态加载语法 import() 来实现动态加载路由组件。import() 默认在内部调用 Promise
函数,最终返回一个 Promise
对象,对于不支持 Promise
的浏览器,需要引入 es6-promise 或 promise-polyfill 或者直接引入 polyfill.io 动态引入 polyfill
来做兼容。
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
// 静态加载,不论用户是否访问该页面都在初始化应用时加载
import home from '~/components/home')
// 动态加载方式一,仅在 webpack 请求该模块的 chunk 时才会加载,即只有在用户访问时加载
// 该组件
const app = () => import(/* webpackChunkName: "app" */ '~/components/app')
export default new VueRouter({
routes: [
{
path: '/',
component: home,
children: [
{
path: '/app',
component: app
},
{
path: '/info',
/**
* 动态加载方式二
* 1. chunk 命名必须配合 webpack 中 output.chunkFileName: '[name].js' 指
* 定占位符为 `[name]` 使用
*/
component: () => import(/* webpackChunkName: "info" */ '~/components/info')
}
]
}
]
})
以上关键点在于使用 import('./module') 代替静态模块加载语法 import module from './module'
。
/* webpackChunkName: "info" */
注释用于将单个路由下所有组件都打包在同一个异步 chunk
中。即被请求的模块和其应用的所有子模块都会分离到同一个异步 chunk
中。
// webpack 相应配置为:
module.exports = {
// ...
output: {
filename; '[name].bundle.js',
+ chunkFileName: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
// ...
}
以上配置中,output.chunkFilename 用于定义非入口 chunk
的名字。这些文件名在 runtime
时生成,以便于发送 chunks
请求。基于这一点像 [name]
和 [chunkhash]
的占位符需要通过 webpack runtime
将从 chunk id
到占位符的值(该映射即是 [id]
占位符)映射到输出的 bundle
中。当任何 chunk
的占位符的值改变时,都将导致 bundle
失效。output.chunkFilename 的默认值为 [id].js
或根据 output.filename 进行推断(即将其中的 [name]
替换为 [id]
,或者直接使用 output.filename 中的 [id]
占位符。)
拓展阅读:
占位符 | 描述 |
---|---|
[name] | 表示使用 entry 中定义的名称 |
[chunkhash] | 基于单个 chunk 内容生成的 hash 值 |
[id] | 表示使用内部 chunk id |
什么是 MPA
MPA
即为 multiple page application
的缩写,意为多页面应用模型,与 SPA
对比最大的不同即是页面路由切换由原生浏览器文档跳转(navigating across documents
)控制。
-
优势:
-
因为
MPA
各个页面相互独立,那么可将每一个页面都看作一个单一的微服务
。各个页面达到相互独立与 解耦 的目的。 -
因其解耦特性,因而更加适合 前端去中心化 的复杂
web
应用。 -
因为页面互相独立的特性,那么有利于应用本地数据的模块化。
-
因为页面相互独立的特性,移除一个单页或增加一个单页不会对其他
MPA
单页造成影响。那么降低了我们页面迭代的门槛。不用担心对其他单页组件的蝴蝶效应
。 -
单个页面相互独立,且页面在初始时,就具有页面内容而非
无状态
,那么相对于SPA
,更加有利于SEO
。
-
-
弊端:
-
MPA
路由基于原生浏览器的文档跳转(navigating across documents
)。因此每一次的页面更新都是一次页面重载,这将带来巨大的重启性能消耗。 -
MPA
的前端页面与后端是一一对应,耦合的。在开发时,增加了开发成本,前后端开发进度必须统一协作。
-
与 SPA
的高性能侧重点不同的是,MPA
更加注重于页面之间的相互解耦。以页面为单位形成多个独立组件。多个页面组件构成一个完成的 web
应用。
MPA 优化
相对于 SPA
应用,MPA
应用天生具有重启性能消耗高的弊端。因为每一次的页面切换都将导致所有的组件都会被浏览器刷新,那么一些可复用组件也必须重新加载。这个行为的本质是 MPA
路由切换由浏览器的 navigating across documents
所决定的,它默认执行无差别式的页面级更新,而不管页面中的组件是否可以复用。正因具有很高的重启消耗,那么在页面切换间隙,重启整个页面将导致有较大几率出现较长时间的页面空白。在我们应用的页面没有完全加载完成之前,这个空白页面将一直持续。
为了解决 MPA
加载页面时的页面空白问题。一般借鉴原生应用的 skeleton screen
的方式来解决切换时页面空白的问题。本质上,MPA
在加载时,页面加载完全由浏览器控制,而不是像 SPA
那样可以加入前端路由来控制页面加载。因为在 MPA
中我们无法引入前端路由,也就无法实现插入类似像 SPA
那样的 loading
组件。既然无法从前端路由解决页面空白的问题,那么从切换页面时页面加载的内容着手即是成为了一个很好的切入点。
一般解决的思路是通过 SSR
来向 MPA
的初始单页组件替换为加入无状态的 skeleton screen
,使得我们在加载轻量的 skeleton screen
之后展示 skeleton
页面而非空白。继续开始加载单页组件,待单页组件页面内容完全加载完成时,页面内容将取代 skeleton screen
。
在整个过程中,因为有 skeleton screen
的存在。那么在加载单页组件期间,页面将始终呈现 skeleton screen
组件,这样大大缩减了应用单页组件初始化时的出现页面空白时间。我们之所以这么做的原因是,skeleton screen
仅仅只是一个包含页面骨架的 html
,本质上 skeleton screen
的大小远远小于 MPA
单页组件。那么加载 skeleton
的时间远远小于单个页面组件,那么我们就可以借助展示 skeleton screen
来缩短页面空白时间。
一般 skeleton screen
的实现有 page-skeleton-webpack-plugin 和 vue-skeleton-webpack-plugin。
MPA 中的 skeleton 适用与 SPA 吗
在 MPA
上我们可以使用 skeleton screen
来缩短页面加载时的空白时间,那么它是否同样可以适用于 SPA
呢?
我们应该明确的一点就是,skeleton screen
本质上是解决基于 浏览器 所控制的页面路由(而非前端路由)时的加载等待时间中的页面空白问题。在 SPA
中,页面切换除了第一次应用初始化是由浏览器控制加载应用之外,后续的所有应用页面切换都只与前端路由有关,而与浏览器路由切换机制无关,而我们所说的 skeleton screen
正是与浏览器自身的路由切换机制相关。那么即使在 SPA
中使用 skeleton screen
也 只有 在初始化应用时会出现 skeleton screen
。其实在一般情况下,在 SPA
中,我们可创建一个 loading
组件来实现类似 skeleton screen
的效果。在 SPA
加载期间展示 loading
组件表现上与 MPA
的 skeleton screen
是一致的。它们都致力于减少页面的空白时间。
基于 SPA 与 MPA 的优化
通过以上分别对于 SPA
和 MPA
的分析。不论是 MPA
还是 SPA
我们都可以实现加载时的非空白页面过渡。但是这也许还不够,下载 skeleton screen
仍然需要时间,其中还是会存在间隙。那么我们是否可以找到一个缓存应用基础架构的方式?其实我们可以通过 service-workers
来实现对应用的 app shell
缓存。对于 MPA
应用来说,我们可以缓存基础的 skeleton screen
,那么在下一次启动 MPA
时,仅需要验证是否存在更新,在不存在 app shell
更新的情况下,即可调用本地 skeleton screen
缓存,借此用来缩短应用初始化时间。相对于 SPA
来说,原理与 MPA
缓存应用基础架构本质也是相似的。
App shell
本质是 PWA
的基础应用架构,通过 service-worker
来实现对应用基础架构做本地缓存,但是 service-worker
并不应该局限于用来实现 PWA
,service-worker
与 PWA
并不是包含关系。我们亦可调用 service-worker
仅仅用来缓存 web
应用的非业务逻辑基础组件。那么基于这些对 service-worker
的分析,不论是 MPA
还是 SPA
的非业务逻辑基础组件都可以被 service-worker
缓存下来。MPA
和 SPA
本身是作为网页应用模型的存在,借用 App shell
的缓存机制,实现了对 MPA
和 SPA
的基础架构缓存优化。这里尤其需要注意的思维陷阱就是 PWA
并不等于 SPA
或者 MPA
,service-worker
与 PWA
也不是包含关系。
技术选型
针对以上的分析,我们知道了 MPA
与 SPA
最大的不同之处在于页面路由切换的方式不同。SPA
的路由切换是由前端路由来实现,MPA
的路由切换则完全依靠浏览器的路由切换机制。针对于二者不同的路由切换方式,也注定了二者在应用架构模型方面的侧重点不同。
SPA
更加注重于接近原生应用的体验,基于代码组件的可复用特性,使得开发效率和成本方面具有得天独厚的优势,那么 SPA
更加适合开发有快速迭代需求的应用。而且基于可复用组件的特点,应用虽然在初始化时的成本高于 MPA
,但其页面切换之间的成本是明显小于 MPA
的切换成本。虽然 SPA
因其架构特点导致其初始化成本高,但可以通过动态加载异步组件来显著降低 SPA
应用的初始化成本。
而对于 MPA
来说,更加注重于单个页面组件的之间的解耦,同时因为页面之间相互独立,那么使得 MPA
应用初始化成本小,但是页面重启的成本高。基于应用组件之间解耦的特性,MPA
更加适合开发大型复杂的 web
应用。在开发成本上来说,虽然代码复用性低于 SPA
,但其解耦所带来的便捷拓展性,是 SPA
无法比拟的。MPA
无论是增加还是删除页面,对于其他的单页面组件影响都很小。但是,SPA
之间存在组件复用时,也就存在了代码耦合。特别是存在复杂逻辑的组件之间拓展功能时需要更加的小心翼翼。
虽然以上简要总结了 SPA
与 MPA
之间的差异,共同点以及他们的优化方式。但是我们在技术选型时,不仅需要考虑了以上因素,而且还需要结合特定的应用场景与自身的开发条件来选择合适的应用架构。MPA
与 SPA
没有说一定是要有对应的适用应用类型方式,因为应用场景,开发条件都是必须考虑进去的选型因素。