AJAX 单页面应用的两种实现思路

现在,单页面应用已经是一种趋势,这不仅能提升用户体验,还能降低服务器资源的损耗,也是 Web App 与原生 App 一战的最大资本!

在我们还无法完全享受 fetch API 带给我们的便利时,我们的单页面开发的根基仍旧是 AJAX。当我们知道了 AJAX 这个东西后,感觉前路一片光明,但是真正用起来就会发现还有很多问题是我们需要考虑的。

本文我会首先介绍一下我所理解的前后端分离,然后我们介绍 AJAX 实现单页面应用的两种思路及其对比。 

前后端分离

做为一个在学校一直和 CMS 打交道,同时还要负责数据库、服务器运维的程序员,我是深深体会过“上古时代” 的前后端耦合带来的痛苦的。所幸的是,前端、后端都是自己一个人做,也就不需要去和前端或者后端撕逼了。

上古时代的撕逼

小前:诶,后端,我的页面有代码更新了,我把新代码传给你,你帮我更新一下后端的模板。 
小后:更你妹,你今天都™叫我更新十多次了,还要我更新?没得商量,100块一次,再后面的每次修改加价 20! 
小前:咱两谁跟谁呀,谈钱多伤感情呀! 
小后:谈感情才伤钱,去去去,自己花点时间学者写我们后端的代码,我可以教你,200 包会! 
小前:滚!都赖设计,今天都改了十多次了。 
小设:啊,这十多次里有七八次是产品把需求改了,能赖我吗? 
小汪:改点需求怎么了,老大说了要让产品做到极致,那就得改。你们是在用代码改变世界,你想让用户边用你的东西边骂你吗?还不快利索点改! 
……

而 AJAX 和 Node 的出现于流行则让整个 Web 开发步入了 “大前端” 时代。网上一大片关于现在前后端分离与 “大前端” 趋势的文章,而想真的尝到这些甜头,自己实践就是必须的了,这里不说太多。

这里先明确一下,下面要讲的内容里的 AJAX 单页面应用的架构是这样(为了方便,这里明确了技术栈每一项的方向,真实的开发可以自己选择语言和数据库):

  1. 后端使用 Java + MySQL 为公司内网的服务器提供内网数据 API,供内网其他 Web 服务器调取。
  2. 前端编写 Node 服务器,模板渲染,吐出首屏,路由管理,以及提供直接面向浏览器的数据 API。
  3. 再靠前一点,使用 Apache 或 Nginx 做负载均衡,转发请求到内网的其他服务器上。
  4. 浏览器端只有在首屏是接收服务器返回的整个页面,之后全部采用 AJAX 来进行数据的更新,利用服务器端 API 返回的数据进行模板渲染,达到页面的更新。

这里有几个问题可以延伸去思考:

  1. 由服务器端进行首屏渲染的好处
  2. 这里面可能存在的数据安全问题?如何避免?

就这样,大家各司其职,前端利用 JavaScript + Node 入侵了服务器端,后端的工作变得更加专一,前端的控制力变得更加强。虽然前端的任务似乎加重了,但是整个开发的效率则是大大提升,前后端唯一需要耦合的就是数据 API 的标准规范!

今天我们主要目标是前端使用 AJAX 进行单页面开发这一环。说到 AJAX 就脱离不了数据 API,网上有着许多免费、公开的的 API 服务提供,当然也可以换一种思路:拦截 AJAX 请求,返回假数据。很幸运,后面那种思路已经有 “轮子” 帮我们做了,这里选择 Mock.js 进行 AJAX 请求的拦截与特定模板假数据的生成。

思路1:url hash + hashChange 事件

页面不刷新而带来 url 变化我们最先想到的肯定就是 url hash 了。我们使用 location.hash 可以轻松的访问与变更 hash 值。

至于 hash 值变动带来页面可能的上下闪动(页面上可能有对应 hash 值 id 的元素),我们只需要禁用锚点点击的默认事件就行。

hash 值的变动同时还会触发全局对象上的 hashChange 事件,在这个事件里我们就能做很多事情了。我们在这个事件阶段需要做的就是依照 hash 值得变动,解析 url 之后,向对应的服务器端 API 发起 AJAX 请求获得数据更新页面。

