真・二手前端

《二手的前端开发》续集,继续折腾,跳了React的大坑。。。

掐指一算,已经近3个月没更新了,再创新纪录,可喜可贺。
这段时间呢,可以说是忙,做着各种奇奇怪怪的事,跟各种各样的人打交道,写着各种“片段式”的代码。感觉很忙却又说不清自己在做什么。总是感觉时间很零碎,很久没有整块的时间去潜心做一件事了。这个有机会再单独写一篇去吐槽吧。
但也可以说是懒,毕竟还是有时间把《权利的游戏》1~5季追完。。。
人啊,总是会有“去tmd工作我什么都不想干就想宅着”的时候。

言归正传,本篇的主角是React。之前我也搞过一点前端的工作,但是比较low,大多是jquery+bootstrap之类的东西,感觉就是在逆时代潮流而动啊。。。也听说过React,一直很口水,身不能至心向往之。尤其是React Native出来之后,感觉就是“卧槽这也行”,颇有点当年java的“write once, run everywhere”的风范。不过java的这个牛皮是吹破了,react的口号也跟这个不太一样,“learn once, write everywhere”,后文再详细说。
正好之前有点空闲时间,新项目还没开始,于是就研究了下React和Ant Design。初衷很简单,如果我能自己写前端,那做项目就不用求人了,前端资源真是很紧张。

一边学一边写,最后搞了这么个东西出来。本意是想简化下各种后台页面的开发,尽量做的通用一点,欢迎各种吐槽。

注意,本文不是一个React教程,只是总结下学习过程中一些个人的感想。React教程实在太多了,遍地都是。。。

最佳实践?

首先我要抱怨下前端开发的现状,对于新人,对于二手前端,真是太不友好了。。。因为乱。

引用一张图片:

原图在这里,列举了前端的大部分框架和相关的工具。反正我第一眼看到感觉就是“日居然要学这么多东西”。。。

之前我就觉得前端很乱。不是难,是乱。技术么,不管多难,只要肯下功夫总能学会的。但是工程化的标准、开发风格、开发框架之类的,前端一直没有一个“业界标准”,太多框架、太多工具可以选择。我用react,能实现需求;你用angular?也行啊;什么他要写原生js?貌似也可以。。。

一个很让人头疼的问题就是工程结构。一个典型的java工程,一般都会有src/main/javasrc/test/java之类的目录放置源码,target目录放置构建结果,可以很清楚整个工程结构是什么样的,其他人接手也很简单。这就是maven提倡的“约定优于配置”的理念。但前端世界完全没有这种规范,编译后的目标目录,有人叫build,有人叫dist,也有target/result的。。。源码目录的结构更是一团糟,完全取决于开发者的个人兴趣。每次看别人的工程都很头疼。这时才感受到maven的可贵啊。

至于css/js/html/兼容性上的各种混乱,更是无力吐槽了。。。不过值得欣慰的是,至少比以前的洪荒时代好多了。

还有啊,为什么某个组件我升级个小版本就导致整个项目挂了啊,你们这版本号也太乱来了吧。。。

前端开发另一个怪现象就是很喜欢自己造轮子,即使有可用的工具也要自己重新造一套,我觉得大概有几个原因:

  • 确实现在前端生态圈还不完善,各个领域没有一个决定性的优胜者,只能说各擅胜场
  • 因为没有标准,每个人都觉得自己掌握的才是宇宙真理。。。看别人的工具总是不爽,“文人相轻”比较严重,各种“派系”林立
  • js语言太灵活。一门语言如果表达力很强,就意味着不同的人写同样的功能,写出来的代码可能天差地别。而且你总觉得有更好的写法。。。所以说,太灵活的语言不适合协作,但适合炫技。。。

不要说专业前端了,我这个二手前端都会一遍遍重构自己的代码。我会特意去用ES6的各种新特性,class/箭头函数之类的,不用不舒服斯基。说实话挺头疼的,尤其是对于强迫症。

