第 5 页|IT-北北报

Web 前端慢加密

2015/12 06 23:12

0×00 前言

 

天下武功,唯快不破。但密码加密不同。算法越快,越容易破。

 

0×01 ************

 

************,就是把加密后的密码还原成明文密码。似乎有不少方法,但最终都得走一条路:暴力穷举。

 

也许你会说还可以查表,瞬间就出结果。虽然查表不用穷举,但表的制造过程仍然需要。查表只是将穷举提前了而已。

 

密码加密,用的都是单向散列计算。既然单向,那就是不可逆,那只能穷举。

 

穷举的原理很简单。只要知道密文是用什么算法加密的,我们也用相同的算法,把常用的词组跑一遍。若有结果和密文一样,那就猜中了。

 

穷举的速度有多快?这和加密算法有关。加密一次有多快,猜一次也这么快。

 

例如 MD5 加密是非常快的。加密一次耗费 1 微秒,那破解时随便猜一个词组,也只需 1 微秒(假设机器性能一样,词组长度也差不多)。攻击者一秒钟就可以猜 100 万个,而且这还只是单线程的速度。

 

所以,加密算法越快,破解起来就越容易。

 

0×02 慢加密

 

如果能提高加密时间,显然也能增加破解时间。

 

如果加密一次提高到 10 毫秒,那么攻击者每秒只能猜 100 个,破解速度就慢了一万倍。

 

怎样才能让加密变慢?最简单的,就是对加密后的结果再加密,重复多次。

 

例如,原本 1 微秒的加密,重复一万次,就慢一万倍了:

 

for i = 0 ~ 10000

x = md5(x)

end

 

加密时多花一点时间,就可以换取攻击者大量的破解时间。

 

事实上,这样的「慢加密」算法早已存在,例如 bcrypt、PBKDF2 等等。它们都有一个难度系数因子,可以控制加密时间,想多慢就多慢。

 

加密越慢,破解时间越长。

 

0×03 慢加密应用

 

最需要慢加密的场合,就是网站数据库里的密码。

 

近几年,经常能听到网站被「拖库」的新闻。用户资料都是明文存储,泄露了也无法挽回。唯独密码,还可以和攻击者对抗一下。

 

然而不少网站,使用的都是快速加密算法,因此轻易就能破解出一堆弱口令账号。

 

当然,有时只想破解某个特定人物的账号。只要不是特别复杂的词汇,跑上几天,很可能就破出来。

 

但网站用了慢加密,结果可能就不一样了。如果把加密时间提高 100 倍,破解时间就得长达数月,变得难以接受。

 

即使数据泄露,也能保障「密码」这最后一道隐私。

 

0×04 慢加密缺点

 

不过,慢加密也有明显的缺点:消耗大量计算资源。

 

使用慢加密的网站,如果同时来了多个用户,服务器 CPU 可能就不够用了。要是遇到恶意用户,发起大量的登录请求,甚至造成资源被耗尽。

 

性能和安全总是难以兼得。所以,一般也不会使用太高的强度。

 

一些大型网站,甚至为此投入集群,用来处理大量的加密计算。但这需要不少的成本。

 

有没有什么方法,可以让我们使用算力强劲、同时又免费的计算资源?

 

0×05 前端加密

 

在过去,个人电脑和服务器的速度,还是有较大差距的。但如今,随着硬件发展进入瓶颈,这个差距正缩小。在单线任务处理上,甚至不相上下。

 

客户端拥有强大的算力,能不能分担一些服务器的工作?

 

尤其像「慢加密」这种算法开源、但计算沉重的任务,为何不交给客户端来完成?

 

过去,提交的是明文密码;现在,提交的则是明文密码的「慢加密结果」。无论是注册,还是登陆。

 

而服务端,无需任何改动。将收到的「慢加密结果」,当做原来的明文密码 就行。以前是怎么保存的,现在还是怎么保存。

 

这样就算被拖库,攻击者破解出来的也只是「慢加密结果」,还需再破解一次,才能还原出「明文密码」。

 

事实上,「慢加密结果」这个中间值,是不可能破解出来的!

 

因为它是一个散列值 —— 毫无规律的随机串,例如 32 位十六进制字符串,而字典都是有意义的词组,几乎不可能跑到它!

 

除非字节逐个穷举。但这有 16^32 种组合,是个天文数字。

 

所以「慢加密结果」是无法通过数据库里泄露的密文「逆推」出来的。

 

或许你在想,即使不知道明文密码,也可以直接用「慢加密结果」来登录。事实上后端储存时再次加密,就无法逆推出这个散列值了。

当然,不能逆推,但可以顺推。把字典里的词组,用前后端的算法依次执行一次:

 

back_fast_hash( front_slow_hash(password) )

 

然后对比密文,即可判断有没有猜中。这样就可以用跑字典来破解。

 

但是有 front_slow_hash 这个障碍,破解速度就大幅降低了。

 

0×06 对抗预先计算

 

不过,前端的一切都是公开的。所以 front_slow_hash 的算法大家都知道。

 

