大前端公共知识杂谈

大前端公共知识杂谈

近年来,随着移动化联网浪潮的汹涌而来与浏览器性能的提升,iOS、Android、Web 等前端开发技术各领风骚,大前端的概念也日渐成为某种共识。其中特别是 Web 开发的领域,以单页应用为代表的富客户端应用迅速流行,各种框架理念争妍斗艳,百花竞放。而 Web 技术的蓬勃发展也催生了一系列跨端混合开发技术,希望能够结合 Web 的开发便捷性与原生应用的高性能性;其中以 Cordova、PWA 为代表的方向致力于为 Web 应用尽可能添加原生体验,而以 NativeScript、ReactNative、Weex 为代表的利用 Web 技术或者理念开发原生应用。平心而论,无论哪一种开发领域或者技术,他们本质上都是进行图形用户界面(GUI)应用程序的开发,面对的问题、思考的方式、架构的设计很大程度上仍然可以回溯到当年以 MFC、Swing、WPF 为主导的桌面应用程序开发时代,其术不同而道相似。

任何的前端开发学习中,我们都需要掌握基本的编程语言语法与接口;譬如在 Android 开发中使用的 Java 或者 Kotlin,在 iOS 开发中使用的 Objective-C 或者 Swift,在 Web 开发中使用的 JavaScript、HTML 与 CSS 等。编程语言的学习中我们往往关注于语法基础、数据结构、功能调用、泛型编程、元编程等内容,譬如如何声明表达式、如何理解作用域与闭包、如何进行基本的流程控制与异常处理、如何实践面向对象编程、如何进行网络请求通信等等。接下来我们就需要了解如何构建基础的界面,譬如利用 HTML 与 CSS 绘制简单 Web 页面、利用代码创建并使用简单的 Activity、利用 StoryBoard 快速构建界面原型等等。然后我们需要去学习使用常见的系统功能,譬如如何进行网络交互,如何访问远端的 RESTful 接口以获取需要的数据、如何读取本地文件或者利用 SharedPreference、localStorage、CoreData 来存取数据、如何进行组件间或者应用间信息交互等内容。到这里我们已经能够进行基础的界面开发,并且为其增添必要的特性,不过在真实的项目中我们往往还会用到很多的组件或者插件,iOS 或者 Android 中为我们提供了丰富的 SDK,譬如 UITableView 或者 RecycleView 可以帮助我们快速构建高性能列表组件,Android 5.0 之后默认的 Material Design 也是非常优秀的界面样式设计指南;而 Web 开发中我们往往需要引入第三方模式库,譬如著名的 BootStrap、React Material UI、Vue element 都为我们提供了很多预置的样式组件,react-virtualized 也为我们提供了高性能的类似于 ListView 这样的部分项渲染机制。然后我们需要将应用真实地发布给用户使用,我们需要考虑很多工程实践的问题,譬如如何进行测试与调试、如何进行性能优化并且在生产环境下完成应用状态跟踪、热更新等操作、如何统一开发团队的代码风格与约定等等;这里 Web 因为其特性而自带了热更新的功能,而在 Android 或者 iOS 我们则可以利用插件化技术或者 JSPatch 来实现热更新。Java 与 Swift 都是强类型语言,其能够在编译阶段帮开发者排查问题减少潜在风险;而我们也可以使用 TypeScript 或者 Flow 为 JavaScript 添加静态类型检测的特性,在 VSCode 等现代编辑器中同样可以达到类似于 Android Studio、XCode 中的即时检查与提示的功能。最后,随着应用功能的增加、代码库的扩展,我们需要考虑整体的应用架构与工程化的问题;在应用架构中我们往往需要考虑模块化、组件化以及状态管理等多个方面,选择合适的 MVC、MVP、MVVM、Flux、VIPER 等不同的架构模式来引导应用中的代码组织与职责分割;我们也需要考虑选择合适的构建与部署工具来简化或者自动化应用发布流程,在 Android 开发中我们会选择 Gradle 及其自带的多模块特性来管理依赖与分割代码,而在 Web 中我们可以使用 Webpack、Rollup 等工具来自动处理依赖并且进行构建,iOS 中我们也可以选择 CoocaPods。

