这篇文章介绍我在实现sprite(移动端的单页web应用开发框架)的过程中,关于应用单页化的理解以及实现单页化的技术选型的思考和解决方案。

(ps: 本文关于单页web的原理介绍参考了这篇文章:单页应用开发权威指南 这是一本专门介绍单页应用的电子书,写的非常专业和深入,如果有对单页应用很感兴趣的同学建议收藏这个地址~)

(ps1: 如果嫌关于单页化的介绍比较啰嗦的话,可以直接跳到文章末尾,我在末尾介绍了sprite框架实现单页化的源码)

(ps2: 如果想直接看sprite框架的源码,请直接访问:sprite框架之源码篇)

web应用单页化的历史进程


1.传统web应用

单页这个词汇可能对非前端的同学来说会感到比较陌生。

我们就先来说说传统的web应用。传统的web应用中,有一个显著的特点,那就是用户在浏览器的每一个请求,都是一次新的url请求,而服务器后台往往会有一个大的拦截器dispatcher来处理、分发这些请求,拒接掉不合理的请求,把合理的请求分发给对应的处理器,处理器负责处理并响应请求,在处理过程中可能还会有请求重定向的操作。

这意味着什么呢,比如你正在浏览一个用传统web应用的方式实现的一个博客网站,当你看完第一页博客内容而点击“下一页”时,这个时候浏览器会向服务器发送一个新的url请求,比如:xxxblog.com/article?page=2 也就是向后台请求了第二页的数据,但是这个时候你会发现,在这个博客网站上除了文章列表是你希望更新的之外,其余的如菜单栏、最新发表的文章、文章分类栏等在内的页面内容其实是没必要更新的,可是由于浏览器发送了一个新的url请求,这会导致浏览器重新刷新当前的窗口,改用新的url请求返回的内容渲染页面。

所以当你进入第二页的页面时,虽然只看到文章列表更新了,但其实服务器不仅给你返回了第二页的文章列表,还将菜单栏、最新发表的文章、文章分类栏这些页面上没有变化的内容重新发送了一遍回来。

这就是传统web应用的一个明显的弊端,用户每次请求的响应都太“重”了,不仅要返回页面上产生变化了的内容,还要将那些不变的内容一起打包返回。请求的返回体越大,在网络中传输的时间就越长,因此这大大降低了用户体验,而且也增加了服务器的处理负担,会影响服务器的吞吐量以及并发量。

2.ajax的出现

ajax是一种浏览器与 Web 服务器之间使用异步数据传输 ajax相信作为web开发者来说都是非常熟悉的了,ajax技术的出现非常好的解决了上文提到的问题,使得浏览器可以在不重新加载整个页面的情况下,就能与服务器交换数据并更新部分网页内容。ajax使得web应用可以只请求需要的数据,而不必每次都请求一整个页面。

在上面博客网站的例子中,如果使用了ajax技术,那么在切换到第二页时,只需要向服务器发送一个请求,将第二页的文章列表的数据返回再由浏览器渲染即可,不必再向服务器请求菜单栏、最新发表的文章、文章分类栏这些页面上没有变化的内容。

Ajax 的出现使得网页可以局部更新,使得网页上的一部分可以作为一个功能元件来为用户提供服务。这种形式的网页应用已经具备单页应用的雏型,但并不是标准的单页应用。

3.单页

web应用的单页化,就是基于ajax技术衍生的一种web应用架构。

相较于传统web应用,单页应用将 MVC 前置到了浏览器端:

单页应用的架构模式中,首要的一点就是由前端控制路由变化,而不再像传统web应用一样使用一个后端的路由控制器来分发处理请求。

其次则是应用组件化,将应用的不同功能模块拆分为一个个组件,然后在页面上进行组件的组装,最后形成一个完整的页面提供服务。

而服务端退化成了完全的数据 API,仅仅向前端提供能力,前端通过ajax发送http请求与后台交互。

例如在上文中提到的博客网站,如果采用单页模式来开发,那么文章列表、菜单栏、最新发表的文章、文章分类栏这些功能模块将会被拆分为一个个组件,在渲染博客网站页面时,将这些需要的组件一次性组装到一起加载为一个完整的页面。当点击下一页时,由文章列表这个组件发送ajax请求到后台请求第2页的数据,数据返回后更新文章列表组件内容。在切换下一页的过程中,其他组件将保持原样不会产生额外的请求或者渲染开销。

当然博客网站这个例子并不很恰当,因此看上去使用单页模式和仅仅使用ajax的效果是一样的。

但其实单页应用之所以被称作单页,是因为单页应用的页面在应用的整个生命周期中都在同一个html文件上,在进行功能模块组件的切换时,仅仅是将body标签中的内容替换为组件的模板文件而已,并不会将整个html文件都替换掉。

每种技术都有其利弊,单页应用也是如此。我们先说说好的地方:

  • 无刷新体验,这个应该是最显著的有点,由于路由分发直接在浏览器端完成,页面是不刷新,对用户的响应非常及时,因此提升了用户体验;

  • 完全的前端组件化,前端开发不再以页面为单位,更多地采用组件化的思想,代码结构和组织方式更加规范化,便于修改和调整;

  • API 共享,如果你的服务是多端的(浏览器端、Android、iOS、微信等),单页应用的模式便于你在多个端共用 API,可以显著减少服务端的工作量。容易变化的 UI 部分都已经前置到了多端,只受到业务数据模型影响的 API,更容易稳定下来,便于提供鲁棒的服务;

  • 组件共享,在某些对性能体验要求不高的场景,或者产品处于快速试错阶段,借助于一些技术(Hybrid、React Native),可以在多端共享组件,便于产品的快速迭代,节约资源。

