关于前端的“新东西”们

前边一段时间微博上前端界的论战可谓沸沸扬扬,主要争论的点在于要不要学习并使用层出不穷的新工具、新框架。作为一个(勉强还算)“资深”的前端工作者,这个问题也困扰了我很久,一方面,对于前端已有的知识体系的了解与掌握让自己一度觉得大道在手,天下需求再不出我掌握;另外一方面又隐隐觉得触摸到了瓶颈,一段时间内进步的缓慢让自己感到焦虑。对于新的工具、框架,我既会觉得他们解决的不过是一直以来已有的工具、方法论可以解决的问题,并不能真正地拓展前端工程师可以染指的范围,又担心是自己对它们不够了解而导致误解。所以我很少狂热地追捧某个新的东西,但是会尽可能抽出时间与精力去做选择性的了解与思考,不求每个都在在生产环境投入使用,但求真正了解其优势与不足,不至于在适用的场景下因为个人的视野局限而错失好的方案。

然而有一个不太好的现象是,新的东西出来,不缺的的是一堆文档、教程,相关的分享也是侧重在于特定领域内部,很少看到的是可以让一个普通的前端工程师心悦诚服接受的入门引导。于是入门完全靠折腾的劲头与焦虑感,前者还好说,后者却是一种不健康的状态。方案的优点并不能在第一时间被理解与感受到,这一方面让愿意学习的人难以学习(不便确定着眼点),另一方面也导致较老资格的开发者的抵触情绪。后者大概正是文首提到的论战的真正原因。

我相信很多人跟我的状态与想法类似,这篇文章也打算是跟志同道合的人简单分享这些年来我的实践与思考。主要讨论对象会是最近几年的前端流行或流行过的东西。然后也许会针对性地通过单独的文章具体讲自己相对熟悉的部分。

接下来的部分我会稍加分类,一一说明。

模块化方案

首先是模块化方案,或者叫 module / component(即模块/控件)方案。module 以及 component 是不同的东西,不过我这里倾向于把二者放在一起说,因为在演变过程中,这两者一直紧密相联。我大前端天地初开的时候,没有 module,也没有 component;module 存在在后端同学的代码里,component 存在在客户端开发同学的可视化 IDE 里。后来,有人说,我们也要有 module 方案,于是大家开始探索(踩坑)。Javascript module 的发展基本上经历了

  1. 全局变量上的 namespace 式挂载 + 基于自执行函数的局部作用域
  2. AMD & CommonJS
  3. ES6 module

这些过程,相关资料很多,这里不赘述。而 component 方案紧紧地站在了 Javascript module 发展的基础上。对应上边 module 的三个阶段,component 也可以被整理出以下几个阶段(不严格对应哈):

  1. 暴露多个文件(js, css, ...),通过手插 <script> 以及 <link> 实现 componet 的引入(最典型的例子就是 jquery pluigin)
  2. 依然多个入口文件,js 入口文件通过 AMD/CommonJS 的格式暴露,样式部分通过 less/sass/stylus 的 import 引入
  3. 仅一个 js 入口文件,借助 bundler(或 browser module loader)的能力在 js 文件内声明依赖并加载样式部分
  4. Web Component 的形式,暴露一个 html,以 <link> 标签形式引入

目前最主流的方案是上述 module 方案的 2、3 搭配 component 方案的 3,我不是很喜欢 Web Component 的方案,因为我坚信 component 的未来一定是以 Javascript 为中心的(而不会是 HTML,同样不会是 template,这个在后边也会说到)。ES6 的 module 则在方向上没有大的问题,目前看来把 AMD、CommonJS 这些送进历史博物馆是没问题了。

包管理器

说到 module / component 方案,就不得不提包管理器了。前端史上值得一提的大概有 npmbowercomponent 以及 spm(我不知道会不会有人对 spm 的影响力有异议,虽然我不看好它,但是诚意我还是感受到了的,鉴于曾经基于它做过一个项目,这里还是加上它吧)这些。我在知乎的回答甚少,其中一个就是关于前端包管理器的,链接在此,我的观点尽在其中,不做重复。

