前端工程与性能优化

来源:?fexbaidu? ? ?原文链接

每个参与过开发企业级 web 应用的前端工程师或许都曾思考过前端性能优化方面的问题。我们有雅虎 14 条性能优化原则,还有两本很经典的性能优化指导书:《高性能网站建设指南》、《高性能网站建设进阶指南》。经验丰富的工程师对于前端性能优化方法耳濡目染,基本都能一一列举出来。这些性能优化原则大概是在 7 年前提出的,对于 web 性能优化至今都有非常重要的指导意义。

然而,对于构建大型 web 应用的团队来说,要坚持贯彻这些优化原则并不是一件十分容易的事。因为优化原则中很多要求与工程管理相违背,比如“把 css 放在头部”和“把 js 放在尾部”这两条原则,我们不能让整个团队的工程师在写样式和脚本引用的时候都去修改同一份的页面文件。这会严重影响团队成员间并行开发的效率,尤其是在团队有版本管理的情况下,每天要花大量的时间进行代码修改合并,这项成本是难以接受的。因此在前端工程界,总会看到周期性的性能优化工作,辛勤的前端工程师们每到月圆之夜就会倾巢出动根据优化原则做一次最佳实践。

本文从一个全新的视角来思考 web 性能优化与前端工程之间的关系,通过解读百度前端集成解决方案小组(F.I.S)在打造高性能前端架构并统一百度 40 多条前端产品线的过程中所经历的技术尝试,揭示前端性能优化在前端架构及开发工具设计层面的实现思路。

性能优化原则及分类

笔者先假设本文的读者是有前端开发经验的工程师,并对企业级 web 应用开发及性能优化有一定的思考。因此我不会重复介绍雅虎 14 条性能优化原则,如果您没有这些前续知识的,请移步这里来学习。

首先,我们把雅虎 14 条优化原则,《高性能网站建设指南》以及《高性能网站建设进阶指南》中提到的优化点做一次梳理,如果按照优化方向分类可以得到这样一张表格:

优化方向 优化手段
请求数量 合并脚本和样式表,CSS Sprites,拆分初始化负载,划分主域
请求带宽 开启 GZip,精简 JavaScript,移除重复脚本,图像优化
缓存利用 使用 CDN,使用外部 JavaScript 和 CSS,添加 Expires 头,减少 DNS 查找,配置 ETag,使 AjaX 可缓存
页面结构 将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出
代码校验 避免 CSS 表达式,避免重定向

目前大多数前端团队可以利用yui compressor或者google closure compiler等压缩工具很容易做到“精简 javascript ”这条原则,同样的,也可以使用图片压缩工具对图像进行压缩,实现“图像优化”原则,这两条原则是对单个资源的处理,因此不会引起任何工程方面的问题;很多团队也通过引入代码校验流程来确保实现“避免 css 表达式”和“避免重定向”原则;目前绝大多数互联网公司也已经开启了服务端的 Gzip 压缩,并使用 CDN 实现静态资源的缓存和快速访问;一些技术实力雄厚的前端团队甚至研发出了自动 CSS Sprites 工具,解决了 CSS Sprites 在工程维护方面的难题。使用“查找 – 替换”思路,我们似乎也可以很好的实现“划分主域”原则。

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

优化方向 优化手段
请求数量 合并脚本和样式表,拆分初始化负载
请求带宽 移除重复脚本
缓存利用 添加 Expires 头,配置 ETag,使 Ajax 可缓存
页面结构 将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出

诚然,不可否认现在有很多顶尖的前端团队可以将上述还剩下的优化原则也都一一解决,但业界大多数团队都还没能很好的解决这些问题,因此接下来本文将就这些原则的解决方案做进一步的分析与讲解,从而为那些还没有进入前端工业化开发的团队提供一些基础技术建设意见,也借此机会与业界顶尖的前端团队在工业化工程化方向上交流一下彼此的心得。

静态资源版本更新与缓存

如表格 2 所示,在“缓存利用”分类中保留了“添加 Expires 头”和“配置 ETag ”两项,或许有些人会质疑,明明这两项只要配置了服务器的相关选项就可以实现,为什么说它们难以解决呢?确实,开启这两项很容易,但开启了缓存后,我们的项目就开始面临另一个挑战:如何更新这些缓存。

相信大多数团队也找到了类似的答案,它和《高性能网站建设指南》关于“添加 Expires 头”所说的原则一样——修订文件名。即:

思路没错,但要怎么改变链接呢?变成什么样的链接才能有效更新缓存,又能最大限度避免那些没有修改过的文件缓存不失效呢?

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

或者

大家会采用添加 query 的形式修改链接。这样做是比较直观的解决方案,但在访问量较大的网站,这么做可能将面临一些新的问题。

通常一个大型的 web 应用几乎每天都会有迭代和更新,发布新版本也就是发布新的静态资源和页面的过程。以上述代码为例,假设现在线上运行着 index.html 文件,并且使用了线上的 a.js 资源。index.html 的内容为:

这次我们更新了页面中的一些内容,得到一个 index.html 文件,并开发了新的与之匹配的 a.js 资源来完成页面交互,新的 index.html 文件的内容因此而变成了:

好了,现在要开始将两份新的文件发布到线上去。可以看到,a.html 和 a.js 的资源实际上是要覆盖线上的同名文件的。不管怎样,在发布的过程中,index.html 和 a.js 总有一个先后的顺序,从而中间出现一段或大或小的时间间隔。对于一个大型互联网应用来说即使在一个很小的时间间隔内,都有可能出现新用户访问,而在这个时间间隔中访问了网站的用户会发生什么情况呢:

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

这就是为什么大型 web 应用在版本上线的过程中经常会较集中的出现前端报错日志的原因,也是一些互联网公司选择加班到半夜等待访问低峰期再上线的原因之一。此外,由于静态资源文件版本更新是“覆盖式”的,而页面需要通过修改 query 来更新,对于使用 CDN 缓存的 web 产品来说,还可能面临 CDN 缓存攻击的问题。我们再来观察一下前面说的版本更新手段:

我们不难预测,a.js 的下一个版本是“ 1.0.1 ”,那么就可以刻意构造一串这样的请求“ a.js?v=1.0.1 ”、“ a.js?v=1.0.2 ”、……让 CDN 将当前的资源缓存为“未来的版本”。这样当这个页面所用的资源有更新时,即使更改了链接地址,也会因为 CDN 的原因返回给用户旧版本的静态资源,从而造成页面错误。即便不是刻意制造的攻击,在上线间隙出现访问也可能导致区域性的 CDN 缓存错误。

此外,当版本有更新时,修改所有引用链接也是一件与工程管理相悖的事,至少我们需要一个可以“查找 – 替换”的工具来自动化的解决版本号修改的问题。

对付这个问题,目前来说最优方案就是 基于文件内容的 hash 版本冗余机制 了。也就是说,我们希望工程师源码是这么写的:

但是线上代码是这样的:

其中”_82244e91 ”这串字符是根据 a.js 的文件内容进行 hash 运算得到的,只有文件内容发生变化了才会有更改。由于版本序列是与文件名写在一起的,而不是同名文件覆盖,因此不会出现上述说的那些问题。那么这么做都有哪些好处呢?

  1. 线上的 a.js 不是同名文件覆盖,而是文件名 +hash 的冗余,所以可以先上线静态资源,再上线 html 页面,不存在间隙问题;
  2. 遇到问题回滚版本的时候,无需回滚 a.js,只须回滚页面即可;
  3. 由于静态资源版本号是文件内容的 hash,因此所有静态资源可以开启永久强缓存,只有更新了内容的文件才会缓存失效,缓存利用率大增;
  4. 修改静态资源后会在线上产生新的文件,一个文件对应一个版本,因此不会受到构造 CDN 缓存形式的攻击

虽然这种方案是相比之下最完美的解决方案,但它无法通过手工的形式来维护,因为要依靠手工的形式来计算和替换 hash 只,并生成相应的文件将是一项非常繁琐且容易出错的工作。因此,我们需要借助工具。有了这样的思路,我们下面就来了解一下 fis 是如何完成这项工作的。

首先,之所以有这种工具需求,完全是因为 web 应用运行的根本机制决定的:web 应用所需的资源是以字面的形式通知浏览器下载而聚合在一起运行的。这种资源加载策略使得 web 应用从本质上区别于传统桌面应用的版本更新方式,也是大型 web 应用需要工具处理的最根本原因。为了实现资源定位的字面量替换操作,前端构建工具理论上需要识别所有资源定位的标记,其中包括:

  • css 中的@import url(path)、background:url(path)、backgournd-image:url(path)、filter 中的 src
  • js 中的自定义资源定位函数,在 fis 中我们将其规定为__uri(path)。
  • html 中的<script src=” path ”><link href=” path ”><img src=” path ”>已经 embed、audio、video、object 等具有资源加载功能的标签。

为了工程上的维护方便,我们希望工程师在源码中写的是相对路径,而工具可以将其替换为线上的绝对路径,从而避免相对路径定位错误的问题(比如 js 中需要定位图片路径时不能使用相对路径的情况)。

image2

fis 有一个非常棒的资源定位系统,它是根据用户自己的配置来指定资源发布后的地址,然后由 fis 的资源定位系统识别文件中的定位标记,计算内容 hash,并根据配置替换为上线后的绝对 url 路径。

要想实现具备 hash 版本生成功能的构建工具不是“查找 – 替换”这么简单的,我们考虑这样一种情况:

image3

由于我们的资源版本号是通过对文件内容进行 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 中的引用。

这意味着构建工具需要具备“递归编译”的能力,这也是为什么 fis 团队不得不放弃 gruntjs 等 task-based 系统的根本原因。针对前端项目的构建工具必须是具备递归处理能力的。此外,由于文件之间的交叉引用等原因,fis 构建工具还实现了构建缓存等机制,以提升构建速度。