首先我们来封装一下 AJAX 请求生成器(点击链接后面链接查看源码):ajax.js

准备好首屏页面 index.html(这里简单起见,没有使用模板引擎进行模板+数据的渲染)

 
 
  1. <a class="ajax-anchor" data-href="abc" href="/abc">#abc</a>
  2. <a class="ajax-anchor" data-href="def" href="/def">#def</a>
  3. <a class="ajax-anchor" data-href="hij" href="/hij">#hij</a>
  4. <div id="contariner">
  5. 初始数据!
  6. </div>

然后利用 Mock.js 进行 AJAX 拦截,提供假数据模板:

 
 
  1. Mock.mock(/http:\/\/yangfch3\.com(\/\w+)*\?[\w^\w]*/, {
  2. "array|+1": [
  3. "AMD",
  4. "CMD",
  5. "UMD"
  6. ]
  7. });

禁用 AJAX 请求锚点的默认点击事件(用到了 ES6 的特性,在实际使用过程中请考虑兼容性)

 
 
  1. var ajaxAnchors = document.querySelectorAll('.ajax-anchor');
  2. var contariner = document.querySelector('#contariner');
  3. window.addEventListener('click', function(e) {
  4. if ([...ajaxAnchors].indexOf(e.target) > -1) {
  5. e.preventDefault();
  6. location.hash = e.target.dataset['href'];
  7. }
  8. }, false);

使用 hashChange 事件来触发请求

 
 
  1. var callback = function(responseText, status, xhr) {
  2. contariner.innerHTML = responseText;
  3. };
  4. window.addEventListener('hashchange', function(e) {
  5. var api = 'https://api.yangfch3.com?q=' + location.hash.substr(1);
  6. new Ajax(api, callback);
  7. }, false)

现在我们,点击对应的链接,页面只进行了局部的数据更新,并且我们点击浏览器后退、前进按钮可以恢复之前的页面状态!

浏览器的状态缓存机制(back-forward cache)让我们能在不做任何处理的情况下回到或前进到某一状态。

如果需要在用户每次后退进入或前进进入时页面做出相应的响应,则可以监听 pageshow 和 pahehide 事件进行相应的处理!

pageshow 会在当前页面加载完后、点击浏览器后退/前进按钮重新进入当前页时触发(问题:调用 history 后退/前进 API 时会不会触发? -会);pagehide 在浏览器卸载页面的时候触发,而且是在 unload 事件之前触发

pageshow 与 pagehide 事件对象 persisted 属性可以用于检测当前页是否是由 BFCache 载入。

现在我们总结一下这个方案的优点:

  1. 实现简单
  2. 符合我们的一般思路,兼容性也强
  3. 状态的回退与前进十分方便

那么缺点呢?或者说在某些情境下存在的缺点。

直说吧,这套方案在我们的页面内容需要被搜索引擎收录的时候存在缺陷。搜索引擎收录爬虫在到达某个地址后不会执行页面的 JS,收录时不会像我们的浏览器一样先发起一个 Ajax 请求生成完整内容再收录,这就对网站的 SEO(如果需要的话)带来了不便。

网上有着这个问题的探讨,例如以下文章:

  1. 用 PhantomJS 来给 AJAX 站点做 SEO 优化

  2. 单页面架构的 SEO

  3. 常见搜索引擎 UA

基本思路

  1. 后端:准备两套服务器代码,一套给 AJAX 单页面应用用的数据服务器,一套专门给搜索引擎爬虫用的 旁路渲染服务器(提供的是完整的对应页面的 HTML 代码)。

  2. 后端接入层:一般是 Ngnix 会 Apache,根据请求的 UA,判断请求来自用户还是引擎爬虫,分流至上面后端的某台服务器上。

  3. 浏览器端:给爬虫用的 <a> 的 href 使用跳转型链接,这样爬虫遇到这个链接时才会继续跳转、深爬,爬虫遇到 #xxx 这样的 href 是不理会的;我们的 JavaScript 代码则禁用这些跳转链接的默认行为,代之为变更 hash值,使页面无需刷新。说通俗点就是:给爬虫看的是一套,对用户做的是另一套!

