从requirejs看模块化

requirejs优势(使用场景)

  • 防止js加载阻塞页面渲染(async属性)
  • 避免命名冲突以及为避免冲突所作的若干挣扎…,甚至还有一种情况是:不同版本的依赖库,如jquery,可以通过赋以不同的命名加以区分。
  • 更好的依赖处理,解决:文件依赖导致需要小心翼翼的放置各js,以确保它们的加载顺序。
    教程
  • 各模块专注于单一的功能,仅暴露出必要的部分
  • 改善性能问题。按需加载,减少不必要的加载。也可以将很少更新的合并成一个文件,用缓存,经常变动的独立成模块,互不影响。

实战演练

define跟require的区别在于一个是用于定义模块的,一个是使用已定义的模块的。
以下demo是可以运行的,但是放到codepen中,require存在问题,待解决,此处仅供代码参考。

See the Pen requirejsDemo by lu (@luchen) on CodePen.

fis中的mod.js里面的require

实习的过程中最痛心的就是遗留代码的加功能点…之前的fe是用的旧版本的fis中的mod.js,定义了citylist组件,导致页面上要用到这一部分就得使用mod中的require。
然后比较神奇的有这么几点

  • require函数的大体思想就是,定义一个全局对象,然后把引入的js文件作为一个一个的key,value存进去,每次进require函数,就去找这个对象,如果没有,就添加,如果有,就更新,保证全局唯一,某种程度上避免了不同模块是不同的fe写的,然后重复引用js的问题。但是比较坑爹的是,需要手动加script标签引入…如此反人类…当然也提供了require.async来异步引入,这个函数中它是可以createScript的,也就是说,你只要提供url,它会在head中 append script元素。

  • 还有一点注意的是,require做了部分兼容,就是如果传给他的是一个数组对象,那么它会调用require.async。

  • 关于类比require中的path,mod里面也有resourceMap进行处理,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require.resourceMap({
res:{
'cityList':{
url:'/static/js/app/module_cityList.js'
},
'echarts':{
url:'/static/hotmap/js/libs/echarts/echarts.common.js'
},
'moment':{
url:"/static/hotmap/js/libs/moment/moment.js"
},
'BMap':{
url:'http://api.map.baidu.com/api?v=2.0&ak=8WEtlKYLwwssirUarD5O7ba0.js'
},
'heatForm':{
url:"/static/hotmap/js/components/formRelated.js"
},
},
'HeatmapOverlay':{
deps:['BMap'], //**类似于shim**
url:'http://api.map.baidu.com/library/Heatmap/2.0/src/Heatmap_min.js'
}
}
});

eg.

1
var moment = require(['moment']);

并不是我们通常意义上理解的模块、同步、引入。= =

demo如图。

其中,heatform的文件映射见上

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
define('heatForm',function(require,exports, module){
var moment = require(['moment']);
var $ = require(["jquery"]);
var o = {};
var target = $("#target"); //指标
var productLine = $("#productLine");
var particleSize = $("#particleSize");
var hourRange = $("#hourRange");
var searchBtn = $("#searchBtn");
//对应于size的当前时间的format,如粒度为15时,现在7:35,output:7:30-7:45
function getCurStart(size) {
if (size == 0) size = 1;
var intervals = moment().diff(moment({
h: 0,
m: 0
}), 'm');
var result = moment({ h: 0, m: 0 }).add(Math.floor(intervals / size) * size, 'm').format("HH:mm");
return result;
}
//时间段的下拉菜单html
function generateRangeHtml(size) {
var totalMinutesOneDay = 1440,
durationsArr = [],
intervals;
if (size === 0) {
size = 1;
intervals = moment().diff(moment({
h: 0,
m: 0
}), 'm') + 1;
} else
intervals = totalMinutesOneDay / size;
for (var i = 0; i < intervals; i++) {
var durations = i * size;
var h = Math.floor(durations / 60);
var m = durations - h * 60;
var startTime = moment({
hour: h,
minute: m
});
var endTime = moment({
hour: h,
minute: m
}).add(size, 'm');
durationsArr.push({
startTime: startTime.format('HH:mm'),
endTime: endTime.format("HH:mm")
});
}
var curTime = getCurStart(size);
var newOptions = durationsArr.map(function(one) {
if (one.startTime === curTime) {
return `<option selected=${"selected"} value=${one.startTime}>${one.startTime}-${one.endTime}</option>`
} else
return `<option value=${one.startTime}>${one.startTime}-${one.endTime}</option>`;
}).join("");
return newOptions;
}
function bindEvent() {
particleSize.on("change", function(event) {
var size = parseInt($(this).val());
var newRangeOptions = generateRangeHtml(size);
hourRange.html(newRangeOptions);
});
productLine.on("change", function(event) {
var product = $(this).val();
if (product === "sfc") {
target.html("<option value='callNum'>呼叫量</option>");
}else{
target.html(
["<option value='needNum'>需求数</option>",
"<option value='callNum'>呼叫量</option>"].join(""));
}
});
target.on("change",function(event){
var $this = $(this);
var sfcOption = "<option value='sfc'>顺风车</option>"
if(($this).val()==="needNum"){
productLine.children(".sfc").remove();
}else{
if(productLine.children(".sfc")===-1)
productLine.append(sfcOption);
}
});
//待加入searchBtn的click事件
}
o.bindEvent = bindEvent;
// return o;
module.exports=o;
});

1
2
3
4
//调用
require.async('heatForm', function(heatForm) {
heatForm.bindEvent();
})

AMD,CMD,UMD概览

一般 require 用于处理页面首屏所需要的模块,require.async 用于处理首屏外的按需模块。