唠叨了这么多,对“混乱”抱怨了这么久,所以,前端领域到底有没有所谓的“最佳实践”?
恐怕我无法回答这个问题。但在学习react的过程中,我总结了一些规则,仅供参考:

  • 抱着HTML5的大腿
  • less,不要写原生css。话说css真是难,前端同学你们是怎么记住那么多样式的。我只能抄其他人的,或者找些开源的,再对着css手册一点点改,有时候调css能花上一天。。。
  • 依赖管理都用npm
  • js都用ES6的写法,但也要能看懂ES5。至于各种方言,CoffeeScript/TypeScript之类的,注定是历史的垃圾堆
  • 一切以DOM为基础。虽然大家都说DOM慢,而且很多框架都会屏蔽DOM操作。但它是最底层的操作,理解之后有很多好处。不要上来就搞一堆框架专有的概念,没错我说的就是angular。。。
  • 怎么熟悉DOM?先把jquery/zepto用熟吧少年
  • Babel真是神器,可以让你提前使用ES6甚至ES7的特性。大部分特性是编译时的优化,可以认为是语法糖,但注意有些特性可能需要运行时的polyfill。
  • Webpack是React绝配,有各种神奇的loader和插件,后文详述
  • React全家桶(React+ React Router + Redux)看情况是否采用。一般而言React Router是必须的,但是否用Redux就取决于你的需求了

大概就是这样了。最佳实践不能帮你写代码,但可以少踩些坑。

话说,前端的混乱也是有历史原因的。因为以前的前端都很简单,我记得以前还用记事本写html和js,那时候写前端约等于“写脚本/写样式/做一些动态效果”。但现在前端越来越重了,越来越多的业务逻辑转移到前端,又赶上了移动互联网的大潮,复杂化/工程化是不可避免的。但却缺少约束和规范,野蛮生长,再加上一些先天的缺陷(比如原生js的各种坑爹设计),造成了如今的局面。

不过前端真的是很有活力的一个领域,有各种好玩&吊炸天的东西。比如CodePenJSFiddlethree.jsD3.jsRAPfamo.usMoebio Labs等等很多。

React印象

说实话,react给我的第一印象实在不咋地。因为我先看到的是JSX语法,感觉很难受,比如像下面这种:

1
2
3
4
5
class Hello extends React.Component {
render() {
return <div><h1 className="testStyle">Hello, React!</h1></div>;
}
}

在js里写html,你特么在逗我吧?这是什么鬼?还记得以前的各种xxx.innerHTML=/document.write(xxx)的痛苦么。用js写html早就被证实是不可行了吧,极难维护而且扩展性堪忧,就跟JavaEE早期直接用servlet输出html代码一样。
我不喜欢angular一个原因就是它把太多逻辑揉到html里了,各种ng-xx标签。结果react反其道而行之,把html搞到业务逻辑代码里去了。。。

直到我看了这个教程,颇有茅塞顿开的感觉。这个教程从传统的DOM操作一步步过渡到react,很赞,很多教程上来就是一大堆react的概念,头都晕了。

原来React的本质还是DOM操作,它只不过额外做了几件事:

  • 提出虚拟DOM的概念,解决DOM操作效率低的痛点。至于它是如何做到DOM diff,如何将虚拟DOM渲染到真实DOM上的,完全不用关心。
  • 在虚拟DOM的基础上,提出react元素/react组件的概念,强调高内聚,强调封装性,强调复用。React组件的本质就是一组DOM元素,这组DOM元素的样式和操作逻辑都被封装到组件中,并且可以被渲染到虚拟DOM上。围绕组件又会产生状态/props/生命周期等一系列概念。
  • 提出JSX语法,解决DOM操作繁琐、不易读的痛点。JSX虽然看起来像html,但是一些细节的写法还是有区别的。只是一种语法糖,但习惯之后就再也离不开了。
  • 在“一切皆组件”这个概念基础之上,提出各种“设计模式”,比如下文中的单向数据流/flux等,解决在大型项目中如何应用/解耦的问题。当然这些模式不是强制性的,算是react给出的最佳实践吧。

可以说,react的本质就是对传统DOM操作的一个封装,使之更易用更易规模化。理解这点非常关键。如果对react的认识只停留在组件的层面,出现问题时就很容易懵,不知如何入手去解决。如果知道它背后其实是DOM,知道了react组件的渲染过程,很多概念都好理解了,包括对React的设计哲学也更容易理解。
python有“The Zen of Python”,不知react有没有“The Zen of React”之类的啊。。。
此外,理解生命周期也非常重要,可以帮助理解react的设计理念。

话说,大公司真是有底气自己造轮子啊,JSX这种奇怪的语法,说搞就搞。个人自己造轮子的话,很容易死掉。

虚拟DOM算是react的一大创新吧,也是react能喊出“learn once, write everywhere”的关键。react给我们画了一个大饼:

大意就是虚拟DOM能被不同的平台渲染,就像JVM一样,我们只要针对虚拟DOM去写程序就可以了。看着很美好是吧,但太美好的东西注定不会实现的。首先跨平台的代码肯定要有些修改的,据说facebook自己用React Native也只能做到70%复用。其次DOM这种抽象结构真的能应付所有情况么?我看悬。