到这里我们会发现虽然具体的代码实现、使用的技术不同,但是 Android、iOS 以及 Web 乃至于 React Native 等开发中,我们需要解决的问题、能够用到的架构设计模式都是可以相互借鉴的。在我们从某个领域迁移到其他领域时,我们能很方便地知道应该学习些什么,不同的技术、工具他们的职责是什么,应该选择怎样的架构或者设计模式。古语云,欲穷千里目,更上一层楼,我们想要真正掌握某种客户端开发技术,最好是要了解我们应该掌握哪些方面,本文即是对笔者日前总结出的泛前端知识图谱(Web/iOS/Android/RN) 的简要阐释。

编程语言

编程语言的学习是我们进入软件世界的基础阶梯,著名的 Code Complete 一书中提到:Program into Your Language, Not in it. 我们不应该将自己的编程思维局限于掌握的语言提供的那些特性或者概念,而是能够理解这些语法特性背后能提供的抽象功能与原理,从而能够根据自己想要达到的目标选择最合适的编程语言。而从另一个角度来看,无论哪一门编程语言的学习也是具有极大的共性,从严谨而又被诟病过度冗余的 Java 到需要用游标卡尺的 Python,从挣扎着一路向前的 JavaScript 到含着金汤匙出生的 Swift、Rust,我们都能够发现其中的相通与互相借鉴之处。

语法基础

任何一门编程语言的学习都需要从基本的表达式(Expression)语法开始学习,我们需要了解如何去声明与使用变量、如何为这些变量赋值、如何使用运算符进行简单的变量操作等等。在很多语言之中都有所谓的传值还是传引用的思量,譬如 Java 与 JavaScript 本质上就是 Pass-by-Value 的语言,只不过会将复杂对象的引用值传递给目标变量。这个特性又引发了所谓浅复制与深复制、如何进行复合类型深拷贝等等需要注意的技术点。除此之外,作用域与闭包也是很多语言学习中重点讨论的内容,在 JavaScript 与 Python 的学习中我们就会经常讨论如何利用闭包来保存外部变量,或者在循环中避免闭包带来的意外变量值。表达式是一门编程语言语法基础的重要组成部分,接下来我们就需要去学习流程控制与异常处理、函数定义与调用、类与对象、输入输出流、模块等内容。流程控制的典型代表就是分支选择与循环,譬如不同的语言都为我们提供了基础 for 循环或者更方便地 for-in 循环,而在 JavaScript 中我们还可以使用 forEach 与 for-of 循环,Java 8 之后我们也可以基于 Stream API 中的 forEach 编写声明式地循环执行体,而 Python 中的列表推导也可以看做便捷的循环实现方式。异常处理也是各个编程语言的重要组成部分,合理的异常处理有助于增强应用的鲁棒性;不过很多时候会出现滥用异常的情况,我们只是一层一层地抛出而并未真正地去处理或者利用这些异常。Java 中将异常分为了受控异常与不受控异常这两类,虽然 JavaScript 等语言中并未在数据类型中有所区分,但是却可以引入这种分类方式来进行不同的异常处理;有时候 Let it Crash 也是不错的设计模式。

Eric Elliott 曾在博文中提及,软件开发实际就是 Function Composition 与 DataStructure Design;函数或者方法是软件系统的重要基石与组成。我们需要了解如何去定义函数,包括匿名函数以及 Lambda 表达式等;尽管 Java 中的 Lambda 表达式是对于 FunctionalInterface 的实现,但是鉴于其表现形式我们也可以将其划归到函数这个知识类别中。接下来我们需要了解如何定义与传入函数参数,在 C 这样的语言中我们会去关心指针传递的不同姿势;而在 JavaScript 中我们常常会关心如何设置默认参数,无论是使用对象解构还是可选参数,都各有利弊。Objective-C 与 Swift 中提供的外部参数就是不错的函数自描述,Java 或 Python 中提供的不定参数也能够帮我们更灵活地定义参数,在 JavaScript 中我们则可以通过扩展操作符实现类似的效果。然后我们就需要去考虑如何调用函数,最典型就是就是 JavaScript 中函数调用的四种方式,我们还需要去关心调用时函数内部的 this 指针指向。而装饰器或者注解能帮我们更好地组织代码,以类似于高阶函数的方式如洋葱圈般一层一层地剥离与抽象业务逻辑。最后在函数这部分我们还需要关心下迭代器与生成器,它们是不错的异步实现模式或者流数据构建工具。