在解决了基于内容 hash 的版本更新问题之后,我们可以将所有前端静态资源开启永久强缓存,每次版本发布都可以首先让静态资源全量上线,再进一步上线模板或者页面文件,再也不用担心各种缓存和时间间隙的问题了!

静态资源管理与模板框架

让我们再来看看前面的优化原则表还剩些什么:

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

很不幸,剩下的优化原则都不是使用工具就能很好实现的。或许有人会辩驳:“我用某某工具可以实现脚本和样式表合并”。嗯,必须承认,使用工具进行资源合并并替换引用或许是一个不错的办法,但在大型 web 应用,这种方式有一些非常严重的缺陷,来看一个很熟悉的例子:

image4

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

image5

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

image6

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

image7

C 模块不再需要了,注释掉吧!但 C 资源通常不敢轻易剔除

image8

不知不觉中,性能优化变成了性能恶化……

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

  1. 确实能减少 HTTP 请求,这是基本要求(合并)
  2. 在使用资源的地方引用资源(就近依赖),不使用不加载(按需)
  3. 虽然资源引用不是集中书写的,但资源引用的代码最终还能出现在页面头部(css)或尾部(js)
  4. 能够避免重复加载资源(去重)

将以上要求综合考虑,不难发现,单纯依靠前端技术或者工具处理的是很难达到这些理想要求的。现代大型 web 应用所展示的页面绝大多数都是使用服务端动态语言拼接生成的。有的产品使用模板引擎,比如 smarty、velocity,有的则干脆直接使用动态语言,比如 php、python。无论使用哪种方式实现,前端工程师开发的 html 绝大多数最终都不是以静态的 html 在线上运行的,接下来我会讲述一种新的模板架构设计,用以实现前面说到那些性能优化原则,同时满足工程开发和维护的需要,这种架构设计的核心思想就是:

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


    
        hello world
        
        
        
    
    
        
html of A
html of B
html of C

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


    
        hello world
    
    
        
html of A
html of B
html of C

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


    
        hello world
        
    
    
        {require name="A.css"}
html of A
{require name="B.css"}
html of B
{require name="C.css"}
html of C

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

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

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

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


    
        hello world
        
        {require name="jquery.js"}
        {require name="bootstrap.css"}
    
    
        {require name="A/A.css"}{widget name="A/A.tpl"}
        {script}console.log('A loaded'){/script}
        {require name="B/B.css"}{widget name="B/B.tpl"}
        {require name="C/C.css"}{widget name="C/C.tpl"}
        
    

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


    
        hello world
        
        
        
        
    
    
        
html of A
html of B
html of C

不难看出,我们目前已经实现了“按需加载”,“将脚本放在底部”,“将样式表放在头部”三项优化原则。

前面讲到静态资源在上线后需要添加 hash 戳作为版本标识,那么这种使用模板语言来收集的静态资源该如何实现这项功能呢?答案是:静态资源依赖关系表。 假设前面讲到的模板源代码所对应的目录结构为下图所示:

image9

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

{
  "res": {
    "A/A.css": {
      "uri": "/A/A_1688c82.css",
      "type": "css"
    },
    "B/B.css": {
      "uri": "/B/B_52923ed.css",
      "type": "css"
    },
    "C/C.css": {
      "uri": "/C/C_6dda653.css",
      "type": "css"
    },
    "bootstrap.css": {
      "uri": "bootstrap_08f2256.css",
      "type": "css"
    },
    "jquery.js": {
      "uri": "jquery_9155343.css",
      "type": "js"
    },
  },
  "pkg": {}
}

基于这张表,我们就很容易实现 {require name=” id ”} 这个模板接口了。只须查表即可。比如执行{require name=” jquery.js ”},查表得到它的 url 是“/jquery_9151577.js ”,声明一个数组收集起来就好了。这样,整个页面执行完毕之后,收集资源加载需求,并替换页面的占位符,即可实现资源的 hash 定位,得到:


    
        hello world
        
        
        
        
    
    
        
html of A
html of B
html of C

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


    
        hello world
        
    
    
        
html of A
html of B
html of C

这个“/combo?files=file1,file2,file3,…”的 url 请求响应就是动态 combo 服务提供的,它的原理很简单,就是根据 get 请求的 files 参数找到对应的多个文件,合并成一个文件来响应请求,并将其缓存,以加快访问速度。

