静态资源管理与模块化框架
解决了静态资源缓存问题之后,让我们再来看看前面的优化原则表还剩些什么:
优化方向 优化手段
请求数量 合并脚本和样式表,拆分初始化负载
请求带宽 移除重复脚本
缓存利用 使Ajax可缓存
页面结构 将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出
很不幸,剩下的优化原则都不是使用工具就能很好实现的。或许有人会辩驳:“我用某某工具可以实现脚本和样式表合并”。嗯,必须承认,使用工具进行资源合并并替换引用或许是一个不错的办法,但在大型web应用,这种方式有一些非常严重的缺陷,来看一个很熟悉的例子 :
某个web产品页面有A、B、C三个资源
工程师根据“减少HTTP请求”的优化原则合并了资源
产品经理要求C模块按需出现,此时C资源已出现多余的可能
C模块不再需要了,注释掉吧!代码1秒钟搞定,但C资源通常不敢轻易剔除
不知不觉中,性能优化变成了性能恶化……
这个例子来自 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注释 <!--[CSS LINKS PLACEHOLDER]--> 作为占位,而将原来字面书写的资源引用改成模板接口 require_static 调用,该接口负责收集页面所需资源。
requirestatic接口实现非常简单,就是准备一个数组,收集资源引用,并且可以去重。最后在页面输出的前一刻,我们将requirestatic在运行时收集到的 a.css、b.css、c.css 三个资源拼接成html标签,替换掉注释占位 <!--[CSS LINKS PLACEHOLDER]-->,从而得到我们需要的页面结构。
经过实践总结,可以发现模板层面只要实现三个开发接口,就可以比较完美的实现目前遗留的大部分性能优化原则,这三个接口分别是:
requirestatic(resid):收集资源加载需求的接口,参数是静态资源id。
loadwidget(wigetid):加载拆分成小组件模板的接口。你可以叫它为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戳作为版本标识,那么这种使用模板语言来收集的静态资源该如何实现这项功能呢?
答案是:静态资源依赖关系表。
考虑这样的目录结构:
project
- widget
- a
- a.css
- a.js
- a.php
- b
- b.css
- b.js
- b.php
- c
- c.css
- c.js
- c.php
- jquery.js
- bootstrap.css
- bootstrap.js
- index.php
如果我们可以使用工具扫描整个project目录,然后创建一张资源表,同时记录每个资源的部署路径,得到这样的一张表:
{
"res" : {
"widget/a/a.css" : "/widget/a/a_1688c82.css",
"widget/a/a.js" : "/widget/a/a_ac3123s.js",
"widget/b/b.css" : "/widget/b/b_52923ed.css",
"widget/b/b.js" : "/widget/b/b_a5cd123.js",
"widget/c/c.css" : "/widget/c/c_03cab13.css",
"widget/c/c.js" : "/widget/c/c_bf0ae3f.js",
"jquery.js" : "/jquery_9151577.js",
"bootstrap.css" : "/bootstrap_f5ba12d.css",
"bootstrap.js" : "/bootstrap_a0b3ef9.js"
},
"pkg" : {}}
基于这张表,我们就很容易实现 require_static(file_id),load_widget(widget_id) 这两个模板接口了。以load_widget为例:
function load_widget($id){
//从json文件中读取资源表
$map = load_map();
//查找静态资源
$filename = 'widget/' . $id . '/' . $id;
//查找js文件
$js = $filename . '.js';
if(isset($map['res'][$js])) {
//如果有对应的js资源,就收集起来
collect_js_static($map['res'][$js]);
}
//查找css文件
$css = $filename . '.css';
if(isset($map['res'][$css])) {
//如果有对应的css资源,就收集起来
collect_css_static($map['res'][$css]);
}
include $filename . '.php';}
利用查表来解决md5戳的问题,这样,我们的页面最终送达给用户的结果就是这样的:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="/bootstrap_f5ba12d.css"/>
<link rel="stylesheet" type="text/css" href="/widget/a/a_1688c82.css"/>
<link rel="stylesheet" type="text/css" href="/widget/b/b_52923ed.css"/>
<link rel="stylesheet" type="text/css" href="/widget/c/c_03cab13.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_9151577.js"></script>
<script type="text/javascript" src="/bootstrap_a0b3ef9.js"></script>
<script type="text/javascript" src="/widget/a/a_ac3123s.js"></script>
<script type="text/javascript" src="/widget/b/b_a5cd123.js"></script>
<script type="text/javascript" src="/widget/c/c_bf0ae3f.js"></script></body></html>
接下来,我们讨论基于表的设计思想上是如何实现静态资源合并的。或许有些团队使用过combo服务,也就是我们在最终拼接生成页面资源引用的时候,并不是生成多个独立的link标签,而是将资源地址拼接成一个url路径,请求一种线上的动态资源合并服务,从而实现减少HTTP请求的需求,比如前面的例子,稍作调整即可得到这样的结果:
<html><head>
<title>page</title>
<link rel="stylesheet" type="text/css" href="/??bootstrap_f5ba12d.css,widget/a/a_1688c82.css,widget/b/b_52923ed.css,widget/c/c_03cab13.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_9151577.js,bootstrap_a0b3ef9.js,widget/a/a_ac3123s.js,widget/b/b_a5cd123.js,widget/c/c_bf0ae3f.js"></script></body></html>
这个 /??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,就是资源合并生成的新资源,表的结构会变成:
{
"res" : {
"widget/a/a.css" : "/widget/a/a_1688c82.css",
"widget/a/a.js" : "/widget/a/a_ac3123s.js",
"widget/b/b.css" : "/widget/b/b_52923ed.css",
"widget/b/b.js" : "/widget/b/b_a5cd123.js",
"widget/c/c.css" : "/widget/c/c_03cab13.css",
"widget/c/c.js" : "/widget/c/c_bf0ae3f.js",
"jquery.js" : "/jquery_9151577.js",
"bootstrap.css" : "/bootstrap_f5ba12d.css",
"bootstrap.js" : "/bootstrap_a0b3ef9.js"
},
"pkg" : {
"p0" : {
"url" : "/pkg/lib_cef213d.js",
"has" : [ "jquery.js", "bootstrap.js" ]
},
"p1" : {
"url" : "/pkg/lib_afec33f.css",
"has" : [ "bootstrap.css" ]
},
"p2" : {
"url" : "/pkg/widgets_22feac1.js",
"has" : [
"widget/a/a.js",
"widget/b/b.js",
"widget/c/c.js"
]
},
"p3" : {
"url" : "/pkg/widgets_af23ce5.css",
"has" : [
"widget/a/a.css",
"widget/b/b.css",
"widget/c/c.css"
]
}
}}
相比之前的表,可以看到新表中多了一个pkg字段,并且记录了打包后的文件所包含的独立资源。这样,我们重新设计一下 requirestatic、loadwidget 这两个模板接口,实现这样的逻辑:
在查表的时候,如果一个静态资源有pkg字段,那么就去加载pkg字段所指向的打包文件,否则加载资源本身。
比如执行require_static('bootstrap.js'),查表得知bootstrap.js被打包在了p1中,因此取出p1包的url /pkg/lib_cef213d.js,并且记录页面已加载了 jquery.js 和 bootstrap.js 两个资源。这样一来,之前的模板代码执行之后得到的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></body></html>
虽然这种策略请求有4个,不如combo形式的请求少,但可能在统计上是性能更好的方案。由于两个lib打包的文件修改的可能性很小,因此这两个请求的缓存利用率会非常高,每次项目发布后,用户需要重新下载的静态资源可能要比combo请求节省很多带宽。
性能优化既是一个工程问题,又是一个统计问题。优化性能时如果只关注一个页面的首次加载是很片面的。还应该考虑全站页面间跳转、项目迭代后更新资源等情况下的优化策略。
此时,我们又引入了一个新的问题:如何决定哪些文件被打包?
从经验来看,项目初期可以采用人工配置的方式来指定打包情况,比如:
{
"pack" : {
"lib.js" : [ "jquery.js", "bootstrap.js" ],
"lib.css" : "bootstrap.css",
"widgets.js" : "widget/**.js",
"widgets.css" : "widget/**.css"
}}
但随着系统规模的增大,人工配置会带来非常高的维护成本,此时需要一个辅助系统,通过分析线上访问日志和静态资源组合加载情况来自动生成这份配置文件,系统设计如图:
至此,我们通过基于表的静态资源管理系统和三个模板接口实现了几个重要的性能优化原则,现在我们再来回顾一下前面的性能优化原则分类表,剔除掉已经做到了的,看看还剩下哪些没做到的:
优化方向 优化手段
请求数量 拆分初始化负载
缓存利用 使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可缓存的优化原则了。由于篇幅关系,就不在此展开了,后续还会撰文详细解读这两项技术。
总结
其实在前端开发工程管理领域还有很多细节值得探索和挖掘,提升前端团队生产力水平并不是一句空话,它需要我们能对前端开发及代码运行有更深刻的认识,对性能优化原则有更细致的分析与研究。在前端工业化开发的所有环节均有可节省的人力成本,这些成本非常可观,相信现在很多大型互联网公司也都有了这样的共识。
本文只是将这个领域中很小的一部分知识的展开讨论,抛砖引玉,希望能为业界相关领域的工作者提供一些不一样的思路。
来源:http://div.io/topic/371
- PHP技术文章
- PHP中session和cookie的区别
- php设计模式(一):简介及创建型模式
- php设计模式结构型模式
- Php设计模式(三):行为型模式
- 十款最出色的 PHP 安全开发库中文详细介绍
- 12个提问频率最高的PHP面试题
- PHP 语言需要避免的 10 大误区
- PHP 死锁问题分析
- 致PHP路上的“年轻人”
- PHP网站常见安全漏洞,及相应防范措施总结
- 各开源框架使用与设计总结(一)
- 数据库的本质、概念及其应用实践(二)
- PHP导出MySQL数据到Excel文件(fputcsv)
- PHP中14种排序算法评测
- 深入理解PHP原理之--echo的实现
- PHP性能分析相关的函数
- PHP 性能分析10则
- 10 位顶级 PHP 大师的开发原则
- 30条爆笑的程序员梗 PHP是最好的语言
- PHP底层的运行机制与原理
- PHP 性能分析与实验——性能的宏观分析
- PHP7 性能翻倍关键大揭露
- 鸟哥:写在PHP7发布之际一些话
- PHP与MySQL通讯那点事
- Php session内部执行流程的再次剖析
- 关于 PHP 中的 Class 的几点个人看法
- PHP Socket 编程过程详解
- PHP过往及现在及变革
- PHP吉祥物大象的由来
- PHP生成静态页面的方法
- 吊炸天的 PHP 7 ,你值得拥有!
- PHP开发中文件操作疑难问答
- MongoDB PHP Driver的连接处理解析
- PHP 杂谈《重构-改善既有代码的设计》之二 对象
- 在php中判断一个请求是ajax请求还是普通请求的方法
- 使用HAProxy、PHP、Redis和MySQL支撑10亿请求每周架构细节
- HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、Web Services 是什么?
- 重构-改善既有代码的设计
- PHP场景中getshell防御思路分享
- 移动互联时代,你看看除了PHP你还会些什么
- 安卓系统上搭建本地php服务器环境
- PHP中常见的缓存技术!
- PHP里10个鲜为人知但却非常有用的函数
- 成为一名PHP专家其实并不难
- PHP 命令行?是的,您可以!
- PHP开发提高效率技巧
- PHP八大安全函数解析
- PHP实现四种基本排序算法
- PHP开发中的中文编码问题
- php.get.post
- php发送get、post请求的6种方法简明总结
- 中高级PHP开发者应该掌握哪些技术?
- 前端开发
- web前端知识体系大全
- 前端工程与性能优化(下)
- 前端工程与性能优化(上)
- 2016 年技术发展方向
- Web应用检查清单
- 如何成为一名优秀的web前端工程师
- 前端组件化开发实践
- 移动端H5页面高清多屏适配方案
- 2015前端框架何去何从
- 从前端看“百度迁徙”的技术实现(一)
- 从前端看“百度迁徙”的技术实现(二)
- 前端路上的旅行
- 大公司里怎样开发和部署前端代码?
- 5个经典的前端面试问题
- 前端工程师新手必读
- 手机淘宝前端的图片相关工作流程梳理
- 一个自动化的前端项目实现(附源码)
- 前端代码异常日志收集与监控
- 15年双11手淘前端技术总结 - H5性能最佳实践
- 深入理解javascript原型和闭包系列
- 一切都是对象
- 函数和对象的关系
- prototype原型
- 隐式原型
- instanceof
- 继承
- 原型的灵活性
- 简述【执行上下文】上
- 简述【执行上下文】下
- this
- 执行上下文栈
- 简介【作用域】
- 【作用域】和【上下文环境】
- 从【自由变量】到【作用域链】
- 闭包
- 完结
- 补充:上下文环境和作用域的关系
- Linux私房菜