近几年随着前端富客户端应用的迅猛发展与服务端并发编程的深入应用,函数式编程以及 Haskell 这样的函数式编程语言也是引领风骚。尽管面向对象编程也有着很多其他被人诟病的地方,但是在大型复杂业务逻辑的应用开发中我们还是会倾向使用面向对象编程的范式;这就要求我们对于类与对象的基本语法有所掌握。我们首先要去了解如何定义类,定义类的属性、方法以及使用访问修饰符等方式进行访问控制。其次我们需要了解如何从类中实例化出对象,如何在具体的语言中实践单例模式等。然后我们就需要去了解面向对象的继承与多态的特性,应该如何实现类继承,子类与父类在静态属性、静态方法、类属性、构造函数上的调用顺序是怎样的;以及如何利用纯虚函数、抽象类、接口、协议这些不同的关键字在具体语言中实现多态与约定。最后我们还需要去关注下语言是否支持内部类,譬如 Java 就分为了静态内部类、成员内部类、局部内部类与匿名内部类这四种不同的分类。在整个语法基础部分的最后,我们还需要去了解下输入输出流与模块化相关的知识,譬如 Java 9 中即将推出 JPMS 模块化系统,而 JavaScript 的模块化标准则历经了 CommonJS、AMD、UMD、ES6 Modules 等多轮变迁。

数据结构与功能

语法基础是我们掌握某门编程语言的敲门砖,而学习内建的数据结构与功能语法则是能够用该语言进行实际应用开发的重要前提。在数据结构的学习中我们首先要对该语言内建的数据类型有所概览,我们要了解如何进行常见的类型与值判断以及类型间转换;譬如如何进行引用与值的等价性判断、如何进行动态类型检查、如何对复合对象的常用属性进行判断等等。很多编程语言中会将数据类型划分为原始类型(Primitive)与复合类型(Composite),不过这里为了保证通用性还是将学习复杂度较低的数据类型划归到基本类型中。常见的基本类型囊括了数值类型、空类型、布尔类型、可选类型(Optional)以及枚举类型(Enum)等等。学习数值类型的时候我们还需要了解如何进行随机数生成、如何进行常见的科学计算,这也是基础的数值理论算法的重要组成。JavaScript 中提供了 undefined 与 null 两个关键字,二者都可以认为是空类型不过又有所区别;而可选类型则能够帮我们更好地处理可能为空地对象,避免很多的运行时错误。接下来我们就要将目光投注于字符串类型上,我们需要了解如何创建、删除、复制、替换某个字符串或者其他内容;很多语言也提供了模板字符串或者格式化字符串的方式来创建新字符串。我们还需要知道如何对字符串进行索引遍历,如何对字符串进行常见的类型编码以及如何实践模式匹配。模式匹配中最直接的方式就是使用正则表达式,这也是我们应用开发中经常会使用到的技术点。除此之外我们还需要关注字符串校验、以及如何进行高效模糊搜索等等内容;我们也可以学习使用 KMP、Sunday 等常见的模式匹配算法来处理搜索问题。

然后我们需要学习常见的时间与日期处理方式,了解如何时间戳、时区、RFC2822、ISO8601 这些基础的时间与日期相关的概念,了解如何从时间戳或者时间字符串中解析出当前编程语言支持的时间与日期对象。我们还需要了解时区转换、时间比较以及如何格式化地展示时间等内容,有时候我们还需要利用日历等对象进行事件的增减以及偏移计算。接下来就是非常重要的集合类型,无论哪种编程语言都会提供类似于 Array、List、Set、Dictionary、Map 等相关的数据结构实现,而我们也就需要去了解这些常见集合类型中的增删复替以及索引遍历这些基础操作以及每个集合的特点;譬如对于序列类型我们要能熟练使用 map、reduce、filter、sort 这些常见的变换进行序列变换与生成。进阶而言的话我们可以多了解下这些数据结构的底层算法实现,譬如 Java 8 中对于 HashMap 的链表/红黑树实现,或者 V8 中是如何利用 Hidden Class 进行快速索引的。接下来的话我们可以对于像 Java 中 SteamAPI 或者各种语言的 Immutable 对象的实现方式有所了解,还有就是常见的 JSON、XML、CSV 这些类型的序列化与反序列化操作库也是实际开发中经常用到的。