这种方法很巧妙,有些服务器甚至直接集成了这类模块来方便的开启此项服务,这种做法也是大多数大型 web 应用的资源合并做法。但它也存在一些缺陷:

  1. 浏览器有 url 长度限制,因此不能无限制的合并资源。
  2. 如果用户在网站内有公共资源的两个页面间跳转访问,由于两个页面的 combo 的 url 不一样导致用户不能利用浏览器缓存来加快对公共资源的访问速度。

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

  • 假设网站有两个页面 A 和 B
  • A 页面使用了 a,b,c,d 四个资源
  • B 页面使用了 a,b,e,f 四个资源
  • 如果使用 combo 服务,我们会得:
    • A 页面的资源引用为:/combo?files=a,b,c,d
    • B 页面的资源引用为:/combo?files=a,b,e,f
  • 两个页面引用的资源是不同的 url,因此浏览器会请求两个合并后的资源文件,跨页面访问没能很好的利用 a、b 这两个资源的缓存。

很明显,如果 combo 服务能聪明的知道 A 页面使用的资源引用为“/combo?files=a,b ”和“/combo?files=c,d ”,而 B 页面使用的资源引用为“/combo?files=a,b ”,“/combo?files=e,f ”就好了。这样当用户在访问 A 页面之后再访问 B 页面时,只需要下载 B 页面的第二个 combo 文件即可,第一个文件已经在访问 A 页面时缓存好了的。

基于这样的思考,fis 在资源表上新增了一个字段,取名为“ pkg ”,就是资源合并生成的新资源,表的结构会变成:

{
  "res": {
    "A/A.css": {
      "uri": "/A/A_1688c82.css",
      "type": "css"
    },
    "B/B.css": {
      "uri": "/B/B_52923ed.css",
      "type": "css"
    },
    "C/C.css": {
      "uri": "/C/C_6dda653.css",
      "type": "css"
    },
    "bootstrap.css": {
      "uri": "bootstrap_08f2256.css",
      "type": "css"
    },
    "jquery.js": {
      "uri": "jquery_9155343.css",
      "type": "js"
    },
  },
  "pkg": {
    "p0": {
      "uri": "/pkg/utils_b967346.css",
      "type": "css",
      "has": ["bootstrap.css", "A/A.css"]
    },
    "p1": {
      "uri": "/pkg/others_0d4552a.css",
      "type": "css",
      "has": ["B/B.css", "C/C.css"]
    }
  }
}

相比之前的表,可以看到新表中多了一个 pkg 字段,并且记录了打包后的文件所包含的独立资源。这样,我们重新设计一下{require name=” id ”}这个模板接口:在查表的时候,如果一个静态资源有 pkg 字段,那么就去加载 pkg 字段所指向的打包文件,否则加载资源本身。比如执行{require name=” bootstrap.css ”},查表得知 bootstrap.css 被打包在了“ p0 ”中,因此取出 p0 包的 url “/pkg/utils_b967346.css”,并且记录页面已加载了“ bootstrap.css ”和“ A/A.css ”两个资源。这样一来,之前的模板代码执行之后得到的 html 就变成了:


    
        hello world
        
        
    
    
        
html of A
html of B
html of C

css 资源请求数由原来的 4 个减少为 2 个。 这样的打包结果是怎么来的呢?答案是配置得到的。 我们来看一下带有打包结果的资源表的 fis 配置:

fis.config.set('pack', {
  'pkg/util.css': [ 'bootstrap.css', 'A/A.css'],
  'pkg/other.css': [ '**.css' ]
});

我们将“ bootstrap.css ”、“ A/A.css ”打包在一起,其他 css 另外打包,从而生成两个打包文件,当页面需要打包文件中的资源时,模块框架就会收集并计算出最优的资源加载结果,从而解决静态资源合并的问题。

这样做的原因是为了弥补 combo 在前面讲到的两点技术上的不足而设计的。但也不难发现这种打包策略是需要配置的,这就意味着维护成本的增加。但好在它有两个优势可以一定程度上弥补这个问题:

  1. 打包的资源只是原来独立资源的备份。打包与否不会导致资源的丢失,最多是没有合并的很好而已。
  2. 配置可以由工程师根据经验人工维护,也可以由统计日志生成,这为性能优化自适应网站设计提供了非常好的基础。

关于第二点,fis 有这样辅助系统来支持自适应打包算法:

image10

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

优化方向 优化手段
请求数量 拆分初始化负载
请求带宽 拆分初始化负载
缓存利用 使 Ajax 可缓存
页面结构 尽早刷新文档的输出

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



    hello world
    {require name="jquery.js"}


    
    {script}
        $('#myBtn').click(function(){
            var dialog = require('dialog/dialog.js');
            dialog.alert('you catch me!');
        });
    {/script}
    

在 fis 给百度内部团队开发的架构中,如果这样书写代码,页面最终的执行结果会变成:



    hello world


    
    
    
    
    

fis 系统会分析页面中 require(id)函数的调用,并将依赖关系记录到资源表对应资源的 deps 字段中,从而在页面渲染查表时可以加载依赖的资源。但此时 dialog.js 是以 script 标签的形式同步加载的,这样会在页面初始化时出现资源的浪费。因此,fis 团队提供了 require.async 的接口,用于异步加载一些资源,源码修改为:



    hello world
    {require name="jquery.js"}


    
    {script}
        $('#myBtn').click(function() {
            require.async('dialog/dialog.js', function( dialog ) {
                dialog.alert('you catch me!');
            });
        });
    {/script}
    

