大家是如何做好前端工程化和静态能源管理,前
分类:澳门新萄京最大平台

我们是如何做好前端工程化和静态资源管理

2016/07/30 · 基础技术 · 工程化, 静态资源

原文出处: 凹凸实验室   

澳门新萄京 1

随着互联网的发展,我们的业务也日益变得更加复杂且多样化起来,前端工程师也不再只是做简单的页面开发这么简单,我们需要面对的十分复杂的系统性问题,例如,业务愈来愈复杂,我们要如何清晰地梳理;团队人员愈来愈多,我们要如何更好地进行团队协作;功能愈来愈多,我们要如何保证页面的性能不至于下降,等等。所有的这些都可以归结为如何提升开发体验和性能问题。

模块化是一种处理复杂系统分解成为更好的可管理模块的方式,它可以把系统代码划分为一系列职责单一,高度解耦且可替换的模块,系统中某一部分的变化将如何影响其它部分就会变得显而易见,系统的可维护性更加简单易得。

Model View Controller

FreeMarker(FreeMarker Template Language) 一个java类库 view层完全独立 显示逻辑和业务逻辑分离 轻量级框架 不需要Servlet环境
HTML静态化
Template DataModel = HTML
Java代码决定读取哪一个Template
FreeMarker模板不编译成类,不能写任何java代码,严格的MVC分离
性能优于JSP 支持JSP标签

宏定义是什么?

前端框架主要为了解决什么问题?如何解决?
1.资源定位
工程路径 --> 部署路径,
相对路径 --> 绝对路径 md5戳 域名 --> 解决版本迭代后静态资源缓存在客户端的问题, 实现模块独立,任务文件间都可以进行内嵌
2.模块化开发
核心问题:依赖管理和加载
构建工具只负责生成依赖关系表 框架自己绝对什么时候加载哪些资源

  • 规范

    • 开发规范
      • 模块化开发:js模块化,css模块化
      • 组件化开发:模板,js,css维护在一起
    • 部署规范
      • 采用nodejs后端,基本部署规范应该参考 express 项目部署
      • 按版本号做非覆盖式发布
      • 公共模块可发布给第三方共享
  • 框架

    • js模块化框架,支持请求合并,按需加载等性能优化点
  • 工具

    • 可以编译stylus为css
    • 支持js、css、图片压缩
    • 允许图片压缩后以base64编码形式嵌入到css、js或html中
    • 与ci平台集成
    • 文件监听、浏览器自动刷新
    • 本地预览、数据模拟

模块化框架

  • 模块管理
  • 资源加载
  • 性能优化(按需,请求合并)
  • 组件开发的基础框架

实现一个页面功能总是需要 JavaScript、CSS 和 Template 三种语言相互组织,所以我们真正需要的是一种可以将 JavaScript、CSS 和 Template 同时都考虑进去的模块化方案。

性能优化方向分类

提升开发体验

我们主要从以下三个方面来提升我们的开发体验。

前端开发领域(JavaScript、CSS、Template)并没有为开发者们提供以一种简洁、有条理地的方式来管理模块的方法。CommonJS(致力于设计、规划并标准化 JavaScript API)的诞生开启了“ JavaScript 模块化的时代”。CommonJS 的模块提案为在服务器端的 JavaScript 模块化做出了很大的贡献,但是在浏览器下的 JavaScript 模块应用很有限。随之而来又诞生了其它前端领域的模块化方案,像 requireJS、SeaJS 等,然而这些模块化方案并不是十分适用 ,并没有从根本上解决模块化的问题。

前端模块化带来的性能问题

很多主流的模块化解决方案通过 JavaScript 运行时来支持“匿名闭包”、“依赖分析”和“模块加载”等功能,例如“依赖分析”需要在 JavaScript 运行时通过正则匹配到模块的依赖关系,然后顺着依赖链(也就是顺着模块声明的依赖层层进入,直到没有依赖为止)把所有需要加载的模块按顺序一一加载完毕, 当模块很多、依赖关系复杂的情况下会严重影响页面性能。

  • 请求数量:
  • 合并脚本和样式表,
  • CSS Sprites,
  • 拆分初始化负载,
  • 划分主域(使用“查找-替换”思路,我们似乎也可以很好的实现 划分主域
    原则)
  • 请求带宽:
  • 开启GZip (开启了服务端的Gzip压缩)
  • 精简JavaScript(利用 yui compressor 或者 google closure compiler 等压缩工具很容易做到 ),
  • 移除重复脚本,
  • 图像优化(也可以使用图片压缩工具对图像进行压缩,实现 图像优化
    原则)
  • 缓存利用:
  • 使用CDN(实现静态资源的缓存和快速访问),
  • 使用外部Javascript和Css,
  • 添加Expires,
  • 减少DNS查找,
  • 配置ETag,
  • 使用Ajax
  • 页面结构:
  • 将样式表放在顶部,
  • 尽早刷新文档的输出
  • 代码校验:
  • 避免CSS表达式(一些技术实力雄厚的前端团队甚至研发出了自动CSS Sprites工具,解决了CSS Sprites在工程维护方面的难题),
  • 避免重定向(通过引入代码校验流程来确保实现 避免css表达式和 避免重定向原则)

规范化

当团队人员不断扩充时,我们需要制定统一的规范来对平时的开发工作做出一定约束和指导。统一的规范包括前端的代码规范,根据规范定义好一套代码检查的规则,在代码提交的时候进行检查,让开发人员知道自己的代码情况。

大家是如何做好前端工程化和静态能源管理,前端工程与品质优化。同时,根据以往的开发经验,我们制定了统一的项目框架,根据业务功能不同,将一个项目(app)拆分成不同的业务模块(module),而每一个模块都包含自身的页面(page)以及构成页面所需要的组件(widget),每一个项目涉及到app、module、page、widget这些已经约定好的概念,这样让项目结构更加清晰,而且让团队内不同业务的人员之间切换无障碍。

澳门新萄京 2

前端模块化并不等于 JavaScript 模块化

前端开发相对其他语言来说比较特殊,因为我们实现一个页面功能总是需要 JavaScript、CSS 和 Template 三种语言相互组织才行,如果一个功能仅仅只有 JavaScript 实现了模块化,CSS 和 Template 还是处于原始状态,那我们调用这个功能的时候并不能完全通过模块化的方式,那么这样的模块化方案并不是完整的,所以我们真正需要的是一种可以将 JavaScript、CSS 和 Template 同时都考虑进去的模块化方案,而非仅仅 JavaScript 模块化方案。

模块化为打包部署带来的极大不便

传统的模块化方案更多的考虑是如何将代码进行拆分,但是当我们部署上线的时候需要将静态资源进行合并(打包),这个时候会发现困难重重,每个文件里 只能有一个模块,因为模块使用的是“匿名定义”,经过一番研究,我们会发现一些解决方案,无论是“ combo 插件”还是“ flush 插件”,都需要我们修改模块化调用的代码,这无疑是雪上加霜,开发者不仅仅需要在本地开发关注模块化的拆分,在调用的时候还需要关注在一个请求里面加载哪 些模块比较合适,模块化的初衷是为了提高开发效率、降低维护成本,但我们发现这样的模块化方案实际上并没有降低维护成本,某种程度上来说使得整个项目更加 复杂了。

首先我们来看一下一个 web 项目是如何通过“一体化”的模块化方案来划分目录结构:

澳门新萄京 3

  • 站点(site):一般指能独立提供服务,具有单独二级域名的产品线。如旅游产品线或者特大站点的子站点(lv.baidu.com)。
  • 子系统(module):具有较清晰业务逻辑关系的功能业务集合,一般也叫系统子模块,多个子系统构成一个站点。子系统(module)包括 两类: common 子系统, 为其他业务子系统提供规范、资源复用的通用模块;业务子系统:,根据业务、URI 等将站点进行划分的子系统站点。
  • 大家是如何做好前端工程化和静态能源管理,前端工程与品质优化。页面(page): 具有独立 URL 的输出内容,多个页面一般可组成子系统。
  • 模块(widget):能独立提供功能且能够复用的模块化代码,根据复用的方式不同分为 Template 模块、JS 模块、CSS 模块三种类型。
  • 静态资源(static):非模块化资源目录,包括模板页面引用的静态资源和其他静态资源(favicon,crossdomain.xml 等)。

前端模块(widget),是能独立提供功能且能够复用的模块化代码,根据复用的方式不同分为 Template 模块、JS 模块、CSS 模块三种类型,CSS 组件,一般来说,CSS 模块是最简单的模块,它只涉及 CSS 代码与 HTML 代码; JS 模块,稍为复杂,涉及 JS 代码,CSS 代码和 HTML 代码。一般,JS 组件可以封装 CSS 组件的代码; Template 模块,涉及代码最多,可以综合处理 HTML、JavaScript、CSS 等各种模块化资源,一般情况,Template 会将 JS 资源封装成私有 JS 模块、CSS 资源封装成自己的私有 CSS 模块。下面我们来一一介绍这几种模块的模块化方案。

名词解释

组件化

在项目中引入组件化的概念,这里的组件对应上文讲到的widget,每一个组件都会包含组件自身的模板、css、js、图片以及说明文件,我们使用组件来拼装页面,像搭积木一样来拼装我们的页面,同时一个组件内可以调用另一个组件。

澳门新萄京 4

在拿到设计稿后,我们首先需要确定哪些需要做成公共组件,那些是要做成独立组件,以及组件间如何进行通信。在页面中调用这些组件后,会自动加载组件的模板以及组件的静态资源,而当组件不再需要时,只要移除掉组件引用,那么相应的模板和静态资源也会不再加载。

组件化的好处主要有这么几点

  • 管理方便,我们可以把一个独立功能相关的文件在工程目录中放在一起,这样代码管理起来会非常便利
  • 组件复用,通过抽取公共组件,可以实现组件复用,从而减少工作量,创造价值
  • 分而治之,这是组件化最重要的一点,将页面组件化,就是对页面功能的拆分,将一个大的工程拆成小的零件,我们只需要关注每一个零件的功能,极大地降低了页面的开发与维护的难度

JavaScript 模块化并不等于异步模块化

主流的 JavaScript 模块化方案都使用“异步模块定义”的方式,这种方式给开发带来了极大的不便,所有的同步代码都需要修改为异步的方式,我们是否可以在前端开发中使用“ CommonJS ”的方式,开发者可以使用自然、容易理解的模块定义和调用方式,不需要关注模块是否异步,不需要改变开发者的开发行为。

模板模块

我们可以将任何一段可复用的模板代码放到一个 smarty 文件中,这样就可以定义一个模板模块。在 widget 目录下的 smarty 模板(本文仅以 Smarty 模板为例)即为模板模块,例如 common 子系统的 widget/nav/ 目录

├── nav.css
├── nav.js
└── nav.tpl

下 nav.tpl 内容如下:

<nav id="nav" class="navigation" role="navigation">
    <ul>
        <%foreach $data as $doc%>
        <li class="active">
            <a href="#section-{$doc@index}">
                <i class="icon-{$doc.icon} icon-white"></i>{$doc.title}
            </a>
        </li>
        <%/foreach%>
    </ul>
</nav>