task runner

然后是 task runner。前端自动化工具的风气应该是被 ruby 社区带起来的,最初的完善的前端构建工作流应该是 ror 的那一套,许多影响至今的做法(比如静态资源文件名加 md5 以实现缓存利用的最大化)都发源于此。随着 Node.js 的产生与流行,前端工程师可以很方便地自己编写自己所需要的工具,前端工具链的生态得到爆炸式发展(顺便说到,我认为 Node.js 最大的价值在于完善前端工具链,而非用来开发生产环境的 webserver)。随之产生的就是在这一生态中处于标准制定者地位的 task runner,可以说这是生态发展的必然,工具与工具之间需要协作,工具本身需要被管理,于是 Grunt 应运而生。当然你要是现在才入门前端,那么就别管 Grunt 了,Gulp 会更有生命力一点。相对 Grunt 的配置(声明)式 task 组合,Gulp 采用 coding 的方式进行组合,在灵活性上是颠覆性的。基于 stream 这一特性是锦上添花,Grunt 也可以搞 stream,但是 task 的组织方式从根本上落后了。现在有看法认为 task runner 的引入可以避免,将 task runner 做的事情分拆到 bunder 跟更简单更原生的工具(npm script 或 Makefile)中。我的看法是,npm script 作为一个包管理的副产品确实很强大,但是要完全替代 task runner 是不够的(Makefile 同理)。不过随着 bundler 的膨胀与平台化(这个后边说),小型的项目确实可以不再需要 task runner 了,而在较大型的项目中,task runner 作为各种任务的组合管理者,依然有其价值。

bundler

然后说 bundler 吧。bundler 本来是个出发点很简单的东西:前端静态资源合并以减少请求数。它跟 module 方案的关系也比较暧昧,一方面没有相对完善的 module 方案,bundle 也很难做到安全高效;另一方面一个完善的 module 方案基本都会对应(自带或衍生)一个事实标准上的 bundler 实现。 我接触过的最早的 bundler 是 r.js,是 RequireJS 的官方工具,用于合并 AMD 工具。当时在爱奇艺,组内用的是 seajs,面临构建性能低下的问题。基于对 r.js 的研究,我跟永生一起使用 Node.js 重新实现了一个 CMD(seajs 提出的 module 方案,跟 AMD 比较像)模块的 bundler。我对前端的工程化方案的兴趣便始于此。后来一段时间内 AMD 在前端几乎处于牢牢的统治地位,前端 bundler 的发展也一度停滞,简单的 module 合并结合页面上的 module loader 成为主流方案。说起来有趣,在这样的局面下,前端 bundler 的新一轮可以说是革命性发展是从 Node.js 领域开始的。我来粗略推演一下这个过程吧,Node.js 催生了一定的以 Javascript 为工具的 webserver 开发者,这拨人跟前端工程师的重合度相当高(很好理解,对其他后端工程师来说,做的是跟以前一样的事,谁没事愿意换 Javascript 这么一个设计拙劣、饱受鄙视的语言呢),所以这种项目大多前后端也都是同一拨人维护,那么两边都是 Javascript,前后端的逻辑(代码)共享就成了合情合理的需求。可是,Node.js module 是 CommonJS 规范的呀,这代码在浏览器里是没法直接执行的,不不不,有 loader 也不行。于是 Browserify 诞生了,它的颠覆点在于,以往的 bundler 基本都只做简单的文件合并,最多就是把匿名 module 转换成具名 module 再合并;而 Browserify 创造性地用 bundler 承载了 CommonJS 代码转换的任务,把本来不能在浏览器中正常执行的代码转换为可以按照 CommonJS 规范所预期的方式在浏览器中执行的代码。这个先河一旦开启,无数旧的新的任务都落到了 bundler 头上,抱着来者不拒的态度,bundler 变身插件平台,通过支持灵活强大的 plugin,bundler 简直无所不能。我对 Browserify 相当有好感,不仅因为它是我使用的第一个“现代”bundler(允许我这么定义),它的 plugin(即 transform)一致的 -ify 命名风格也深深地俘获了我的心。不过 Browserify 的辉煌期比较短,webpack - 可以看成从前端开发者角度出发,吸收了 Browserify 优点的产出物,迅速攻城掠地,一举成为最流行(截至目前)的 bundler 工具。它支持并拥有很多相当强大的 loader(对应 Browserify 的 transform)实现,支持 AMD、CommonJS、ES6 module,支持简单优雅的 code splitting;虽然在我看来 webpack 并没有在 Browserify 基础上做出颠覆性的改变,但它看起来很完美,几乎满足了一个前端开发者可能有的所有关于 bundler 的期望。这时的 bundler,bundle 已经不是重点,transform 才是它真正的价值所在,借助各种 transform 插件,可以非常方便地形成一套定制的 component 方案。当时的我负责百度糯米移动端网站的前端,在 webpack 的帮助下,我们重构了旧有代码,抛弃了基于高度定制的 SMARTY 模板形成的一套 component 方案(典型的以 template 为中心),建立了以 Javascript 为中心的 component 方案,在自己定制维护的 Javascript / PHP 双语言支持的模板引擎基础上开发了对应的 webpack loader,以相对较小的代价(相比引入 Node.js server 的方案)完成了前端架构的现代化。较大项目的重构、尽可能小的代价、合理的新技术引入以及丰富的难点与挑战性,该有的点都有了,我很自豪主导了这样的一个项目。然后不得不提的是 jspm(也许它其实应该算在包管理器里?),不过我了解不多,大概知道的特点包括:拥抱 ES6 module 规范,对未来的 HTTP2 更友好。这些目前还不足够支持我去深入了解或投入使用,所以暂且观望吧。