很多人喜欢对比react和angular,但其实二者并不是对等的。angular是典型的google风格,all in one,非常重量级,你前端开发需要的所有东西都能在angular中找到。它是一个封闭的圈子,有自己发明很多奇怪的概念,对你的编码方式/思路侵入性很强。react则如我之前所说,只是对DOM操作的一个封装,而且非常轻量级,抽象层次很浅,DOM做不到的东西,它也做不到。很多事情都要借助第三方。react上手很快,像我这种用惯了jquery的很容易就能理解它的思路。React Native也是大杀器啊,是不是以后我也可以说自己会开发app了😄。
而且,angular还有一个很大的劣势,就是1和2不兼容。。。而且2居然是用TypeScript写的。。。感觉这是邪路啊。微软在开源界的口碑可一向不怎么样。

单向数据流

单向数据流是react中很重要的一个哲学。我的理解,一个组件被渲染成什么样子,取决于:

  1. 组件自身的状态。根据封装性原则,每个组件内部维护自己的状态,外界不能也不应该去干涉
  2. 外界传过来的props。就是组件对外界因素的响应,也是外界干涉组件的唯一方式

一个是内因,一个是外因。
不知为何,写这段时我总是会想到草履虫。。。想到生物课上讲的“应激性”。。。

所谓单向数据流,就是指props只能从父组件流向子组件。这样,每个组件要做的事情就很简单了:1. 维护自己的状态;2. 接收外部的props并做出反应;3. 看情况是否更新子组件的props。
这样看来,每个组件的职责都很明确,非常利于解耦。

这样的设计很简单很美是不是?如果大家都能遵从这样的原则,世界就和平了。可惜,看着很美的设计往往都是有缺陷的,不能覆盖所有情况。就像不可变对象,能减少线程同步的问题,那就把所有对象都做成不可变的?are you kidding me ?
这个设计最大的问题是假设子组件与子组件之间不需要通信,并且父组件也无需获知子组件的状态。一些简单的应用确实可以这么假设。但只要稍微复杂一点的应用中,组件之间往往需要共享状态/互相传props,甚至子组件可能修改父组件的状态,为了达到这个目的,就只能写一些很恶心的代码

  1. 很多子组件自身的状态要“提升”到父组件中,只是为了能和其他子组件共享
  2. props搞得很重,有很多不必要的属性在里面,因为是跟子组件交互的唯一方式
  3. 父组件必须传回调函数给子组件,这样子组件状态变化时才能通知到上层

写这种代码很头疼,完全破坏了组件的封装性。而且状态的变化很难搞清楚,经常写着写着思路就乱了,要停下来想想。。。

为了解决这种问题,facebook提出了Flux模式,可以类比常见的MVC模式。为啥facebook不套用MVC模式而要重新发明个轮子呢?好像跟react自身的特点有关,网上也有很多讨论。
注意这只是一种思路,可以有很多种具体实现,最著名的实现就是Redux,也算是React全家桶中的元老了。我没实际用过Redux,但我写过那种恶心的代码,知道它要解决的痛点是什么。。。我的理解,它提供了一个统一的状态存储的地方,而且数据变化时会有消息通知到对应的组件,有点类似消息队列?

当然Flux/Redux也不是万能的。引入这种模式必然会增加你的应用的复杂度,而且可能连带的引入一些其他的问题,是否使用还是要看自己的需求。如果你像我一样,写组件的过程中觉得组件之间通信太恶心了,别犹豫了快试试吧,人生苦短何必为难自己。。。

状态是万恶之源

现在越来越觉得,在设计一个系统/模块的时候,“状态”才是优先要考虑的事,无论是前端/后端,无论什么需求,无论什么开发语言。

没有状态的东西(所谓“东西”,可能是一个对象/API/进程/模块/机器,甚至可能是更抽象的概念),就意味着可以快速复制出一个一模一样的。不用为状态操心,也可以避免很多潜在的坑,在解耦、可用性上会有很大的便利。一个例子是不可变对象;另一个印象深刻的例子是storm的nimbus进程,无状态所以随时重启;REST风格API也要求请求是无状态的;MapReduce的share nothing理念也是这样一个思路。与之相对的就是Oracle RAC,share everything,就会导致出问题时非常难处理。

状态让一切变得复杂。在函数式编程中,也会一再强调不可变/幂等性/无副作用的函数,一个道理。

但状态也不可能完全避免,否则这个系统还有啥用,就像人没了记忆一样。如何设计状态、如何设计状态之间的变化、如何设计系统对不同状态的响应,就是我们要思考的事情了。安利下一位大牛的文章:舌尖上的状态机

