0%

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 前端并不是大多数人们眼中的“做做网页而已”,它的理念走在了时代的前言,它的生态也比很多其它方向丰富、健全。

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

参考