Thinking in FE 更现代的 Web 开发

前端,是一个经常会被小觑的技术领域,在大多不明所以的人眼里,前端不过是排排版、布布局,甚至是一些前端的新手也会这样认为(这里的前端并不特指 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
2
3
4
5
6
7
8
9
[#if condition]
<div>true</div>
[#end]

<ul>
[#foreach item in list]
<li>[#item]</li>
[#end]
</ul>

从 css 所衍化出来的,便是和样式相关的语言了,与 html 语言一样,css 也是一种静态的描述语言,本身不支持变量和条件分支。作为对 css 的扩充,市面上出现了像 less、sass 这样一些语言,它们使得样式的描述更加结构化,并且可以通过变量很方面的来修改和维护,这对需要提供样式定制化的第三方组件而言还是非常有用的。下面是 sass 的变量和嵌套示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$primaryColor: #333;

nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}

li { display: inline-block; }

a {
color: $primaryColor;
display: block;
padding: 6px 12px;
text-decoration: none;
}
}

最后从 javascript 中衍化出来的,便是很多对 javascript 特性进行补充的语言了。javascript 本身是基于原型的语言,自身也有一些设计上的缺陷,最常见的便是变量的作用域问题,也就是所谓的变量提升问题。不过在 ES6 出来后,javascript 得到了质的提升,而在这之前,出现了 javascript 的替代语言,以 typescript 和 coffeescript 最为常用,并且现在还被广泛使用着。无论是 typescript 还是 coffeescript,它们都是对 javascript 的补充,而 coffeescript 更像是一门新的语言。它们使得 javascript 更加的面向对象,并引入了更多函数式语言的特性,让书写更加优雅、舒适,下面一段 coffeescript(摘自Coffee-Script中文网),大家感受下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 赋值:
number = 42
opposite = true

# 条件:
number = -42 if opposite

# 函数:
square = (x) -> x * x

# 数组:
list = [1, 2, 3, 4, 5]

# 对象:
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x

# Splats:
race = (winner, runners...) ->
print winner, runners

# 存在性:
alert "I knew it!" if elvis?

# 数组 推导 (comprehensions):
cubes = (math.cube num for num in list)

自 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
2
3
4
5
6
7
8
9
10
11
12
var HelloWorld = React.createClass({
getInitialState: function() {
return {hidden: false};
},
render: function() {
return (
<div>
{this.state.hidden ? <span>***</span> : <span>Hello World</span>}
</div>
);

}
});

上面这个组件拥有一个hidden的状态,而render方法中的内容也是让人一目了然(JSX语法让组件更加内聚)。通过界面交互或其它一些手段我们可以改变hidden的值,而这会实时体现到render方法中。React 自己维护了一套虚拟 DOM,一般情况下我们不必刻意考虑渲染性能问题,但如果你想自己控制是否重绘的话,React 的组件也给你提供了这样的控制能力。

React 的组件除了内聚之外,还可以进行组合,一个组件可以嵌入其它多个组件。这使得我们在进行实际开发之前,需要对即将完成的内容进行组件划分,在通用简单两方面来作权衡。也就是说,React 的思想已经颠覆了我们思考问题的方式,而它给我们带来的收获是组件的不断积累,以及开发速度和可维护性的提高。

单项数据流 Flux

在绝大数MV*系列架构的框架中,视图 DOM 和视图模型之间是进行双向绑定的,这种强绑定的情形在很多复杂的场景下会带来让人无法维护的问题。当这样的情况越来越普遍时,Facebook 提出了单向数据流的概念,并把这种思想称之为Flux,且推出了官方实现flux。不得不说 Facebook 是个了不起的公司,也不得不说 Web 前端一直是这些新思维的探路者。不过flux很快就被另一个开源项目慢慢取代了,也就是社区中非常火爆的redux,但思想还是一致的。

这里有必要解释一下单向的概念,整个 Flux 的数据流如下:

  1. 用户触发 View 的某个操作,View 向 Dispatcher 发出一个 Action
  2. Dispatcher 收到 Action 后,对 Store 进行更新
  3. Store 更新后,发出事件通知 View
  4. View 收到事件后,进行页面更新

这里整个数据流都是单向流动的(概念抽象中没有双向箭头),所有状态都维护在 Store 之中,这让我们对状态变更进行追踪变得非常简单。在redux的实现中,从 Dispatcher 到 Store 之间,我们还可以安装很多自定义的中间件,来进行一些切面处理,比如日志、授权、统计等。

Facebook 的 React 仅仅是提供了组件化的构建方案,而对于组件所构成的模块并没有提供更多架构上的支持,这点基于 Flux 思想的redux刚好可以对其进行补充。在解释如何让它们衔接之前,我们有必要先看点其它内容。

异步任务编织

所有的项目开发中,为了追求更好的用户体验,我们不可避免的要面对异步问题。同步操作下,我们对流程管理和安排非常简单清晰,相比之下,异步就没有那么容易去维护了。

而在 Web 前端,很长一段时间里,ajax 几乎就成了异步的代名词,因为在实际开发中,80%以上的异步都来自于异步网络请求。时至今日,我觉得需要重新定义下异步在 Web 前端中的定位了,特别是在使用了redux之后。从最初的action派发,到最终的状态变更,以及状态变更后引发的视图渲染,这一系列的步骤,我们都应该将其视为异步(参考上文 Flux 的图片)。

众所周知,javascript 处于一个单线程的运行环境中,但异步的引入使得我们也需要面临一些多线程下才存在的问题。并且我们重新定义了并发的概念,在 javascript 中,并发指的是一个异步任务尚未完成,同时又产生了其它异步任务。比如,我们同时发出了两个 ajax 请求,那么我们就必须要面对这两个请求返回时间、顺序不确定性的结果。

在 ES6 的语言标准中,引入了Promise概念,可以方便我们对异步任务进行链式编排,并且可以统一进行错误处理,下面是一个简单的例子:

1
2
3
4
5
6
7
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// 返回其它 Promise
}).catch(function(error) {
// 处理前面三个 Promise 产生的错误
});

虽然这种链式调用从某种程度上让代码更加清晰,但在对异步返回数据需要进行条件分支判断,或者一些更加复杂的逻辑操作时,Promise也就显得有些力不从心了。在 ES6 中还引入了另外一个概念,叫Generator,与之对应的关键字是yieldGenerator的特性是函数内部维护了上一次执行到的位置,而在外部调用next()控制它进一步执行(关于这方面更多的知识,请参考相关ES6手册)。其实这点无疑是走了微软 C# 的老路,并且在 ES7 中引入的asyncawait也是与 C# 同出一辙,在 C# 推出yield关键字后,社区也有达人以此实现了一套异步任务编织的框架,那么 Web 前端自然也不例外了。

这里不得不说一下redux的一个中间件redux-saga,它是完全基于Generator特性实现的一套异步任务编织框架,并且非常强大。一个saga对应一个Generator函数,并且saga分为两种:

  1. watcher saga: 负责监控 redux 的 action,并且对任务进行具体编排
  2. worker saga: 处理由watcher saga编排的具体任务

如果想要了解更多关于redux-saga的内容,还是建议去翻阅下官方文档,这里给出一个简单的示例,一睹它的威力(摘自saga文档):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
return token
} catch(error) {
if(!isCancelError(error))
yield put({type: 'LOGIN_ERROR', error})
}
}