接下来我们就需要对语言提供的常用外部功能相关的 API 或者语法有所掌握,主要也是分为存储、网络与系统进程这三个部分。在存储部分我们需要掌握如何与 MySQL、Redis、Mongodb 等关系型或者非关系型数据库进行数据交互,掌握如何对文件系统进行如文件寻址、文件监控等操作,并且还需要能够使用一些譬如 Java 堆外存储这样的应用内缓存来存放数据。而网络部分我们应该掌握如何利用 HTTP 客户端进行网络交互、如何使用相对底层的 Socket 套接字建立 TCP 连接、或者使用语言内置的一些远程调用框架与远端服务进行交互。最后我们需要对如何利用语言进行系统进程操作有所了解,本部分笔者认为最重要的当属并发编程相关知识。在而今服务器性能不断提升、处理的数据量越来越多的情况下,我们不可避免地需要使用并发操作来提高应用吞吐量。并发编程领域我们应该去学习如何使用线程、线程池或者协程来实现并发,如何利用锁、事务等方式进行并发控制并保持数据一致性,如何使用回调、Promise、Generator、Async/Await 等异步编程模式。除此之外,我们还需要对切面编程、系统调用以及本地跨语言调用有所了解。

工程实践与进阶

编程语言初学阶段的最后我们需要了解下工程实践以及一些偏原理与底层实现的进阶内容。首先开发者应当对具体编程语言中如何实现 S.O.L.I.D 编程原则与数十种设计模式有所了解,当然也不能邯郸学步只求形似,而是能够根据业务功能需求灵活地选择适用的范式。而在团队开发中我们往往还需要统一团队内的样式指南,包括代码风格约定中常见的命名约定、文档与注释约定、项目与模块的目录架构以及语法检查规范等。接下来我们还需要对语言或者常用开发工具的调试方式有所了解,掌握基本的单步调试等技巧,并且能够为代码编写合适的单元测试用例。工程实践方面的最后则是要求我们对代码性能优化所有了解,尽量避免反模式。进阶内容的话则相对更加地抽象或者需要花费更多的精力去学习,其中包括泛型编程、元编程、函数式编程、响应式编程、内存管理、数据结构与算法等几个部分。泛型编程与元编程中的反射、代码生成、依赖注入等还是属于语言本身提供的语法特性之一,而函数式编程与响应式编程则偏向于实际应用开发中有所偏爱的开发范式。即使 Java 这样纯粹的面向对象的语言,当我们借鉴纯函数、不可变对象、高阶函数、Monad 等函数式编程中常见的名词时,也能为代码优化开辟新的思路。响应式编程是非常不错的异步编程范式,这里我们还需要注意下并发编程与异步编程之间的差异。而内存管理则有助于我们理解编程语言运行地底层机制,譬如对于 JVM 或者 V8 的内存结构、内存分配、垃圾回收机制有所了解的话能够反过来有助于我们编写高性能地应用程序,并且对于线上应用错误的调试也能更加得心应手。

界面基础