1
2
3
4
5
6
7
8
9
10
11
{script type="text/javascript"}
// 同步调用 jquery
var $ = require('common:widget/jquery/jquery.js');
$('#btn').click(function() {
// 异步调用 respClick 模块
require.async(['/widget/ui/respClick/respClick.js'], function() {
respClick.hello();
});
});
{/script}

三者代码示例

AMD:

见上

CMD:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//commonjs
// 文件名: foo.js
var $ = require('jquery');
var _ = require('underscore');
// methods
function a(){}; // 私有方法,因为它没在module.exports中 (见下面)
function b(){}; // 公共方法,因为它在module.exports中定义了
function c(){}; // 公共方法,因为它在module.exports中定义了
// 暴露公共方法
module.exports = {
b: b,
c: c
};

此处需要注意exports是module.exports的一个引用,如果直接使用

1
2
3
exports = {
...
}

暴露模块的输出的话,是无效的。因为此时相当于exports的引用对象更改了,两者并不指向同一个对象。module.exports指向的才是。

UMD:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//UMD
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS之类的
module.exports = factory(require('jquery'), require('underscore'));
} else {
// 浏览器全局变量(root 即 window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// 方法
function a(){}; // 私有方法,因为它没被返回 (见下面)
function b(){}; // 公共方法,因为被返回了
function c(){}; // 公共方法,因为被返回了
// 暴露公共方法
return {
b: b,
c: c
}
}));

函数有两个参数,第一个参数是当前运行时环境,第二个参数是模块的定义体。在执行UMD规范时,会优先判断是当前环境是否支持AMD环境,然后再检验是否支持CommonJS环境,否则认为当前环境为浏览器环境( window )。

从模块加载流程看amd,cmd区别

总结来说,cmd是异步加载、延迟且同步执行、依赖就近
amd是异步加载(下载)、异步执行、依赖前置。

(1)示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//AMD
define(["./a", "./b"], function(a, b) {
//BEGIN 1
if (true) {
a.doSomething();
} else {
b.doSomething();
}
//END
});
//CMD
define(function(require) {
// BEGIN 2
if(some_condition) {
require('./a').doSomething();
} else {
require('./b').soSomething();
}
// END
});

在BEGIN1位置处a、b模块都需要被执行一次。CMD中BEGIN 2处a、b都没有被执行,在END处,a、b只有一个被实际执行过。这就是cmd所说的延迟加载。

(2)大致流程

以requirejs为例,其实amd,cmd的流程基本一样,区别在于后面的执行方面。

requirejs:用registry({id:module})来维持一个全局的模块资源表,保证不重复,每次根据id来查找,有则返回,无则去new Module。new Module的时候会触发Module.init函数,依次进行createScript,loading,将模块的依赖加入到依赖数组里,触发自己的completeLoad事件,在该事件中,依次去get依赖的Module,(在getModule时又会回到上面的步骤,有则返回,无则new Module).再接下来会做 checkLoaded,其中每隔50ms去checkoutLoadTimeoutId,因为模块是异步加载的,所以用这个来保证加载结束。 define函数中进行了兼容,包括无id,无依赖的,commonjs写法的(这种情况下同seajs一样,factory.toString()后正则匹配出依赖项)等多种情况。

seajs:
异步下载脚本模块,下载后,浏览器会自动执行 define(fn),define 方法会保存 fn 和提取该模块的依赖模块(利用 fn.toString),按照上面的方法,把依赖模块都下载好
接着根据依赖关系,依次执行各个模块的 fn。

(3)区别/相同点

  • 同:amd,cmd都是通过设置createElement(script),且设置async属性=true,因此它们都是async,异步下载的。
  • 异1(执行时刻):amd是并行加载后就执行,而cmd是as lazy as possible,先并行加载,但是直到require使用时才会执行。因此相对会耗费了时间。
    举个栗子,amd中,define(id,[],factory),[]指示的依赖数组就相当于cmd里面的那个require,那个require就是执行~
  • 异2(执行顺序):amd是异步执行(执行顺序不定),而cmd是同步执行(当然也可以设置require.async去异步执行)

注:
amd,cmd都是基于commonjs的,commonjs服务于服务端,cmd和amd作用于浏览器端,因为cmd中的require是同步执行,需要执行完才能执行下面的代码,对于浏览器端来说,是一个很大的性能问题,因为模块在服务器端,完全拼网速。

“ 因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。” –阮一峰

(4)补充
requirejs性能好点的原因:

  • 解析依赖时直接就可以知道,不需要像seajs一样 function.to String()后正则匹配require后的依赖
  • 异步加载执行,而cmd同步执行耗时
    参考amd,cmd

cmd 中require的加载与执行的关系?require的时候实际上是已经加载好了,去执行 exec()

1
2
3
4
5
6
7
8
seajs.require = function(id) {
var mod = Module.get(Module.resolve(id))
if (mod.status < STATUS.EXECUTING) {
mod.onload()
mod.exec()
}
return mod.exports
}


补充知识

  1. 浏览器通常解析script的时候是同步下载、同步阻塞执行。
    除非手动设置了async,defer等属性
    具体总结来说
  • defer:
    异步下载、最后(document被解析之后)执行,仍然在DOMContentLoaded之前

  • async:
    异步下载后立刻异步执行(执行时可能页面还在解析,不block parse,顺序不定)
    且在window的load事件之前执行

  • 不设置:
    同步下载、同步执行,在页面继续解析之前,因此会阻塞

2,动态创建script标签并插入==设置为async


references

  1. Asynchronous and deferred JavaScript execution explained
  2. script的defer和async
  3. 以代码爱好者角度来看AMD与CMD
  4. seajs 源码解读
  5. Deep dive into the murky waters of script loading 主要涉及到异步加载顺序执行的方法

to think:

  1. es6中的module机制以及webpack的处理