前端,是一个经常会被小觑的技术领域,在大多不明所以的人眼里,前端不过是排排版、布布局,甚至是一些前端的新手也会这样认为(_这里的前端并不特指 Web 前端,移动端也可归结为前端_)。那么前端真的就如此无趣且一成不变么?
之所以本系列取名为 Thinking in FE,是因为 Thinking 让人沉静、不浮躁,就该用这种心态来面对前端。作为本系列的第一篇,我觉得是很有必要把 Web 前端拿出来说说,这几年 Web 前端变革得太快,如果你还是以为吃透了 float 就吃透了整个布局,搞定了 css + div 就能纵横 Web FE 的话,本篇就是为你而准备的。
开发模式的变革
前几年,当我还奋战在 Web 前端开发的第一线时,那时候的项目开发模式是简单但容易出问题的。当时后端主要使用的技术是微软的 WebAPI,前端的 IDE 自然就被 Visual Studio 包揽了(_当然那时 WebStorm、PhpStorm 也都在不同项目中承担着 IDE 的角色_)。IDE 倒不是什么问题,当时主要的问题在于前端第三方库依赖的管理,基本上都是手动引用,长时间后会连一些库的具体版本都忘记了。这在多人协作开发时很容易出问题,也不利于项目的持续和快速发展,久而久之一个项目会变得陈旧、死气沉沉。
现在的开发模式,悄然变得更轻却又更重了。更轻的是 IDE,我们开始倾向于使用像 Sublime、Atom、VSCode 这种轻量级的文本编辑器,再配合一些日常需要的小插件;而更重的是项目的依赖管理和构建方式,依赖管理已经被 NodeJS 的包管理工具npm
包揽了,基本上我们需要什么样的库,只要简单的npm install
一下就可以了,而构建工具很多,比如 Webpack、Gulp、Grunt、Yeoman 等,但也慢慢的有被 Webpack 一统江湖的趋势。
有了依赖管理、构建工具,并且可以通过npm
配合其他工具来执行单元测试,我们便可以很容易的将项目进行持续集成。这才是更加现代化的开发模式,而整个 Web 前端的生态也趋向完整了。
百花齐放的开发语言
作为一个站在时代前沿的 Web 前端开发者,可能会是所有开发工种中接触开发语言最多的一个,至少你需要掌握三门语言:html、css、javascript,这是最终的宿主,也就是浏览器所原生支持的三种语言,分别用于:结构、样式、交互。但如果你真的只会这三种语言,那你肯定算不上一个合格的 Web 前端开发者,随着广大先驱者的智慧凝结,这三种基础的语言衍化出了很多独立的语言,而这些衍化的产物已经越来越被现代化的 Web 前端所广泛使用。
从 html 所衍化出来的是各种模板语言,比如 backbone、angular 所提供的。模板的作用是将结构高度抽象,从而避免很多不必要的重复工作,并且使得前端页面更加动态化。html 本身是静态的描述语言,有了模板的支持,我们可以像下面这样来让其动态化:
1 | [#if condition] |
从 css 所衍化出来的,便是和样式相关的语言了,与 html 语言一样,css 也是一种静态的描述语言,本身不支持变量和条件分支。作为对 css 的扩充,市面上出现了像 less、sass 这样一些语言,它们使得样式的描述更加结构化,并且可以通过变量很方面的来修改和维护,这对需要提供样式定制化的第三方组件而言还是非常有用的。下面是 sass 的变量和嵌套示例:
1 | $primaryColor: #333; |
最后从 javascript 中衍化出来的,便是很多对 javascript 特性进行补充的语言了。javascript 本身是基于原型的语言,自身也有一些设计上的缺陷,最常见的便是变量的作用域问题,也就是所谓的变量提升
问题。不过在 ES6 出来后,javascript 得到了质的提升,而在这之前,出现了 javascript 的替代语言,以 typescript 和 coffeescript 最为常用,并且现在还被广泛使用着。无论是 typescript 还是 coffeescript,它们都是对 javascript 的补充,而 coffeescript 更像是一门新的语言。它们使得 javascript 更加的面向对象,并引入了更多函数式语言的特性,让书写更加优雅、舒适,下面一段 coffeescript(_摘自Coffee-Script中文网_),大家感受下:
1 | # 赋值: |
自 ES6 出来并受到很多工具的支持后,已经更加推荐直接使用 ES6 来编写项目了,ES6 弥补了 javascript 之前一直缺乏的原生模块化支持(_这里说的是原生,排除 CommonJS、AMD、CMD 规范的第三方实现_),对面向对象也有了更好的支持,并且明确了变量、作用域,也引入了很多函数式编程的概念。最重要的是在2013年 ES6 标准就已经确定了,对于新的提案 TC39 只会往 ES7 纳入,所以在项目中使用不会面临像使用 Swift 一样不断变更的窘境。
上面说过了,浏览器原生只支持最基本的那三种语言,那么如果想使用这些衍化出来的语言或者是现在还不被很好支持的ES6、ES7,我们需要相应的转换工具。而这些转换操作都可以非常简单的使用 webpack 对应的 loader 来完成。不得不说 webpack 已经成为了 Web 前端构建的一站式工具,通过组合不同的 loader,我们可以完成转换->合并->压缩->打包
等一系列中间过程。
React 的颠覆
如果要论这几年来,对 Web 前端思想产生颠覆性的框架,那应该是非 React 莫属了。Facebook 在2013年开源了这个框架,由此引发了一系列的变革。React 的核心思想是组件化,化整为零,分而治之。而 React 出现的原因,也正是因为 Facebook 对当时市面上所有的前端框架都不满意,既然不满意,他们就立马自己做了一个。
在 React 出来之前,市面上使用较多的都是一些MV*
系列的框架,比较有代表性的应该算是谷歌的 Angular 了。但这类框架的学习曲线还是比较高的,最重要的是,对于一般人而言它们所表述的意图不够直观。从视图到模型,虽然力求低耦合,但还是不得不进行约定、依赖,因为最终视图和模型需要绑定,那无论如何解耦都不可能做到干净利落,约定只会徒增维护的复杂度。
对此,React 提出了组件的概念(当然这个概念在其它领域早就有过),一个组件就是一个高内聚的封装。对外部而言组件的输入是属性(_props_),输出是最终的视图,属性是恒定的,也就是说外部输入之后,就不会被改变了。而让组件改变的是状态(_state_),对于 React 而言,状态是由组件内部进行维护的,这种思想让组件变得更加内聚、可控。下面是一个非常简单的 React 组件:
1 | var HelloWorld = React.createClass({ |
上面这个组件拥有一个hidden
的状态,而render
方法中的内容也是让人一目了然(_JSX语法让组件更加内聚_)。通过界面交互或其它一些手段我们可以改变hidden
的值,而这会实时体现到render
方法中。React 自己维护了一套虚拟 DOM,一般情况下我们不必刻意考虑渲染性能问题,但如果你想自己控制是否重绘的话,React 的组件也给你提供了这样的控制能力。
React 的组件除了内聚之外,还可以进行组合,一个组件可以嵌入其它多个组件。这使得我们在进行实际开发之前,需要对即将完成的内容进行组件划分,在通用和简单两方面来作权衡。也就是说,React 的思想已经颠覆了我们思考问题的方式,而它给我们带来的收获是组件的不断积累,以及开发速度和可维护性的提高。
单项数据流 Flux
在绝大数MV*
系列架构的框架中,视图 DOM 和视图模型之间是进行双向绑定的,这种强绑定的情形在很多复杂的场景下会带来让人无法维护的问题。当这样的情况越来越普遍时,Facebook 提出了单向数据流的概念,并把这种思想称之为Flux
,且推出了官方实现flux
。不得不说 Facebook 是个了不起的公司,也不得不说 Web 前端一直是这些新思维的探路者。不过flux
很快就被另一个开源项目慢慢取代了,也就是社区中非常火爆的redux
,但思想还是一致的。
这里有必要解释一下单向的概念,整个 Flux 的数据流如下:
- 用户触发 View 的某个操作,View 向 Dispatcher 发出一个 Action
- Dispatcher 收到 Action 后,对 Store 进行更新
- Store 更新后,发出事件通知 View
- View 收到事件后,进行页面更新
这里整个数据流都是单向流动的(_概念抽象中没有双向箭头_),所有状态都维护在 Store 之中,这让我们对状态变更进行追踪变得非常简单。在redux
的实现中,从 Dispatcher 到 Store 之间,我们还可以安装很多自定义的中间件,来进行一些切面处理,比如日志、授权、统计等。
Facebook 的 React 仅仅是提供了组件化的构建方案,而对于组件所构成的模块并没有提供更多架构上的支持,这点基于 Flux 思想的redux
刚好可以对其进行补充。在解释如何让它们衔接之前,我们有必要先看点其它内容。
异步任务编织
所有的项目开发中,为了追求更好的用户体验,我们不可避免的要面对异步问题。同步操作下,我们对流程管理和安排非常简单清晰,相比之下,异步就没有那么容易去维护了。
而在 Web 前端,很长一段时间里,ajax 几乎就成了异步的代名词,因为在实际开发中,80%以上的异步都来自于异步网络请求。时至今日,我觉得需要重新定义下异步在 Web 前端中的定位了,特别是在使用了redux
之后。从最初的action
派发,到最终的状态变更,以及状态变更后引发的视图渲染,这一系列的步骤,我们都应该将其视为异步(_参考上文 Flux 的图片_)。
众所周知,javascript 处于一个单线程的运行环境中,但异步的引入使得我们也需要面临一些多线程下才存在的问题。并且我们重新定义了并发的概念,在 javascript 中,并发指的是一个异步任务尚未完成,同时又产生了其它异步任务。比如,我们同时发出了两个 ajax 请求,那么我们就必须要面对这两个请求返回时间、顺序不确定性的结果。
在 ES6 的语言标准中,引入了Promise
概念,可以方便我们对异步任务进行链式编排,并且可以统一进行错误处理,下面是一个简单的例子:
1 | getJSON("/post/1.json").then(function(post) { |
虽然这种链式调用从某种程度上让代码更加清晰,但在对异步返回数据需要进行条件分支判断,或者一些更加复杂的逻辑操作时,Promise
也就显得有些力不从心了。在 ES6 中还引入了另外一个概念,叫Generator
,与之对应的关键字是yield
,Generator
的特性是函数内部维护了上一次执行到的位置,而在外部调用next()
控制它进一步执行(_关于这方面更多的知识,请参考相关ES6手册_)。其实这点无疑是走了微软 C# 的老路,并且在 ES7 中引入的async
和await
也是与 C# 同出一辙,在 C# 推出yield
关键字后,社区也有达人以此实现了一套异步任务编织的框架,那么 Web 前端自然也不例外了。
这里不得不说一下redux
的一个中间件redux-saga
,它是完全基于Generator
特性实现的一套异步任务编织框架,并且非常强大。一个saga
对应一个Generator
函数,并且saga
分为两种:
- watcher saga: 负责监控 redux 的 action,并且对任务进行具体编排
- worker saga: 处理由
watcher saga
编排的具体任务
如果想要了解更多关于redux-saga
的内容,还是建议去翻阅下官方文档,这里给出一个简单的示例,一睹它的威力(_摘自saga文档_):
1 | function* authorize(user, password) { |
上面的示例中有两个saga
,其中loginFlow
为 watcher 而authorize
为 worker。在loginFlow
中,当我们收到LOGIN_REQUEST
的action
时,取出其中的user
和password
状态,非阻塞的去调用authorize
,并且开始监控LOGOUT
和LOGIN_ERROR
两个action
,当收到的action
为LOGOUT
,此时前一个LOGIN_REQUEST
可能并未执行完成,所以我们需要取消它,在这一切完成后,我们调用clearItem
来清空本地存储的token
,再次回归到监控LOGIN_REQUEST
。而authorize
这个saga
中的流程也相对简单明了,这里就不作更多的阐述了。
通过saga
的实现,可以与Promise
进行对比,不难发现它更加的同步化,所有的代码完全看不出异步的影子,所以在进行复杂的异步任务编排和分支控制时,会非常的简洁明了。
项目的最佳实践
上面说到了项目构建工具、各种新兴的开发语言、React、Redux 以及 Redux-saga,那么在一个实际的项目中,我们如何将它们融合起来,进行更加现代化的 Web 开发呢?其实很简单,我们可以通过npm
来进行项目包依赖管理,并且通过webpack
来将它们全部串联起来。
对于webpack
,我们需要安装一系列的loader
并且在webpack.config.js
中进行配置,大概会用到下面这些loader
:
- babel-loader:用于转换 ES6、ES7、JSX 语法
- file-loader:用于简单的文件拷贝
- css-loader:用户 CSS 的压缩,打包
- less-loader:用户 LESS 的转换
- url-loader:用户图片资源的转换、打包
- html-loader:用户 HTML 文件的链接替换、打包
- html-minify-loader:用于 HTML 文件的压缩
具体的配置需要根据项目本身而定,详细的细节可以参考webpack
的官方文档。当我们把webpack
这些loader
配置完毕后,可以按照一些我们需要的框架和中间件了,大体应该如下:
- react:react 组件化的核心库
- react-dom:react 提供的一些 DOM 操作辅助方法库,用于在 DOM 上渲染 react 组件
- react-router:使用 react 开发单页应用时,这个库是必须的,提供了基于 react 组件化思路的路由解决方案
- redux:redux 库,上文中已经有所解释,这里就不多说了
- redux-actions:方便在 redux 中 action 和对应的
state
进行管理 - redux-saga:redux 的中间件,用于异步任务编织
- react-router-redux:redux 的中间件,用于同步 redux 中的路由状态,可以通过 redux 的 ation 来控制路由
- reselect:在较大型项目中使用,用于react和redux连接时
connect
中map
参数的管理,可以有效减少状态变更,减少组件渲染的次数
有了上面的这些组件,我们基本上可以愉快的进行项目开发了,那么在整个项目的流程中,它们是如何进行协作的呢?可以参考下图:
可以看到,贯穿其中最多的便是action
和state
,而redux
显然是整个项目连接起来的枢纽,saga
则是用于封装了所有的异步网络请求(_封装成更加业务化的任务_)。这里举一条比较常见的流程:用户从界面上点击了某个按钮,然后发送了网络请求,以及请求响应后对界面的更新:
- 首先从 react 组件的 View 上派发了一个
action
到 Redux - Redux 的中间件 Saga 会监控这个
action
并作出网络请求 - 响应回来后,Saga 会通过
put
将响应的action
派发到 Redux - Redux 接收到这个
action
对相应状态作出变更 - react 连接到(_connect_)这个状态的相应组件会收到状态变更,重新渲染 View
似乎看起来比较复杂,但在了解透彻相应组件的职责后,其实并没那么复杂。为了让开发过程中调试更加快捷,我们还可以安装一些开发中需要用到的工具模块。这里推荐使用 dora,配合dora-plugin-proxy
我们可以在开发过程中模拟后端响应数据,再配合dora-plugin-webpack-hmr
可以实现开发过程中,模块的热加载,让我们无需不断刷新浏览器就能看到最新的界面效果,当然选择还有很多,dora 只是我觉得比较好用的一款。
Thinking
本篇带着大家走马观花的将现代化的 Web 前端看了个大概,这其中更多的是在阐述思想和理念,随着时代的发展,Web 前端并不是大多数人们眼中的“做做网页而已”,它的理念走在了时代的前言,它的生态也比很多其它方向丰富、健全。
不小看、自以为是,要永远抱着敬畏的态度,这是成长的基础,也是我们作为技术人员该有的素质。
参考
- Webpack: https://webpack.github.io
- dora-js: https://github.com/dora-js
- React: https://facebook.github.io/react/index.html
- Redux 中文文档: http://cn.redux.js.org/
- React-Router: https://github.com/reactjs/react-router
- reselect: https://github.com/reactjs/reselect
- Redux-Sage: http://yelouafi.github.io/redux-saga/index.html
- Flux: http://reactjs.cn/react/docs/flux-overview.html
- ECMAScript 6 中文文档: http://es6.ruanyifeng.com