攻击者可以用这套算法,把常用词组的「慢加密结果」提前算出来,制作成一个「新字典」。将来拖库后,就可以直接跑这个新字典了。

 

对抗这种方法,还得用经典的手段:加盐。最简单的,将用户名作为盐值:

 

front_slow_hash(password + username)

 

这样,即使相同的密码,对于不同的用户,「慢加密结果」也不一样了。

 

也许你会说,这个盐值不合理,因为用户名是公开的。攻击者可以对某个重要人物的账号,单独为他建立一个字典。

 

那么,是否可以提供一个隐蔽的盐值?答案是:不可以。

 

因为这是在前端。用户还没登录,那返回谁的盐值?登陆前就能获得账号的盐值,这不还是公开的吗。

 

所以,前端加密的盐值无法隐藏,只能公开。

 

当然,即使公开,单独提供一个盐值参数,也比用户名要好。因为用户名永远不变,而独立的盐值可以定期更换。

 

盐值可以由前端生成。例如注册时:

 

# 前端生成盐值

salt = rand()

password = front_slow_hash(password + salt)

 

# 提交时带上盐值

submit(…, password, salt)

 

后端将用户的盐值也储存起来。

 

登录时,输完用户名,就可以开始查询用户对应的盐值:

 

 

当然要注意的是,这个接口可以测试用户是否存在,所以得有一定的控制。

 

盐值的更换,也非常简单,甚至可以自动完成:

 

 

前端在加密当前密码时,同时开启一个新线程,计算新盐值和新密码。提交时,将它们全都带上。

 

如果「当前密码」验证成功,则用「新密码」和「新盐值」覆盖旧的。

 

这样更换盐值,还是只用到前端的算力。

 

这一切都是自动的,相当于 在用户无感知的情况下,定期帮他更换密码!

 

密文变了,针对「特定盐值」制作的字典,也就失效了。攻击者得重新制作一次。

 

0×07 强度策略

 

密码学上的问题到此结束,下面讨论实现上的问题。

 

现实中,用户的算力是不均衡的。有人用的是神级配置,也有的是古董机。这样,加密强度就很难设定。

 

如果古董机用户登录会卡上几十秒,那肯定是不行的。对于这种情况,只有以下选择:

 

  • 强度固定
  • 强度可变

 

1.强度固定

 

根据大众的配置,制定一个适中的强度,绝大多数用户都可接受。

 

但如果超过规定时间还没完成,就把算到一半的 Hash 和步数提交上来,剩余部分让服务器来完成。

 

[前端] 完成 70% —-> [后端] 计算 30%

 

不过,这需要「可序列化」的算法,才能在服务端还原进度。如果计算中会有大量的临时内存,这种方案就不可行了。

 

相比过去 100% 后端慢加密,这种少量用户「前后参半」的方式,可以节省不少服务器资源。

 

对于请求协助的用户,也必须有一定的限制,防止恶意透支服务器资源。

 

2.强度可变

 

如果后端不提供任何协助,那只能根据自身条件做取舍了。配置差的用户,就少加密一点。

 

用户注册时,加密算法不限步数放开跑,看看特定时间里能算到多少步:

 

# [注册阶段] 算力评估(线程 1 秒后中止)

while

x = hash(x)

step = step + 1

end

 

这个步数,就是加密强度,会保存到他的账号信息里。

 

和盐值一样,强度也是公开的。因为在登录时,前端加密需要知道这个强度值。

 

# [登录阶段] 先获得 step

for i = 0 ~ step

x = hash(x)

end

 

这个方案,可以让高配置的用户享受更高的安全性;低配置的用户,也不会影响基本使用。(用上好电脑还能提升安全性,很有优越感吧~)

 

但这有个重要的前提:注册和登录,必须在性能相近的设备上。

 

如果是在高配置电脑上注册的账号,某天去古董机登录,那就悲剧了,可能半天都算不出来。。。

 

3.动态调整方案

 

上述情况,现实中是普遍存在的。比如 PC 端注册的账号,在移动端登录,算力可能就不够用。

 

如果没有后端协助,那只能等。要是经常在低端设备上登陆,那每次都得干等吗?

 

等一两次就算了,如果每次都等,不如重新估量下自己的能力吧。把加密强度动态调低,更好的适应当前环境。

 

将来如果不用低端设备了,再自动的调整回来。让加密强度,能动态适应常用的设备的算力。

 

实现原理,和上一节的自动更换盐值类似。

 

4.异想天开方案

 

下面 YY 一个脑洞大开的方案,前提是网站有足够大的访问量。

 

如果当前有很多在线用户,它们不就是一堆免费的计算节点吗?计算量大的问题,扔给他们来解决。

 

 

不过这样做也有一些疑虑,万一正好推送给了坏人怎么办?

 

显然,不能把太多的敏感数据放出去。节点只管计算,完全不用知道、也不能知道这个任务的最终目的。

 

但是,如果遇到恶作剧节点,故意把数据算错怎么办?

 

所以不能只推送给一个节点。多选几个,最终结果一致才算正确。这样风险概率就降低了。

 