不知不觉码了好多字,剩下来的简单说吧。

MVC、MVVM 框架

说实在的这两者我了解也不多,不过可以扯扯,欢迎指正。先说前端 MVC,我一直认为这是一种代码冗余度很高的范式(paradigm),而一旦试图把其中造成冗余的部分去掉,就会发现没剩下什么有价值的东西。至于 MVVM,angular 也好,vue、ember 也好,我都没有真正地使用之开发过一个正式的项目,惭愧地说,我对它们的了解止步于文档以及与同事的交流。我尝试过去接受、试用,不过在这之前会做一些权衡与思考,向有开发经验的人请教。对我来说接受 MVVM 的最大阻力在于它以 template 为中心的思路,包括逻辑承载与 component 方案。基于 template 的 component 组织本身也是业务逻辑,而模板承载逻辑这事是不合适的,因为 HTML 是标记语言,而不是编程语言。要拿 HTML 表达逻辑,不可避免要引入更多语法与约定。Javascript 再烂起码是一个图灵完备的编程语言,而 MVVM 却在模板里重新定义了一套 if/else, for... 然后扔给开发者去记忆去编写。这不仅是学习成本的问题,更多的是前端同时拥有两套逻辑声明语法下的维护成本,前者是一次性的,而后者不是。

React

最后是 React。它把我好长一段时间脑海中隐约产生的,却不能清晰描述,更不能克服重重困难加以实现的范式实现了一遍给我看,所以一度我是跪着用它的。当然,随着项目规模的增大与使用的深入,渐渐搞清楚了它的局限与边界,很多之前的疑团也逐渐解开。我会专门写一篇文章讲我对 React 的思考,我希望我的思考能够像这篇文章一开始说到的那样,让更多的习惯了传统前端开发方式的工程师能够感同身受到 React 所解决的痛点,并喜欢上 React 或者一些可能比 React 更优秀的解决方案。

以上。