IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    PC端大型单页式商业内容管理系统的JS模块化构建探索

    efe发表于 2015-02-13 01:51:18
    love 0

    前提

    为了不被喷得太惨,给标题加了这么多的限制定语也是相当不容易的了。此文讨论的是我所处的环境下对JavaScript构建的一些简单探索,因此有相当多的前提限制。

    首先,何为大型。从我们的系统来看,20多个业务模块,近100个页面组成的单页系统,对应的业务源码代码量如下:

    对应的依赖库,除underscore和moment外均为公司内部库,代码量为:

    其次,所谓的“模块化”指我们使用AMD进行构建,使用符合社区AMD标准的Loader进行模块的加载。

    而“PC 端单页式商业内容管理系统”则代表着系统的不少特性:

    • 使用是相对强制的,对用户来说这是一项工作,而不是爱用不用的用户产品。
    • 商业公司通常拥有较好的网络环境,面向PC设计更使得带宽不是一个需要着重考虑的因素。
    • 单页系统使得所有功能被包含在一个HTML页面内,不存在页间的跳转,因此资源不以页面为单位进行切分。

    为何要构建

    第一个问题是,AMD有自然的按需加载的属性,按需加载也是一直被提倡的一种模式。那么,如果不进行任何的构建,让模块自然地按需加载,是否可行?

    如果看了这个图,你还相信按需加载的话,可以停止此文的阅读了:

    简单来说,按需加载与构建并不冲突,我们不能将所有资源最细粒度地使用按需加载进行管理,必要的构建来减少资源请求是必要的。

    随之而来的,我们会考虑标准的代码合并方案。相当多的站点会将所有的JavaScript合并为一个文件,这也是最简单粗暴有效的方案。

    但是对于大型的单页系统而言,所有JavaScript合并后生成的文件会非常之巨大,其体积在浏览器单线程的下载模式下已经成为系统的性能瓶颈。因此我们需要一些更好的策略,让系统的启动性能得以优化。

    最后,常用于业界的还有一种方案,即自动化的运行时合并。通过在服务器端配置一个处理程序,可以运行时检测需要文件的依赖,进行依赖打包并响应至客户端。

    这种方案有其成本小、透明化等多方面的优势,但在精益求精的场景下仍旧略有不足。其最大的缺点是当有2个以上模块依赖同一个模块时,被依赖模块可能会被重复打包到多份.js文件中,造成不必要的网络传输。

    当然有很多的方法解决这一问题,诸如在Session中记录用户已经拥有的模块,或由客户端记录并提供已有模块列表,来保证打包过程不会加入无用的模块。但这些方法会提升一定的开发成本,同时前后端合作才可以完成的方案往往在推进上会遇到一些小阻碍。

    基于这些原因,从前端静态化的构建入手,在构建阶段实现较为优化的打包方案,是现阶段我们采取的策略。

    准则

    从系统运行时来分析,对于JavaScript的构建,可以提出以下的原则。

    控制请求段的数量

    “请求段”是一个很模糊的概念,简单来说,在一个带宽足够的环境下,我们并不看重运行时产生了多少个请求,而是看重这些请求在瀑布图中被分为几段。由于浏览器并行加载的特性,系统真正的可用时间是由段的数量和每一个段的时间来决定的。

    对于常见的浏览器,其并行加载的请求个数为4-6个,也即一个段可以加入4-6个的请求。从分段越少越好的角度来考虑,我们规划的系统启动分为3个段:

    1. 加载必要的前置条件,其中最为主要的是AMD Loader。.css文件可以在这个阶段加载,以避免影响后面更重量级的.js的加载效率。
    2. 主要的JavaScript模块的加载,在此段通过对并行数的控制,期望在一个段内加载完所有必要的模块。同时从段的用时 = 段内加载总大小 / 并发数这一公式考虑,应尽可能将模块打包为浏览器可接受的最大并发数个文件。
    3. 一些动态的信息,如用户登录信息、系统常量表等,这些信息是易变或动态的,因此从缓存的角度考虑不适合进行打包。

    从我们的系统来看,仅看JavaScript的资源,很明显地分为3段进行加载(红线分隔):

    文件大小

    从经验值来看,单个资源的大小尽量控制在未Gzip前500KB以内,过大的文件会成为瓶颈,除非你可以很好地规划一个HTTP请求段,使得一个大文件加载的用时内浏览器会利用另一个TCP链接加载多个小文件。

    Gzip对纯文本文件的压缩率一般在16%左右,文件的形式和内容不会对此比率造成特别大的影响。如果使用Linux或OSX系统,可以简单地使用以下命令来看一个文件Gzip后的近似大小:

    1
    gzip -c {file} | wc -c

    同时,由于短板效应的存在,一个段的加载时间会由这个段内最大的资源决定,因此尽量使得各资源的大小相近。

    请求控制

    并不是所有的文件都需要合并在一起,也不是所有资源都可以按需加载,对于请求数量的控制并不仅仅体现在系统启动时,也贯穿整个系统的使用流程,来提供用户一致的性能体验。

    我对一个大型CMS系统的请求数量的总结可以概括为3点:

    1. 在导航通过一次操作可到达的页面内,不应该产生额外的请求,即系统启动过程中这些模块都应当就位。
    2. 在一级页面中进行下探才可以到达的页面,尽量控制一个页面仅产生一个请求。考虑到单个页面的资源不会很大,因此再行拆分并行加载反而可能因为TCP链接、网络延迟等因素有负面的效果。
    3. 对于特别大的页面,或者页面中一定条件下才会访问的区域,相关资源使用按需加载的策略配置。

    基于以上的这些原则,我在系统中有进一步的实践。

    实践

    文件合并

    将JavaScript文件分为多个“启动脚本”,一个启动脚本中会包含一系列的模块,现有系统中我将启动脚本分割为4个,分别为:

    1. 特别大的库单独拥有自己的一个脚本,比如ECharts之类的图表库。
    2. UI控件库,包含基础UI控件和业务UI控件,合并为一个脚本。
    3. MVC框架、页面基类、工具类、系统通用功能层等业务无关的逻辑合并为一个脚本。
    4. 一级页面的业务模块合并为一个脚本。

    需要特别注意的是,各启动脚本间应该保持完美的正交,即不应该有任何一个模块被重复合并到多个脚本中。

    除了启动脚本外,前面也有提到单一页面应该尽可能只加载一个资源,因此页面相关会被打包在一起,比较典型的是将Controller、Model和View合并到Controller对应的文件中。

    由于二级页面并不能确定哪一个会被先访问,因此各页面打包文件中是会存在一定的模块重复的,经典如util模块就会同时被列表、表单、只读等页使用。这会导致加载多个页面时部分资源被重复加载,但是此类资源通常体积很小,产生的副作用在可控范围内。

    文件加载

    在文件加载这一方向上,需要有一个特别的处理。由于AMD的依赖管理和运行时依赖分析功能,通过Loader的require函数加载一个模块的化,Loader会自动分析依赖并通过零碎的HTTP请求去请求相关的资源,而无视这些资源是否可能被下一个脚本打包在一起。

    用一个实例来说明,我们的依赖关系为a -> b以及c -> b,即b模块是一个通用模块,被两边所依赖。当我们将a和c分开打包为2个文件时,b会出现在其中一个中(为了实现完美正交)。假设b被打包在a.js中,那么当c.js被Loader加载时,如果a.js还未就位,就会产生一个单独的HTTP请求b.js。由于并行下载时,谁先完成是不可预知的,就有很大的可能性产生无意义的零碎请求。

    解决这一问题的方法是,使用



沪ICP备19023445号-2号
友情链接