相比 P2P 计算,网站是有中心、实名的,管理起来会容易一些。对于恶作剧用户,可以进行惩罚;参与过帮助的用户,也给予一定奖励。

 

想象就到此,继续讨论实际的。

 

0×08 性能优化

 

1.为什么要优化

 

或许你会问,「慢加密」不就是希望计算更慢吗,为什么还要去优化?

 

假如这是一个自创的隐蔽式算法,并且混淆到外人根本无法读懂,那不优化也没事。甚至可以在里面放一些空循环,故意消耗时间。

 

但事实上,我们选择的肯定是「密码学家推荐」的公开算法。它们每一个操作,都是有数学上的意义的。

 

原本一个操作只需一条 CPU 指令,因为不够优化,用了两条指令,那么额外的时间就是内耗。导致加密用时更久,强度却未提升。

 

2.前端计算软肋

 

如果是本地程序,根本不用考虑这个问题,交给编译器就行。

 

但在 Web 环境里,我们只能用浏览器计算!相比本地程序,脚本要慢的多,因此内耗会很大。

 

脚本为什么慢?主要还是这几点:

 

  • 弱类型
  • 解释型
  • 沙箱

 

3.弱类型

 

脚本,是用来处理简单逻辑的,并不是用来密集计算的,所以没必要强类型。

 

不过如今有了一个黑科技:asm.js。它能通过语法糖,为 JS 提供真正的强类型。

 

这样计算速度就大幅提升了,可以接近本地程序的性能!

 

但是不支持 asm.js 的浏览器怎么办?例如,国内还有大量的 IE 用户,他们的算力是非常低的。

 

好在还有个后补方案 —— Flash,它有各种高性能语言的特征。类型,自然不在话下。

 

相比 asm.js,Flash 还是要慢一些,但比 IE 还是快多了。

 

4.解释型

 

解释型语言,不仅需要语法分析,更是失去了「编译时深度优化」带来的性能提升。

 

好在 Mozilla 提供了一个可以从 C/C++ 编译成 asm.js 的工具:emscripten。

 

有了它,就不用裸写了。而且编译时经过 LLVM 的优化,生成的代码质量会更高。

 

事实上,这个概念在 Flash 里早有了。

 

曾经有个叫 Alchemy 的工具,能把 C/C++ 交叉编译成 Flash 虚拟机指令,速度比 ActionScript 快不少。

 

Alchemy 现在改名 FlasCC,还有开源版的 crossbridge

 

5.沙箱

 

一些本地语言看似很简单的操作,在沙箱里就未必如此。例如数组操作:

 

vector[k] = v

 

虚拟机首先得检查索引是否越界,否则会有严重的问题。

 

如果「前端慢加密」算法涉及到大量内存随机访问,那就会有很多无意义的内耗,因此得慎重考虑。

 

不过有些特殊场合,脚本速度甚至能超过本地程序!例如开头提到的 MD5 大量反复计算。

 

这其实不难解释:

 

首先,MD5 算法很简单。没有查表这样的内存操作,使用的都是局部变量。局部变量的位置都是固定的,避免了越界检查开销。

 

其次,emscripten 的优化能力,并不比本地编译器差。

 

最后,本地程序编译之后,机器指令就不会再变了;而如今脚本引擎,都有 JIT 这个利器。它会根据运行时的情况,实时生成更优化的机器指令。

 

所以选择加密算法时,还得兼顾实际运行环境,扬长避短,发挥出最大功效。

 

0×09 对抗 GPU

 

众所周知,跑密码使用 GPU 可以快很多倍。

 

GPU 可以想象成一个有几百上千核的处理器,但只能执行一些简单的指令。虽然单核速度不及 CPU,但可以通过数量取胜。

 

暴力穷举时,可以从字典里取出上千个词汇同时跑,破解效率就提高了。

 

那能否在算法里添加一些特征,正好命中 GPU 的软肋呢?

 

1.显存瓶颈

 

大家听过说「莱特币」吧。不同于比特币,莱特币挖矿使用了 scrypt 算法。

 

这种算法对内存依赖非常大,需要频繁读写一个表。GPU 虽然每个线程都能独立计算,但显存只有一个,大家共享使用。

 

这意味着,同时只有一个线程能操作显存,其他有需要的只能等待了。这样,就极大遏制了并发的优势。

 

2.移植难度

 

山寨币遍地开花的时候,还出现了一个叫 X11Coin 的币,据称能对抗 ASIC。

 

它的原理很简单,里面掺杂了 11 种不同的加密算法。这样,制造出相应的 ASIC 复杂度大幅增加了。

 

尽管这不是一个长久的对抗方案,但思路还是可以借鉴的。如果一件事过于复杂,很多攻击者就望而生畏了,不如去做更容易到手的事。

 

3.其他想法

 

之所以 GPU 能大行其道,是因为目前的加密算法,都是简单的公式运算。这对 CPU 并没太大的优势。

 

能否设计一个算法,充分依赖 CPU 的优势?

 

CPU 有很多隐藏的强项,例如流水线。如果算法中有大量的条件分支,也许 GPU 就不擅长了。

 