然后,我们只需要一行代码就可以调用这个包含 smarty、JS、CSS 资源的模板模块,

// 调用模块的路径为 子系统名称:模板在 widget 目录下的路劲
{widget name="common:widget/nav/nav.tpl" }

这个模板模块(nav)目录下有与模板同名的 JS、CSS 文件,在模板被执行渲染时这些资源会被自动加载。如上所示,定义 template 模块的时候,只需要将 template 所依赖的 JS 模块、CSS 模块存放在同一目录(默认 JavaScript 模块、CSS 模块与 Template 模块同名)下即可,调用者调用 Template 模块只需要写一行代码即可,不需要关注所调用的 template 模块所依赖的静态资源,模板模块会帮助我们自动处理依赖关系以及资源加载。

CSS Sprites【在国内很多人叫css精灵,是一种网页图片应用处理方式。它允许你将一个页面涉及到的所有零星图片都包含到一张大图中去,这样一来,当访问该页面时,载入的图片就不会像以前那样一幅一幅地慢慢显示出来了。对于当前网络流行的速度而言,不高于200KB的单张图片的所需载入时间基本是差不多的,所以无需顾忌这个问题】

自动化编译

在前端开发中,我们总是会去使用很多工具、手段来优化代码、提升开发效率,例如,我们会使用sass、less等CSS预处理工具来编写更好维护的样式代码,我们也会使用CSSLint、eslint等代码检查工具来检查代码的语法错误,使用文件合并压缩等手段来减少资源大小,除此之外我们还会去做雪碧图合并、多倍图处理、字体压缩处理、代码发布等等。

曾经有大神说过,超过90s的工作都应该自动化掉。而以上所有的这些工作,贯穿我们整个开发流程,但是不同工具的切换不但显得凌乱,而且影响开发效率。在自动化、工程编译的思想早已深入人心的当下,我们当然也要紧跟潮流,所以我们考虑通过自动化手段来提升我们的效率,让所有操作可以一键式开速执行完。

我们将通过定义好一系列的编译任务,按照一定顺序依次对我们的项目自动进行编译操作,最后产生出可上线的代码。

前端模块化带来的性能问题

很多主流的模块化解决方案通过 JavaScript 运行时来支持“匿名闭包”、“依赖分析”和“模块加载”等功能,例如“依赖分析”需要在 JavaScript 运行时通过正则匹配到模块的依赖关系,然后顺着依赖链(也就是顺着模块声明的依赖层层进入,直到没有依赖为止)把所有需要加载的模块按顺序一一加载完毕,当模块很多、依赖关系复杂的情况下会严重影响页面性能。

JavaScript 模块

上面我们介绍了一个模板模块是如何定义、调用以及处理依赖的,接下来我们来介绍一下模板模块所依赖的 JavaScript 模块是如何来处理模块交互的。我们可以将任何一段可复用的 JavaScript 代码放到一个 JS 文件中,这样就可以定义为一个 JavaScript 类型的模块,我们无须关心“ define ”闭包的问题,我们可以获得“ CommonJS ”一样的开发体验,下面是 nav.js 中的源码.

// common/widget/nav/nav.js
var $ = require('common:widget/jquery/jquery.js');

exports.init = function() {
    ...
};

我们可以通过 require、require.async 的方式在任何一个地方(包括 html、JavaScript 模块内部)来调用我们需要的 JavaScript 类型模块,require 提供的是一种类似于后端语言的同步调用方式,调用的时候默认所需要的模块都已经加载完成,解决方案会负责完成静态资源的加载。require.async 提供的是一种异步加载方式,主要用来满足“按需加载”的场景,在 require.async 被执行的时候才去加载所需要的模块,当模块加载回来会执行相应的回调函数,语法如下:

// 模块名: 文件所在 widget 中路径
require.async(["common:widget/menu/menu.js"], function( menu ) {
    menu.init();
});

一般 require 用于处理页面首屏所需要的模块,require.async 用于处理首屏外的按需模块。

把以上这些已经成熟应用到实际生产中的优化手段去除掉,留下那些还没有很好实现的优化原则。再来回顾一下之前的性能优化分类:

提升性能

我们主要从以下四个方面来做好性能优化。

模块化为打包部署带来的极大不便

传统的模块化方案更多的考虑是如何将代码进行拆分,但是当我们部署上线的时候需要将静态资源进行合并(打包),这个时候会发现困难重重,每个文件里只能有一个模块,因为模块使用的是“匿名定义”,经过一番研究,我们会发现一些解决方案,无论是“ combo 插件”还是“ flush 插件”,都需要我们修改模块化调用的代码,这无疑是雪上加霜,开发者不仅仅需要在本地开发关注模块化的拆分,在调用的时候还需要关注在一个请求里面加载哪些模块比较合适,模块化的初衷是为了提高开发效率、降低维护成本,但我们发现这样的模块化方案实际上并没有降低维护成本,某种程度上来说使得整个项目更加复杂了。

CSS 模块

在模板模块中以及 JS 模块中对应同名的 CSS 模块会自动与模板模块、JS 模块添加依赖关系,进行加载管理,用户不需要显示进行调用加载。那么如何在一个 CSS 模块中声明对另一个 CSS 模块的依赖关系呢,我们可以通过在注释中的@require 字段标记的依赖关系,这些分析处理对 html 的 style 标签内容同样有效,

/**
 * demo.css
 * @require reset.css
 */
  • 请求数量: 合并脚本和样式表,拆分初始化负载
  • 请求带宽 :移除重复脚本
  • 缓存利用:添加Expries头,配置ETag,使用Ajax可缓存
  • 页面结构: 将样式表放在头部,将脚本放在底部,尽早刷新文档的输出

首屏优化

页面的打开速度一直是大家非常关心的一个指标,一个页面打开太慢会让让用户失去等待的耐心,为了让用户更快地看到页面,我们考虑将页面中部分静态资源代码直接嵌入页面中,我们通过工具处理,在工程编译阶段,将指定的静态资源代码内嵌入页面中,这样可以减少HTTP请求,提升首屏加载速度,同时降低页面裸奔风险。

一体化的前端模块化实践方案

写到这里,其实我们的“前端工程之块化”才正式开始,本文面向对前端模块化开发有所实践或有所研究的同学,接下来我们所介绍的前端模块化解决方案, 有别于 JavaScript 模块化方案或 CSS 模块化方案,它是一种可以综合处理前端各种资源的模块化方案;它可以极大提升开发者的开发体验,并为性能优化提供良好的支持。下面让我们来进一步来了解什么是“一体化”的模块化实践方案。

首先我们来看一下一个 web 项目是如何通过“一体化”的模块化方案来划分目录结构:

澳门新萄京 5

  • 站点(site):一般指能独立提供服务,具有单独二级域名的产品线。如旅游产品线或者特大站点的子站点(lv.baidu.com)。
  • 子系统(module):具有较清晰业务逻辑关系的功能业务集合,一般也叫系统子模块,多个子系统构成一个站点。子系统(module)包括两类: common 子系统, 为其他业务子系统提供规范、资源复用的通用模块;业务子系统:,根据业务、URI 等将站点进行划分的子系统站点。
  • 页面(page): 具有独立 URL 的输出内容,多个页面一般可组成子系统。
  • 模块(widget):能独立提供功能且能够复用的模块化代码,根据复用的方式不同分为 Template 模块、JS 模块、CSS 模块三种类型。
  • 静态资源(static):非模块化资源目录,包括模板页面引用的静态资源和其他静态资源(favicon,crossdomain.xml 等)。

前端模块(widget),是能独立提供功能且能够复用的模块化代码,根据复用的方式不同分为 Template 模块、JS 模块、CSS 模块三种类型,CSS 组件,一般来说,CSS 模块是最简单的模块,它只涉及 CSS 代码与 HTML 代码; JS 模块,稍为复杂,涉及 JS 代码,CSS 代码和 HTML 代码。一般,JS 组件可以封装 CSS 组件的代码; Template 模块,涉及代码最多,可以综合处理 HTML、JavaScript、CSS 等各种模块化资源,一般情况,Template 会将 JS 资源封装成私有 JS 模块、CSS 资源封装成自己的私有 CSS 模块。下面我们来一一介绍这几种模块的模块化方案。

非模块化资源

在实际开发过程中可能存在一些不适合做模块化的静态资源,那么我们依然可以通过声明依赖关系来托管给静态资源管理系统来统一管理和加载,

{require name="home:static/index/index.css" }

如果通过如上语法可以在页面声明对一个非模块化资源的依赖,在页面运行时可以自动加载相关资源。

静态资源版本更新与缓存

按需加载

同时,我们考虑通过尽量减小页面体积来提升页面打开速度,在业务上我们将页面划分为一个个楼层组件,以京东美妆馆为例,页面中从上而下分为首焦、至IN尖货、今日特惠、潮流前沿、口碑榜单这么几个楼层组件,其实这个页面还有很长,内容非常多且复杂。

澳门新萄京 6

之前我们的做法是整个页面直出,这样一次性加载的内容会非常多,为了提升打开速度,我们考虑通过按需加载的方式来优化页面的加载。我们在页面中只放每一个楼层的框架性代码,楼层的模板和数据都通过异步的方式去拉取,来实现楼层组件的按需加载,同时我们可以对模板以及数据进行缓存,以此来减少请求,做更极致的优化。在开发中我们以正常组件的方式去开发整个页面,随后通过编译工具,在代码编译阶段自动将楼层的模板抽离成一个独立的JS文件,并给楼层容器打上标记位,通过页面加载逻辑去按需拉取模板,再进行渲染。

通过给楼层容器和模板分别加上标记位 o2-out-tpl-wrapper o2-out-tpl

澳门新萄京 7

在编译时自动将指定的模板代码抽离成独立js文件

澳门新萄京 8

并且给楼层容器打上标记

澳门新萄京 9

同时在逻辑脚本适当位置自动加入模板的版本

澳门新萄京 10

通过上述步骤,实现按需加载的自动化生成,在提升性能的同时,很好地解放我们生产力。

模板模块

我们可以将任何一段可复用的模板代码放到一个 smarty 文件中,这样就可以定义一个模板模块。在 widget 目录下的 smarty 模板(本文仅以 Smarty 模板为例)即为模板模块,例如 common 子系统的 widget/nav/ 目录

├── nav.css
├── nav.js
└── nav.tpl

下 nav.tpl 内容如下:

<nav id="nav" class="navigation" role="navigation">
    <ul>
        <%foreach $data as $doc%>
        <li class="active">
            <a href="#section-{$doc@index}">
                <i class="icon-{$doc.icon} icon-white"></i>{$doc.title}
            </a>
        </li>
        <%/foreach%>
    </ul>
</nav>

然后,我们只需要一行代码就可以调用这个包含 smarty、JS、CSS 资源的模板模块,

// 调用模块的路径为 子系统名称:模板在 widget 目录下的路劲
{widget name="common:widget/nav/nav.tpl" }