这样书写之后,fis 系统会在表里以 async 字段来标准资源依赖关系是异步的。fis 提供的静态资源管理系统会将页面输出的结果修改为:



    hello world


    
    
    
    
    

dialog.js 不会在页面以 script src 的形式输出,而是变成了资源注册,这样,当页面点击按钮触发 require.async 执行的时候,async 函数才会查表找到资源的 url 并加载它,加载完毕后触发回调函数。

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

优化方向 优化手段
缓存利用 使 Ajax 可缓存
页面结构 尽早刷新文档的输出

剩下的两项优化原则要做到并不容易,真正可缓存的 Ajax 在现实开发中比较少见,而尽早刷新文档的输出的情况 facebook 在 2010 年的 velocity 上提到过,就是 BigPipe 技术。当时 facebook 团队还讲到了 Quickling 和 PageCache 两项技术,其中的 PageCache 算是比较彻底的实现 Ajax 可缓存的优化原则了。fis 团队也曾与某产品线合作基于静态资源表、模板组件化等技术实现了页面的 PipeLine 输出、以及 Quickling 和 PageCache 功能,但最终效果没有达到理想的性能优化预期,因此这两个方向尚在探索中,相信在不久的将来会有新的突破。

总结

其实在前端开发工程管理领域还有很多细节值得探索和挖掘,提升前端团队生产力水平并不是一句空话,它需要我们能对前端开发及代码运行有更深刻的认识,对性能优化原则有更细致的分析与研究。fis 团队一直致力于从架构而非经验的角度实现性能优化原则;解决前端工程师开发、调试、部署中遇到的工程问题;提供组件化框架,提高代码复用率;提供开发工具集,提升工程师的开发效率。在前端工业化开发的所有环节均有可节省的人力成本,这些成本非常可观,相信现在很多大型互联网公司也都有了这样的共识。 本文只是将这个领域中很小的一部分知识的展开讨论,抛砖引玉,希望能为业界相关领域的工作者提供一些不一样的思路。欢迎关注fis项目,对本文有任何意见或建议都可以在 fis 开源项目中进行反馈和讨论。

一个密码改变了我的人生

英文原文:How a password changed my life.

接下来的故事,发生于沮丧和幸福之间

  “她怎么能对我做出这种事?”这个问题不停在我脑海里盘旋。一刻也不停,每天每刻。

  在 2011 年,那时候渐变还很流行,iOS 的图标还有点理智,大家都还用体香剂,而我正深陷在沮丧的情绪中。我离婚了。

  幸运的是,我还是足够理智(还有一群了不起的朋友)。所以我找了些方法来维持下去。

  有天我到办公室,开启电脑准备一天的工作。事情都很顺利,直到我看到这条消息:

你的密码已经到期,点击“更改密码”设置新的密码

  卧槽。我当时以为“更改密码”是别的什么设置。

  我看着这个愚蠢的消息,好像有个愤怒的老奶奶在我耳旁絮叨:这该死的密码到期了。

  在我的公司,IT 管理设置微软 Exchange 服务要求全球上千的同事更改密码。每 30 天一次。

  狗屎的是:服务器强制要求至少一个大写字母,至少一个小写字母,至少一个特殊字符和至少一个数字。而且这堆麻烦的东西还必须超过 8 位。还有,过去三个月内的密码不能重复!

  那天早上我真是气炸了。上午 9 点 40 分,礼拜二。相当热的一天,我到公司的时候已经满身是汗,而我还得去工作。我迟到了,我还带着机车头盔。我好像还没吃早饭。嘴里一股类似香烟的味道。我必须得在早上 10 点的会议前把这个混蛋问题给解决了,而我面临的是浪费的大把时间。

  来看看吧…输入框带着跳动的光标,等着我输入一个要连续输入 30 天的密码,一天要输入无数遍的密码。

  好吧。摆脱这些挫折的事,我想起从前领导?Rasmus?学到的一个方法。他成功的将待办清单和密码结合起来。而我要试试增强版。

  我要用一个密码来改变自己的生活

  显而易见,如果维持那时候的生活习惯和状态,我几乎没有办法完成任何事。当然,我清楚知道自己需要做什么,或者要达成什么来重新掌控自己的生活,但是我们通常会对它们视而不见。

  我的密码可以作为一个暗示。我的密码能提醒我不要再让自己作为分手的受害者,而我足够坚强来做些什么。

  我的密码改成了:“Forgive@h3r”

  开会的时候我一直在回想自己刚刚做的事。好像有些什么东西给我的脸颊画上了一个微笑。

  剩下的一周里,我每天都要重复输入这个密码。每次我的电脑锁屏,每次我的屏保(她的照片)出现,每次我孤零零吃过午饭回来。

  我觉得给自己释放了一个咒语,我不是在输入密码,而是在不停提醒自己“Forgive her(原谅她)”。

  这个简单的行动改变了我对前妻的态度。不停地重复提醒我去原谅她,让我能够接受婚姻结束的事实,并能够拥抱新的生活,坦然面对沮丧,不再沉浸其中。

  接下来的日子,我的状态大大提升。第二周末,我发现密码的效率下降了,开始失去了效果。我重新释放了这个“咒语”。在我每次输入密码的时候,想着我原谅她。那种治愈的效果马上就再次生效。

  一个月后,亲爱的 Exchange 服务器又问我改密码了。我想了想下一件应该做的事。

  我的新密码是Quit@smoking4ever

  猜猜看发生了什么?我了个去,我第二天就戒烟了。几乎没人相信我是这么做到的。我试过各种书、电子香烟、膏药等等,都没用,但是这个小戏法就起到了效果。

  这个密码在那个月相当令人难受,但每次我输入这个声明,都是让我在脑海里对自己呐喊。这不断激励我完成每个月的目标。

  有一个月过去,我的密码改成Save4trip@thailand

  想想我三个月后去了哪儿?泰国。

  还有些结余。

  看到这些积极的肯定和提醒是如何帮助我物质化每个月的目标,帮助我保持能动性和积极性了吗?我们得承认:找出下一个目标不是容易的事。有时候发现我们需要改变什么,或者我们往哪个方向继续不是件容易的事。

  密码用最简单的形式让你在数字世界达到一个目的地。比如拷贝一个文件、解锁电脑、发个邮件。这种微成就,这种“神秘咒语帮助我搞定事情”的想法能够帮助建立起不断激励你保持专注于每月目标的动力。这种不起眼的习惯改变了我。

  就这样,我发现如果我用正确的方式,真的能够改变自己的人生。我正是这样月复一月,取得了了不起的结果。

  下面摘取一些我在过去两年用过的密码,让大家了解下我的人生都发生了哪些改变,多亏了这个方法:

  • Forgive@her 致我的前妻,让我开始了这段旅程。
  • Quit@smoking4ever 奏效。
  • Save4trip@thailand 奏效。
  • Eat2times@day 呃,没用,还是很胖。
  • Sleep@before12 奏效。
  • Ask@her4date?奏效。我又恋爱了。
  • No@drinking2months 奏效。感觉棒极了!
  • MovE@togeth3r 奏效。
  • Get@c4t! 奏效。我们养了只超级可爱的喵星人。
  • Facetime2mom@sunday 奏效。我每周都和妈妈通电话。

  上个月我用得这个:

  • Save4@ring?嗯哼。人生又要进入新的篇章了。很快。

  现在我每个月都有些小期待,每个月都能更改一次密码,进入下一个专注阶段,来激励我搞定下一件事。

  在过去的两年间,这个方法对我一直奏效。我和几位亲密的朋友和亲戚分享了它。我想这种小习惯可能不是什么重大突破,但它确实对我的人生产生积极的影响。因此,我在这里向大家分享这个方法。

  不妨试一下。带着正确的心态和态度,写下写这些声明,你会改变你的生活。

  当然,处于安全的考虑,试着一些复杂的描述。使用一些符号或者数字,让密码长一些,在密码的最开始或结尾用一些特殊的手段处理。S4f3ty_f1rst!(安全第一!)

  记得把这个方法告诉其他你觉得可能需要的朋友们。

  2014 年 6 月 21 日更新:她说“我愿意”。

来自:?jianshu.io

JavaScript类型转换 type conversions

var a = new Boolean(false);
console.log(a);//{}
console.log(a == false)//true
console.log(a === false)//false
if(a){
    console.log("new Boolean(false) is true")//new Boolean(false) is true
}else{
    console.log("new Boolean(false) is false")
}

这里a转换为true,因为a是一个Boolean的object,而object如果不是null或者undefined,就会被转换为true
这里主要需要分清楚值类型(primitive)和对象类型(object)
下面是JavaScript类型转换

JavaScript type conversions
转换为:
String Number Boolean Object
undefined "undefined" NaN false throws TypeError
null "null" 0 false throws TypeError
true "true" 1

new Boolean(true)

false

"false"

0

new Boolean(false)

""?(empty string)

0

false

new String("")

"1.2"?(nonempty, numeric)

1.2

true

new String("1.2")

"one"?(nonempty, non-numeric)

NaN

true

new String("one")

0

"0"

false

new Number(0)

-0

"0"

false

new Number(-0)

NaN

"NaN"

false

new Number(NaN)

Infinity

"Infinity"

true

new Number(Infinity)

-Infinity

"-Infinity"

true

new Number(-Infinity)

1?(finite, non-zero)

"1"

true

new Number(1)

{}?(any object)

true

[]?(empty array)

""

0

true

[9]?(1 numeric elt)

"9"

9

true

['a']?(any other array)

use join() method

NaN

true

function(){}?(any function)

NaN true