当然,这里只是设想。自己创造加密算法,是非常困难的,也不推荐这么做。

 

0x0A 额外意义

 

除了能降低************速度,前端慢加密还有一些其他意义:

 

1.减少泄露风险

 

用户输入的明文密码,在前端内存里就已加密。离开浏览器,泄露风险就已结束。

 

即使通信被窃听,或是服务器上的恶意中间件,都无法拿到明文密码。

 

除非网页本身有恶意代码,或是用户系统存在恶意软件。

 

2.无法私藏明文

 

尽管大部分网站都声称,不会存储用户的明文密码。但这并没有证据,也许私下里仍在悄悄储存。

 

如果在前端加密,网站就无法拿到用户的明文密码了。

 

也许正是这一点,很多网站不愿意使用前端加密。

 

事实上,其实网站不愿意也没关系,我们可以自己做一个单机版的慢加密插件。

 

当选中网页密码框时,弹出我们插件。在插件里输入密码后,开始慢加密计算。最后将结果填入页面密码框里。

 

这样,所有的网站都可以使用了。当然,已注册的账号是不行的,得手动调整一下。

 

3.增加撞库成本

 

「前端慢加密」需要消耗用户的计算力,这个缺点有时也是件好事。

 

对于正常用户来说,登录时多等一秒影响并不大。但对于频繁登录的用户来说,这就是一个障碍了。

 

谁会频繁登录?也许就是撞库攻击者。他们无法拖下这个网站的数据库,于是就用在线登录的方式,不断的测试弱口令账号。

 

如果通过 IP 来控制频率,攻击者可以找大量的代理 —— 网速有多快,就能试多快。

 

但使用了前端慢加密,攻击者每试一个密码,就得消耗大量的计算,于是将瓶颈卡在硬件上 —— 能算多快,才能试多快。

 

所以,这里有点类似 PoW(Proof-of-Work,工作量证明)的意义。关于 PoW,以后我们会详细介绍。

 

0x0B 无法做到的

 

尽管「前端慢加密」有不少优势,但也不是万能的。

 

上一节也提到,能减少风险,而不是消除风险。如果本地环境有问题,那任何密码输入都有风险。

 

下面我们来思考一个场景:某网站使用了「前端慢加密」,但没有使用 HTTPS —— 这会导致链路被窃听。

 

回顾 0×05 小节,如果拿到「慢加密结果」,就可以直接登上账号,即使不知道明文密码。

 

的确如此。但请仔细想一想,这不也降低损失了吗?

 

本来不仅账号被盗用,而且明文密码也会泄露;而如今,只是账号被盗用,明文密码对方仍无法获得。

 

所以,前端慢加密的真正保护的是「密码」而不是「账号」。账号被盗,密码拿不到!

 

如果攻击者不仅能窃听,还能控制流量的话,就可以往页面注入攻击脚本,从而获得明文密码。当然,这和电脑中毒、键盘******一样,都属于「环境有问题」,不在本文讨论范围内。本文讨论的是数据库泄露的场景。

 

0x0C 多线程慢加密

 

用户的配置越来越好,不少都是四核、八核处理器。能否利用多线程的优势,将慢加密计算进行分解?

 

如果每一步计算都依赖之前的结果,是无法进行拆解的。例如:

 

for i = 0 ~ 10000

x = hash(x)

end

 

这是一个串行的计算。然而只有并行的问题,才能分解成多个小任务。

 

不过,换一种方式的多线程也是可以的。例如我们使用 4 个线程:

 

# 线程 1

x1 = hash(password + “salt1″)

for i = 0 ~ 2500

x1 = hash(x1)

end

 

# 线程 2

x2 = hash(password + “salt2″)

for i = 0 ~ 2500

x2 = hash(x2)

end

 

# …

 

最终将 4 个结果合并起来,再做一次加密,作为慢加密结果。

 

但这样会导致更容易破解吗?留着给大家思考。

 

0x0D 总结

 

前端慢加密,就是让每个用户贡献少量的计算资源,使加密变得更强劲。

 

即使数据泄露,其中也凝聚了全网站用户的算力,从而大幅增加破解成本。

 

0xFF 后记

 

前些年比特币流行时,突发奇想用浏览器来挖矿。虽然没做成,不过获得了一些密码学姿势。

 

近期重新进行了整理,并添加了一些新想法,于是写篇详细的文章分享一下。

 

因为密码学属于传统领域,所以结合当下流行的 Web 技术,才能更有新意。

 

如果你对算法有疑惑,可以先仔细看 0×05 这节。

box-sizing 属性

2015/10 21 10:10

实例

规定两个并排的带边框的框:

div
{
box-sizing:border-box;
-moz-box-sizing:border-box; /* Firefox */
-webkit-box-sizing:border-box; /* Safari */
width:50%;
float:left;
}

亲自试一试

页面底部有更多实例。

浏览器支持

IE Firefox Chrome Safari Opera

Internet Explorer、Opera 以及 Chrome 支持 box-sizing 属性。