单页应用的优点有时候也是缺点:

  • 首次加载大量资源,要在一个页面上为用户提供产品的所有功能,在这个页面加载的时候,首先要加载大量的静态资源,这个加载时间相对比较长;

  • 较高的前端开发门槛,MVC 前置,对前端工程师的要求提高了,不再是『切切图,画画页面这么简单』;同时工作量也会增加数倍,开发这类应用前端工程师的数量往往多于后端;

  • 不利于 SEO,单页页面,数据在前端渲染,就意味着没有 SEO,或者需要使用变通的方案。

4.如何实现单页

上文介绍过,单页只是一种实现web应用的架构模式,至于如何实现单页,其实可以有很多种方式。

我所在的公司,就在PC端的web应用上基于如下的技术架构实现了一套单页应用框架:

  • 模块化:requirejs
  • 路由:Director
  • 模板引擎:Hogan.js from Twitter(语法参考Mustache)

这套框架采用Director实现前端路由向业务逻辑的映射,通过requirejs将应用的功能模块分拆,再加上hogan这套支持循环、分支、逻辑判断的前端模板渲染引擎,共同支撑起一个应用的运行。

框架运行的大致原理:

  1. 通过director可以使得一个前端路由对应一个js的function,当前端路由产生变化,director将会执行该路由对应的function;

  2. 在requirejs帮助下,将应用的各个功能模块划分成了单独的js文件,每个js文件都有一个入口方法,并且配套一个或多个由hogan模板引擎语法编写的模板页面
    即:一个功能模块 = 1个js文件 + n个模板页面;

  3. 将每个模块的js文件的入口方法都注册为一个前端路由,并且默认将第一个模块作为应用的首页,从而使得前端路由与模块关联起来,通过前端路由的切换实现模块的切换;

  4. 在模块的js文件的入口方法中,通过ajax请求,动态地加载数据以及模块对应的模板文件,通过hogan引擎将数据和模板文件编译成html文件,实现数据的渲染;

5.使用Vue + VueRouter实现单页

既然我们公司在PC端已经有现成的框架了,所以我首先就想着能不能把这套框架拿过来修改修改就能实现移动端单页web应用的框架。

但是大致的查看了源码后,我发现这套框架的代码已经与我们公司的一些PC端的诸如权限体系、一键换肤、国际化等需求强关联在一起了。代码的量级也比较庞大,已经不容易拆分出来复用了。

常见的单页应用实现方式有AngularJs,React还有Vue。由于我之前就有过Vue的开发经验,对于vue实现单页的方式比较熟悉,因此最后就敲定在框架的单页化这一项上,选用Vue + VueRouter来实现。

使用Vue + vueRouter创建单页应用,是比较轻松地。通过使用Vue可以组合组件来组成应用程序, 再通过vue-router将组件(components)映射到路由(routes),然后告诉 vue-router 应该在哪里、在何时渲染它们。

下面来看看sprite框架关于单页化的源码:

index.html: 整个应用唯一的html页面,所有的组件都将在该页面上渲染和切换

<html>

<head>
    <!-- 用来禁止浏览器缓存,使得每次请求index.html都从服务器下载最新版本 -->
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="0" />
    <!-- 标识html使用UTF-8编码 -->
    <meta charset="utf-8">
    <!-- 让viewport的宽度等于物理设备上的真实分辨率,不允许用户缩放 -->
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <link rel="stylesheet" href="public/css/style.css">
    <!-- 引入require -->
    <script src="lib/require.js"></script>
    <!-- 引入本应用的业务页面注册文件 -->
    <script src="config/pageRegister.js"></script>
    <!-- 引入框架的配置文件 -->
    <script src="config/pubConfig.js"></script>
    <!-- 应用主入口 -->
    <script src="main.js"></script>
</head>

<body>
    <div id="app">
        <!-- 主路由出口 -->
        <!-- 路由匹配到的组件将渲染在这里 -->
        <div class="main__content">
            <router-view></router-view>
        </div>
    </div>
</body>

</html>

index.html中的head部分用于配置主要的meta信息和框架所需的js文件以及css文件。main.js的介绍见下文,而require.js、pageRegister.js、pubConfig.js这三个文件我会在下一章介绍。

index.html中的body标签内有且仅有一个div块,这个div块将会在main.js中被整个应用最大的vue对象挂载,挂载之后,所有的组件(即功能模块)都会通过VueRouter在这个div内的<router-view></router-view>标签中进行加载和切换,从而实现了应用的整体单页化。

main.js: 应用的入口文件,用于将vue对象挂载到index.html中id为“app”的div上

require(REQUIRE_MODULES.all, function(Vue, VueRouter, mintUI, util, routeParse, $, axios) {
    //将axios输出到全局作用域
    window.axios = axios;
    //使用vue路由组件
    Vue.use(VueRouter);
    //使用饿了么移动端组件mint-ui
    Vue.use(mintUI);

    //根据pageRegister.js中的配置生成vue路由数组
    var routes = routeParse.getVueRoute(arguments);
    //生成VueRouter对象
    var router = new VueRouter({
        routes: routes
    });

    //挂载主vue对象
    app = new Vue({
        el: '#app',
        router
    });

});

main.js文件作为整个应用的入口文件,主要的作用在于通过requireJs将所有的模块(包括Vue,VueRouter,mintUI以及jQuery等在内的外部模块以及应用的页面模块)加载到index.html的head标签中去,从而使得在index.html页面可以完成整个应用的所有页面展示和业务逻辑。

关于main.js文件中代码原理,以前main.js和index.html是如何配合起来使用的,就需要介绍我是如何使用requireJs+vue来实现应用的模块化了,这一部分内容会在下一章中介绍~

One thought on “sprite框架之单页应用的原理与实现(二)”

发表评论

电子邮件地址不会被公开。 必填项已用*标注