从HTML说起(老鸟可跳过本节)
学前端应该都是从 HTML 开始的吧,1991年的时候出现了世界上第一个网页,感受下当年的源代码:
<HEADER>
<TITLE>The World Wide Web project</TITLE>
<NEXTID N="55">
</HEADER>
<BODY>
<H1>World Wide Web</H1>
The WorldWideWeb (W3) is a wide-area
<A NAME=0 HREF="WhatIs.html">hypermedia ...</A>
一开始全都是标签,而且没有规范,于是一帮人组成了复仇者万维网联盟专门来搞网络标准化,也就是今天常说的 W3C。
当时的网页只能做简单信息展示还很 LOW,后来一家叫 SUN 的公司搞了个 Java 小程序(Applet),可以跑在网页中制造很酷炫的效果。另外一家叫网景的公司觉得很牛逼,于是赶紧招人准备出复刻版,由于项目紧,程序员花了两周时间搞出了JavaScript,结果项目上线后效果出乎意外的好!于是 JS 大火。
有了 JS 用户就可以和网页更好的交互了,但是页面还是很 LOW,于是又过了一年,CSS 出现了,专门用来美化页面。至此,前端三巨头诞生了,网页看起来终于有模有样了,于是 WEB 发展进入繁荣时代,出现了一大波新技术比如 Spring/JSP/ASP/AJAX 等等,也出现了一大波浏览器比如 IE、Firefox、Safari、Chrome、Opera...
于是前端出现了一个非常棘手的问题:浏览器兼容问题。同一个DOM操作需要写很多适配代码来兼容不同浏览器,这是一个很枯燥低效的事情。于是jQuery 诞生了:一套代码,多端运行。众前端大喜,纷纷使用至今。
由于技术发展飞快,人们对网页的要求越来越高,这时一家叫 Google 的公司认为浏览器需要更好的体验和性能,JS 需要一款更快速的引擎来迎接现代化 Web 应用。而当时微软的 IE6 占据了大部分市场份额,微软认为 IE6 已经很完美了,于是解散了IE6开发团队。
后来的事大家都知道了,IE 成了前端开发的阴影,而谷歌的 Chrome 搭上 V8 引擎上了天,美滋滋,也就由于浏览器性能的提升,W3C也为此提供了 HTML 的新特性,也就是我们现在说的 HTML5、CSS3,让我们能在网页是很快的完成一些酷炫的特效。。。
从模块化说起
模块化开发的主要目的是实现代码的结构化,提高代码复用性、可维护性、可扩展性。JavaScript 天生没有模块化的概念(直到ES6),而不像后端语言源生自带模块功能, 比如 Java 的 import、C++ 的 include,所以需要通过其他的方式来实现模块化。
没有模块化的日子
JavaScript 的最初,为了达到模块化的目的,我们往往会把一些重复使用的代码封装成一个 function,需要用到的时候时候去调用它,那么这可以看做是一种模块化,如下一段代码:
function show(element) { // 展示一个元素 }
function hide(element) { // 隐藏一个元素 }
代码很直观,就是要显示、隐藏一个 dom 元素,往往这种方法需要大范围多次调用,一般我们可能会放到 util.js 这样的文件里,这是第一步。接下来我们可以在业务代码 page1.js、page2.js 中使用这两个方法,我们把两个js引入到html中:
<body>
<script src="lib/utils.js"></script>
<script src="lib/page1.js"></script>
<script src="lib/page2.js"></script>
</body>
存在的问题
以现在的经验来看,上面的写法会带来非常明显的问题,当然也是在这个模块化引入阶段逐步暴露的。
- 全局变量冲突风险:如果编写 page-1 的同学不知道 utils 里面有一个 show/hide 方法,然后他自个也写了一个,同时还添加了额外逻辑,自然就覆盖了原来的方法,那么 page-2 同学在不知道的情况下调用了这方法,自然会发生错误.
- 人工维护依赖关系:因为存在依赖关系,所以必须先加载 util,然后才能加载 page-1/2,这里的例子非常简单,但在实际项目场景了,这样的依赖会非常多且复杂,维护非常困难,很难建立清晰的依赖关系。
尝试去解决存在的问题
针对问题1,全局变量冲突的风险,我们可以参考其他后端语言的特性,将变量声明到特定的命名空间里面,只要按照特定的规范来声明变量及命名空间,那么基本可以避免变量冲突的问题,如,可以按照实际情况具体到部门、team、类库来声明命名空间:
var com.company.departure.team.utils = {
show: function(ele) {}, // 显示元素
hide: function(ele) {} // 隐藏元素
}
但是还存在另外一个问题,就是 show、hide 函数里面可能依赖一些外部的变量,那么这些变量我们定义在哪的,这个时候我们可以封装一个立即执行函数,把这些变量都存放在里面,避免这些变量污染到全局变量,如下:
var mudule = {}; // 全局存放的模块
(function() {
var Company = Company || {};
Company.Base = Company.Base || {};
var name = 'username'
function show () {} // 内部引用 name 变量
function hide () {}
mudule['Company.Base.Util'] = Company.Base.Util = {
show: show,
hide: hide
}
})();
这个立即执行函数,通过闭包把需要导出的模块丢到我们全局配置 mudule 中,从而达到了模块化的目的,同时也解决了全局变量冲突的风险。
那针对第二个问题人工维护依赖关系,在客户端层面,一直没有一个比较比较好的解决方案,当然,我们能想到最好的方式,那就是只有一个js,被依赖的库在这个js的前面先被声明,这样技能减少http请求,也能把依赖问题解决。那么,为了达到这样的效果,我们需要在代码运行之前,事先知道模块之间的依赖关系,这个时候我们需要对js进行预处理,然后把所有js代码进行合并。
后来,一个来自雅虎,叫 YUI compressor 的工具横空出世,它可以完成代码的合并与压缩,因为这个,那个时候业界内的网站优化以雅虎为标准,然而,适合模块化的通用工具并未出现,我们还是得手动确定依赖进行混淆合并压缩,而且 YUI compressor 是基于 Java 的,对前端同学来说,并不是很友好。
NodeJs 来了
2009年,一个基于 JavaScript 的服务端语言 NodeJS 发布了,这给前端同学带来了无限的可能,也就是说,以往需要通过其他语言工具执行的编译过程也可以由前端一手接管。同时,node 也带来了 commonJS,给前端的模块化提供了新的思路。当然,commonJS 并不是 NodeJs 的产物,他只是按照该规范做了一套实现。
commonJs
简单概括下 commonJS 的几个概念
- 每个文件是一个模块,有自己的作用域。这里面定义到函数、变量、类都是私有的,对其他文件不可见;
- 每个模块内部,module 变量代表当前模块,它是一个对象;
- module 的 exports 属性(即 module.exports)是对外的接口;加载某个模块,其实是加载该模块的 module.exports 属性如果文件中没有 exports 属性,那么外部引用不到任何东西;
- 使用 require 关键字加载对应的文件,也就是模块;
- require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象,如果没有发现该模块,报错。
我们利用 commonJs 对上面的例子进行改进:
util.js
function show() {}
function hide() {}
module.exports.show = show;
module.exports.hide = hide;
page-1.js
const util = require('./util.js');
util.show();
然而,这样的代码无法在客户端执行,因为浏览器识别不到 commonJS,我们需要解析 commonJS,让他变成浏览器可以识别的代码。
打包编译工具
对于前端同学来说 node 有着天然的亲和力,让我们多了一个全新施展本领的领域,因此,为了解析 commonJS 以及提高开发效率,社区里各种构建工程不断涌出, 具有代表性的有 grunt、gulp、browserify,webpack,前端模块化可以更进一步。
说说 webpack
说到这里,也就出现了我们目前最流行的编译构建工具 webpack,本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器,在它打包静态资源时候,会根据模块规范,递归地构建一个依赖关系数,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个浏览器可运行的文件。它不仅支持 commonJS 模块, AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS 等。
ES6 的模块化方案
后来,万维网联盟也认识到了前端开发的痛点,实现了一套模块化的方案纳入了 W3C 的标准,也就是我们现在的 ES6(ECMAScript 6) 模块化方案,这种方案是使用 import 关键字导入模块,使用 export 关键字导出模块,当然在实际开发过程中我们通常会使用 babel 来将其转化为 ES5,import 转为了 require,export 转为了 module.exports,即 commonJS。
从react说起
react 的出现可以说是前端发展的一个里程碑,其双向数据绑定和 MVVM 模式将前端从 DOM 操作中解放出来,可以把重点放在数据和业务上,那么,什么是 MVVM 模式呢?
传统模式
回到当初,当我们的 MVVM 没有出现的时候,最早我们是使用原生 JavaScript API 对 DOM 进行操作,手动的把视图与数据模型进行同步,因为原生 API 又臭又长,也需要手动完成浏览器兼容问题,后来,jQuery 出现了,它谈不上是框架,只是一个用来简化 DOM 操作的 JavaScript 库,当然它也提供了很多 DOM 以外的工具函数,刚好满足了那个时候的大部分前端开发的需求。但是,随着时代的发展,业务也越是复杂。。。那么手动同步视图与数据模型将是一个噩梦,大量的 DOM 操作代码使得代码臃肿,难以扩展、维护,这个时候 MVVM 的出现无疑是一套完美的解决方案。
MVVM 模式
说到 MVVM,我们得先从 MVC 说起,众所周知,MVC 是开发客户端最经典的设计模式,但是 MVC 有让人无法忽视的严重问题。
在通常的开发中,除了简单的 Model、View 以外的所有部分都被放在了 Controller 里面。Controller 负责显示界面、响应用户的操作、网络请求以及与 Model 交互。这就导致了 Controller:
- 逻辑复杂,难以维护。
- View 紧耦合,无法测试。
于是微软的大牛提出了 MVVM,竟然 view 与 controller 紧密相连,那干脆把它们连接在一起,放到一个新的对象里,即 ViewModel,它把 view 与 model 完全隔离开来,通过 ViewModel 层交互,达到双向数据绑定的效果。
双向数据绑定
其实,所有 MVVM 模式的框架,如 VueJS、AngularJS、ReactJS,最终都是为了解决一个问题,那就是视图 view 与数据模型 model 的同步,当我们视图中的数据被用户或者其他因素改变时,数据模型会自动得到同步,反过来,数据模型的数据发生变化时,视图也自动得到了更新,这就是双向数据绑定的最终效果。
如何运作
react 其实只是被定义为一个 view 层,通过组件化的形式,只关注与数据呈现,也就是当外部传入的属性发生变化时候,组件会自动进行冲渲染,完成我们数据与视图同步的工作,当然 react 也提供了 state 来充当 ViewModel 这一角色,这是这种方式不被推荐,因为对于大型的系统当中,我们需要对数据进行统一的管理,react 组件内的 state 在组件通信上会显得非常吃力,因此出现了 redux、mobx 这类数据模型状态管理中心库,也就是我们的 ViewModel,下面我们针对 mobx 进行讲解。
我们先定义一个 ViewModel:
import { observable, action } from 'mobx'
class ViewModel {
@observable
name = 'ming.xiao'; // 观测name的变化
@action
setName(name) {
this.name = name;
}
}
export default ViewModel;
上面代码我们可以把 name 当前 model 数据,下面我们在定义我们的 view:
import React from 'react'
import { inject, observer } from 'mobx-react'
@inject('viewModel')
@observer
class View extends React.Component {
onChange(e) {
this.props.viewModel.setName(e.target.value);
}
onClick(e) {
this.props.viewModel.setName('hong.xiao');
}
render() {
var name = this.props.viewModel.name;
return <div>
<input onChange={this.onChange.bind(this)} value={name}/>
<button onClick={this.onClick.bind(this)}>改变名字</button>
</div>
}
}
export default View;
mobx 作为一个统一管理数据的中心,提供了 mobx-react 让我们可以把数据 inject 注入到组件里,在渲染方法render里获取并使用,当我们在输入框输入内容时候,viewModel 执行 action 将输入框的值设置到模型数据中,这样我们就达到了视图同步模型数据的目的,当我们点击按钮的时候,viewModel 执行 action 将名字设置为hong.xiao,这个时候组件发现依赖到模型数据name发生了变化,重新执行render方法,这样,我们就实现了数据同步视图的目的,那么我们现在就完成了前面我们所说的双向数据绑定。
那么,这是如何实现的呢?其实,mobx主要做的一件事情就是收集视图数据的依赖,当组件被 observer 的时候,mobx开启了它的依赖收集,然后让组件使用到 ViewModel 里的被 observable 的属性(setter和getter方法被重写)的时候,把组件存入到 mobx 内部,当这个被 observable 的属性被改变的时候,mobx 会通过 observer 改变 inject 到 react 组件的数据,也就是传入到组件内的属性发生了变化,然后组件自动完成重渲染工作,用设计模式来解释就是,使用了发布/订阅模式,mobx 收集依赖的时候属于订阅过程,observable 数据被改变属于发布通知过程,把所有依赖该 observable 的组件进行了一次通知,让其进行重渲染。
总结
前端技术领域的发展总是趋向于业务,所以,前端基于业务,无法脱离业务,业务飞速发展,前端技术也得快速的更新迭代。。。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。