Firefox 支持替代的-moz-box-sizing 属性。

定义和用法

box-sizing 属性允许您以特定的方式定义匹配某个区域的特定元素。

例如,假如您需要并排放置两个带边框的框,可通过将 box-sizing 设置为 “border-box”。这可令浏览器呈现出带有指定宽度和高度的框,并把边框和内边距放入框中。

默认值: content-box
继承性: no
版本: CSS3
JavaScript 语法: object.style.boxSizing=”border-box”

语法

box-sizing: content-box|border-box|inherit;
描述
content-box 这是由 CSS2.1 规定的宽度高度行为。

宽度和高度分别应用到元素的内容框。

在宽度和高度之外绘制元素的内边距和边框。

border-box 为元素设定的宽度和高度决定了元素的边框盒。

就是说,为元素指定的任何内边距和边框都将在已设定的宽度和高度内进行绘制

通过从已设定的宽度和高度分别减去边框和内边距才能得到内容的宽度和高度。

inherit 规定应从父元素继承 box-sizing 属性的值。

前端工程四个阶段

2015/10 12 15:10

前端,是一种GUI软件

 

现如今前端可谓包罗万象,产品形态五花八门,涉猎极广,什么高大上的基础库/框架,拽炫酷的宣传页面,还有屌炸天的小游戏……不过这些一两个文件的小项目并非是前端技术的主要应用场景,更具商业价值的则是复杂的Web应用,它们功能完善,界面繁多,为用户提供了完整的产品体验,可能是新闻聚合网站,可能是在线购物平台,可能是社交网络,可能是金融信贷应用,可能是音乐互动社区,也可能是视频上传与分享平台……

 

从本质上讲,所有Web应用都是一种运行在网页浏览器中的软件,这些软件的图形用户界面(Graphical User Interface,简称GUI)即为前端。

 

如此复杂的Web应用,动辄几十上百人共同开发维护,其前端界面通常也颇具规模,工程量不亚于一般的传统GUI软件:

 

 

尽管Web应用的复杂程度与日俱增,用户对其前端界面也提出了更高的要求,但时至今日仍然没有多少前端开发者会从软件工程的角度去思考前端开发,来助力团队的开发效率,更有甚者还对前端保留着”如玩具般简单“的刻板印象,日复一日,刀耕火种。

 

历史悠久的前端开发,始终像是放养的野孩子,原始如斯,不免让人慨叹!

 

前端工程的三个阶段

 

现在的前端开发倒也并非一无所有,回顾一下曾经经历过或听闻过的项目,为了提升其前端开发效率和运行性能,前端团队的工程建设大致会经历三个阶段:

 

第一阶段:库/框架选型

 

 

前端工程建设的第一项任务就是根据项目特征进行技术选型。

 

基本上现在没有人完全从0开始做网站,哪怕是政府项目用个jquery都很正常吧,React/Angularjs等框架横空出世,解放了不少生产力,合理的技术选型可以为项目节省许多工程量这点毋庸置疑。

 

第二阶段:简单构建优化

 

 

选型之后基本上就可以开始敲码了,不过光解决开发效率还不够,必须要兼顾运行性能。前端工程进行到第二阶段会选型一种构建工具,对代码进行压缩,校验,之后再以页面为单位进行简单的资源合并。

 

前端开发工程化程度之低,常常出乎我的意料,我之前在百度工作时是没有多少概念的,直到离开大公司的温室,去到业界与更多的团队交流才发现,能做到这个阶段在业界来说已然超出平均水平,属于“具备较高工程化程度”的团队了,查看网上形形色色的网页源代码,能做到最基本的JS/CSS压缩的Web应用都已跨入标准互联网公司行列,不难理解为什么很多前端团队对于前端工程构建的认知还仅停留在“压缩、校验、合并”这种程度。

 

第三阶段:JS/CSS模块化开发

 

 

分而治之是软件工程中的重要思想,是复杂系统开发和维护的基石,这点放在前端开发中同样适用。在解决了基本开发效率运行效率问题之后,前端团队开始思考维护效率,模块化是目前前端最流行的分治手段。

 

很多人觉得模块化开发的工程意义是复用,我不太认可这种看法,在我看来,模块化开发的最大价值应该是分治,是分治,分治!(重说三)。

 

不管你将来是否要复用某段代码,你都有充分的理由将其分治为一个模块。

 

JS模块化方案很多,AMD/CommonJS/UMD/ES6 Module等,对应的框架和工具也一大堆,说起来很烦,大家自行百度吧;CSS模块化开发基本都是在less、sass、stylus等预处理器的import/mixin特性支持下实现的。

 

虽然这些技术由来已久,在如今这个“言必及React”的时代略显落伍,但想想业界的绝大多数团队的工程化落后程度,放眼望去,毫不夸张的说,能达到第三阶段的前端团队已属于高端行列,基本具备了开发维护一般规模Web应用的能力。

 

然而,做到这些就够了么?Naive!

 

第四阶段

 

前端是一种技术问题较少、工程问题较多的软件开发领域。

 