用户界面是前端应用程序的核心组成部分,而我们涉足前端开发的第一步往往也就是从简单的界面搭建开始。我们可能是在 Android 中编写简单的基于 XML 布局的 Activity,在 iOS 中利用 StoryBoard 快速构建导航界面,或者在 Web 中使用某个框架实现 TODOList。而界面开发最基础的部分就是布局与定位,无论在何端开发中我们往往都会使用相对布局、绝对布局、弹性布局、网格布局等布局方式;并且面向多尺寸的屏幕我们往往也需要进行响应式布局的考虑,从横竖屏响应式切换到不同分辨率下的布局与尺寸的调整,都是为了给予用户较好的使用体验。而了解了布局与定位之后,我们往往就需要来学习如何使用基本的界面容器,譬如常见的滚动视图、导航视图、页卡视图与伸缩视图。Android 与 iOS 往往也为我们对这些基本容器进行了较好地封装,而 Web 中则往往需要我们自己动手去实现相应功能。譬如在滚动视图中,我们需要去提供常见的滚动事件控制,典型的有如何在不同环境下保证平滑滚动体验、如何设置优美的滚动条、如何设置滚动监听等等。除此之外,我们往往还需要针对列表或者长阅读界面封装一些高级事件响应,譬如上拉加载、下拉刷新以及无限滚动时需要的滚动触发规则实现。作为最常见的用户交互方式之一,无论是在移动端还是桌面端,我们也都需要实现一些优美的动画;譬如视差滚动就可以给用户带来不一样的视觉感受,而像 Swiper 这样的整页滚动则是很好地产品展示或者讲演页的交互方式。在基础的界面容器使用中我们已经接触了一些用户交互的监听与响应的实现,接下来我们则是需要深入全面地了解用户交互相关内容。最基础的我们需要了解常用事件与手势操作,了解如何进行事件监听与绑定、如何捕获事件并且进行分发、如何进行缩放、拖拽、摇晃等复杂手势动作地监听与识别、如何响应键盘事件并且进行响应处理。除此之外,笔者将音视频录制与播放,指纹、计步器等传感器的使用,本地通知与远程推送等内容也都归纳于了用户交互这个分类下。在 Android 与 iOS 开发中相信对于这些 API 的使用并不会陌生,而随着 HTML5 的流行以及现代浏览器的发展,相信未来 Web 应用也会越来越多地添加这些与系统层面进行交互地功能。我们在本部分还需要了解下动画与变换、绘图及数据可视化等相关内容。常见的动画引擎包含了属性控制与帧动画两种方式,前者更趋向于命令式编程而后者则适用于声明式编程;除了了解这些基础的语法,我们还需要对常用的动画进行收集与汇总,以便在项目开发中能够灵活应用。而随着大数据时代的到来,数据可视化相关应用也成了前端开发常见的任务之一。在这个部分,我们需要对 SVG、Canvas、WebGL 等相关绘画基础有所了解,能够运用 D3.js 或者其他类似的库进行简单图形绘制。并且我们要能够利用 ECharts 等优秀的外部绘图库进行散点图、折线图、流程图等常见类型图表进行绘制。最后,地图以及相关技术也是我们需要去了解的,作为开发者我们要能够基于百度地图等第三方 API 或者 SDK 开发导航、地理位置信息可视化等相关的功能。

系统功能

与界面基础相对的就是常见的系统功能以及 API 的使用语法,其主要分为系统与进程、数据存储以及网络交互这三个部分。

进程与存储

在开发多界面应用程序或者利用 Service、ServiceWorker 等方式启动后台线程时,我们就需要考虑如何进行组件间通信;譬如在 Android 开发中我们可以利用 Otto 等库以消息总线的方式在 Activity、Fragment、Service 等组件之间传递消息。而在 Android 或者 iOS 开发中我们也常常需要考虑并发编程,可能会涉及到如何利用 Thread、GCD 等方式实现多线程并行、如何利用 RxJava 等响应式扩展优化异步编程模型、如何利用锁等同步方式进行并发控制等等内容。有时候我们也需要去更多地了解系统服务相关的内容,特别是在 Android 或者桌面应用程序开发时,我们需要考虑如何实现守护进程以协调并且保障各个组件的正常运行。在系统与进程部分的最后,我们还需要去接触些系统辅助相关的功能实现,譬如如何进行运行环境检测、如何利用 DeepLink 进行 APP 之间跳转、如何进行应用的权限管理等等。接下来我们讨论下数据存储部分应该掌握哪些内容,最简单的就是类似于 SharedPreference、NSUserDefaults、localStorage 这样的键值类存储;复杂一点的情况我们可能会利用到 SQLite 或者 IndexedDB 这样的简化关系型或者文档型数据库,有时候 Realm 这样的第三方解决方案也是不错的选择。很多时候我们还需要了解如何控制缓存或者剪贴板中的内容,以及如何对文件系统进行基本的操作,譬如读写配置文件与资源文件、浏览列举文件系统中的文件并且根据不同的文件类型选用不同的处理方式。