这个模板模块(nav)目录下有与模板同名的 JS、CSS 文件,在模板被执行渲染时这些资源会被自动加载。如上所示,定义 template 模块的时候,只需要将 template 所依赖的 JS 模块、CSS 模块存放在同一目录(默认 JavaScript 模块、CSS 模块与 Template 模块同名)下即可,调用者调用 Template 模块只需要写一行代码即可,不需要关注所调用的 template 模块所依赖的静态资源,模板模块会帮助我们自动处理依赖关系以及资源加载。

项目实例

下面我们来看一下在一个实际项目中,如果在通过页面来调用各种类型的 widget,首先是目录结构:

├── common
│   ├── fis-conf.js
│   ├── page
│   ├── plugin
│   ├── static
│   └── widget
└── photo
    ├── fis-conf.js
    ├── output
    ├── page
    ├── static
    ├── test
    └── widget

我们有两个子系统,一个 common 子系统(用作通用),一个业务子系统,page 目录用来存放页面,widget 目录用来存放各种类型的模块,static 用于存放非模块化的静态资源,首先我们来看一下 photo/page/index.tpl 页面的源码,

{extends file="common/page/layout/layout.tpl"}
{block name="main"}
    {require name="photo:static/index/index.css"}
    {require name="photo:static/index/index.js"}
    <h3>demo 1</h3>
    <button id="btn">Button</button>
    {script type="text/javascript"}
        // 同步调用 jquery
        var $ = require('common:widget/jquery/jquery.js');

        $('#btn').click(function() {
            // 异步调用 respClick 模块
            require.async(['/widget/ui/respClick/respClick.js'], function() {
                respClick.hello();
            });
        });
    {/script}

    // 调用 renderBox 模块
    {widget name="photo:widget/renderBox/renderBox.tpl"}
{/block}

第一处代码是对非模块化资源的调用方式;第二处是用 require 的方式调用一个 JavaScript 模块;第三处是通过 require.async 通过异步的方式来调用一个 JavaScript 模块;最后一处是通过 widget 语法来调用一个模板模块。 respclick 模块的源码如下:

exports.hello = function() {
    alert('hello world');
};

renderBox 模板模块的目录结构如下:

└── widget
    └── renderBox
        ├── renderBox.css
        ├── renderBox.js
        ├── renderBox.tpl
        └── shell.jpeg

虽然 renderBox 下面包括 renderBox.js、renderBox.js、renderBox.tpl 等多种模块,我们再调用的时候只需要一行代码就可以了,并不需要关注内部的依赖,以及各种模块的初始化问题。

 

fis开源项目前端工程模块化:

添加Expires头 和 配置ETag两项只要配置了服务器的相关选项就可以实现但是问题在于开启缓存后如何更新思路:最有效的解决方案是修改其所有链接,这样,全新的请求将从原始服务器下载最新的内容
但要怎么改变链接呢?变成什么样的链接才能有效更新缓存,又能最大限度避免那些没有修改过的文件缓存不失效呢?

基于资源表加载

根据页面组件化,通过工具分析,我们将获得页面与组件的依赖关系表,同时也能确认页面所引用资源的依赖关系,例如,我们在页面hello中同步引用组件topbar,那么依赖关系表中将会记录同步引用关系hello引用topbar.tpl、topbar.css、topbar.js,那么页面hello将会自动加载组件topbar的CSS与JS,同时依赖表会记录异步引用的关系,假如我们在组件C中通过API异步引用了组件D的js,那么会在依赖表中记录C异步引用D.js这一个依赖关系,这样D.js这个资源将会在用到的时候被异步调用。

澳门新萄京 11

澳门新萄京 12

同步引用的资源通过生成combo形式链接,在服务端进行文件合并,这样在页面加载的时候,页面只会加载自己需要的同步资源,异步的资源将会在用到的时候再加载,有效避免资源冗余。同时删除、增加组件也非常方便,只需改动模板中对组件调用,通过编译工具会自动重新生成模板以及combo链接。

我们可以将资源加载的操作抽离出来,形成一套统一的资源加载框架设计,这样我们使用的模板可以变得更加灵活,无论是纯html模板,还是PHP或Java之类的后端模板都能有效支持。编译工具扫描代码后只生成资源依赖表,我们通过实现各语言平台的资源加载框架,让不同语言的模板都能基于同一个资源依赖表进行资源加载。

同时,对资源进行MD5重命名处理,文件md5重命名也是一种提升性能的有效手段,使用文件md5后开启服务器强缓存,可以提升缓存的利用率并避免不必要的缓存判断处理。但文件md5重命名后会出现开发时引用的文件名对不上的问题,这就需要在资源表中记录原文件名与md5重命名后之间的对应关系,当我们引用一个资源时,就会通过查表获取重命名后的资源名,然后利用代码中引用资源定位的能力来进行资源名自动替换。

澳门新萄京 13

JavaScript 模块

上面我们介绍了一个模板模块是如何定义、调用以及处理依赖的,接下来我们来介绍一下模板模块所依赖的 JavaScript 模块是如何来处理模块交互的。我们可以将任何一段可复用的 JavaScript 代码放到一个 JS 文件中,这样就可以定义为一个 JavaScript 类型的模块,我们无须关心“ define ”闭包的问题,我们可以获得“ CommonJS ”一样的开发体验,下面是 nav.js 中的源码.

// common/widget/nav/nav.js
var $ = require('common:widget/jquery/jquery.js');

exports.init = function() {
    ...
};

我们可以通过 require、require.async 的方式在任何一个地方(包括 html、JavaScript 模块内部)来调用我们需要的 JavaScript 类型模块,require 提供的是一种类似于后端语言的同步调用方式,调用的时候默认所需要的模块都已经加载完成,解决方案会负责完成静态资源的加载。require.async 提供的是一种异步加载方式,主要用来满足“按需加载”的场景,在 require.async 被执行的时候才去加载所需要的模块,当模块加载回来会执行相应的回调函数,语法如下:

// 模块名: 文件所在 widget 中路径
require.async(["common:widget/menu/menu.js"], function( menu ) {
    menu.init();
});

一般 require 用于处理页面首屏所需要的模块,require.async 用于处理首屏外的按需模块。

先来看看现在一般前端团队的做法:

静态资源预加载

所谓静态资源预加载,就是当用户在进行浏览页面的时候,我们可以在当前页面静默加载下一个页面的静态资源,这样当用户进入到下一个页面时就能快速打开页面,从而在不知不觉中提升页面的打开速度。

澳门新萄京 14

我们会在静态资源预加载平台上配置每一个页面id对应需要预加载页面资源的id,然后系统通过读取资源依赖表获取到所需要预加载的静态资源,生成预加载资源列表文件,再将文件推送到线上服务器,通过页面挂载js请求获取预加载资源列表,随后静默加载资源。在有了资源依赖表后,我们可以准确地分析到每一个页面引用资源的请求,就可以很好地实现静态资源预加载的功能。

澳门新萄京 15

CSS 模块

在模板模块中以及 JS 模块中对应同名的 CSS 模块会自动与模板模块、JS 模块添加依赖关系,进行加载管理,用户不需要显示进行调用加载。那么如何在一个 CSS 模块中声明对另一个 CSS 模块的依赖关系呢,我们可以通过在注释中的@require 字段标记的依赖关系,这些分析处理对 html 的 style 标签内容同样有效,

/**
 * demo.css
 * @require reset.css
 */
<h1>hello world</h1>
<script type="text/javascript" src="a.js?t=201404231123"></script>
<script type="text/javascript" src="b.js?t=201404231123"></script>
<script type="text/javascript" src="c.js?t=201404231123"></script>
<script type="text/javascript" src="d.js?t=201404231123"></script>
<script type="text/javascript" src="e.js?t=201404231123"></script>

Athena

澳门新萄京,工欲善其事,必现利其器。为了实现我们对提升开发效率和产品性能的诉求,我们提出了比较完整的工程化解决方案以及对应的工具Athena。

Athena是由京东【凹凸实验室】(aotu.io) 推出的一套项目流程工具,通过Athena,我们可以很流程地跑完整个开发流程。Athena分为两部分,一是本地自动化编译工具,二是资源管理平台,其架构如下

澳门新萄京 16

非模块化资源

在实际开发过程中可能存在一些不适合做模块化的静态资源,那么我们依然可以通过声明依赖关系来托管给静态资源管理系统来统一管理和加载,

{require name="home:static/index/index.css" }

如果通过如上语法可以在页面声明对一个非模块化资源的依赖,在页面运行时可以自动加载相关资源。

也有团队采用构建版本号为静态资源请求添加query,它们在本质上是没有区别的

本地自动化工具

Athena本地编译工具是一个基于NodeJs的命令行工具,通过执行命令的方式来优化我们的开发流程,目前Athena的主要功能有

  • 自动创建项目、模块、页面、组件结构
  • 轻量组件化功能,根据组件加载情况生成资源依赖表
  • Sass/less 编译
  • 代码检查
  • CSS prefix等处理
  • CSS合并压缩,JS合并压缩
  • 自动生成雪碧图,自动多倍图,图片压缩
  • 字体文件压缩
  • 自定义图片转base64
  • 文件内联,可以内联样式及JS代码
  • 文件MD5戳,将文件进行使用MD5进行重命名
  • 本地预览,直接查看整个项目
  • 资源定位(图片等资源路径替换)
  • 生成CSS页面片,提供将页面引用的CSS/JS抽离成页面片的形式,方便管理CSS资源
  • 部署到预览机和开发机

项目实例

下面我们来看一下在一个实际项目中,如果在通过页面来调用各种类型的 widget,首先是目录结构:

├── common
│   ├── fis-conf.js
│   ├── page
│   ├── plugin
│   ├── static
│   └── widget
└── photo
    ├── fis-conf.js
    ├── output
    ├── page
    ├── static
    ├── test
    └── widget

我们有两个子系统,一个 common 子系统(用作通用),一个业务子系统,page 目录用来存放页面,widget 目录用来存放各种类型的模块,static 用于存放非模块化的静态资源,首先我们来看一下 photo/page/index.tpl 页面的源码,

{extends file="common/page/layout/layout.tpl"}
{block name="main"}
    {require name="photo:static/index/index.css"}
    {require name="photo:static/index/index.js"}
    <h3>demo 1</h3>
    <button id="btn">Button</button>
    {script type="text/javascript"}
        // 同步调用 jquery
        var $ = require('common:widget/jquery/jquery.js');

        $('#btn').click(function() {
            // 异步调用 respClick 模块
            require.async(['/widget/ui/respClick/respClick.js'], function() {
                respClick.hello();
            });
        });
    {/script}

    // 调用 renderBox 模块
    {widget name="photo:widget/renderBox/renderBox.tpl"}
{/block}

第一处代码是对非模块化资源的调用方式;第二处是用 require 的方式调用一个 JavaScript 模块;第三处是通过 require.async 通过异步的方式来调用一个 JavaScript 模块;最后一处是通过 widget 语法来调用一个模板模块。 respclick 模块的源码如下:

exports.hello = function() {
    alert('hello world');
};

renderBox 模板模块的目录结构如下:

└── widget
    └── renderBox
        ├── renderBox.css
        ├── renderBox.js
        ├── renderBox.tpl
        └── shell.jpeg

虽然 renderBox 下面包括 renderBox.js、renderBox.js、renderBox.tpl 等多种模块,我们再调用的时候只需要一行代码就可以了,并不需要关注内部的依赖,以及各种模块的初始化问题。