当我们要开发一款完整的Web应用时,前端将面临更多的工程问题,比如:

 

  • 大体量:多功能、多页面、多状态、多系统;
  • 大规模:多人甚至多团队合作开发;
  • 高性能:CDN部署、缓存控制、文件指纹、缓存复用、请求合并、按需加载、同步/异步加载、移动端首屏CSS内嵌、HTTP 2.0服务端资源推送。

 

这些无疑是一系列严肃的系统工程问题。

 

前面讲的三个阶段虽然相比曾经“茹毛饮血”的时代进步不少,但用于支撑第四阶段的多人合作开发以及精细的性能优化似乎还欠缺点什么。

 

到底,缺什么呢?

 

没有银弹

 

读过《人月神话》的人应该都听说过,软件工程 没有银弹。没错,前端开发同样没有银弹,可是现在是连™******都没有的年月!(刚有了*****,摔)

 

前端历来以“简单”著称,在前端开发者群体中,小而美的价值观占据着主要的话语权,甚至成为了某种信仰,想与其他人交流一下工程方面的心得,得到的回应往往都是两个字:太重。

 

重你妹!你的脑容量只有4K吗?

 

工程方案其实也可以小而美!只不过它的小而美不是指代码量,而是指“规则”。找到问题的根源,用最少最简单明了的规则制定出最容易遵守最容易理解的开发规范或工具,以提升开发效率和工程质量,这同样是小而美的典范!

 

2011年我有幸参与到 FIS 项目中,与百度众多大中型项目的前端研发团队共同合作,不断探索实践前端开发的工程化解决方案,13年离开百度去往UC,面对完全不同的产品形态,不同的业务场景,不同的适配终端,甚至不同的网络环境,过往的方法论仍然能够快速落地,为多个团队的不同业务场景量身定制出合理的前端解决方案。

 

这些经历让我明悟了一个道理:

 

进入第四阶段,我们只需做好两件事就能大幅提升前端开发效率,并且兼顾运行性能,那就是——组件化开发与资源管理。

 

第一件事:组件化开发

 

分治的确是非常重要的工程优化手段。在我看来,前端作为一种GUI软件,光有JS/CSS的模块化还不够,对于UI组件的分治也有着同样迫切的需求:

 

 

如上图,这是我所信仰的前端组件化开发理念,简单解读一下:

 

  1. 页面上的每个 独立的 可视/可交互区域视为一个组件;
  2. 每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护;
  3. 由于组件具有独立性,因此组件与组件之间可以 自由组合;
  4. 页面只不过是组件的容器,负责组合组件形成功能完整的界面;
  5. 当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换。

 

其中第二项描述的就近维护原则,是我觉得最具工程价值的地方,它为前端开发提供了很好的分治策略,每个开发者都将清楚的知道,自己所开发维护的功能单元,其代码必然存在于对应的组件目录中,在那个目录下能找到有关这个功能单元的所有内部逻辑,样式也好,JS也好,页面结构也好,都在那里。

 

组件化开发具有较高的通用性,无论是前端渲染的单页面应用,还是后端模板渲染的多页面应用,组件化开发的概念都能适用。组件HTML部分根据业务选型的不同,可以是静态的HTML文件,可以是前端模板,也可以是后端模板:

 

 

不同的技术选型决定了不同的组件封装和调用策略。

 

基于这样的工程理念,我们很容易将系统以独立的组件为单元进行分工划分:

 

 

由于系统功能被分治到独立的模块或组件中,粒度比较精细,组织形式松散,开发者之间不会产生开发时序的依赖,大幅提升并行的开发效率,理论上允许随时加入新成员认领组件开发或维护工作,也更容易支持多个团队共同维护一个大型站点的开发。

 

结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:

 

名称 说明 举例
JS模块 独立的算法和数据单元 浏览器环境检测(detect),网络请求(ajax),应用配置(config),DOM操作(dom),工具函数(utils),以及组件里的JS单元
CSS模块 独立的功能性样式单元 栅格系统(grid),字体图标(icon-fonts),动画样式(animate),以及组件里的CSS单元
UI组件 独立的可视/可交互功能单元 页头(header),页尾(footer),导航栏(nav),搜索框(search)
页面 前端这种GUI软件的界面状态,是UI组件的容器 首页(index),列表页(list),用户管理(user)
应用 整个项目或整个站点被称之为应用,由多个页面组成

 

以上5种开发概念以相对较少的规则组成了前端开发的基本工程结构,基于这些理念,我眼中的前端开发就成了这个样子:

 

示意图 描述
整个Web应用由页面组成
页面由组件组成
一个组件一个目录,资源就近维护
组件可组合,
组件的JS可依赖其他JS模块,
CSS可依赖其他CSS单元

 

综合上面的描述,对于一般中小规模的项目,大致可以规划出这样的源码目录结构:

 

 

如果项目规模较大,涉及多个团队协作,还可以将具有相关业务功能的页面组织在一起,形成一个子系统,进一步将整个站点拆分出多个子系统来分配给不同团队维护,针对这种情况后面我会单开文章详细介绍。

 