function* loginFlow() {
while (true) {
const {user, password} = yield take('LOGIN_REQUEST')
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if (action.type === 'LOGOUT')
yield cancel(task)
yield call(Api.clearItem, 'token')
}
}

上面的示例中有两个saga,其中loginFlowwatcherauthorizeworker。在loginFlow中,当我们收到LOGIN_REQUESTaction时,取出其中的userpassword状态,非阻塞的去调用authorize,并且开始监控LOGOUTLOGIN_ERROR两个action,当收到的actionLOGOUT,此时前一个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连接时connectmap参数的管理,可以有效减少状态变更,减少组件渲染的次数

有了上面的这些组件,我们基本上可以愉快的进行项目开发了,那么在整个项目的流程中,它们是如何进行协作的呢?可以参考下图:

可以看到,贯穿其中最多的便是actionstate,而redux显然是整个项目连接起来的枢纽,saga则是用于封装了所有的异步网络请求(封装成更加业务化的任务)。这里举一条比较常见的流程:用户从界面上点击了某个按钮,然后发送了网络请求,以及请求响应后对界面的更新:

  1. 首先从 react 组件的 View 上派发了一个 actionRedux
  2. Redux 的中间件 Saga 会监控这个 action 并作出网络请求
  3. 响应回来后,Saga 会通过 put 将响应的 action 派发到 Redux
  4. Redux 接收到这个 action 对相应状态作出变更
  5. react 连接到(connect)这个状态的相应组件会收到状态变更,重新渲染 View

似乎看起来比较复杂,但在了解透彻相应组件的职责后,其实并没那么复杂。为了让开发过程中调试更加快捷,我们还可以安装一些开发中需要用到的工具模块。这里推荐使用 dora,配合dora-plugin-proxy我们可以在开发过程中模拟后端响应数据,再配合dora-plugin-webpack-hmr可以实现开发过程中,模块的热加载,让我们无需不断刷新浏览器就能看到最新的界面效果,当然选择还有很多,dora 只是我觉得比较好用的一款。

Thinking

本篇带着大家走马观花的将现代化的 Web 前端看了个大概,这其中更多的是在阐述思想和理念,随着时代的发展,Web 前端并不是大多数人们眼中的“做做网页而已”,它的理念走在了时代的前言,它的生态也比很多其它方向丰富、健全。

不小看、自以为是,要永远抱着敬畏的态度,这是成长的基础,也是我们作为技术人员该有的素质。

参考