SPA详解
SPA ( single-page application ) 仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI与用户的交互,避免页面的重新加载。
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
 - 基于上面一点,SPA相对对服务器压力小;
 - 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
 
缺点:
- 初次加载耗时多:为实现单页Web应用功能及显示效果,需要在加载页面的时候将JavaScript、CSS统一加载,部分页面按需加载;
 - 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
 - SEO难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
 
实现一个SPA
原理:
- 
监听地址栏中
hash变化驱动界面变化 - 
用
pushsate记录浏览器的历史,驱动界面发送变化 
hash 模式
核心通过监听url中的hash来进行路由跳转
// 定义 Router  
class Router {  
    constructor () {  
        this.routes = {}; // 存放路由path及callback  
        this.currentUrl = '';  
          
        // 监听路由change调用相对应的路由回调  
        window.addEventListener('load', this.refresh, false);  
        window.addEventListener('hashchange', this.refresh, false);  
    }  
      
    route(path, callback){  
        this.routes[path] = callback;  
    }  
      
    push(path) {  
        this.routes[path] && this.routes[path]()  
    }  
}  
  
// 使用 router  
window.miniRouter = new Router();  
miniRouter.route('/', () => console.log('page1'))  
miniRouter.route('/page2', () => console.log('page2'))  
  
miniRouter.push('/') // page1  
miniRouter.push('/page2') // page2  
history模式
history 模式核心借用 HTML5 history api,api 提供了丰富的 router 相关属性先了解一个几个相关的api
history.pushState浏览器历史纪录添加记录history.replaceState修改浏览器历史纪录中当前纪录history.popState当history发生变化时触发
// 定义 Router  
class Router {  
    constructor () {  
        this.routes = {};  
        this.listerPopState()  
    }  
      
    init(path) {  
        history.replaceState({path: path}, null, path);  
        this.routes[path] && this.routes[path]();  
    }  
      
    route(path, callback){  
        this.routes[path] = callback;  
    }  
      
    push(path) {  
        history.pushState({path: path}, null, path);  
        this.routes[path] && this.routes[path]();  
    }  
      
    listerPopState () {  
        window.addEventListener('popstate' , e => {  
            const path = e.state && e.state.path;  
            this.routers[path] && this.routers[path]()  
        })  
    }  
}  
  
// 使用 Router  
  
window.miniRouter = new Router();  
miniRouter.route('/', ()=> console.log('page1'))  
miniRouter.route('/page2', ()=> console.log('page2'))  
  
// 跳转  
miniRouter.push('/page2')  // page2  
如何给SPA做SEO
- 
SSR服务端渲染
将组件或页面通过服务器生成html,再返回给浏览器,如
nuxt.js - 
静态化
目前主流的静态化主要有两种:
- 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
 - 另外一种是通过WEB服务器的 
URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果 
 - 
使用
Phantomjs针对爬虫处理原理是通过
Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图
 
SPA首屏加载速度慢的解决方案
首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容
首屏加载可以说是用户体验中最重要的环节
关于计算首屏时间
利用performance.timing提供的数据:

通过DOMContentLoad或者performance来计算出首屏时间
// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
    console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime
// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
  name: "first-contentful-paint",
  entryType: "paint",
  startTime: 507.80000002123415,
  duration: 0,
};
加载慢的原因
在页面渲染的过程,导致加载速度慢的因素可能如下:
- 网络延时问题
 - 资源文件体积是否过大
 - 资源是否重复发送请求去加载了
 - 加载脚本的时候,渲染内容堵塞了
 
解决方案
部分方法可以参考:Vue性能优化详解 | QT-7274 (qblog.top)
常见的几种SPA首屏优化方式
- 减小入口文件积
 - 静态资源本地缓存
 - UI框架按需加载
 - 图片资源的压缩
 - 组件重复打包
 - 开启GZip压缩
 - 使用SSR
 
减小入口文件体积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
静态资源本地缓存
详见:浏览器缓存详解 | QT-7274 (qblog.top)
后端返回资源问题:
- 采用
HTTP缓存,设置Cache-Control,Last-Modified,Etag等响应头 - 采用
Service Worker离线缓存 
前端合理利用localStorage
UI框架按需加载
在日常使用UI框架,例如element-UI、或者antd,最好按需引入。
组件重复打包
假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载
解决方案:在webpack的config文件中,修改CommonsChunkPlugin的配置
minChunks: 3
minChunks为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素
对于所有的图片资源,我们可以进行适当的压缩
对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。