网络交互

而网络交互部分更多地关注如何与服务端或者第三方系统进行交互,实际上对于如何在需求动态变化的情况下较好地协调服务端与客户端对于接口的定义是很多项目开发的痛点。不过从基础使用的角度,我们首先需要了解如何利用网络客户端进行基于 HTTP 或 HTTPS 的网络请求。这部分我们需要了解如何构造、分析、编码 URI,如何管理请求头、设置请求方法与请求参数,如何同步、异步或者并发地执行请求,如何进行响应解析,如何进行复杂的请求管理等等内容。除了这些,我们还要能够利用基础的 Socket 进行通信,这样有助于我们理解通信网络与 TCP/IP 实现原理;我们往往还需要关心如何利用 WebSocket 等技术实现推送与长连接功能,如何进行远程与本地方法调用等等。除了这三个偏功能实现的知识点,我们还可以尝试去了解下系统的底层设计原理。譬如在 Android 开发中我们可以尝试去了解 Dalvik 虚拟机的工作原理,使用 Xposed 或类似工具进行系统层面的一些操作;对于 Web 开发而言我们可以去更多地关注浏览器工作原理,了解现代浏览器的运行机制等等内容。

界面插件

在掌握了如何构建基本的界面并且为应用添加必须的功能之后,我们就需要去尝试进行应用项目开发。每个应用可以按照用户交互地逻辑切分为多个独立界面,而每个界面的开发中我们往往又需要编写导航、菜单、列表、表单等等可重复使用的界面插件。实际上前端开发中最核心的工作之一就是界面插件的开发,好的开发者能够在项目开发中沉淀出可复用的界面插件库;这类可复用的界面插件往往会独立于具体的业务逻辑,其分类自然也应按照显示或者交互逻辑本身,而不应该受制于不同的业务场景。笔者习惯地会将界面插件区分为指示器(Indicator)、输入器(Picker)、列表与表单(TableGrid)、对话框(Dialog)、画廊(Galley)、WebView 等几个部分。

指示器与输入器

指示器与输入器算是两个宽泛的界面插件分类,最常见的指示器当属文本显示类别的插件,譬如标签。标签多用于表单中的输入域描述、用户引导等场景,而除了文字标签之外我们也会使用图标或者所谓的 Tags。除此之外我们还会关注于 MarkDown 等富文本的展示、如何针对不同屏幕对页面进行排版与字体设置、如何针对不同地区的用户进行国际化切换、如何为文本添加合适的动画等等方面。在应用开发中我们也会添加专门的介绍或引导页,一方面引导用户使用,另一方面也可以进行后台资源请求与处理;譬如我们往往会在应用启动时设置闪屏页(Splash),记得最早在 Uber 见到以短视频为背景的闪屏页很有耳目一新的感觉。除此之外,我们常见的指示器还包括了进度指示与时间指示这两种。在进行数据请求或者数据处理等需要用户等待的场景中,我们往往会给用户以进度条方式地友好反馈,这种进度条就是典型地进度指示。常用的进度条设计有线性进度条、圆形进度条或者固定在页首或者页尾的进度条,有些设计中我们也会以背景投射地方式反馈当前进度,这种方式可能更具有视觉冲击力。而除了进度条之外,无限循环的加载效果、分页器或者步骤跟踪显示器也是常见的进度指示的表现形式之一。而所谓的时间指示即譬如界面上放置的拟物时钟或者电子时钟、常见于社交媒体上的时间轴或者日历效果以及倒计数效果等。

而输入器的典型代表则为按钮与文本输入,譬如我们除了常见的 Primary、Secondary 按钮之外,我们可能还会用到悬浮按钮、可扩展的按钮或者在喜欢与点赞时用到的具有一定动画效果的按钮。而文本输入系列的插件中,除了常见的文本框或者富文本编辑器,有时我们也需要去编写具有自动补全或者类似于密码、勾选之类的特殊格式的输入框。选择器也是我们常用到的输入器之一,譬如开关、单选按钮、勾选按钮、分段输入以及常用于两个列表互选的左右穿梭器等等。除了这些,搜索、菜单、解锁界面也是归属于输入器这个类别中。