以上架构设计历经许多不同公司不同业务场景的前端团队验证,收获了不错的口碑,是行之有效的前端工程分治方案。

 

吐槽:我本人非常反对某些前端团队将前端开发划分为“JS开发”和“页面重构”两种岗位,更倾向于组件粒度的开发理念,对GUI软件开发的分工规划应该以功能为单位,而不是开发语言;对开发者的技术要求也应该是掌握完整的端内技术。

 

第二件事:“智能”静态资源管理

 

上面提到的模块化/组件化开发,仅仅描述了一种开发理念,也可以认为是一种开发规范,倘若你认可这规范,对它的分治策略产生了共鸣,那我们就可以继续聊聊它的具体实现了。

 

很明显,模块化/组件化开发之后,我们最终要解决的,就是模块/组件加载的技术问题。然而前端与客户端GUI软件有一个很大的不同:

 

前端是一种远程部署,运行时增量下载的GUI软件

前端应用没有安装过程,其所需程序资源都部署在远程服务器,用户使用浏览器访问不同的页面来加载不同的资源,随着页面访问的增加,渐进式的将整个程序下载到本地运行,“增量下载”是前端在工程上有别于客户端GUI软件的根本原因。

 

 

上图展示了一款界面繁多功能丰富的应用,如果采用Web实现,相信也是不小的体量,如果用户第一次访问页面就强制其加载全站静态资源再展示,相信会有很多用户因为失去耐心而流失。根据“增量”的原则,我们应该精心规划每个页面的资源加载策略,使得用户无论访问哪个页面都能按需加载页面所需资源,没访问过的无需加载,访问过的可以缓存复用,最终带来流畅的应用体验。

 

这正是Web应用“免安装”的魅力所在。

 

由“增量”原则引申出的前端优化技巧几乎成为了性能优化的核心,有加载相关的按需加载、延迟加载、预加载、请求合并等策略;有缓存相关的浏览器缓存利用,缓存更新、缓存共享、非覆盖式发布等方案;还有复杂的BigRender、BigPipe、Quickling、PageCache等技术。这些优化方案无不围绕着如何将增量原则做到极致而展开。

 

所以我觉得:

 

第四阶段前端开发最迫切需要做好的就是在基础架构中贯彻增量原则。

 

相信这种贯彻不会随着时间的推移而改变,在可预见的未来,无论在HTTP1.x还是HTTP2.0时代,无论在ES5亦或者ES6/7时代,无论是AMD/CommonJS/UMD亦或者ES6 module时代,无论端内技术如何变迁,我们都有足够充分的理由要做好前端程序资源的增量加载。

 

正如前面说到的,第三阶段前端工程缺少点什么呢?我觉得是在其基础架构中缺少这样一种“智能”的资源加载方案。没有这样的方案,很难将前端应用的规模发展到第四阶段,很难实现落地前面介绍的那种组件化开发方案,也很难让多方合作高效率的完成一项大型应用的开发,并保证其最终运行性能良好。在第四阶段,我们需要强大的工程化手段来管理”玩具般简单“的前端开发。

 

在我的印象中,Facebook是这方面探索的伟大先驱之一,早在2010年的Velocity China大会上,来自Facebook的David Wei博士就为业界展示了他们令人惊艳的静态网页资源管理和优化技术。

 

David Wei博士在当年的交流会上提到过一些关于Facebook的一些产品数据:

 

  • Facebook整站有10000+个静态资源;
  • 每个静态资源都有可能被翻译成超过100种语言版本;
  • 每种资源又会针对浏览器生成3种不同的版本;
  • 要针对不同带宽的用户做5种不同的打包方法;
  • 有3、4个不同的用户组,用于小批次体验新的产品功能;
  • 还要考虑不同的送达方法,可以直接送达,或者通过iframe的方式提升资源并行加载的速度;
  • 静态资源的压缩和非压缩状态可切换,用于调试和定位线上问题

 

这是一个状态爆炸的问题,将所有状态乘起来,整个网站的资源组合方式会达到几百万种之多(去重之后统计大概有300万种组合方式)。支撑这么大规模前端项目运行的底层架构正是魏博士在那次演讲中分享的Static Resource Management System(静态资源管理系统),用以解决Facebook项目中有关前端工程的3D问题(Development,Deployment,Debugging)。

 

 

那段时间 FIS 项目正好遇到瓶颈,当时的FIS还是一个用php写的task-based构建工具,那时候对于前端工程的认知度很低,觉得前端构建不就是几个压缩优化校验打包任务的组合吗,写好流程调度,就针对不同需求写插件呗,看似非常简单。但当我们支撑越来越多的业务团队,接触到各种不同的业务场景时,我们深刻的感受到task-based工具的粗糙,团队每天疲于根据各种业务场景编写各种打包插件,构建逻辑异常复杂,隐隐看到不可控的迹象。

 

