谈谈前端『新』技术
最近有位(大家都知道是谁)的『前辈』在微博上天天叫唤,相信很多人都看烦了。我跟他也算是吵了一架,污染了大家的时间线,先说声抱歉。但是我觉得让这么一个撒泼的人误人子弟,实在看不下去,所以咱上点干货以正视听。
对待新事物的态度问题
我这个人在技术讨论的时候信奉很简单的一个道理:没有研究过就没有发言权。对于我不懂的东西,我要么闭嘴,要么去研究、试用之后再开口。在我看来,这是进行技术讨论的一种基本涵养。技术这个行当,永远会有新东西出来,不进则退。更关键的是,前端比起整个软件工程乃至计算机科学体系来说,是个相对新生草莽的领域,近年来前端生态的发展其实都是在向其他领域吸收和学习,不论是开发理念、工程实践还是平台本身(规范、浏览器)。所谓的『根正苗红』的前端,不过是整个发展进程中探索的一个阶段而已,那个时代的最佳实践,很多到今天都已经不再适用。过往的经验固然有价值,但这些经验如果不结合对新事物本身的了解,就很难产生正确的判断。这里需要强调的是,学习新事物并不是为了不考虑实际需求的滥用,而是为了获取足够的信息从而作出更靠谱的判断。在这一点上,某人的态度是不停强调自己过去的八年资历,强调『经验和观察』的重要性,却对新事物本身采取蔑视和抗拒到了夸张的程度,宁可在微博上撕逼,也不愿意花一两个小时FQ了解一下外面的世界。举例来说,他在批评 Angular/React 的时候是以『就是把服务器 MVC 那一套搬到了前端』这样的预设去批判的,完全鸡同鸭讲;在阐述他自己那套 Widget + OO 的时候,也压根不了解这套思维和现在基于组件的新框架的共通点。这样偏颇的思维方式,如何做得出靠谱的决策?作为一个技术负责人简直可以说是不负责任。盲目跟风不可取,盲目抗拒也不可取,要有自己的判断,但是这个判断要建立在足够的一手信息量基础上。
为什么要用『新技术』?
说完态度,我们来深入谈谈为什么这些『新技术』会有市场。首先明确一点:任何技术都有针对的适用场景取舍。在对一个技术进行评估后发现不适合,这很正常,比如项目的类型、规模、历史包袱,以及团队的学习能力,都是制约技术选型的因素。但这并不妨碍我们分析一项技术本身解决了什么问题,以及我们的实际需求中是否存在这些问题。接下来我们一项项分析:
1. CSS 预/后处理器
比如:Sass, Less, Stylus, PostCSS。CSS 本身的设计有很多不利于工程化、影响开发效率的地方,这正是 CSS 预/后处理器要解决的问题。
(1) 默认的全局 namespace。在全局 namespace 下,任何一条规则都可能产生全局的影响,不利于模块化的多人协作;同时选择器的优先级如果没有严格的书写规范,很快就会难以管理,然后产生各种 !important hack。这一点借助预处理器虽然不能完全解决,但借助 nesting 可以让书写体验得到改善。
(2) 作为一个 DSL 缺乏抽象能力:没有变量,没有函数,没有运算符,没有混入和继承,代码的可复用性差,经常需要大量重复,而通过组合类名的方式来复用灵活性非常有限。这些都可以借助预处理器进行缓解,甚至可以抽象出常用技巧的混入,比如一个混入解决垂直居中,大大加强 CSS 代码的书写效率和可维护性。
(3) 文件组织:通过原生的 @import 引入其他文件会产生过多的请求,而预处理器可以直接合并成一个文件,在文件组织上不再有顾虑。
(4) 智能避免重复劳动:自动根据目标浏览器范围添加前缀。
2. JavaScript 编译器
比如:Babel, CoffeeScript, TypeScript。为什么要编译 JavaScript,本质上目标依然是:提高开发效率,提高可维护性。以 Babel 为例,JS 本身的 prototype 原型继承,之前几乎每个人都有自己实现一套 OO 模拟,现在有原生的 class extends 语法,从语言层面进行统一;函数的参数结构和默认值,避免了手动的默认值分配和参数为 0 的坑;箭头函数避免了 this 上下文的坑;块级的 let/const 避免了 var hoisting 的坑;templateString 避免繁琐的手动字符串拼接;更好的 Unicode 支持;ES2015 模块(为什么,见这个演讲 //benjamn.github.io/empirenode-2015/#/) ; 还有 async await 对于异步流程处理本质上的改善。一个更好的语言,一个已经正式发布的标准,浏览器支持情况不一,有人写了工具让你今天就能用,某人对此的态度居然是『没有问题也要创造问题』…
CoffeeScript 和 TypeScript 很显然有各自的主要受众:Ruby 开发者和 Java/C# 开发者。这时候某人又要搬出『伪前端』的大帽子来了,问题是,谁能搞前端并不是你说了算,前端的风气也不会因为这两个东西而受什么影响。这两个东西的意义就在于它们能够提高特定人群的开发效率,会去用它们的人很少是因为跟风,而是因为它们确实符合了一部分人的开发习惯。你不属于那部分人,不代表这些东西没有意义,它们被喷真是躺着也中枪。(web前端学习交流群:328058344 禁止闲聊,非喜勿进!)
3. 模块化/构建工具
比如:RequireJS, SeaJS,Webpack, Browserify, SystemJS。模块化的重要性想必不必多言了,连某『前辈』都在用 RequireJS。前面那两个现在已经渐渐式微了,那为什么要有后面这三个?其中一个核心价值在于基于模块规范的包管理方案。由于对于 Node 包格式的兼容,使得后面三个方案都可以利用 npm 作为包管理的机制。有了包管理器,你可以将跨项目的基础库进行细粒度的单独封装,通过 semver 版本保证 API 兼容性,在多个项目中按需复用代码逻辑,还可以直接使用发布在 npm 上的海量第三方库。更进一步,配合下面要提到的组件化框架,更可以实现 UI 组件的跨项目复用。SeaJS/spm 和 Arale 其实有这个愿景,但是玉伯明智的发现了社区的方向而选择了避免重复的努力。
另一方面,是在于后面这些新工具强大的扩展机制(尤其是 Webpack)所带来的一种新的前端打包思路:不仅仅是 JavaScript,而是将 HTML、CSS 和其他静态资源统统作为『模块』来看待。因为在实际开发中,不仅仅是 JavaScript 的模块之间存在依赖关系,HTML、CSS 和其他静态文件之间也会有依赖关系。实际开发中,开发环境和生产环境中这些静态资源之间的相对路径关系经常是不一样的,这就导致我们以往在开发环境到生产环境的上线过程中有很多繁琐的步骤,比如改写静态资源引用的 URL(版本戳,静态资源域名/CDN),图片优化,根据文件大小做成内联、模块的切分和按需加载等等,这些琐碎的事情固然可以手动解决,但我们要的是效率!效率!一次配置完毕,让开发者能够将后续精力专注于应用本身而不是其他东西。除了上面提到的几个国外方案,国内也有优秀的类似方案 FIS。 最后,就是基于构建工具我们能够提供更好的开发体验。Webpack 的热重载,在修改代码后不重载页面的情况下替换单一模块,对开发体验带来质的提升。举例来说,你在修改一个打开应用后需要 N 次操作才能看到的组件,如果你改一次就要重复这些操作,那样效率实在太低。
4. 组件化框架
比如:React, Angular 2, Vue。在我看来现代化的组件化框架提供三个核心价值:
(1) 数据到 DOM 的声明式映射。无论是 virtual dom render 还是模板,其本质都是声明式地描述『基于这样的数据,最终应该呈现给用户这样的界面』。在大部分场景下,用户通常不需要再进行命令式的 DOM 操作。声明式的代码比命令式的代码更简洁,更容易维护。
(2) 组件的组织方式。一个组件的各个部分是分散在多个文件中,还是有合理的组织方式?组件如何发布、如何在多个项目中复用?对此,React 的选择是把所有东西都放进 JS,而 Vue 则是基于构建工具实现类似 Web Component 的单文件组件格式(也可以拆分,同时支持预处理器)。Angular 2 目前要么分开多个文件,要么直接将 HTML/CSS 作为字符串内联。
(3) 组件之间如何组合与沟通。这里的共同要点也是声明式 > 命令式:通过在 render function / 模板中用标签形式在父组件中渲染子组件,让数据驱动组件的存在,从而自然地得到树状的组件树结构,而不是命令式地用 this.add(child) 这样的方法去管理组件树。数据沟通方面,通常都采用了从上至下的单向数据传递,而子组件则可以通过事件冒泡或是传递一个回调的方式来对父组件做出反馈。注意这里的重点是,子组件并不能任意地改写父组件的状态,无论是触发事件还是调用回调,最终父组件发生了什么还是由父组件自身来决定的,这就保证了子组件对父组件的解耦,从而使得子组件可移植/复用。在大型应用里,还要考虑如何让一个事件的后果能够被清晰的理解,让一个开发者可以迅速理解另一个人的代码的意图,让应用不会随着规模的增长而失控?这则是 Flux Redux 这样的状态管理方案试图解决的问题。其核心在于让副作用可控,最终目的也是可维护性。
除了以上三点之外,还有额外的一点,那就是 CSS 和组件的关系。理想情况下,一个高内聚的组件应当包含这个组件所需要的 js 逻辑、HTML 结构和 CSS。但是由于之前提到过的 CSS 全局 namespace 的问题,使得跟随组件的局部 CSS,尤其是可移植性,一直是一个难题。这一点上目前有几个方案:React 为代表的 CSS in JS,CSS modules,Shadow DOM(依赖浏览器实现),以及 Vue/Angular 2 的编译时局部 CSS/模板改写。几个方案各有千秋,但核心是都一定程度上解决了 CSS 全局 namespace 的问题,使得跟随组件的 CSS 不再影响外部,获得了可移植性,从而让组件达成真正的高内聚。而高内聚的组件才可以独立作为包分发,实现跨项目复用。
5. 测试/代码规范工具
据某『前辈』自己说,他因为时间不够,所以是放弃写测试的。为什么时间不够呢,因为工具落后,开发效率太低呗。而且用古老的比如 Qunit / YUI test 写的测试,必须手动打开一个一个浏览器测,也没有代码覆盖率信息。现在呢,代码规范有 ESLint,单元测试有 Karma,集成测试有 CasperJS/Nightwatch,代码覆盖率用 istanbul,配置完毕后一行命令自动多浏览器测试汇报结果和覆盖率,如果搭好持续集成环境,甚至不用你自己手动跑,push 到仓库就行了。这些东西也不需要团队人人都懂,架构师搭好底子,新人只要用就行了,但是却可以大幅提高整体的代码质量。
说了这么多,无非是为了说明,新技术的出现,永远是因为有对应的问题可以解决才出现的,其核心诉求都是提高开发效率和可维护性,这正是作为工程师应该追求的东西。在研究、配置、推广任何新技术的过程中,肯定会有成本,你觉得为了解决这些问题不值得付出这些成本,不代表别人也是这样,更不代表你有资格去蔑视别人为解决问题做出的努力。至于揣测他人是『靠信息不对等闯名声』,除了小人之心我还能说什么呢?