说回react。状态在react中是尤其重要的一件事,因为每个组件都可以有自己的状态,而状态的变化会自动触发render方法。所以,先整理好状态的变化是很重要的,不然写着写着代码就懵了。。。

一般的设计原则就是“最小化”,使用尽量少的状态,参考官方文档。facebook甚至专门提出了“无状态组件”的概念。总之状态越少越好维护,但也不能矫枉过正。

关于Ant Design

口水Ant Design(简称antd)很久了,一直想研究下,终于得偿所愿。

对antd的第一印象就是“哎呦这个界面看着不错哦”。其实围绕React的UI KIT也有很多了,比较出名的是React-BootstrapMaterial-UI。但antd最大的优势是语言,无论文档还是issues,都是中文,方便了很多。如果有几个差不多的开源项目摆在我面前,我当然选中文的那个啊。无论你英文多熟练,也还是中文看着舒服对不。而且antd社区也很活跃,版本更新很快,提出问题也能很快有人解答,用的人也算比较多。希望国内能有更多的这种开源项目啊。

最近antd又发布了Ant Design Mobile(简称antd-mobile),可以写移动端的H5了,对二手前端来说真是喜大普奔,我也写了个简单的DEMO。简单的需求都不用去求前端同学了,自己搞定。而且antd-mobile似乎也准备支持React Native,持续关注中。

antd对我而言,最大的意义就是不用写样式了(当然也不可能完全避免)。。。毕竟它已经封装好各种各样的组件,直接用就可以了。就像我之前说的,前端开发中最痛苦的事情就是写css。js好歹还算一门语言,对于标准的engineer来说都能很快上手,写各种业务逻辑驾轻就熟。但css就不一样了,每种样式在不同浏览器下有什么区别?常用的布局属性?各种overflow、display搞的头都要炸了。。。搞好css需要大量的经验和实践,不是我这种半路出家的二手前端能很快掌握的。
最关键的,我对自己的审美没什么信心。。。我觉得不错的界面,其他人觉得一般;我觉得还可以的界面,其他人一般觉得丑。。。

so,antd真是大救星啊。就像以前jquery时代的Bootstrap/Metronic/AdminLTE一样,但是好用了很多。而且也能追赶下React的潮流不是。

但我还是要吐槽下,Form组件实在是难用。。。这个组件跟其他组件的用法截然不同,各种东西都要从props里取,antd搞了很多黑盒的操作。但这也不能说是antd的锅,而是react的痼疾。react处理各种input元素,本来就很麻烦,见官方文档Controlled Components。这种设计会导致表单中输入项一多,很有可能会卡,因为每输入一个字符都要重新render。希望antd能想办法优化下吧。

Webpack是个好东西

有人说,前端开发一半的时间都在搭环境。其实很有道理,尤其在前端现在的混乱情况下,对于一个强迫症而言,想搭一个顺手的开发环境,真是各种纠结。

刚开始接触antd时,我直接使用了官方的脚手架。但我讨厌黑盒,很多东西如果我不了解基本的原理,用着心虚。官方的脚手架就是一个黑盒,在npm + webpack的基础上各种包装,搞出一些奇奇怪怪的东西,比如antd-tools/antd-init/dora(这个好像不是antd团队搞出来的,但也是支付宝的)之类的,美其名曰一站式解决方案。要不怎么说前端喜欢自己造轮子呢。

也许对新人而言这种方式确实比较好吧,毕竟可以很快开始投入编码。但碰到问题怎么办?去研究这些黑盒?就算花很大力气研究明白了,对自己又有什么益处呢?与其研究这些自己造的轮子,我为啥不去直接去研究“业界标准”webpack呢?webpack如果研究明白了,以后很长时间都会受益吧。

同理我也不想用各种react工程模版,比如这个还是WebStorm官方推荐的,感觉都是加了特技的啊,各种不需要的功能都duang duang duang的扔进去。

于是我就走上了绕过官方脚手架,自己搭建环境的不归路。。。纠结了好久啊,结果见这里

说回webpack。最开始我以为这是跟maven类似的一个构建工具。但实际用下来发现不太一样:

  • webpack对工程结构没有任何约定
  • maven的依赖管理,用npm去实现,跟webpack没啥关系
  • webpack有点像maven-compiler-plugin插件的加强版

说白了,webpack就是一个编译工具,又兼一点打包的功能。但它的强大之处在于:

  1. 各种loader可以支持特定语法。比如babel-loader可以让你使用ES6甚至ES7的语法,也可以支持react特定的JSX语法。loader还可以实现各种神奇的功能,比如react-hot-loader,发挥自己的想象力吧。
  2. 各种plugin,也能实现很多神奇的功能。常见的就是压缩&混淆代码。