在程序员的眼里,用户是这样使用他们开发的软件的

我曾经说过,程序员不是一般的人,是具有某种超能里的人。但问题是,程序员往往意识不到自己的这种特异功能,在他们的眼里,会认为自己很普通,跟常人一样,所以,程序员能做到的事情,其他人——比如他们的客户/软件用户——也应该很容易做到。但事实上,由于大部分人——绝大部分人(包括软件开发公司的客户/购买软件的用户)——都是电脑小白(对电脑知识/计算机知识/软件知识知之甚少的人)。一个对于程序员来说很显而易见的软件操作,换成让用户来操作,就会出现各种各样奇怪的事情。这让程序员非常痛苦。
记得有一次,一个客户打电话给我,说他电脑桌面上的大e找不到了,我没听懂,什么大e找不到了?客户解释说:就是那个长的像大个儿的英文字母e的图标找不到了。我倒。终于明白了他指的是桌面上的 IE 浏览器的图标不见了。
还有一次,有个客户提出一个需求,要求在页面上增加一个搜索功能,我问它,系统里有搜索功能,为什么还要在这个地方新增一个搜索功能,他说他要的不是那个搜索,他要的是在这个页面上搜在某个关键词。经过进一步的沟通,我明白了,他要的是浏览器上的快捷键 CTRL+F 的功能。
因为用户的这些特征,导致了程序员认为完美的程序,到了客户的手里,却变成极其难用的软件,投诉电话如乡下骂街的泼妇似的响个不停。而事后分析发现,根本原因都是应为程序员高估了用户对软件的掌控能力,低估了自己对软件的创造能力,于是导致了他们看这些客户使用他们开发的软件时,都是那样一种可笑的行为,如下图:
在程序员的眼里,用户是这样使用他们开发的软件的
在程序员的眼里,用户是这样使用他们开发的软件的
如果是脾气暴躁的程序员,遇到这种情况,难免会对着客户发一顿牢骚,而且,程序员的脾气一般都不是很好,所以,通常跟客户沟通时,项目经理一般都是跟着一起,以免事态激化。
用户虽然给程序员带来很多麻烦,但其实程序员的所有荣耀感都来自客户,因为只有客户用得满意,程序员才会有成就感。比如像下面这几个客户在使用一个新款软件时显露出来的表情,足够让一个处在北京重度雾霾的下午的程序员也能露出笑容:
用户在使用一款新软件时的表情
用户在使用一款新软件时的样子
程序员虽然脾气不好,但他们都是为工作着想,不带任何个人恩怨。当开发软件有紧急任务时,他们都是任劳任怨的加班加点,当在已经发布的软件中出现了重大 bug 时,他们都会深深在自责,会连夜赶制出紧急修复 bug,如果不能在第一时间让用户满意,他们会茶不思、饭不想、觉不睡。即使在实在没有短期内完整的补救措施的情况下,他们也会想出一些歪招,但也是行之有效的方案,让用户暂时度过难关。比如,下面就是一个紧急修复补丁:
紧急修复补丁
紧急修复补丁
用户应该体谅程序员。程序员的生活实际处在一种十分矛盾的状态中。编程不像其它行业,比如泥瓦匠砌砖,砌一层砖,墙就会高一次。但编程不一样,有时候一个程序员写了一天的代码,急得满头大汗,但开发进度未必就有所进展,有时候甚至还会倒退。软件编程是一个亦虚亦实的世界,有时候你搞不清一段代码为什么好用,有时候也会诧异由那样的代码构成的软件也能跑起来,正如下面这张图片中所示:
软件中有鬼
软件中有鬼
最后,说一下跟程序员打交道的一些注意事项。程序员因为整天和编程逻辑打交道,所以对因果关系特别敏感。如果你的话语的因果关系不是很明确,这会让他们感到疑惑,如果你的话语的因果关系不完整,这会让他们办错事。如果你的话中有if,最好后面用then做结束,或者用else给出选择,主语要明晰。如果不明晰,就会出现下图中出现的事故:
程序员是这样理解这个指示牌上的话的
程序员是这样理解这个指示牌上的话的
如果你是一个程序员,你会理解我说的话。

javascript:null and undefined

声明而没有赋值的变量是undefined
没有返回值的函数返回的是undefined

var a;//undefined
console.log(a);//undefined
console.log(typeof a);//undefined
console.log(typeof a == undefined)//false
console.log(typeof a == "undefined")//true
console.log(typeof a === undefined)//false
console.log(typeof a === "undefined")//true
console.log(a==null);//true
console.log(a==undefined);//true
console.log(a===null);//false
console.log(a===undefined);//true
console.log("*******************");
var b = null;//null
console.log(b);//null
console.log(typeof b);//object
console.log(typeof b == undefined)//false
console.log(typeof b == "undefined")//false
console.log(typeof b === undefined)//false
console.log(typeof b === "undefined")//false
console.log(b==null);//true
console.log(b==undefined);//true
console.log(b===null);//true
console.log(b===undefined);//false