列表、画廊与对话框

在这两个大类之外算得上最常用的插件的当属列表、网格与表单这个系列的控件;基本上每个应用都会包含列表或者网格布局,对于海量数据的列表渲染也是前端常见的挑战之一。Android 中内置的 RecycleView 与 iOS 中内置的 UITableView 都为我们提供了不错的懒加载、局部渲染的功能,而 Web 中我们往往需要自己定制或者寻求第三方库的帮助。对于列表的交互也是常见问题之一,除了允许用户正常的点击,我们还需要添加左滑右滑时的反馈、可伸缩或者允许排序、拖拽的方式进行交互,有时候还需要为了列表项添加进出时的转场动画,以这种微互动增加整个界面的友好性。最后我们来聊聊画廊与对话框,画廊最典型的插件就是提供图片或者视频预览的走马灯效果的轮播插件,笔者也是将图片加载、呈现、处理相关的插件划分到了画廊这一系列插件中。而在端开发中我们常常需要对相册或者缓存中的图片进行浏览,或者将图片以瀑布流的方式呈现给用户,这种性质的插件也应归属到画廊这一类中。对话框的分类则稍显的有些生硬,譬如 ActionSheet、HUD 是系统提供的消息提示性质的插件,这种弹出与显示层自然会划归到对话框这个系列的组件中。而在 Web 中我们常常需要自定义的模态对话框、覆盖层也属于对话框系列,有时候我们还需要考虑如何为对话框提供拖拽支持,或者在对话框显示和消失之际添加转场动画。

工程化与应用架构

前面我们讨论了开发某个前端应用所需要的必备技能,而在需要持续交付的团队项目开发中,我们还需要考虑很多工程实践相关的方法与技巧。命令式编程到声明式编程的变化,将更多地功能性工作交于框架处理,而开发人员更加地专注于业务逻辑的实现。

工程实践

代码调试是每个程序员都掌握的技能,不过如何较好地调试代码以快速定位错误所在却并不是那么容易。在开发中我们常常需要热加载、增量编译等相关技术来避免过长的等待,而单步调试则能够帮助我们梳理代码逻辑、循序渐进地发现问题所在。可能 iOS、Android 的开发人员更习惯使用单步调试,而在 Web 或者 Node.js 开发中我们也应适当地多使用 Chrome 等工具进行代码的单步调试;有时候单步调试也是不错的浏览分析第三方源代码库的方式。另一方面,日志无论在开发环境还是生产环境中都能够帮我们记录应用运行状态等信息。接下来我们还要了解应用开发周期中不同阶段使用的单元测试、集成测试以及端到端测试的具体的实现方式,在团队协同开发中统一代码风格与约定,能够利用多种方式对应用进行性能优化,以及在发布到生产环境之后能够混淆加密、进行应用更新以及应用状态跟踪。

应用架构