接下来,项目升级,比如页面上的html结构发生变化,对应还要修改 a.js 这个文件,得到的构建结果如下:

创建项目结构

在执行创建命令时,Athena会从管理平台下载自定义好的项目模板,可以根据模板创建项目、模块、页面、和组件。Athena有四个创建命令:

通过执行 $ ath app demo 命令就可以生成定义好目录结构的项目。

澳门新萄京 17

随后可以通过 $ ath module home来创建一个业务模块;

通过 $ ath page index 来创建页面;

通过 $ ath widget widgetName 来创建组件。

模块化基础架构

<header>hello world</header>
<script type="text/javascript" src="a.js?t=201404231826"></script>
<script type="text/javascript" src="b.js?t=201404231826"></script>
<script type="text/javascript" src="c.js?t=201404231826"></script>
<script type="text/javascript" src="d.js?t=201404231826"></script>
<script type="text/javascript" src="e.js?t=201404231826"></script>

开发使用

总体架构

为了实现一种自然、便捷、高性能、一体化的模块化方案,我们需要解决以下一些问题,

  • 模块静态资源管理,一般模块总会包含 JavaScript、CSS 等其他静态资源,需要记录与管理这些静态资源
  • 模块依赖关系处理,模块间存在各种依赖关系,在加载模块的时候需要处理好这些依赖关系
  • 模块加载,在模块初始化之前需要将模块的静态资源以及所依赖的模块加载并准备好
  • 模块沙箱(模块闭包),在 JavaScript 模块中我们需要自动对模块添加闭包用于解决作用域问题

** 使用编译工具来管理模块 **

我们可以通过编译工具(自动化工具) 对模块进行编译处理,包括对静态资源进行预处理(对 JavaScript 模块添加闭包、对 CSS 进行 LESS 预处理等)、记录每个静态资源的部署路径以及依赖关系并生成资源表(resource map)。我们可以通过编译工具来托管所有的静态资源,这样可以帮我们解决模块静态资源管理、模块依赖关系、模块沙箱问题。

** 使用静态资源加载框架来加载模块 **

那么如何解决模块加载问题,我们可以通过静态资源加载框架来解决,主要包含前端模块加载框架,用于 JavaScript 模块化支持,控制资源的异步加载。后端模块化框架,用于解决 JavaScript 同步加载、CSS 和模板等模块资源的加载,静态资源加载框架可以用于对页面进行持续的自适应的前端性能优化,自动对页面的不同情况投递不同的资源加载方案,帮助开发者管理静态资源,抹平本地开发到部署上线的性能沟壑。 编译工具和静态资源加载框架的流程图如下:

澳门新萄京 18

为了触发用户浏览器的缓存更新,我们需要更改静态资源的url地址,如果采用构建信息(时间戳、版本号等)作为url修改的依据,如上述代码所示,我们只修改了一个a.js文件,但再次构建会让所有请求都更改了url地址,用户再度访问页面那些没有修改过的静态资源的(b.js,b.js,c.js,d.js,e.js)的浏览器缓存也一同失效了。使用构建信息作为静态资源更新标记会导致每次构建发布后所有静态资源都被迫更新,浏览器缓存利用率降低,给性能带来伤害

组件化

Athena中实现组件化主要是分为两种,一是针对纯HTML模板,通过扩展模板引擎方法实现,提供了组件化API widget.load,它可以方法接收三个参数,第一个参数是widget的名称,后面两个参数是可选参数,第二个是向widget传递的一些参数,第三个是widget所属的模块,如果是本模块,可以不传例如

JavaScript

<%= widget.load('user') %> <%= widget.load('user', { param: 'test' }) %> <%= widget.load('user', null, 'gb') %>

1
2
3
4
5
6
7
<%= widget.load('user') %>
<%=
widget.load('user', {
param: 'test'
})
%>
<%= widget.load('user', null, 'gb') %>

通过模板引擎编译,执行widget.load方法,可以实现加载模板,记录依赖关系的目的。

澳门新萄京 19

二是针对不同语言的后端模板,通过实现各自的组件化框架来进行组件的加载,例如 PHP 下使用 <?= $widget->load('user', NULL, 'gb') ?>来进行组件加载,再通过代码扫描得出组件依赖关系。

编译工具

自动化工具会扫描目录下的模块进行编译处理并输出产出文件:

静态资源,经过编译处理过的 JavaScript、CSS、Image 等文件,部署在 CDN 服务器自动添加闭包,我们希望工程师在开发 JavaScript 模块的时候不需要关心” define ”闭包的事情,所以采用工具自动帮工程师添加闭包支持,例如如上定义的 nav.js 模块在经过自动化工具处理后变成如下,

define('common:widget/nav/nav.js', function( require, exports, module ) {
    // common/widget/nav/nav.js
    var $ = require('common:widget/jquery/jquery.js');

    exports.init = function() {
        ...
    };
});

模板文件,经过编译处理过的 smarty 文件,自动部署在模板服务器

资源表,记录每个静态资源的部署路径以及依赖关系,用于静态资源加载框架 静态资源加载框架(SR Management System)会加载 source maps 拿到页面所需要的所有模块以及静态资源的 url,然后组织资源输出最终页面。

此外,采用添加query的方式来清除缓存还有一个弊端,就是 覆盖式发布的上线问题

Athena中的API

Athena针对模板提供了一系列的API来扩展丰富的功能,例如前面提到的 <%= widget.load() %> 来实现组件化。

同时Athena中还提供了其他API:

<%= getCSS() %><%= getJS() %> 用来引用CSS/JS文件,传入文件名和模块名;

<%= uri() %> 提供了资源定位功能,可以在模板中标记资源,编译过程中会进行替换,而且在JS中也有资源定位API __uri()

<%= inline() %> 提供了内联资源的功能,传入文件名和模块名,可以在模板中内联任意资源,例如图片以及JS脚本;而且 inline 也可以内联一段网络资源,例如线上的JS文件,同样的在JS中也有内联资源API __inline()

雪碧图标识 ?__sprite ,在CSS中引用图片最后加上标识 ?__sprite 可以自动生成自定义名称雪碧图,同时支持自定义生成多张雪碧图,只需要要标识后面带上一个文件名,就可以生成一张以这个文件名来命名的雪碧图,例如 ?__sprite=icons ,这样所有带同样标识的图片就会生成一张以 icons为文件名的雪碧图。

静态资源加载框架

下面我们会详细讲解如何加载模块,如下所示,

澳门新萄京 20

在流程开始前我们需要准备两个数据结构:

  • uris = [],数组,顺序存放要输出资源的 uri
  • has = {},hash 表,存放已收集的静态资源,防止重复加载
  1. 加载资源表(resource map):

    {
        "res": {
            "A/A.tpl": {
                "uri": "/templates/A.tpl",
                "deps": ["A/A.css"]
            },
            "A/A.css": {
                "uri": "/static/css/A_7defa41.css"
            },
            "B/B.tpl": {
                "uri": "/templates/B.tpl",
                "deps": ["B/B.css"]
            },
            "B/B.css": {
                "uri": "/static/css/B_33c5143.css"
            },
            "C/C.tpl": {
                "uri": "/templates/C.tpl",
                "deps": ["C/C.css"]
            },
            "C/C.css": {
                "uri": "/static/css/C_6a59c31.css"
            }
        }
    }
    
  2. 执行 {widget name="A"}

    • 在表中查找 id 为 A/A.tpl 的资源,取得它的资源路径 /template/A.tpl,记为 tplpath,加载并渲染 tplpath 所指向的模板文件,即 /template/A.tpl,并输出它的 html 内容
    • 查看 A/A.tpl 资源的 deps 属性,发现它依赖资源 A/A.css,在表中查找 id 为 A/A.css 的资源,取得它的资源路径为 /static/css/A7defa41.css_,存入 uris 数组 中,并在 has 表 里标记已加载 A/A.css 资源,我们得到:

      urls = [

      '/static/css/A_7defa41.css'
      

      ];

      has = {

      "A/A.css": true
      

      }

  3. 依次执行 {widget name="B"}、{widget name="c"},步骤与上述步骤 3 相同,得到,

    urls = [
        '/static/css/A_7defa41.css',
        '/static/css/B_33c5143.css',
        '/static/css/C_6a59c31.css'
    ];
    
    has = {
        "A/A.css": true,
        "B/B.css": true,
        "C/C.css": true
    }
    
  4. 在要输出的 html 前面,我们读取 uris 数组的数据,生成静态资源外链,我们得到最终的 html 结果:

    <html>
        <link rel="stylesheet" href="/static/css/A_7defa41.css">
        <link rel="stylesheet" href="/static/css/B_33c5143.css">
        <link rel="stylesheet" href="/static/css/C_6a59c31.css">
        <div>html of A</div>
        <div>html of B</div>
        <div>html of C</div>
    </html>
    

    上面讲的是对模板和 CSS 资源的加载,用于描述静态资源加载的流程,下面我们再来详细讲解下对于 JavaScript 模块的处理,要想在前端实现类似“ commonJS ”一样的模块化开发体验需要前端模块化框架和后端模块化框架一起作用来实现,

前端模块化框架,原理上大家可以选择使用 requireJS 或 SeaJS 来作为模块化支持,但是我们并不建议这么做,我们建议大家使用一个 mininal AMD API,例如 requireJS 的 almond 版本或者其他的精简版本,requireJS 完整版有 2000 余行,而精简版模块化框架只需要 100 行代码左右就可以实现,只需要实现以下功能:

  • 模块定义,只需要实现如下接口 define (id, factory),因为 define 闭包是工具生成,所以我们不需要考虑匿名闭包的实现,同时也不需要考虑“依赖前置”的支持,我们只需要支持一种最简单直接的模块化定义即可
  • 模块同步调用,require (id),静态资源管理系统会保证所需的模块都已预先加载,因此 require 可以立即返回该模块
  • 模块异步调用,考虑到有些模块无需再启动时载入,因此我们需要提供一个可以在运行时加载模块的接口 require.async (names, callback),names 可以是一个 id,或者是数组形式的 id 列表。当所有都加载都完成时,callback 被调用,names 对应的模块实例将依次传入。
  • 模块自执行,即 AMD 规范的提前执行,之所选择这样做的原因是考虑到 Template 模块的特殊性,一般 Template 模块都会依赖 JavaScript 模块来做初始化工作,选择模块自执行的方式我们就不需要显式的在 Template 页面上书写 require 依赖,静态资源系统会自动加载 Template 模块的依赖,当模块并行加载结束后会一次自执行。大家可能会认为如果页面存在一些用不到的模块那都自执行岂不会浪费资源,这里大家可以不用担心,静态资源系统投放到前端的模块都是页面初始化所需要的,不存在浪费资源的情况。
  • Resource map 前端支持,主要用于为异步模块调用提供 uri 支持,resourceMap 为静态资源管理系统自动生成,无需人工调用,用于查询一个异步模块的真正 url,用于自动处理异步模块的 CDN、资源打包合并、强缓存问题,格式如下,

    require.resourceMap({
        "res": {
            "common:widget/sidebar/sidebar.async.js": {
                "url": "/static/common/widget/sidebar/sidebar.async_449e169.js"
            }
        }
    });
    
  • 处理循环引用,参照 nodeJS 处理循环引用的方式,在造成循环依赖的 require 之前把需要的东西 exports 出去,例如

    // a.js
    console.log('a string');
    exports.done = false;
    var b = require('./b.js');
    console.log('in a, b.done = '   b.done);
    exorts.done = true;
    console.log('b done');
    
    // b.js
    console.log('b starting');
    exports.done = false;
    
    var a = require('./a.js');
    console.log('in b, a.done = '   a.done);
    exports.done = true;
    console.log('b done');
    
    // main.js
    console.log('main starting');
    var a = require('./a.js');
    var b = require('./b.js');
    console.log('in main. a.done = '   a.done   ', b.done = '   b.done);
    

    如果在加载 a 的过程中,有其他的代码(假设为 b)require a.js 的话,那么 b 可以从 cache 中直接取到 a 的 module,从而不会引起重复加载的死循环。但带来的代价就是在 load 过程中,b 看到的是不完整的 a。

