前言
现在学习到webpack,然后重新回过头来回顾前端模块化一路发展过程定义的规范,遂写下这篇文。
一、模块化的价值
适用:功能繁多的单页面(富应用页面)
多人合作的时候,如果都写成js文件script标签引入,那么就要关心js的顺序,避免被覆盖,以及关心变量,会不会冲突。都是全局变量,很容易冲突。这在代码量大,多人协作的情况下,很灾难。
同时,多人协作的大项目上,js文件很多,彼此之间可能会有依赖关系,比如某个js需要先加载jquery之后才生效,这样的。js文件一多,顺序摆放就很重要,不然依赖关系就会乱掉,代码可能就不会发生作用。这样可能需要做js文件的整理记录以及位置顺序记录,这样很麻烦,对新人也不友好。浪费不必要精力。
富应用的页面越来越多,很多页面逻辑迁移到客户端实现,前端代码越来越多。单薄的javascript,无法支撑代码组织的燃眉之急。
那么前端开发者们就开始想着使用JavaScript来模拟其他后台开发语言(例如:Java)的代码组织方式,例如package(包):将逻辑相关的代码放到同一个包中,每个包之间互不影响,即使包内变量命名相同也不冲突。使用包时,就import加载使用。
在JavaScript中要实现类似的功能,最先想到的是函数。将所有函数抽离出来放置到一个js文件中,然后引用js文件位置,调用函数。但这个方式,还是无法解决命名冲突的问题,函数的命名还是要避免冲突。这种纯函数的方式,需要排除。
那么就来看对象的方式。
对象封装写法一:
<script>
/*封装为一个对象*/
var block_mock={
a:1,
b:2,
fn1:function(){
//dosomething
}
fn2:function(){
//dosomething
}
}
/*调用的写法*/
block_mock.fn1();
block_mock.fn2();
/*存在问题:对象内的变量可以被随意更改*/
block_mock.a=100;
</script>
对于写法一存在的问题,我们来看写法二:
/*改进写法:写成一个立即执行函数,开始具备模块化的感觉*/
var block_mock=(function(){
var a=1;/*局部变量,函数fn1,fn2可以使用,避免全局污染*/
var b=2;
function fn1(){
//dosomething
};
function fn2(){
//dosomething
};
return{
fn1:fn1,
fn2:fn2
};
})()
/*调用的写法*/
block_mock.fn1;
block_mock.fn2;
写法二中,return是最重要一步,return出来,等于只暴露这一块,在模块外部无法修改我们没有暴露的变量和函数。
使用对象封装的写法,我们可以看到JavaScript具备模块化的基础,来达到隔离和组装复杂代码的作用。继而,后续就发展出完善的前端模块化的规范。这些发展接着往下讲。
往下讲之前,我们先结合前面讲的这一堆,总结下模块化的实现,带给我们的预期好处:
- 解决命名冲突问题
- 解决繁琐的文件依赖问题,实现管理
- 代码能分块出来,可读性提高了。想修改代码,也不用从头到尾找,只需要找到对应的模块代码修改就行
- 提高代码复用性:比如曝光组件抽象出来实现ajax+懒加载+无限加载功能。
二、JavaScript的模块规范的发展
1、CommonJS
服务器端的JavaScript——NodeJS在服务端实现第一个模块化的规范:CommonJS。
具体用法是:
1、模块定义:根据CommonJS规范,一个单独的js文件就是一个模块。一个模块是一个单独的作用域,模块内部定义的变量,外部模块无法访问。
2、模块输出:既然外部模块无法访问,就需要我模块本身主动输出,这个模块才是有意义的。按照规范使用module.exports
对象。
3、模块访问:想要加载某个模块时,使用require
方法,该方法读取一个文件并执行,返回文件内部的module.exports
对象。
看下面例子:
注意:下面例子是js文件,在node端运行node+文件名
,执行出结果。
定义模块a,并输出:
var people={
name:'hyh',
sayName:function(){
console.log(this.name);
}
}
/*最重要,输出模块*/
module.exports=people;
定义模块c,并输出:
var rabot={
name:'I am a ranbot',
walking:function(){
console.log('I can walking');
}
}
module.exports=rabot;
注意:测试在a.js里写多一个rabot对象时,前面写的people对象,在执行module.exports=people时会被后面写的对象module.exports=rabot覆盖而无效,所以只能是一个js文件定义一个对象,作为一个模块。
定义模块b,并在模块b中加载模块a和b,使用模块a和b的方法:
var p=require('./a');
var r=require('./c');
console.log(p);
p.sayName();
r.walking();
console.log('hahahahahaha');
在git后台中执行js文件b.js,得到执行结果:
Paste_Image.png注意:见代码可知调用的js——b.js,是可以同时调用多个js来使用。
CommonJS是在NodeJS服务端运行使用的规范。使用CommonJS规范的js文件调用,是一个同步的过程,b.js执行模块内容,是需要在本地读取同个文件夹下的a.js文件和c.js文件,然后进行操作。
CommonJS的这套过程暴露了缺点,不适合在浏览器端执行:
1、实现模式是同步的,需要将所有require都下载下来,然后才执行p.sayName()这样具体的函数语句。如果在浏览器端做同步实现,那用户需要等到全部的js文件都下载完成之后,才可以执行操作。这样的长时间等待,显然让用户抓狂。而且浏览器端的script标签天生是异步的,所有的js文件下载时无法保证按照定义的依赖顺序来下载,那么就未免会产生错误,导致页面无效。
2、在浏览器端,路径很难定义,不再是像服务器一样,从本地读取require('./a'),这样的路径写法。
所以,虽然CommonJS是第一个出现的JavaScript模块化规范,但却是不适用于浏览端使用的。所以还是看往后发展的AMD规范:
2、AMD规范
AMD 即Asynchronous Module Definition,中文名是异步模块定义。ADM规范其实是使用requireJS框架的模块化写法时要求的规范,是在浏览器端实现模块化开发的规范。
具体用法有:
1、定义模块:
define(id?[dependencies]?factory)
,使用定义的define函数来定义模块。
id和dependencies为可填项,如果不设定id,加载这个模块时,就默认使用这个模块的文件命名,否则使用id。dependencies为当前这个模块会使用到的依赖。factory是必填项,为模块的主体内容。可以是函数,也可以是对象。如果是函数,只会被执行一次。如果是对象,则是这个模块的输出值。
2、加载模块:
require([dependencies],function(){})
require()函数接收两个参数:第一个参数是一个数组,表示所依赖的模块。第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
** 重点:require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。**
也就是requireJS实现满足浏览器的异步要求,避免了因为加载js文件而页面停止渲染,加载文件越多,页面失去响应时间越长的问题了。同时使用require.JS,我们开始不用操心因为js文件间的依赖关系而需要关注的文件调用顺序了。因为所有需要的依赖在数组中指定后加载,只有加载成功了,回调函数才会执行。
使用例子:
define('sayname',[],function(){
var name = 'Byron';
function sayName(){
console.log(name);
}
return {
sayName: sayName
};
}());
require(['jquery','sayname'], function($,my){
my.sayName();
});
$和my参数就是jquery执行后,以及sayname执行后返回的return的内容。需要在requireJS框架下执行,因为浏览器不支持require函数。
但AMD这样的规范写法,需要我一次把所有依赖写在数组里,当依赖特别多时,就麻烦凸显。所以我们就再追求实现,在仍然支持依赖异步加载的基础上,对依赖能够按需加载。顺着这个思路,就讲到CMD规范:
3、CMD规范
相对于AMD,最重要的就是实现按需加载js文件依赖
CMD是基于Sea.js的框架的实现的要求写法,现在Sea.js已经废弃不用了,仅作了解。
看例子:
// 定义模块 myModule.js
define(function(require, exports, module) {
var $ = require('jquery.js')
$('div').addClass('active');
var timeout=require('...') /*依赖文件所在路径*/
timeout.init()
});
seajs.use(['myModule.js'], function(my){
//do something
});
看栗子可知,CMD是需要用到什么,才require什么,属于懒执行。AMD对待依赖的态度是预执行。
结语
作为前端模块化,组件化的基础,所以需要回看一路发展的规范。
发展到现在,AMD和CMD已经实现相互支持了。在requireJS中也可以使用CMD规范的写法,实现相同的效果:例如回到顶部功能的实现
现在requireJS也是不怎么用了,发展为主流的webpack也是支持CMD和AMD规范的。
var React = require('react');
var MyComponent = React.createClass({
// do something
});
module.exports = MyComponent;
网友评论