我们很快意识到把基础架构放到构建工具中实现是一件很愚蠢的事,试图依靠构建工具实现各种优化策略使得构建变成了一个巨大的黑盒,一旦发生问题,定位起来非常困难,而且每种业务场景都有不同的优化需求,构建工具只能通过静态分析来优化加载,具有很大的局限性,单页面/多页面/PC端/移动端/前端渲染/后端渲染/多语言/多皮肤/高级优化等等资源加载问题,总不能给每个都写一套工具吧,更何况这些问题彼此之间还可以有多种组合应用,工具根本写不过来。

 

Facebook的做法无疑为我们亮起了一盏明灯,不过可惜它并不开源(不是技术封锁,而是这个系统依赖FB体系中的其他方面,通用性不强,开源意义不大),我们只能尝试挖掘相关信息,网上对它的完整介绍还是非常非常少,分析facebook的前端代码也没有太多收获,后来无意中发现了facebook使用的项目管理工具phabricator中的一个静态管理方案Celerity,以及相关的说明,看它的描述很像是Facebook静态资源管理系统的一个mini版!

 

简单看过整个系统之后发现原理并不复杂(小而美的典范),它是通过一个小工具扫描所有静态资源,生成一张资源表,然后有一个PHP实现的资源管理框架(Celerity)提供了资源加载接口,替代了传统的script/link等静态的资源加载标签,最终通过查表来加载资源。

 

虽然没有真正看过FB的那套系统,但眼前的这个小小的框架给了当时的我们足够多的启示:

 

静态资源管理系统 = 资源表 + 资源加载框架

多么优雅的实现啊!

 

资源表是一份数据文件(比如JSON),是项目中所有静态资源(主要是JS和CSS)的构建信息记录,通过构建工具扫描项目源码生成,是一种k-v结构的数据,以每个资源的id为key,记录了资源的类别、部署路径、依赖关系、打包合并等内容,比如:

 

{

“a.js”: {

“url”: “/static/js/a.5f100fa.js”,

“dep”: [ "b.js", "a.css" ]

},

“a.css”: {

“url”: “/static/css/a.63cf374.css”,

“dep”: [ "button.css" ]

},

“b.js”: {

“url”: “/static/js/b.97193bf.js”

},

“button.css”: {

“url”: “/static/css/button.de33108.js”

}

}

 

而资源加载框架则提供一些资源引用的API,让开发者根据id来引用资源,替代静态的script/link标签来收集、去重、按需加载资源。调用这些接口时,框架通过查表来查找资源的各项信息,并递归查找其依赖的资源的信息,然后我们可以在这个过程中实现各种性能优化算法来“智能”加载资源。

 

根据业务场景的不同,加载框架可以在浏览器中用JS实现,也可以是后端模板引擎中用服务端语言实现,甚至二者的组合,不一而足。

 

 

这种设计很快被验证具有足够的灵活性,能够完美支撑不同团队不同技术规范下的性能优化需求,前面提到的按需加载、延迟加载、预加载、请求合并、文件指纹、CDN部署、Bigpipe、Quickling、BigRender、首屏CSS内嵌、HTTP 2.0服务端推送等等性能优化手段都可以很容易的在这种架构上实现,甚至可以根据性能日志自动进行优化(Facebook已实现)。

 

因为有了资源表,我们可以很方便的控制资源加载,通过各种手段在运行时计算页面的资源使用情况,从而获得最佳加载性能。无论是前端渲染的单页面应用,还是后端渲染的多页面应用,这种方法都同样适用。

 

此外,它还很巧妙的约束了构建工具的职责——只生成资源表。资源表是非常通用的数据结构,无论什么业务场景,其业务代码最终都可以被扫描为相同结构的表数据,并标记资源间的依赖关系,有了表之后我们只需根据不同的业务场景定制不同的资源加载框架就行了,从此彻底告别一个团队维护一套工具的时代!!!

 

 

恩,如你所见,虽然彻底告别了一个团队一套工具的时代,但似乎又进入了一个团队一套框架的时代。其实还是有差别的,因为框架具有很大的灵活性,而且不那么黑盒,采用框架实现资源管理相比构建更容易调试、定位和升级变更。

深耕静态资源加载框架可以带来许多收益,而且有足够的灵活性和健壮性面向未来的技术变革,这个我们留作后话。

 

总结

 

回顾一下前面提到过的前端工程三个阶段:

 

 

现在补充上第四阶段:

 

  • 第四阶段:组件化开发与资源管理

 

由于先天缺陷,前端相比其他软件开发,在基础架构上更加迫切的需要组件化开发和资源管理,而解决资源管理的方法其实一点也不复杂:

 

一个通用的资源表生成工具 + 基于表的资源加载框架

 

近几年来各种你听到过的各种资源加载优化策略大部分都可以在这样一套基础上实现,而这种优化对于业务来说是完全透明的,不需要重构的性能优化——这不正是我们一直所期盼的吗?正如魏小亮博士所说:我们可以把优秀的人集中起来去优化加载。

 

如何选型技术、如何定制规范、如何分治系统、如何优化性能、如何加载资源,当你从切图开始转变为思考这些问题的时候,我想说:

 

你好,工程师!

点击此处查看原文



无觅相关文章插件,快速提升流量