后端模块加载框架,主要用于处理模块的依赖并生成模块静态资源外链,下面我们将以实例讲解静态资源管理系统是如何对 JavaScript 模块进行加载的,如下我们有一个 sidebar 模块,目录下有如下资源

├── sidebar.async.js
├── sidebar.css
├── sidebar.js
└── sidebar.tpl

sidebar.tpl 中的内容如下,

<a id="btn-navbar" class="btn-navbar">



</a>

{script}
    $('a.btn-navbar').click(function() {
        require.async('./sidebar.async.js', function( sidebar ) {
            sidebar.run();
        });
    });
{/script}

对项目编译后,自动化工具会分析模块的依赖关系,并生成 map.json,如下

"common:widget/sidebar/sidebar.tpl": {
    "uri": "common/widget/sidebsr/sidebar.tpl",
    "type": "tpl",
    "extras": {
        "async": [
            "common:widget/sidebar/sidebar.async.js"
        ]
    },
    "deps": [
        "common:widget/sidebar/sidebar.js",
        "common:widget/sidebar/sidebar.css"
    ]
}

在 sidebar 模块被调用后,静态资源管理系统通过查询 map.json 可以得知,当前 sidebar 模块同步依赖 sidebar.js、sidebar.css,异步依赖 sdebar.async.js,在要输出的 html 前面,我们读取 uris 数组的数据,生成静态资源外链,我们得到最终的 html

<script type="text/javascript">
    require.resourceMap({
        "res": {
            "common:widget/sidebar/sidebar.async.js": {
                "url": "/satic/common/widget/sidebar/sidebar.async_449e169.js"
            }
        }
    });
</script>
<script type="text/javascript" src="/static/common/widget/sidebar/sidebar_$12cd4.js"></script>

如上可见,后端模块化框架将同步模块的 script url 统一生成到页面底部,将 css url 统一生成在 head 中,对于异步模块(require.async)注册 resourceMap 代码,框架会通过{script}标签收集到页面所有 script,统一管理并按顺序输出 script 到相应位置。

澳门新萄京 21

编译预览

自适应的性能优化

现在,当我们想对模块进行打包,该如何处理呢,我们首先使用一个 pack 配置项(下面是 fis 的打包配置项),对网站的静态资源进行打包,配置文件大致为,

fis.config.merge({
    pack: {
        'pkg/aio.css': '**.css'
    }
});

我们编译项目看一下产出的 map.json(resource map),有何变化,

{
    "res": {
        "A/A.tpl": {
            "uri": "/template/A.tpl",
            "deps": ["A/A.css"]
        },
        "A/A.css": {
            "uri": "/static/csss/A_7defa41.css",
            "pkg": "p0"
        },
        "B/B.tpl": {
            "uri": "/template/B.tpl",
            "deps": ["B/B.css"]
        },
        "B/B.css": {
            "uri": "/static/csss/B_33c5143.css",
            "pkg": "p0"
        },
        "C/C.tpl": {
            "uri": "/template/C.tpl",
            "deps": ["C/C.css"]
        },
        "C/C.css": {
            "uri": "/static/csss/C_ba59c31.css",
            "pkg": "p0"
        },
    },
    "pkg": {
        "p0": {
            "uri": "/static/pkg/aio_0cb4a19.css",
            "has": ["A/A.css", "B/B.css", "C/C.css"]
        }
    }
}

大家注意到了么,表里多了一张 pkg 表,所有被打包的资源会有一个 pkg 属性 指向该表中的资源,而这个资源,正是我们配置的打包策略。这样静态资源管理系统在表中查找 id 为 A/A.css 的资源,我们发现该资源有 pkg 属性,表明它被备份在了一个打包文件中。

我们使用它的 pkg 属性值 p0 作为 key,在 pkg 表里读取信息,取的这个包的资源路径为 /static/pkg/aio0cb4a19.css_ 存入 uris 数组 中将 p0 包的 has 属性所声明的资源加入到 has 表,在要输出的 html 前面,我们读取 uris 数组 的数据,生成静态资源外链,我们得到最终的 html 结果:

<html>
    <link href="/static/pkg/aio_0cb4a19.css">
    <div>html of A</div>
    <div>html of B</div>
    <div>html of C</div>
</html>

静态资源管理系统可以十分灵活的适应各种性能优化场景,我们还可以统计 {widget} 插件的调用情况,然后自动生成最优的打包配置,让网站可以自适应优化,这样工程师不用关心资源在哪,怎么来的,怎么没的,所有资源定位的事情,都交给静态资源管理系统就好了。静态资源路径都带 md5 戳,这个值只跟内容有关,静态资源服务器从此可以放心开启强缓存了!还能实现静态资源的分级发布,轻松回滚!我们还可以继续研究,比如根据国际化、皮肤,终端等信息约定一种资源路径规范,当后端适配到特定地区、特定机型的访问时,静态资源管理系统帮你送达不同的资源给不同的用户。说到这里,大家应该比较清楚整个“一体化”的模块化解决方案了,有人可能会问,这样做岂不是增加了后端性能开销?对于这个问题,我们实践过的经验是,这非常值得!其实这个后端开销很少,算法非常简单直白,但他所换来的前端工程化水平提高非常大!

覆盖式发布

编译任务

在编写完项目,就可以通过命令来对项目进行编译了,执行编译命令 $ ath build,会针对指定模块执行已经定义好的编译任务,根据项目需求,目前编译都是基于业务模块去编译,编译任务的最小执行单位是页面,每次编译都会执行以下编译列表

澳门新萄京 22

澳门新萄京 23

总结

本文是 fis 前端工程系列文章中的一部分,其实在前端开发工程管理领域还有很多细节值得探索和挖掘,提升前端团队生产力水平并不是一句空话,它需要我们能对前端开发及代码运行有更深刻的认识,对性能优化原则有更细致的分析与研究。fis 团队一直致力于从架构而非经验的角度实现性能优化原则,解决前端工程师开发、调试、部署中遇到的工程问题,提供组件化框架,提高代码复用率,提供开发工具集,提升工程师的开发效率。在前端工业化开发的所有环节均有可节省的人力成本,这些成本非常可观,相信现在很多大型互联网公司也都有了这样的共识。

本文只是将这个领域中很小的一部分知识的展开讨论,抛砖引玉,希望能为业界相关领域的工作者提供一些不一样的思路。欢迎关注fis项目,对本文有任何意见或建议都可以在fis开源项目中进行反馈和讨论。

