博客系统,对于一般程序员来说,实现权限管理、信息备份、文件上传等等都不是什么大问题。技术问题也就是几个,怎么做出一个PV值高的博客系统,很值得我们思考。
它们
网上有众多的博客系统、信息共享网站,它们都是很好的参照对象。
就那些点击量比较搞的web前端技术交流方面网站而言,它们各具特点,它们的优秀不仅仅在于发表者技术高深、逻辑思考紧密、文章结构、措辞、语法、风格上有所创新,更多的是它们别出心裁的用户体验设计。
无疑,一个及格的博客系统必须具备流畅无报错的注册、发布、管理操作流程。然而,排版整洁、自适应不同设备屏幕、代码演示和下载便捷,这些也只是标准配置。优秀的它们要不就是使用现今流行的瀑布流排版给你新鲜感,要不就丰富多彩的UI图吸引你的眼球,要不就博取你的好感。
面对这些独具匠心的人才写出来的网站,过去的你仅纠结在编码难点、固步自封在程序领域、自溺在网站编码,忽视建站的目的和网站的真正需求,任凭你做出难度多大、功能多丰富的网站,恐怕欣赏的人不会很多吧!脱离用户体验的网站走不远,这个系统demo只能当作你这个程序员的一个玩物罢了。
HTML在线文本编辑器
在我看来,博客系统与一般系统不同在于,他需要提供一个具备排版功能的文章编辑器。这个编辑器可以“加粗、斜体、字体、字号,插入图片、插入链接”,如果你打算打造一个程序员世界,那么还必须具备“插入代码”的功能,代码可能可能种什么html/css/js等等,如果你不作处理,所不定对所发表文章甚至是系统造成什么影响。
为了实现插入图片,你还要附带一个上传文件的功能。以上种种,就编辑文章这一模块,工作量就不少了。
有良好方便快捷的文本编辑器,是博客系统的关键。
富文本编辑器常用于编辑博客、用户交互,富文本编辑器分为两种:所见即所得和非所见即所得。两种富文本编辑器的实现原理是不相同的。
wysijwyg (所见即所得)
文字编辑这几种效果,无法在textarea中见到立竿见影、所见即所得的效果(需要添加预览功能,获取textarea的值然后转换为html代码)。如何做到编辑像文本域,又能够即时所见呢?
实现wysijwyg原理很简单,使用iframe作为内容编辑区域。iframe本身也是一个嵌套页面,但是它如何能够被编辑呢?contentEditable和designMode属性,可以让iframe可以被编辑。
<html>
<head>
<meta charset="UTF-8" />
<script language="javascript" type="text/javascript">
//初始化编辑器
function init() {
var ifr = document.getElementById("editor");
var doc = ifr.contentDocument || ifr.contentWindow.document; // W3C || IE
doc.designMode="on";
doc.contentEditable= true;
doc.write('<html><head></head><body>默认内容</body></html>');
//获取文本内容doc.body.innerHTML
}
//设置选定的文本为粗体/正常
function setBold() {
var win=document.getElementById("editor").contentWindow;
win.document.execCommand("Bold",false,null);
win.focus();
}
</script>
</head>
<body onload ="init()">
<input type="button" id="bBtn" value="加粗" style="font-weight:bold" onclick="setBold();" /><br />
<iframe id="editor" width="400px" height="200px" style="border:solid 1px;"></iframe>
</body>
</html>
备注:
(1) ifr.contentDocument || ifr.contentWindow.document获取iframe的文档对象
(2) 设置designMode属性为’on’,contentEditable属性为’true’让iframe可编辑
(3) 通过doc.body.innerHTML方法获取内容,这些内容是最终要插入到数据库或显示在页面上的(例如用户评论)
修改文字大小时使用execCommand('FontSize', false, value)只是7种大小可选,编辑器切换为源代码方式看<font size="7"> </font>的方式
为了实现更好的兼容性,需要在doc.write()方法前后加上doc.open(), doc.close(),原因是在写之前开启,写完之后关闭。
非所见即所得
这种编辑器的实现原理很简单,使用textarea元素就可以实现。个人更倾向于这种富文本编辑器,如果想玩弄技术的话,它可以实现得很复杂,呵呵。
考虑到要实现粗体、斜体、下划线、颜色字、图片的效果,则要加上自定义标签,例如: [b]富文本编辑器[b] ,[img]src=”photo.png”[img]。当然这些规则都需要通过js进行定制。当POST提交后,再把这些标签转换为html标签或者下次加载文章时转换为html标签。
然而,你不可能编辑的时候就想好格式,事先插入自定义的标签,于是需要考虑到,文字选中以及光标处插入。可以参考以下文章:
微博插入话题的效果实现实例页面
话说回我们的系统,我的想法是模仿浏览器控制台,一半页面加载编辑好的内容,一半页面用于编辑内容。(下图仅为博客系统发表新博客基本位置的构想,真正的编辑器正在努力实现,望体谅)
或者
优势很明显:
- 自定义编辑文章格式,灵活多变。发表文章到程序员的知识分享网站嘛,懂点html和css是必须的,我们面向的是专业用户。
- 无论是可见即所得还是非可见即所得,都需要增设浏览功能,才能看到文章在真正展示页面的效果。这种实现模式,能够避免节约发表人浏览页面的时间,同时看到文章格式在不同屏宽的效果。
前端优化
浏览器发送 HTTP 请求,服务器收到请求全文后,返回 HTTP 响应,在浏览器接受到之后结束这个过程。浏览器和服务器只有一次互动的机会,浏览器主动发起,而服务器被动的根据收到的请求内容返回结果。一个完整的请求都需要经过DNS寻址、与服务器建立连接、发送数据、等待服务器响应、接收数据这样一个”漫长”而复杂的过程。这个章节仅讨论到前端优化这个环节。
前端是庞大的,包括HTML、CSS、Javascript、Image、Flash等等各种各样的资源。前端优化是复杂的,针对方方面面的资源都有不同的方式。
早前所参加过的项目都是政府项目或者是企业内网系统,完全不用考虑这方面,虽然后台同学经常抱怨,每次将本地项目完整新版本更新到SVN都要等好久,前端文件太多了。直到现在自己搭建项目,HTTP流出流入流量多少、MySQL请求多少等等都是资源,每个请求都是有成本的。不仅页面加载快了,资源节省了,每月的资费也减少了。
前端优化的目的
1. 从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供更为友好的体验。
2. 从服务商角度而言,优化能够减少页面请求数、或者减小请求所占带宽,能够节省可观的资源。
总之,恰当的优化不仅能够改善站点的用户体验并且能够节省相当的资源利用。
前端优化的途径有很多,按粒度大致可以分为两类,
第一类是页面级别的优化,例如HTTP请求数、脚本的无阻塞加载、内联脚本的位置优化等;
第二类则是代码级别的优化,例如Javascript中的DOM操作优化、CSS选择符优化、图片优化以及HTML结构优化等等。另外,本着提高投入产出比的目的,后文提到的各种优化策略大致按照投入产出比从大到小的顺序排列。
一、页面级优化
1. 减少HTTP请求数
这条策略基本上所有前端人都知道,而且也是最重要最有效的。都说要减少HTTP请求,那请求多了到底会怎么样呢?首先,每个请求都是有成本的,既包含时间成本也包含资源成本。时间成本就是用户需要看到或者”感受”到这个资源是必须要等待这个过程结束的,资源上由于每个请求都需要携带数据,因此每个请求都需要占用带宽。另外,由于浏览器进行并发请求的请求数是有上限的,因此请求数多了以后,浏览器需要分批进行请求,因此会增加用户的等待时间,会给用户造成站点速度慢这样一个印象,即使可能用户能看到的第一屏的资源都已经请求完了,但是浏览器的进度条会一直存在。
减少HTTP请求数的主要途径包括:
(1). 从设计实现层面简化页面
保持页面简洁、减少资源的使用时最直接的。能使用CSS替代效果就尽量少使用图片。
(2). 合理设置HTTP缓存
缓存的力量是强大的,恰当的缓存设置可以大大的减少HTTP请求。举个例子,当浏览器没有缓存的时候访问一共会发出78个请求,共600多K数据,而当第二次访问即浏览器已缓存之后访问可能则仅有10个请求,共20多K数据。被缓存资源的请求服务器是304响应,只有Header没有Body,可以节省带宽。对于多个页面都可能使用到的代码,尽量拆分到同一个文件中。如果是嵌入页面的话换来的是增大了页面的体积,而且无法利用浏览器缓存。
(3). 资源合并与压缩
如果可以的话,尽可能的将外部的脚本、样式进行合并,多个合为一个。另外,CSS、Javascript、Image都可以用相应的工具进行压缩,压缩后往往能省下不少空间。
(4). CSS Sprites
合并CSS图片,减少请求数的又一个好办法。
(5). Lazy Load Image
这条策略实际上并不一定能减少HTTP请求数,但是却能在某些条件下或者页面刚加载时减少HTTP请求数。对于图片而言,在页面刚加载的时候可以只加载第一屏,当用户继续往后滚屏的时候才加载后续的图片。这样一来,假如用户只对第一屏的内容感兴趣时,那剩余的图片请求就都节省了。曾经的做法是在加载的时候把第一屏之后的图片地址缓存在Textarea标签中,待用户往下滚屏的时候才”惰性”加载。百度图片和花瓣网也使用这种流行的瀑布流加载图片。
2. 将外部脚本置底
前文有谈到,浏览器是可以并发请求的,这一特点使得其能够更快的加载资源,然而外链脚本在加载时却会阻塞其他资源,例如在脚本加载完成之前,它后面的图片、样式以及其他脚本都处于阻塞状态,直到脚本加载完成后才会开始加载。如果将脚本放在比较靠前的位置,则会影响整个页面的加载速度从而影响用户体验。解决这一问题的方法有很多,最简单可依赖的方法就是将脚本尽可能的往后挪,减少对并发下载的影响。如果时效性允许的话,可以考虑在DOMLoaded事件触发的时候加载,或者使用setTimeout方式来灵活的控制加载的时机。
3. 异步执行inline脚本
inline脚本对性能的影响与外部脚本相比,是有过之而无不及。首页,与外部脚本一样,inline脚本在执行的时候一样会阻塞并发请求,除此之外,由于浏览器在页面处理方面是单线程的,当inline脚本在页面渲染之前执行时,页面的渲染工作则会被推迟。简而言之,inline脚本在执行的时候,页面处于空白状态。鉴于以上两点原因,建议将执行时间较长的inline脚本异步执行,异步的方式有很多种,例如使用script元素的defer属性(存在兼容性问题和其他一些问题,例如不能使用document.write)、使用setTimeout,此外,在HTML5中引入了Web Workers的机制,恰恰可以解决此类问题。
4. Lazy Load Javascript
随着Javascript框架的流行,越来越多的站点也使用起了框架。不过,一个框架往往包括了很多的功能实现,这些功能并不是每一个页面都需要的,如果下载了不需要的脚本则算得上是一种资源浪费-既浪费了带宽又浪费了执行花费的时间。目前的做法大概有两种,一种是为那些流量特别大的页面专门定制一个专用的mini版框架,另一种则是Lazy Load,最初只加载核心模块,其他模块可以等到需要使用的时候才加载,类似于java的swing,引入需要的组件库文件。
5. 将CSS放在HEAD中
如果将CSS放在其他地方比如BODY中,则浏览器有可能还未下载和解析到CSS就已经开始渲染页面了,这就导致页面由无CSS状态跳转到CSS状态,用户体验比较糟糕。除此之外,有些浏览器会在CSS下载完成后才开始渲染页面,如果CSS放在靠下的位置则会导致浏览器将渲染时间推迟。
6. 减少不必要的HTTP跳转
对于以目录形式访问的HTTP链接,很多人都会忽略链接最后是否带’/’,假如你的服务器对此是区别对待的话,那么你也需要注意,这其中很可能隐藏了301跳转,增加了多余请求。具体参见下图,其中第一个链接是以无’/’结尾的方式访问的,于是服务器有了一次跳转。
二、代码级优化
1. Javascript
(1). DOM
DOM操作应该是脚本中最耗性能的一类操作,例如增加、修改、删除DOM元素或者对DOM集合进行操作。如果脚本中包含了大量的DOM操作则需要注意HTML Collection。
在脚本中document.images、document.forms、getElementsByTagName()返回的都是HTMLCollection类型的集合,在平时使用的时候大多将它作为数组来使用,因为它有length属性,也可以使用索引访问每一个元素。不过在访问性能上则比数组要差很多,原因是这个集合并不是一个静态的结果,它表示的仅仅是一个特定的查询,每次访问该集合时都会重新执行这个查询从而更新查询结果。所谓的”访问集合”包括读取集合的length属性、访问集合中的元素。
因此,当你需要遍历HTML Collection的时候,尽量将它转为数组后再访问,以提高性能。即使不转换为数组,也请尽可能少的访问它,例如在遍历的时候可以将length属性、成员保存到局部变量后再使用局部变量。
(2). 慎用with
with(obj){ p = 1}; 代码块的行为实际上是修改了代码块中的执行环境,将obj放在了其作用域链的最前端,在with代码块中访问非局部变量是都是先从obj上开始查找,如果没有再依次按作用域链向上查找,因此使用with相当于增加了作用域链长度。而每次查找作用域链都是要消耗时间的,过长的作用域链会导致查找性能下降。
因此,除非你能肯定在with代码中只访问obj中的属性,否则慎用with,替代的可以使用局部变量缓存需要访问的属性。
(3). 避免使用eval和Function
每次 eval 或 Function 构造函数作用于字符串表示的源代码时,脚本引擎都需要将源代码转换成可执行代码。这是很消耗资源的操作 —— 通常比简单的函数调用慢100倍以上。
eval 函数效率特别低,由于事先无法知晓传给 eval 的字符串中的内容,eval在其上下文中解释要处理的代码,也就是说编译器无法优化上下文,因此只能有浏览器在运行时解释代码。这对性能影响很大。
Function 构造函数比eval略好,因为使用此代码不会影响周围代码;但其速度仍很慢。
此外,使用eval和Function也不利于Javascript压缩工具执行压缩。
(4). 减少作用域链查找
前文谈到了作用域链查找问题,这一点在循环中是尤其需要注意的问题。如果在循环中需要访问非本作用域下的变量时请在遍历之前用局部变量缓存该变量,并在遍历结束后再重写那个变量,这一点对全局变量尤其重要,因为全局变量处于作用域链的最顶端,访问时的查找次数是最多的。
低效率的写法:
//全局变量 var globalVar = 1; function myCallback(info){ for( var i = 100000; i–;){ //每次访问globalVar都需要查找到作用域链最顶端,本例中需要访问100000次 globalVar += i; } }
更高效的写法:
//全局变量 var globalVar = 1; function myCallback(info){ //局部变量缓存全局变量 var localVar = globalVar; for( var i = 100000; i–;){ //访问局部变量是最快的 localVar += i; } //本例中只需要访问2次全局变量 globalVar = localVar; }
此外,要减少作用域链查找还应该减少闭包的使用。
必报的变量可能保存到内存中,内存消耗恨到,解决方法是在退出函数前,将不使用的局部变量删除。
(5). 数据访问
Javascript中的数据访问包括直接量(字符串、正则表达式)、变量、对象属性以及数组,其中对直接量和局部变量的访问是最快的,对对象属性以及数组的访问需要更大的开销。当出现以下情况时,建议将数据放入局部变量:
a. 对任何对象属性的访问超过1次
b. 对任何数组成员的访问次数超过1次
另外,还应当尽可能的减少对对象以及数组深度查找。
(6). 字符串拼接
在Javascript中使用”+”号来拼接字符串效率是比较低的,因为每次运行都会开辟新的内存并生成新的字符串变量,然后将拼接结果赋值给新变量。早前使用jQuery+Ajax交接页面,很多时候都是将后台传输过来的数据和前端html结构拼接成字符串,然后呈现在页面html容器里。
与之相比更为高效的做法是使用数组的join方法,即将需要拼接的字符串放在数组中最后调用其join方法得到结果。不过由于使用数组也有一定的开销,因此当需要拼接的字符串较多的时候可以考虑用此方法。
2. CSS选择符
在大多数人的观念中,都觉得浏览器对CSS选择符的解析式从左往右进行的,例如#toc A { color: #444; }
这样一个选择符,如果是从右往左解析则效率会很高,因为第一个ID选择基本上就把查找的范围限定了,但实际上浏览器对选择符的解析是从右往左进行的。如上面的选择符,浏览器必须遍历查找每一个A标签的祖先节点,效率并不像之前想象的那样高。根据浏览器的这一行为特点,在写选择符的时候需要注意很多事项,这里就不一一列举。
总结
针对不同系统有不同难点和突破点,需要方方面面的实践。早前参与的几乎都是政府项目或者是企业内网系统,用户面有局限、对于用户交互和体验实践不能深入;同时由于资源硬件设备优良,内网项目加载很快,带宽充足,因此也丧失了对前端优化的思考和实践机会。今次独立开发完整项目,还跑在因特网上面,收获良多。
不建议为了学习某门技术,而逐一编写demo验证。一方面是没有动力,另外一方面是demo实用性不强。做一个完整的东西,实践比起看多一本书或者多些几个功能demo进步更快。