译者水平有限,如果有错误欢迎指正!
未来你会编写越来越多的 JS 和 CSS ,并且最终将找到一个合适的系统来管理它们。
这个是未雨绸缪专栏,汇总了一些web工程化问题,你应该在遇到它们之前就思考它们。
自动化处理依赖
在页面中添加静态资源最简单的办法,就是在页面渲染之前,在页面顶部一个个的引入它们,看 Facebook 之前的方式:
<?php
require_js('js/base.js');
require_js('js/utils.js');
require_js('js/ajax.js');
require_js('js/dialog.js');
// ...
复制代码
这样做在早期是可行的,但从 2007 年开始情况变得不可控了。因为你需要按正确的顺序逐条手动列出这些静态文件,导致其他人需要把这一坨文件复制粘贴到各个页面。这样页面需要加载大量的 JS,影响了前端性能。
于是我们转向了一个被称作 Haste 的系统,该系统使用类似 docblock 的头部声明 JS 依赖:
/**
* @provides dialog
* @requires utils ajax base
*/
复制代码
我们给每个文件手动添加注释,虽然理论上可以用静态分析工具代替(我们没有真正这么做是因为我们的 JS 是非结构化的)。这样我们可以通过一次调用请求组件的所有依赖链:
require_static('dialog');
复制代码
...而不是复制粘贴那一大坨依赖文件。
按需加载
早期的方法带来的另一个问题是,所有的静态资源都在页面顶部引入,而不是在实际需要的时候引用。这意味着2点:
- 你需要引入所有可能在页面上出现的资源;
- 如果你要在2个及以上的页面添加新内容,你最好把它放到 base.js 里。
这样每个页面都会有一坨蠢代码,例如 CAPTCHA(因为某些工作流涉及未验证的用户,理论上应该在所有页面上向所有用户展示CAPTCHA),以及其他人在 base.js 里时不时添加的东西。
我们转用了一个系统,JS 和 CSS 标签在页面加载完之后被输出(它们还是出现在页面顶部,只是在输出到浏览器之前被预先准备好,而不是被附加进去,这里有些复杂,超出本文讨论范围),所以require_static()
可能出现在代码的任何地方。然后我们移动所有的require_static()
到调用的地方(只有对话框代码会引入其依赖的 CSS 和 JS,因为并不是每个页面都有对话框),并且把 base.js 拆分成一系列更小的文件。
打包
大部分情况下最大的前端性能杀手是大量的http请求,针对这个问题最有力的武器是把单独的 JS 和 CSS 打包成一个大文件,这样就可以加载一个大的 JS 核心文件而不是加载一堆小文件。一旦其他基本工作到位,这是一个相对容易的更改。我们从手动定义包开始,最终转向基于生产数据的自动生成。
缓存与服务器内容
用最简单的方法引入静态资源,比如用 src="/js/base.js"
写下一个原生 JS 标签。随着站点规模的扩大,这将带来灾难性的问题,因为客户端使用的可能是老版本资源。这会引起一系列微妙的问题(尤其是使用了 CDN 时),最大的问题是当你 push/deploy 站点时用户正访问你的站点,客户端不会为已有的缓存资源发起请求,因此即使你的服务器能正确响应 If-None-Match (ETags) 和 If-Modified-Since (Expires),当你正将静态资源变更 push 到站点时,此时的浏览者将见不到这些变化后的静态资源。
解决这个问题最好的方法是给 URI 加版本号,这样各版本资源文件对应一个独立的 URI,如:
rsrc/af04d14/js/base.js
当你 push 站点时,用户将访问引用新 URI 的页面,浏览器会重新加载资源。
但是,还有一个大问题,一旦你有一堆前端页面:
当你 push 站点时,用户可能会发出一个请求,这个请求由运行新版本代码的服务器处理,返回了一个包含新 URI 的页面。然后浏览器发起了对新 URI 的请求,却被分发到了还未安装新版本代码的服务器上,服务器返回了旧版本的资源。这样客户端就有了脏缓存:新版本的 URI 下缓存了旧版本代码。
有很多巧妙的方法来解决这个问题,Facebook 解决的方法是用数据库而不是硬盘提供静态资源。当一个 push 开始之前,新的静态资源被写在数据库里,这样每个服务器都可以同时处理新旧资源的请求。
这种方式也使一些处理流程变得相对容易(例如删除注释和空格),只需要把压缩/加工的 CSS 和 JS 版本插入数据库即可。
参考实现:Celerity
这里讨论的一些想法在 Phabricator's 的 Celerity 系统中实现,该系统本质上是 Facebook使用的 Haste 系统简化版。