所谓架构二字,核心即是对于对于富客户端的代码组织/职责划分,从具体的代码分割的角度,即是功能的模块化、界面的组件化、应用状态管理这三个方面。纵览这十年内的架构模式变迁,大概可以分为 MV* 与 Unidirectional 两大类,而 Clean Architecture 则是以严格的层次划分独辟蹊径。从笔者的认知来看,从 MVC 到 MVP 的变迁完成了对于 View 与 Model 的解耦合,改进了职责分配与可测试性。而从 MVP 到 MVVM,添加了 View 与 ViewModel 之间的数据绑定,使得 View 完全的无状态化。最后,整个从 MV* 到 Unidirectional 的变迁即是采用了消息队列式的数据流驱动的架构,并且以 Redux 为代表的方案将原本 MV* 中碎片化的状态管理变为了统一的状态管理,保证了状态的有序性与可回溯性。实际上从 MVC、MVP 到 MVVM,一直围绕的核心问题就是如何分割 ViewLogic 与 View,即如何将负责界面展示的代码与负责业务逻辑的代码进行分割。所谓分久必合,合久必分,从笔者自我审视的角度,发现很有趣的一点。Android 与 iOS 中都是从早期的用代码进行组件添加与布局到专门的 XML/Nib/StoryBoard 文件进行布局,Android 中的 Annotation/DataBinding、iOS 中的 IBOutlet 更加地保证了 View 与 ViewLogic 的分割(这一点也是从元素操作到以数据流驱动的变迁,我们不需要再去编写大量的 findViewById。而 Web 的趋势正好有点相反,无论是 WebComponent 还是 ReactiveComponent 都是将 ViewLogic 与 View 置于一起,特别是 JSX 的语法将 JavaScript 与 HTML 混搭,颇有几分当年 PHP/JSP 与 HTML 混搭的风味。

从代码组织的角度来看,项目的构建工具与依赖管理工具会深刻地影响到代码组织,这一点在功能的模块化中尤其显著。譬如笔者对于 Android/Java 构建工具的使用变迁经历了从 Eclipse 到 Maven 再到 Gradle,笔者会将不同功能逻辑的代码封装到不同的相对独立的子项目中,这样就保证了子项目与主项目之间的一定隔离,方便了测试与代码维护。同样的,在 Web 开发中从 AMD/CMD 规范到标准的 ES6 模块与 Webpack 编译打包,也使得代码能够按照功能尽可能地解耦分割与避免冗余编码。而另一方面,依赖管理工具也极大地方便我们使用第三方的代码与发布自定义的依赖项,譬如 Web 中的 NPM 与 Bower,iOS 中的 CocoaPods 都是十分优秀的依赖发布与管理工具,使我们不需要去关心第三方依赖的具体实现细节即能够透明地引入使用。因此选择合适的项目构建工具与依赖管理工具也是好的 GUI 架构模式的重要因素之一。不过从应用程序架构的角度看,无论我们使用怎样的构建工具,都可以实现或者遵循某种架构模式,笔者认为二者之间也并没有必然的因果关系。而组件即是应用中用户交互界面的部分组成,组件可以通过组合封装成更高级的组件。组件可以被放入层次化的结构中,即可以是其他组件的父组件也可以是其他组件的子组件。根据上述的组件定义,笔者认为像 Activity 或者 UIViewController 都不能算是组件,而像 ListView 或者 UITableView 可以看做典型的组件。我们强调的是界面组件的 Composable&Reusable,即可组合性与可重用性。当我们一开始接触到 Android 或者 iOS 时,因为本身 SDK 的完善度与规范度较高,我们能够很多使用封装程度较高的组件;凡事都有双面性,这种较高程度的封装与规范统一的 API 方便了我们的开发,但是也限制了我们自定义的能力。同样的,因为 SDK 的限制,真正意义上可复用/组合的组件也是不多,譬如你不能将两个 ListView 再组合成一个新的 ListView。在 React 中有所谓的 controller-view 的概念,即意味着某个 React 组件同时担负起 MVC 中 Controller 与 View 的责任,也就是 JSX 这种将负责 ViewLogic 的 JavaScript 代码与负责模板的 HTML 混编的方式。界面的组件化还包括一个重要的点就是路由,譬如 Android 中的 AndRouter、iOS 中的 JLRoutes 都是集中式路由的解决方案,不过集中式路由在 Android 或者 iOS 中并没有大规模推广。iOS 中的 StoryBoard 倒是类似于一种集中式路由的方案,不过更偏向于以 UI 设计为核心。笔者认为这一点可能是因为 Android 或者 iOS 本身所有的代码都是存放于客户端本身,而 Web 中较传统的多页应用方式还需要用户跳转页面重新加载,而后在单页流行之后即不存在页面级别的跳转,因此在 Web 单页应用中集中式路由较为流行而 Android、iOS 中反而不流行。所谓可变的与不可预测的状态时软件开发中的万恶之源,我们尽可能地希望组件的无状态性,那么整个应用中的状态管理应该尽量地放置在所谓 High-Order Component 或者 Smart Component 中。在 React 以及 Flux 的概念流行之后,Stateless Component 的概念深入人心,不过其实对于 MVVM 中的 View,也是无状态的 View。通过双向数据绑定将界面上的某个元素与 ViewModel 中的变量相关联,笔者认为很类似于 HOC 模式中的 Container 与 Component 之间的关联。随着应用的界面与功能的扩展,状态管理会变得愈发混乱。