iis 7 – IIS7 URL Rewrite – Add "www" prefix

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <!--<rule name="Add www" patternSyntax="Wildcard" stopProcessing="true">
          <match url="*" />
        <conditions>
        <add input="{HTTP_HOST}" pattern="test.com" />
          </conditions>
        <action type="Redirect" url="http://www.test.com/{R:0}" />
        </rule>-->
        <rule name="Add www" patternSyntax="ECMAScript" stopProcessing="true">
      <match url=".*" />
        <conditions>
          <add input="{HTTP_HOST}" pattern="^test.com$" />
        </conditions>
        <action type="Redirect" url="http://www.test.com/{R:0}" />
    </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

How to Create a Javascript Console in Sublime Text – wikiHow

Two Methods:Using JSC (Mac OS X)Using Node.js

Javascript consoles are very handy for debugging and getting live results from your script. Although Sublime Text comes with build systems for many other scripting languages, it does not come with a built-in Javascript build system. Many sources will tell you to create a .html page with a link to the .js file, then use a web browser console to see the results of your code. This equates to constant window-switching and browser reloading; leading to frustration, heartache, and ultimately inefficiency.

 

Fortunately, constructing your own Javascript build system for Sublime Text is quick and easy!

Method 1 of 2: Using JSC (Mac OS X)

JSC is a command-line Javascript runner, cooked directly into Mac OS X. Because your Mac already contains everything you need to run the script, creating the build system in Sublime Text is incredibly easy. (If you have a Windows computer, see the directions for Node.js below.)

Creating The Build System

  1. 1

    Launch Sublime Text.

  2. ToolsBuildSytemCreate.png
    2

    Go to “Tools > Build System > New Build System” in the top bar.

  3. Screen Shot 2013 10 26 at 10.10.27.png
    3

    Paste this code into the resulting new tab that Sublime Text opened, replacing anything else in it:

    {
      "cmd":["/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc","$file"],
      "selector":"source.js"
    }
    
  4. JSCSave.png
    4

    Save the file as “JSC.sublime-build” in the default “user” folder.?Now you have created your build system!

Usage

  1. 1

    Open the Javascript file that you want to run in Sublime Text.

  2. 2

    Use?debug()?instead of?console.log()?in your script.

  3. ChooseJSC.png
    3

    Go to “Tools > Build System” in the top bar and select “JSC”.?This is the build system that you just created.

  4. JSCBuilded4.png
    4

    Build the Javascript file, using either the shortcut ?B, or by choosing “Build” from the “Tools” menu.?A console will now appear in a pane at the bottom of the window, showing the results of your script!

Method 2 of 2: Using Node.js

Node.js (Node) is a platform built to allow Javascript to run on a server. However, it can also be installed on your local computer, providing a relatively simple way to run Javascript and get the results without using a browser.

  1. 1

    Download the Node installer from the?project’s homepage?and run it.?Simply use the default settings.

  2. ToolsBuildSytemCreate.png
    2

    Go to “Tools > Build System > New Build System” in the top bar.

  3. Creatingnodebuilder.png
    3

    Paste this code into the resulting new tab that Sublime Text opened, replacing anything else in it:

    {
      "cmd":["node","$file"],
      "selector":"source.js"
    }
    
  4. NodeSave.png
    4

    Save the file as “node.sublime-build” in the default “user” folder.?Now you have created your build system!

Usage

  1. 1

    Open the Javascript file that you want to run in Sublime Text.

  2. ChooseNode.png
    2

    Go to “Tools > Build System” in the top bar and select “node”.?This is the build system that you just created.

  3. NodeBuilded1.png
    3

    Build the Javascript file, using either the build shortcut (Ctrl + B for Windows, and ? + B for Mac), or by choosing “Build” from the “Tools” menu.?A console will now appear in a pane at the bottom of the window, showing the results of your script!

DataTableSerializer datatable序列化成对象 datatable to entity serialization

public class DataTableSerializer
{
  public static List ToList(DataTable dt)
  {
    var list = new List();
    if (dt == null || dt.Rows.Count == 0)
      return list;//return empty list instead of null object
    list.AddRange(from DataRow row in dt.Rows select ToEntity(row));
    return list;
  }
  public static T ToEntity( DataRow row)
  {
    var objType = typeof(T);
    var obj = Activator.CreateInstance();
    foreach (DataColumn column in row.Table.Columns)
    {
      var property = objType.GetProperty(column.ColumnName,
          BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
      if (property == null || !property.CanWrite)
      {
        continue;
      }
      var value = row[column.ColumnName];
      if (value == DBNull.Value)
      {
        value = null;
      }
      else
      {
        //add what you need.
        //if (column.DataType == typeof (DateTime))
        //{
        //    value = ((DateTime)value).ToString("yyyy-MM-dd");
        //}
      }
      property.SetValue(obj, value, null);
    }
    return obj;
  }
}