作者:walter (http://weibo.com/u/1916384703) - F.I.S 

采用query更新缓存的方式实际上要覆盖线上文件的,index.html和a.js总有一个先后的顺序,从而中间出现一段或大或小的时间间隔。尤其是当页面是后端渲染的模板的时候,静态资源和模板是部署在不同的机器集群上的,上线的过程中,静态资源和页面文件的部署时间间隔可能会非常长,对于一个大型互联网应用来说即使在一个很小的时间间隔内,都有可能出现新用户访问。在这个时间间隔中,访问了网站的用户会发生什么情况呢?

本地预览

执行预览命令 $ath serve 会执行精简版编译任务来编译项目,编译完项目后会生成一份站点地图,随后打开一个本地服务器来预览项目,使用这个命令可以很方便地进行开发,在预览时会同时watch目录和文件的改动,并且提供了livereload功能,我们可以在预览时任意修改文件,都将实时地反映到页面中,同时可以新建另一个窗口执行新增组件和页面的操作,让整个开发过程非常顺畅,我们只需关注开发本身就好,不需要再关注其他事。

澳门新萄京 24

执行完编译任务后,默认自动打开浏览器,预览站点地图

澳门新萄京 25

  • 如果先覆盖index.html,后覆盖a.js,用户在这个时间间隙访问,会得到新的index.html配合旧的a.js的情况,从而出现错误的页面。
  • 如果先覆盖a.js,后覆盖index.html,用户在这个间隙访问,会得到旧的index.html配合新的a.js的情况,从而也出现了错误的页面。
Mock server

在进行项目预览的同时,Athena同时提供了mock data的服务,我们可以配置相应的路由,以及路由接口对应的假数据,所有的接口请求会发送到mock server上,在mock server中可以选择将请求代理到假数据平台还是代理到线上接口,这样就可以脱离后端进行开发联调了,以此实现数据的前后端分离。

澳门新萄京 26

这就是为什么大型web应用在版本上线的过程中经常会较集中的出现前端报错日志的原因,也是一些互联网公司选择加班到半夜等待访问低峰期再上线的原因之一。

项目部署

在开发预览完后,通过命令 $ ath publish 就可以将项目发布到配置好的测试机上,发布同时支持ftp、sftp以及http形式。

对于静态资源缓存更新的问题,目前来说最优方案就是 基于文件内容的hash版本冗余机制
了。也就是说,我们希望项目源码是这么写的:

组件维护

我们通过组件化的手段已经将我们的项目进行组件化了,这样我们经过业务迭代积累,产出很多业务公共组件,但在以往的项目开发中,公共组件的更新与维护一直很受限制,而且有哪些公共组件、公共组件长什么样子,只能依靠口口相传或者手工维护的文档。所以在Athena中我们加入了组件平台,在组件平台上统一展示各个业务的公共组件,而得益于本地工具,组件平台不需要人工干预维护,我们可以在本地通过命令 $ ath widget-publish [widgetName] 命令来发布一个组件到组件平台,这样其他人就可以立即在组件平台进行组件的预览,而其他人若想使用该组件时,在本地通过命令ath widget-load [widgetId] 就可以下载该组件到自己的模块目录下了。

这样组件的维护更加自动化,公共组件的使用也更加方便了。

组件发布

澳门新萄京 27

组件下载

澳门新萄京 28

<script type="text/javascript" src="a.js"></script>

自身优化

为了提升开发效率,Athena做了一些优化操作

发布后代码变成

精简项目预览时的任务

在开发时进行项目预览时,会执行精简版的编译任务,剔除了类似文件压缩、雪碧图生成、模板抽离处理等耗时的操作,只保留核心、必须的编译任务,这样可以极大地减少编译时间,提升开发的效率。

<script type="text/javascript" src="a_8244e91.js"></script>
预览时监听细化

在开发进行预览时,会对所有文件的改动进行监听,而针对每一类文件都有非常细化的操作,当文件改动时只会执行改文件所需要的编译任务,而不会进行整体编译,这样可以很好地提升开发效率。例如改动某一组件的CSS文件,则只会针对该文件执行一些相关的CSS操作。

同时得益于所有文件依赖关系的记录,在监听时会根据依赖关系进行文件编译,例如某sass文件中引入了另一个sass库文件,修改这个sass库文件的时候,会根据引用关系表同时更新到所有引用到这个sass文件的文件,这样项目文件更新及时,让开发流程更加流畅。

也就是a.js发布出来后被修改了文件名,产生一个新文件,并不是覆盖已有文件。其中”_82244e91”这串字符是根据a.js的文件内容进行hash运算得到的,只有文件内容发生变化了才会有更改。由于将文件发布为带有hash的新文件,而不是同名文件覆盖,因此不会出现上述说的那些问题。同时,这么做还有其他的好处:

编译缓存

在图片压缩和sass编译时,开启文件缓存,将已经编译过且没有改动的文件过滤掉,不再编译,大幅提升编译速度。

  • 上线的a.js不是同名文件覆盖,而是文件名 hash的冗余,所以可以先上线静态资源,再- 上线html页面,不存在间隙问题;
  • 遇到问题回滚版本的时候,无需回滚a.js,只须回滚页面即可;
    由于静态资源版本号是文件内容的hash,因此所有静态资源可以开启永久强缓存,只有更新了内容的文件才会缓存失效,缓存利用率大增
发布缓存

设置发布过滤,根据文件md5过滤掉已经发布过的文件,提升发布速度。

以文件内容的hash值为依据生产新文件的非覆盖式发布策略是解决静态资源缓存更新最有效的手段。###

虽然这种方案是相比之下最完美的解决方案,但它无法通过手工的形式来维护,因为要依靠手工的形式来计算和替换hash值,并生成相应的文件,将是一项非常繁琐且容易出错的工作,因此我们需要借助工具来处理。
用grunt来实现md5功能是非常困难的,因为grunt只是一个task管理器,而md5计算需要构建工具具有递归编译的能,而不是简单的任务调度。考虑这样的例子:

澳门新萄京 29

md5计算过程

由于我们的资源版本号是通过对文件内容进行hash运算得到,如上图所示,index.html中引用的a.css文件的内容其实也包含了a.png的hash运算结果,因此我们在修改index.html中a.css的引用时,不能直接计算a.css的内容hash,而是要先计算出a.png的内容hash,替换a.css中的引用,得到了a.css的最终内容,再做hash运算,最后替换index.html中的引用。
计算index.html中引用的a.css文件的url过程:

  • 压缩a.png后计算其内容的md5值
  • 将a.png的md5写入a.css,再压缩a.css,计算其内容的md5值
  • 将a.css的md5值写入到index.html中

grunt等task-based的工具是很难在task之间协作处理这样的需求的。在解决了基于内容hash的版本更新问题之后,我们可以将所有前端静态资源开启永久强缓存,每次版本发布都可以首先让静态资源全量上线,再进一步上线模板或者页面文件,再也不用担心各种缓存和时间间隙的问题了!

静态资源管理与模块化框架
剩余问题:

  • 请求数量: 合并脚本和样式表,拆分初始化负载
  • 请求带宽 :移除重复脚本
  • 缓存利用:使用Ajax可缓存
  • 页面结构: 将样式表放在头部,将脚本放在底部,尽早刷新文档的输出

剩下的优化原则都不是使用工具就能很好实现的,使用工具进行资源合并并替换引用或许是一个不错的办法,但在大型web应用,这种方式有一些非常严重的缺陷,来看一个很熟悉的例子 :

澳门新萄京 30

第一天

某个web产品页面有A、B、C三个资源

澳门新萄京 31

第二天

工程师根据“减少HTTP请求”的优化原则合并了资源

澳门新萄京 32

第三天

产品经理要求C模块按需出现,此时C资源已出现多余的可能

澳门新萄京 33

第四天

C模块不再需要了,注释掉吧!代码1秒钟搞定,但C资源通常不敢轻易剔除

澳门新萄京 34

后来

不知不觉中,性能优化变成了性能恶化……
这个例子来自 Facebook静态网页资源的管理和优化@Velocity China 2010

事实上,使用工具在线下进行静态资源合并是无法解决资源按需加载的问题的。如果解决不了按需加载,则必会导致资源的冗余;此外,线下通过工具实现的资源合并通常会使得资源加载和使用的分离,比如在页面头部或配置文件中写资源引用及合并信息,而用到这些资源的html组件写在了页面其他地方,这种书写方式在工程上非常容易引起维护不同步的问题,导致使用资源的代码删除了,引用资源的代码却还在的情况。因此,在工业上要实现资源合并至少要满足如下需求:

  • 确实能减少HTTP请求,这是基本要求(合并)
  • 在使用资源的地方引用资源(就近依赖),不使用不加载(按需)虽然资源引用不是集中书写的,但资源引用的代码最终还能出现在页面头部(css)或尾部(js)
    能够避免重复加载资源(去重)
    将以上要求综合考虑,不难发现,单纯依靠前端技术或者工具处理是很难达到这些理想要求的。

接下来我会讲述一种新的模板架构设计,用以实现前面说到那些性能优化原则,同时满足工程开发和维护的需要,这种架构设计的核心思想就是:
基于依赖关系表的静态资源管理系统与模块化框架设计

考虑一段这样的页面代码:

<html><head> 
<title>page</title> 
<link rel="stylesheet" type="text/css" href="a.css"/> 
<link rel="stylesheet" type="text/css" href="b.css"/>
 <link rel="stylesheet" type="text/css" href="c.css"/>
</head><body> 
<div> content of module a </div>
 <div> content of module b </div> 
<div> content of module c </div>
</body>
</html>

根据资源合并需求中的第二项,我们希望资源引用与使用能尽量靠近,这样将来维护起来会更容易一些,因此,理想的源码是:

<html>
<head>
 <title>page</title>
</head>
<body> 
<link rel="stylesheet" type="text/css" href="a.css"/>
 <div> content of module a </div>

 <link rel="stylesheet" type="text/css" href="b.css"/> 
<div> content of module b </div> 

<link rel="stylesheet" type="text/css" href="c.css"/> 
<div> content of module c </div>

</body></html>

当然,把这样的页面直接送达给浏览器用户是会有严重的页面闪烁问题的,所以我们实际上仍然希望最终页面输出的结果还是如最开始的截图一样,将css放在头部输出。这就意味着,页面结构需要有一些调整,并且有能力收集资源加载需求,那么我们考虑一下这样的源码(以php为例):

<html>
<head>
 <title>page</title> 
<!--[ CSS LINKS PLACEHOLDER ]-->
</head>
<body> 
<?php require_static('a.css'); ?> 
<div> content of module a </div>
 <?php require_static('b.css'); ?>
 <div> content of module b </div>
 <?php require_static('c.css'); ?>
 <div> content of module c </div>
</body>
</html>

在页面的头部插入一个html注释 作为占位,而将原来字面书写的资源引用改成模板接口 require_static 调用,该接口负责收集页面所需资源。require_static接口实现非常简单,就是准备一个数组,收集资源引用,并且可以去重。最后在页面输出的前一刻,我们将require_static在运行时收集到的 a.css、b.css,c.css 三个资源拼接成html标签,替换掉注释占位
,从而得到我们需要的页面结构。

经过实践总结,可以发现模板层面只要实现三个开发接口,就可以比较完美的实现目前遗留的大部分性能优化原则,这三个接口分别是:

  • require_static(res_id):收集资源加载需求的接口,参数是静态资源id。
  • load_widget(wiget_id):加载拆分成小组件模板的接口。你可以叫它为widget,component或者pagelet之类的。总之,我们需要一个接口把一个大的页面模板拆分成一个个的小部分来维护,最后在原来的页面中以组件为单位来加载这些小部件。
  • script(code):收集写在模板中的js脚本,使之出现的页面底部,从而实现性能优化原则中的 将js放在页面底部 原则。

实现了这些接口之后,一个重构后的模板页面的源代码可能看起来就是这样的了:

<html><head> 
<title>page</title>
 <?php require_static('jquery.js'); ?> 
<?php require_static('bootstrap.css'); ?>
 <?php require_static('bootstrap.js'); ?> 
<!--[ CSS LINKS PLACEHOLDER ]-->
</head>
<body> 
<?php load_widget('a'); ?>
 <?php load_widget('b'); ?> 
<?php load_widget('c'); ?>
 <!--[ SCRIPTS PLACEHOLDER ]-->
</body>
</html>

而最终在模板解析的过程中,资源收集与去重、页面script收集、占位符替换操作,最终从服务端发送出来的html代码为:

<html><head> 
<title>page</title> 
<link rel="stylesheet" type="text/css" href="bootstrap.css"/> 
<link rel="stylesheet" type="text/css" href="a.css"/> 
<link rel="stylesheet" type="text/css" href="b.css"/>
 <link rel="stylesheet" type="text/css" href="c.css"/>
</head>
<body>
 <div> content of module a </div> 
<div> content of module b </div>
 <div> content of module c </div> 
<script type="text/javascript" src="jquery.js"></script> 
<script type="text/javascript" src="bootstrap.js"></script> 
<script type="text/javascript" src="a.js"></script> 
<script type="text/javascript" src="b.js"></script>
 <script type="text/javascript" src="c.js"></script>
</body>
</html>

不难看出,我们目前已经实现了 按需加载,将脚本放在底部,将样式表放在头部 三项优化原则。
前面讲到静态资源在上线后需要添加hash戳作为版本标识,那么这种使用模板语言来收集的静态资源该如何实现这项功能呢?

技术选型

Athena本地工具早期技术选型是 Yeoman Gulp 的方式,但后来由于安装、更新非常麻烦,命令太长很难打的原因,我们改成了自己开发一个全局安装包的方式,编译核心使用的还是 Gulpvinyl-fs 来实现文件流处理,通过 ES6 Promise 来进行编译流程控制,最小以页面为单位,经过一系列编译任务,最后产出编译好的文件。

澳门新萄京 35

答案是:静态资源依赖关系表。##

考虑这样的目录结构:

![]CI6%_0FBW4.png](http://upload-images.jianshu.io/upload_images/1058258-e4067324e4a4c04e.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)

如果我们可以使用工具扫描整个project目录,然后创建一张资源表,同时记录每个资源的部署路径,得到这样的一张表:

基于这张表,我们就很容易实现 require_static(file_id),load_widget(widget_id)
这两个模板接口了。以load_widget为例:

利用查表来解决md5戳的问题,这样,我们的页面最终送达给用户的结果就是这样的:

接下来,我们讨论基于表的设计思想上是如何实现静态资源合并的。或许有些团队使用过combo服务,也就是我们在最终拼接生成页面资源引用的时候,并不是生成多个独立的link标签,而是将资源地址拼接成一个url路径,请求一种线上的动态资源合并服务,从而实现减少HTTP请求的需求,比如前面的例子,稍作调整即可得到这样的结果:

这个 /??file1,file2,file3,… 的url请求响应就是动态combo服务提供的,它的原理很简单,就是根据url找到对应的多个文件,合并成一个文件来响应请求,并将其缓存,以加快访问速度。
这种方法很巧妙,有些服务器甚至直接集成了这类模块来方便的开启此项服务,这种做法也是大多数大型web应用的资源合并做法。但它也存在一些缺陷:

  • 浏览器有url长度限制,因此不能无限制的合并资源。
  • 如果用户在网站内有公共资源的两个页面间跳转访问,由于两个页面的combo的url不一样导致用户不能利用浏览器缓存来加快对公共资源的访问速度。
  • 如果combo的url中任何一个文件发生改变,都会导致整个url缓存失效,从而导致浏览器缓存利用率降低。

对于上述第二条缺陷,可以举个例子来看说明:

  • 假设网站有两个页面A和B

  • A页面使用了a,b,c,d四个资源

  • B页面使用了a,b,e,f四个资源

  • 如果使用combo服务,我们会得:

  • A页面的资源引用为:/??a,b,c,d

  • B页面的资源引用为:/??a,b,e,f

  • 两个页面引用的资源是不同的url,因此浏览器会请求两个合并后的资源文件,跨页面访问没能很好的利用a、b这两个资源的缓存。

很明显,如果combo服务能聪明的知道A页面使用的资源引用为 /??a,b
和 /??c,d
,而B页面使用的资源引用为 /??a,b
和 /??e,f
就好了。这样当用户在访问A页面之后再访问B页面时,只需要下载B页面的第二个combo文件即可,第一个文件已经在访问A页面时缓存好了的。基于这样的思考,我们在资源表上新增了一个字段,取名为 pkg,就是资源合并生成的新资源,表的结构会变成:

相比之前的表,可以看到新表中多了一个pkg字段,并且记录了打包后的文件所包含的独立资源。这样,我们重新设计一下 require_static、load_widget 这两个模板接口,实现这样的逻辑:
在查表的时候,如果一个静态资源有pkg字段,那么就去加载pkg字段所指向的打包文件,否则加载资源本身。

比如执行require_static('bootstrap.js'),查表得知bootstrap.js被打包在了p1中,因此取出p1包的url /pkg/lib_cef213d.js,并且记录页面已加载了 jquery.js 和 bootstrap.js 两个资源。这样一来,之前的模板代码执行之后得到的html就变成了:

![]6PSM{F1%%UED4R.png](http://upload-images.jianshu.io/upload_images/1058258-8bc134c681a0d7f2.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)

虽然这种策略请求有4个,不如combo形式的请求少,但可能在统计上是性能更好的方案。由于两个lib打包的文件修改的可能性很小,因此这两个请求的缓存利用率会非常高,每次项目发布后,用户需要重新下载的静态资源可能要比combo请求节省很多带宽。

管理平台

性能优化一直是前端工程师探索的课题,很多时候就是资源的分配问题,也就是资源管理。为了更好地配合本地构建工具来管理资源,我们搭建了管理平台。我们来看下,结合本地构建工具和管理平台,工作流程变成了怎样?

性能优化既是一个工程问题,又是一个统计问题。优化性能时如果只关注一个页面的首次加载是很片面的。还应该考虑全站页面间跳转、项目迭代后更新资源等情况下的优化策略。##

此时,我们又引入了一个新的问题:如何决定哪些文件被打包?
从经验来看,项目初期可以采用人工配置的方式来指定打包情况,比如:

但随着系统规模的增大,人工配置会带来非常高的维护成本,此时需要一个辅助系统,通过分析线上访问日志和静态资源组合加载情况来自动生成这份配置文件,系统设计如图:

澳门新萄京 36

至此,我们通过基于表的静态资源管理系统和三个模板接口实现了几个重要的性能优化原则,现在我们再来回顾一下前面的性能优化原则分类表,剔除掉已经做到了的,看看还剩下哪些没做到的:

  • 请求数量: 拆分初始化负载
  • 缓存利用:使用Ajax可缓存
  • 页面结构:尽早刷新文档的输出

拆分初始化负载 的目标是将页面一开始加载时不需要执行的资源从所有资源中分离出来,等到需要的时候再加载。工程师通常没有耐心去区分资源的分类情况,但我们可以利用组件化框架接口来帮助工程师管理资源的使用。还是从例子开始思考,如果我们有一个js文件是用户交互后才需要加载的,会怎样呢:

<html><head> 
<title>page</title> 
<?php require_static('jquery.js'); ?> 
<?php require_static('bootstrap.css'); ?>
 <?php require_static('bootstrap.js'); ?>
 <!--[ CSS LINKS PLACEHOLDER ]-->
</head>
<body>
 <?php load_widget('a'); ?> 
<?php load_widget('b'); ?> 
<?php load_widget('c'); ?> 
<?php script('start'); ?>
 <script> $(document.body).click(function(){
 require.async('dialog.js', function(dialog){
 dialog.show('you catch me!');
 }); 
}); 
</script>
 <?php script('end'); ?>
 <!--[ SCRIPTS PLACEHOLDER ]-->
</body>
</html>

很明显,dialog.js 这个文件我们不需要在初始化的时候就加载,因此它应该在后续的交互中再加载,但文件都加了md5戳,我们如何能在浏览器环境中知道加载的url呢?

答案就是:把静态资源表的一部分输出在页面上,供前端模块化框架加载静态资源。

我就不多解释代码的执行过程了,大家看到完整的html输出就能理解是怎么回事了:

<html><head> <title>page</title>
 <link rel="stylesheet" type="text/css" href="/pkg/lib_afec33f.css"/>
 <link rel="stylesheet" type="text/css" href="/pkg/widgets_af23ce5.css"/><
/head><body>
 <div> content of module a </div> 
<div> content of module b </div> 
<div> content of module c </div> 
<script type="text/javascript" src="/pkg/lib_cef213d.js"></script>
 <script type="text/javascript" src="/pkg/widgets_22feac1.js"></script> 
<script> //将静态资源表输出在前端页面中 
require.config({ res : { 'dialog.js' : '/dialog_fa3df03.js' } }); 
</script> 
<script> $(document.body).click(function(){ //require.async接口查表确定加载资源的url require.async('dialog.js', function(dialog){ dialog.show('you catch me!');
 }); }); 