loader + plugin的机制让webpack变得非常灵活,可以各种自由定制,也让强迫症多了很多纠结的地方。。。最后我的配置文件见这里
React + Webpack的搭配,写起代码来非常爽。如果不用webpack,你的JSX语法就要单独编译,调试起来也很麻烦。而使用webpack的babel-loader,编译过程对你而言是完全透明的。再搭配上webpack-dev-serverreact-hot-loader,调试也非常方便。写React的基本都是这么一套配置。

但webpack似乎更适用于SPA,对于一些多页面的应用似乎不太好用。

ES6也是个好东西

很早就知道ES6,但一直没有大规模的用过,最多用用for-of之类的语法。总的感觉,ES6不算是一次彻底的变革,更像是在ES5的基础上加了各种语法糖,让写代码的时候能够更舒服点,但底层其实是没啥变化的。虽然各大浏览器还没有完全支持ES6,不过有了Babel这种神器,根本不用担心兼容性,最多可能加些运行时的polyfill。

我最常用的ES6特性,无非是class/import/箭头函数之类的,确实很爽。值得庆幸的是终于不用再跟prototype打交道了。我一直觉得js的prototype是个很奇怪的设计。。。强行面向对象。但如果要兼容一些老的代码,还是要用prototype。

感觉以前写js真的是“写脚本”,跟写bash之类的没啥区别;现在写js就真的是“写程序”了。其中区别,自己体验过才能理解。话说js越来越像java了。。。也越来越规范了。ES7都快出来了,这样强行每年一个版本真的好么。。。

ES6的教程我推荐InfoQ的,讲解很详细,还有PDF版本。当然事先要对js有些基础。

React能很好的兼容ES6,但写法上要有些变化。参考这篇文章官方文档。至于说是用ES5风格的React.createClass还是ES6风格的extends React.Component,完全看个人喜好,反正经过babel之后应该都是没差别的。但个人倾向于ES6的写法,包括模块/集合等,也尽量遵循ES6的标准。

零散的知识点

一些零散的东西,记录下备忘:

  1. this.props对当前组件而言是只读的
  2. props和state变化都可能引起重新render
  3. 很多时候,组件是“单例”的
  4. 注意JSX中的className,这是React的一个妥协,不那么优雅
  5. JSX中,内联样式必须通过js对象实现
  6. JSX中,可以为React元素设置一个JSON对象作为属性包,使用{...obj}的语法
  7. mixin感觉是继承的简化版,但很少用到。而且React在ES6语境下是不支持mixin的。
  8. 使用this.props.children就可以访问React子元素
  9. React.findDOMNode(this.refs.q)也是一个不那么优雅的设计,但很有用
  10. 现在跨域请求都流行用CORS了,不用JSONP了,不过貌似safari对CORS的支持有点问题

总结

至此,算是把自己对前端&React的理解总结完毕了。

其实这篇文章2个月之前就想写了,那个时候就开始研究React了。但拖了很久,最主要的原因是我想把之前做的通用后台扔到github上,但从自己的一个玩具到一个公开的项目,还是有很多事情要做的。要完善各种边角的功能、测试各种流程、补充各种文档等等。再加上工作上事情确实比较多,所以进度很慢。好在趁着G20放假的期间,终于基本搞定了。
题外话,正确写一个user guide文档的方法:interesting hello world -> overview -> 分块讲解,写文档过程中的一点小小感悟,虽然这次也没用上就是了。。。

以前我也尝试过学习React,大多半途而废,看了下教程就放弃了。没想到这次居然能坚持下来,还搞了个简单的项目,我想了想原因:

  1. 一个好的入门教程非常非常重要。很多人推荐阮一峰的React教程,但我看了还是感觉一头雾水。反而这个教程我感觉非常赞。教一个零基础的人和一个有基础的人,当然方法也不一样。所以适合自己的教程才是最好的。
  2. Ant Design功不可没,要是没有antd,我估计写个Hello world就该干嘛干嘛去了。
  3. 根本原因:都是被逼的啊!没有前端来给我们写界面啊,只能自己上了啊。好在运营后台对前端性能一般没什么要求,易用性/用户体验上也可以商量,二手前端也能应付。。。

话说,我身为一个java开发,第一次认真搞的开源项目居然React的,世界真是奇妙啊。。。
最后分享一个ppt吧,之前组内分享用的,《二手前端心得》,不过技术人员做的ppt嘛,一般都不咋地。。。