一.前端发展历史
在了解前端代码复杂带来的问题之前,我们先了解前端的发展历史。
1、静态页面阶段
互联网发展的早期,网站的前后端开发是一体的,即前端代码是后端代码的一部分。
1.后端收到浏览器的请求
2.生成静态页面
3.发送到浏览器
那时的前端页面都是静态的,所有前端代码和前端数据都是后端生成的。前端只是纯粹的展示功能,脚本的作用只是增加一些特殊效果,比如那时很流行用脚本控制页面上飞来飞去的广告。
那时的网站开发,采用的是后端 MVC 模式。
Model(模型层):提供/保存数据
Controller(控制层):数据处理,实现业务逻辑
View(视图层):展示数据,提供用户界面
前端只是后端 MVC 的 V。
2、AJAX 阶段
2004年,AJAX 技术诞生,改变了前端开发。Gmail 和 Google 地图这样革命性的产品出现,使得开发者发现,前端的作用不仅仅是展示页面,还可以管理数据并与用户互动。
AJAX 技术指的是脚本独立向服务器请求数据,拿到数据以后,进行处理并更新网页。整个过程中,后端只是负责提供数据,其他事情都由前端处理。前端不再是后端的模板,而是实现了从“获取数据 --》 处理数据 --》展示数据”的完整业务逻辑。
就是从这个阶段开始,前端脚本开始变得复杂,不再仅仅是一些玩具性的功能。
3、前端 MVC 阶段
前端代码有了读写数据、处理数据、生成视图等功能,因此迫切需要辅助工具,方便开发者组织代码。这导致了前端 MVC 框架的诞生。
2010年,第一个前端 MVC 框架 Backbone.js 诞生。它基本上是把 MVC 模式搬到了前端,但是只有 M (读写数据)和 V(展示数据),没有 C(处理数据)。因为,Backbone 认为前端 Controller 与后端不同,不需要、也不应该处理业务逻辑,只需要处理 UI 逻辑,响应用户的一举一动。所以,数据处理都放在后端,前端只用事件响应处理 UI 逻辑(用户操作)。
后来,更多的前端 MVC 框架出现。另一些框架提出 MVVM 模式,用 View Model 代替 Controller。MVVM 模式也将前端应用分成三个部分。
Model:读写数据
View:展示数据
View-Model:数据处理
View Model 是简化的 Controller,所有的数据逻辑都放在这个部分。它的唯一作用就是为 View 提供处理好的数据,不含其他逻辑。也就是说,Model 拿到数据以后,View Model 将数据处理成视图层(View)需要的格式,在视图层展示出来。
这个模型的特点是 View 绑定 View Model。如果 View Model 的数据变了,View(视图层)也跟着变了;反之亦然,如果用户在视图层修改了数据,也立刻反映在 View Model。整个过程完全不需要手工处理。
4、SPA 阶段
前端可以做到读写数据、切换视图、用户交互,这意味着,网页其实是一个应用程序,而不是信息的纯展示。这种单张网页的应用程序称为 SPA(single-page application)。
所谓 SPA,就是指在一张网页(single page)上,通过良好的体验,模拟出多页面应用程序(application)。用户的浏览器只需要将网页载入一次,然后所有操作都可以在这张页面上完成,带有迅速的响应和虚拟的页面切换。
随着 SPA 的兴起,2010年后,前端工程师从开发页面(切模板),逐渐变成了开发“前端应用”(跑在浏览器里面的应用程序)。
目前,最流行的前端框架 Vue、Angular、React 等等,都属于 SPA 开发框架。
二.前端代码复杂带来的问题
从发展历史可以看到,前端慢慢从一个简单的HTML页面发展到CSS.、JS的加入,到后来框架的加入,前端已经不仅仅局限于页面的展示,一些简单甚至复杂的功能也逐渐在前端得到显示,这就不得不使前端的代码越来越多越来越多,这就给前端带来了几乎灾难性的问题。
1.全局变量命名冲突问题
下面我举一个例子来说明全局变量重名所带来的的问题
假设在一个项目开发过程中,有多个人同时开发,这里就假设有
1.项目组长:main.js 用于定义全局规范,会把整个框架搭建,然后再分工
2.小明:aaa.js
3.小红:bbb.js
这三人开发一个项目:
①主界面导入三人的js文件
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<!--在项目开发中,一个项目可能需要多人开发
为了解释全局变量带来的问题,这里模拟三人开发
1.项目组长:main.js 用于定义全局规范,会把整个框架搭建,然后再分工
2.小明:aaa.js
3.小红:bbb.js
-->
<script src="main.js" type="text/javascript" charset="utf-8"></script>
<script src="aaa.js" type="text/javascript" charset="utf-8"></script>
<script src="bbb.js" type="text/javascript" charset="utf-8"></script>
<script src="mmm.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>
②小明做的JS
//小明
var name = '小明'
var age= 22
/*计算两个值的大小*/
function sum(num1, num2) {
return num1+ num2
}
var flag = true
if (flag) {
console.log(sum(10, 20))
}
现在运行html,打开控制台,能看到正确的运算结果,如图所示:
③小红开始做JS
var name = '小红'
//小红也存在与小明同名的变量flag
var flag = false
console.log(name)
现在运行html,打开控制台,仍能看到正确的运算结果,如图所示:
可以看到控制台输出了小红的名字并且小明的也没有出错。
④小明继续开发,创建了另一个js文件
因为他知道之前创建过flag变量,所以他打算直接拿来用。
//如果flag是正确的,就会执行if
if(flag) {
console.log('小红把我的改了!!')
}
然后小明打开网页控制台,发现这一个JS没有起到作用,没有显示到应该显示的那句话
那么问题到底出在哪里呢,明明自己之前写的flag是true啊,怎么会不显示,小明第一时间会查看自己的代码发现自己的代码正确,然后查看小红的代码,发现是小红也有一个跟自己一样的变量。当然这里的示例只有几行左右,但是在真正的项目开发过程中,一个js文件可能存在成千上万行代码,要一个一个查看其工作量可想而知,可能要加班到凌晨三四点还弄不完。这就是全局变量重名问题。
2.js依赖顺序强制性
接上面的例子,小明发现问题之后,便将自己刚做的mmm.js移到小红的bbb.js上方。刷新后发现小明做的mmm.js能够显示了。如图所示:
从上可以看到,一旦JS引入的顺序改变,页面也有可能发生改变,甚至可能带来灾难性的问题。
解决方案
一.匿名函数
我们可以使用匿名函数来解决重名问题,采用闭包来减少全局变量,这样都是在自己的定义域里面定义自己的变量。下面我仍然用上面的例子说明。
1.修改小明的js,将js闭包
(function() {
//小明
var name = '小明'
var age= 22
/*计算两个值的大小*/
function sum(num1, num2) {
return num1+ num2
}
var flag = true
if (flag) {
console.log(sum(10, 20))
}
})()
2.修改小红的js,将js闭包
;(function() {
var name = '小红'
//小红也存在与小明同名的变量flag
var flag = false
console.log(name)
})()
这样小红和小明之间的变量就毫不干扰。
但是这也会带来一些问题,下面当我们刷新网页的时候
会发现mmm.js里面的内容又消失了,因为当我们使用了匿名函数,匿名函数里面的变量就只能在函数里面使用,相当于变成来局部变量,所以对于mmm.js中的flag用不到之前定义的。没办法,为了使用这个flag,mmm.js又必须再定义一次,这样就使代码复用性降低,代码冗余了。
2.为了解决这一问题,我们必须使用模块化(ES5),使用模块作为出口。这里就是本文章的重点了。
小明还是想用aaa.js里面的flag,就必须设置要导出的对象。将自己的js定位模块A
var moduleA= (function() {
//小明
//导出的对象
var obj = {}
var name = '小明'
var age= 22
/*计算两个值的大小*/
function sum(num1, num2) {
return num1+ num2
}
var flag = true
if (flag) {
console.log(sum(10, 20))
}
//obj绑定flag和sum
obj.flag= flag;
obj.sum = sum;
//将obj发送出去
return obj
})()
此时,mmm.js就可以拿到返回的obj
(function() {
//想使用flag
if(moduleA.flag) {
console.log('小红把我的改了!!')
}
//拿到sum
console.log(moduleA.sum(40, 50));
})()
刷新页面可以发现mmm.js可以正常显示
当然,小红也可以定义自己的模块,她将自己的js定义为模块B
;
var moduleB = (function() {
var obj= {}
var name = '小红'
//小红也存在与小明同名的变量flag
var flag = false
console.log(name)
obj.flag= flag;
return obj
})()
以后小红在添加js的时候,就可以使用自己模块里面的东西,从而不会出现全局变量重名的情况了
比如小红创建了nnn.js文件
(function() {
console.log(moduleB.flag);
})()
在这里面flag只会使用指定模块里面的变量。这就是模块化的雏形。
3.模块化开发
常见的模块化规范
- CommonJS
- AMD
- CMD
- ES6中的Modules
示例:CommonJS导出,利用exports进行导出
//CommonJS导入和导出
//小明
var name = '小明'
var age= 22
/*计算两个值的大小*/
function sum(num1, num2) {
return num1+ num2
}
var flag = true
if (flag) {
console.log(sum(10, 20))
}
//导出
module.exports= {
flag: flag,
sum: sum
}
CommonJS导入:
var aaa = require('./aaa.js')
var flag = aaa.flag;
var sum = aaa.sum;
sum(20, 30)