Google 当然也是考虑到了这一点的,所以提出了 #! 方案。

搜索引擎爬虫虽然不会去对你的 #xxx 做出例会,但是能够智能地识别 #!xxx 这样的 href,转化为请求 ?_escaped_fragment=xxx,你需要做的就是在服务器上准备好 ?_escaped_fragment=xxx 对应的 HTML 代码,就能被搜索引擎收录了。

# #! 结构对于程序员来说还是比较容易接受的,但是对于需要直观的链接用于记忆的站点来说就不那么友好了。

有些站点是 abc.com/#/xxx/yyy,有些是 abc.com/#xxx/yyy,还有 abc.com/#!/xxx/yyy 、abc.com/#!xxx/yyy 这样的,同时输入网址时,还需要 shift + 数组组合输入,不方便!

例如以前 twitter 的 https://twitter.com/#!/yangfch3,引来了用户的大量抱怨。

当然,如果你的单页面应用是无需 SEO 的话(例如后台管理界面),那么事情就相对简单一些了!

下面我们开始介绍 Ajax 单页面应用的第二种实现思路,开始逃离 # 和 #!

思路2:histroy API + popstate 事件

有没有一种方案,能够:

  1. 实现页面 url 的变化
  2. 同时不会引起页面刷新
  3. 并且无需采用 # 或 #! 结构,页面的 url 是直观的、贴近用户平时习惯的

很幸运,我们能找到这个东西,HTML5 中 history 新 API 加上 popstate 事件能够完美地做到这一点。

history 对象里的 pushState() 和 replaceState() 来无更新地改变页面的 url,使用 popState 事件来实现浏览器工具栏前进、后退时的状态管理。

流程是这样的:

  1. 页面第一次加载,可以使用 replaceState() 来初始化 history.state 以及处理一些相关的页面初始化事务。
  2. 用户点击链接,触发点击事件
  3. 点击事件的处理函数中,禁用链接的默认跳转,使用 pushState() 来更新页面的 url,同时根据新 url 的对应 API 发起 Ajax 请求获得数据,更新页面内容,同时更新 history.state 对象
  4. 用户点击浏览器的前进、后退按钮,触发 popState 事件,我们在 popstate 事件的处理中实现前、后状态的恢复

相关实现代码,可以查看 demo 的源码。

这样,我们就实现了对用户的友好,接下来就是另外一件事了:解决搜索引擎的收录问题(SEO)

Discourse 做出了很好的探索:因为不使用井号结构,每个URL都是一个不同的请求。所以,要求服务器端对所有这些请求,返回给用户的不能是 404,同时 返回给搜索引擎爬虫的 HTML 也需要包含页面的 SEO 内容!能否将这两者做一下结合呢?看下面的解构:

 
 
  1. <html>
  2.   <body>
  3.     <section id='container'></section>
  4.     <noscript>
  5.       ... ...
  6.     </noscript>
  7.   </body>
  8. </html>

奥秘就在 noscript 标签那,对于不能执行 JS 的引擎爬虫来说,noscript 里的内容专门为其准备,而对于用户来说,这个返回的页面又能正常使用。

当然,对于用户来说,noscript 显得冗余了,所以我们还是可以在服务器上针对用户与爬虫准备两套方案

总而言之,使用 history API 和 popState 事件的最大原因就是我们想去掉 url 里的 # 和 #!,让我们的 url 变得更加亲近、自然!而相比思路 1 麻烦了的一点就是我们需要使用 popState 事件来手动恢复前后的状态,好在这并不是困难的一件事,一般的框架(Vue、React、pjax 等)都有着非常方便地自动管理解决方案。

小结

这两种思路各有好处,到底采用哪一个你需要做出决断,决断的做出需要考虑对用户的友好、实现的难易程度、是否需要 SEO、服务器端解决方案……

总之,单页面应用的前景是光明的,在现阶段,Single Page Web App 是唯一能在移动端叫板原生 App 的角色。

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值