</script>
</body>
</html>

dialog.js不会在页面以script src的形式输出,而是变成了资源注册,这样,当页面点击触发require.async执行的时候,async函数才会查表找到资源的url并加载它,加载完毕后触发回调函数。以上框架示例我实现了一个java-jsp版的,有兴趣的同学请看这里:https://github.com/fouber/fis-java-jsp

到目前为止,我们又以架构的形式实现了一项优化原则(拆分初始化负载),回顾我们的优化分类表,现在仅有两项没能做到了:

  • 缓存利用:使用Ajax可缓存
  • 页面结构:尽早刷新文档的输出

剩下的两项优化原则要做到并不容易,真正可缓存的Ajax在现实开发中比较少见,而 尽早刷新文档的输出原则facebook在2010年的velocity上 提到过,就是BigPipe技术。当时facebook团队还讲到了Quickling和PageCache两项技术,其中的PageCache算是比较彻底的实现Ajax可缓存的优化原则了。由于篇幅关系,就不在此展开了,后续还会撰文详细解读这两项技术。
总结
其实在前端开发工程管理领域还有很多细节值得探索和挖掘,提升前端团队生产力水平并不是一句空话,它需要我们能对前端开发及代码运行有更深刻的认识,对性能优化原则有更细致的分析与研究。在前端工业化开发的所有环节均有可节省的人力成本,这些成本非常可观,相信现在很多大型互联网公司也都有了这样的共识。
问题
1.每个文件改动后生产md5后缀,多次上线后线上会产生:

···
a_xxx1.js
a_xxx2.js
a_xxx3.js
···

也许是我要洁癖,但是这样循环N次后,上线的全量包会越来约大,如何处理这个的?

  • 每次上线,只有修改过的文件才会出现新的md5戳,所以文件冗余没有想象中的那么多
    比较频繁修改的业务模块大概每年会产生100m左右的冗余,预计每3年有必要清理一次
  • 清理的时候,写一个脚本,根据文件名规则找到最后访问的文件然后删除其他的。活着干脆某次上线把发布后的文件之外的其他文件都清理一次,总之这个不成问题

** 2.HTML是后端们JAVA写的动态页面,前端们只写JS,css,然后静态资源发布后,生成了新的md5,那么JAVA写的页面里怎么去获取这个新的MD5,以保证加载正确的静态资源。是要在前端静态文件服务器上搞个监控,把新的MD5存某个地方,然后JAVA那边每次请求页面都要获取下新的MD5,替换生成新的链接?**

java写动态页面不是?不要让他们在java的模板中写这样的代码:

<script src="a.js"></script>

改成写这样的代码:

<fis:require id="a.js"/>

这个 fis:require
的标签,是扩展了jsp的自定义标签。然后,构建工具扫描前端写的js、css,建立一个map资源表,内容大概是:

{ "a.js" : { 
"url": "/static/js/a_0fa0c3b.js", 
"deps": [ "b.js" ] },
 "b.js" : { 
"url": "/static/js/b_4cb04f9.js" 
}
}

然后,我们把这个资源表和java的动态页面放在一起。前面提到的模板中的那个 fis:require 标签,在模板解释执行的时候,会去查这个map表,根据 a.js 这个资源id找到它的带md5戳的url就是“/static/js/a_0fa0c3b.js”,同时还知道这个文件依赖了 b.js
就顺便把b.js的url也收集起来。
最后,在java动态页面生成html之前,把收集到的两个js标签用字符串替换的方式生成script标签插入到页面上,得到:

<script src="/static/js/a_0fa0c3b.js"></script>
<script src="/static/js/b_4cb04f9.js"></script>

有一个项目展示了这个思路的整个实现过程: https://github.com/fouber/fis-java-jsp
**
这个 资源表(map)和fis:require标签是解决这个问题的重点,map是构建工具生成的,通过静态扫描整个前端工程代码得到。map的作用是记录资源的依赖关系和部署路径,然后交给资源管理框架去决定资源加载策略,因此我们最终要把map跟java动态语言部署在一起。fis:require是运行在后端动态模板语言中的资源管理框架,它依赖map表的数据信息,你可以把它理解成一个写在模板引擎中的requirejs。设计这个框架的目的是彻底替<script>标签和<link>标签这种字面量资源定位符,把它们改造成可编程的资源管理框架,在模板渲染的过程中收集页面所用资源,实现去重、依赖管理、资源加载、带md5等等功能**

3、构建工具扫描前端写的js、css,是根据ID匹配文件名截取文件名上的MD5还是扫描文件内容生成MD5?然后生成MAP。
扫描所有文件,计算文件的摘要,然后生成url。再以文件工程路径为key,建立map表,整个过程不会替换任何文件内容,只是建立表。

4、JS源文件是PUSH到server1,然后在server1上fis编译JS,后端代码是放server2,构建工具是往server1上扫描编译好后的js吧,还是源文件?
都是线下编译。线下设置好js、css要发布的server1的域名、路径,然后release,生成编译后的代码和map,把代码发布到server1上,把map发布到server2上,map中写入的js、css的路径都是符合预期的。构建工具扫描的并不是简单的编译后的结果。我们用工具读取所有文件,然后逐个编译,然后把编译后的结果发布为带md5戳的资源,同时在map中记录的是 源码的文件路径(也就是开发中的工程路径)
和 发布后的资源路径
的映射关系,工程路径 ≠ 部署路径,它们有很大差别。部署路径带md5戳,而且可能变换了发布目录。这样我们采用源码的工程路径作为文件id,在java等动态语言中也可以使用工程路径去加载资源,看起来非常符合人类的直觉。

5、我们后端是groovy语言和grails框架写的页面,fis支持吗?
其他语言可以根据fis的map.json结构,和fis资源管理的思想自己实现这个框架,并不复杂

6.map.json的升级问题,有两个方案:

  • 非覆盖式发布map.json,配置fis,让map.json发布的时候带一个构建时间戳,然后把这个时间戳写入到java模板中,先发布map.json,但是线上运行的java页面读取的还是旧的map,然后部署模板,模板中声明了使用新版本的map.json,问题解决
  • 持久化模板中的map数据。模板引擎一般只有再模板修改后才会重新编译模板,你把读取map的逻辑变成编译后静态写入的结果,下次上线后,先覆盖map.json,这个时候所有模板都还只是使用上一个版本的map数据,然后发布模板,再触发一下模板编译,读入新的map
    7.放在require.asyn里面 一样是异步加载 但是在编译的时候 直接把md5后的名字替换了dialog.js这名字 浏览器运行时 在需要的时候加载的也还是对应的资源

require.async要做两件事,一个是加载资源,一个是加载完成后回调。

加载资源不仅仅是加载资源本身,还要加载依赖的资源,以及依赖的依赖。比如这个dialog.js,并不是独立资源,它可能还会依赖其他文件,假设它依赖了component.js和dialog.css两个资源,component.js又依赖component.css,那么我们得到一颗依赖树:

dialog.js
       ├ dialog.css
       └ component.js
       └ component.css

问题来了,我们怎么告诉require.async,在加载dialog.js的时候,要一并加载其他3个资源呢?我们势必要将依赖关系表放在前端才能实现这个优化,也就有了针对require.async加载的依赖配置项。有这个依赖表,还意味着我们根本没必要把 require.async(id, callback)
接口设计成 require.async(url, callback)
,因为保留id,在查询依赖关系的时候最方便。
当然,你或许会想到“我们用文件的url建立依赖关系不就行了么?”,这里还涉及到另外一个问题,就是我们加载dialog.js,未必就是加载dialog.js这个文件的独立url,如果它被打包了,我们其实要加载的是它所在资源包的url,比如dialog.js和component.js合并成了aio.js,我们虽然require.async('dialog.js'),但实际上请求的是aio.js这个url。
你或许又想到了“我们用构建工具把require.async的资源路径改成打包后的url地址不就行了?”,恩,这里又涉及到另外一个资源加载问题:动态请求。比如我们需要根据一些运行时的参数来加载模块:

var mod = isIE ? 'fuck.js' : 'nice.js';
require.async(mod, function(m){
 //blablabla
});

前端只有资源表的好处是支持动态加载模块,只要把依赖表输出给前端,就能实现真正的按需加载,这是单纯的静态分析所无法实现的。
此外,require.async还要监听资源加载完毕时间,require.async(id, callback)
这样的设计,可以让define(id, factory)接口被调用的时候,根据id派发模块加载完毕事件,如果把require.async设计成使用url作为参数,那就要改成通过监听script的onload事件来判断资源加载完成与否,这样也麻烦一些。

8.实际开发debug调试的时候和最终打包发布线上这之间是如何区分的
这其实是一个构建工具的使用技巧,本地开发和上线部署的构建过程稍微有一些差别而已,上线部署的构建过程需要给资源加上domain。

以fis为例,我们把压缩、资源合并、加md5,加域名等构建操作变成命令行的参数,比如我们本地开发这样的命令:

fis release --dest ../dev

就是构建一下代码,把结果发布到dev目录下,然后我们在dev目录下启动服务器进行本地开发调试,而当我们要提测的时候,并不是用dev目录的东西,而是真的源码又发布一次:

fis release --optimize --hash --pack --dest ../test

这回,我们对代码进行了压缩、加md5、资源合并操作,并发布到了另外一个test目录中,测试是在test目录下进行的。

最终上线,我们也不是使用的test目录下的代码,而是又从源码重新发布一份:

fis release --optimize --hash --pack --domain --dest ../prod

有多了一个 --domain 参数,给资源加上CDN的域名,最终上线用的是prod里的代码。设计原则是始终从源码构建出结果,构建结果可能是开发中的,可能是提测用的,也可能是部署到生产环境的。

作为构建构建,至少要保证针对不同环境的构建代码逻辑是等价的,不能引入额外的不确定因素导致测试和部署结果不一致

摘自fouber前端工程与性能优化

工作流程

  1. 在管理平台上创建项目,输入项目名称和预览机,以及选择相应的模板等;
  2. 在终端执行ath app指令,工具会优先拉取远程服务器的项目信息来初始化项目,如果没有获取到相关信息,就会在本地生成项目,并将项目信息上报给服务器;
  3. 项目初始化后,就可以创建模块、页面、组件了;
  4. 在编码过程中,可通过ath server预览页面;
  5. 在本地通过后,可执行ath publish将代码发布到开发机或者预览机。

在上面的publish指令中,工具会扫描所有文件,执行代码检查,扫描页面文件,获取组件依赖关系,根据组件依赖关系进行文件合并,然后会进行样式处理、js处理以及图片的处理,根据配置是否进行md5重命名文件,组装html,插入样式、js和图片,最后将编译好的文件发布到相应的机器。在整个过程里面,会生成资源关系依赖表,最终会将资源关系表及编译后的文件上传至管理平台。

除此之外,每个指令的操作都会上报给管理平台。管理平台收到数据后,会对数据进行处理,最终可以在平台上看到项目相关的信息。

整体工作流程图如下:

澳门新萄京 37

从上面的工作流程中,我们可以看到,管理平台需要有数据统计、资源管理以及项目管理的功能。整体架构图如下:

澳门新萄京 38

数据统计

数据统计包含项目操作日志,主要是用于统计团队每个成员具体的操作,方便项目成员查看项目代码变更;另一部份是统计样式表、脚本以及图片的压缩数据,用于显示工具给我们项目带来的提升。

以下是操作日志统计:

澳门新萄京 39

资源管理

资源管理是管理平台的核心,主要分为4个部分:模块展示、依赖关系、组件预览和权限控制。这部分功能主要通过本地构建工具提供的资源关系表来完成。

模块展示

模块展示,用于记录项目具体包含哪些模块以及模块具体的信息。在平常开发中,我们的项目会分为许多模块,不同的模块有不同的人来开发和维护。当项目越大的时候,可以通过管理平台清晰地看到模块具体的信息。

澳门新萄京 40

依赖关系

依赖关系,主要是html、css、js和图片相互之间的关系。通过分析资源关系依赖表,可以获取到各个资源被引用的情况以及线上版本的情况。当线上环境采用md5来做资源管理时,我们不是很清晰地知道静态资源对应线上哪个版本的资源,而有了这个依赖关系表,当出现问题时,我们可以更快地定位到具体的资源。

澳门新萄京 41

组件管理

我们采用组件来拼凑页面,当项目越大时,组件越多,那么如何管理组件成为了一个棘手的问题。比如说,有一些比较老的冗余组件,我们不确定是否为其他页面所引用,那么就不能愉快地删除它。有了组件管理,可以清晰地知道组件的被调用情况,就可以对组件做相应的操作。

组件管理,结合组件平台来使用,在管理平台上引用组件地址预览组件,同时可以获取到组件被引用以及引用资源(如css、js、图片)的相关情况。

澳门新萄京 42

我们的组件分为两种,一类是通过ath w自动创建的,通过ath pu提交到管理平台的,在管理平台上进行组件的相关分析和编译,得到组件的信息,这类组件主要是跟业务绑定的;另一类是通过ath widget-publish提交到组件平台的,由组件平台进行相关处理,这类组件是通用组件,与业务无关,用于展示给开发以及相关业务方看的。

澳门新萄京 43

在组件平台上可以预览与编辑相关的组件,通过与设计师约定相关的设计规范来促使组件达到尽可能地复用,进而减少设计师的工作量,提升我们的工作效率。

澳门新萄京 44

组件提交到组件平台

通过ath widget-publish指令将组件提交到组件平台,组件平台会对组件源码进行编译,将组件名称md5、组件归类以及组件版本记录等等。

澳门新萄京 45

从组件平台上下载组件

通过ath widget-load指令将组件下载到本地,当本地构建工具向组件平台发起请求时,会带上组件名称,组件平台会将源码进行编译,将组件名称重命名,并且相应地替换源码中的组件名称,同时记录组件的被引用记录。

澳门新萄京 46

权限控制

权限控制,项目中存在公共组件模块,公共组件比较稳定,比如说轮播组件、选项卡组件等等,这部分代码一般比较少变动,可由少部分人来更新和维护,所以加入了权限控制机制,保证公共组件的稳定性。

项目管理

我们在使用本地构建工具时,需要配置多个参数,比如主机信息、选择模版等,在命令行环境下有些不直观。为了简化这个操作,管理平台提供了项目创建的功能,同时提供了模版创建的功能。

澳门新萄京 47

在项目信息、模块信息以及组件信息发生变更的时候,为了第一时间能够通知项目成员更新,加入了消息通知的功能,目前通过发送邮件的方式,后期可以加入微信提醒的功能。

技术选型

管理平台前端采用React Redux的方式,后端采用Express MongoDB,整体技术选型如下:

澳门新萄京 48

假数据服务

存在的问题

在平常的开发中,经常需要前后端联调,但是在项目开始之初,很多接口并没有提供,在以前的开发模式下,需要等待后端提供接口或者自己先定义接口,前端开发的进度可能会受影响。

Mock数据平台

为了不影响前端开发的进度,我们搭建了Mock数据平台,通过与后端协商数据格式,自定义数据接口,这样子就可以做到前后端分离,让前端独立于后端进行开发。

Mock数据平台基于mockjs搭建而成,通过简单的mock语法来生成数据。

Mock数据平台目前有如下功能:

  1. 创建模拟数据,使之符合各种场景;
  2. 生成json数据接口,支持CORS以及jsonp。

澳门新萄京 49

写在最后

本次分享首先讲述了我们在业务膨胀、人员不断增加的背景下遇到的项目开发上的问题,并提出了我们自己对于这些问题思考总结后得出的解决方案与思路,最后产出适合我们团队、业务的开发工具—— Athena。希望我们的方案能给大家带来一定的借鉴作用。

1 赞 14 收藏 评论

澳门新萄京 50

本文由澳门新萄京发布于澳门新萄京最大平台,转载请注明出处:大家是如何做好前端工程化和静态能源管理,前

上一篇:致我们终将组件化的Web,webpack使用理解 下一篇:没有了
猜你喜欢
热门排行
精彩图文