fis项目mod.js浅析

一年多都在用fis,第一次看fis项目里模块化的具体实现方式

在看mod.js之前,我是先了解一下到底啥是AMDCMD的。有助于帮助自己理解。

基本我都注释在代码里,还是先说一下fis项目里的js是怎么用的吧。

首先,一个页面级别的业务js,都是以这样的格式去写的.

1
2
3
4
5
6
var business = {
  init:function(){
    //hello world
  }
};
module.exports = business;

为什么这么写呢,因为fis编译之后会自动给这份js包装一下变成这样。

1
2
3
4
5
6
7
8
define("common:widget/xxx.js",function(require, exports, module){
  var business = {
    init:function(){
      //hello world
    }
  };
  module.exports = business;
});

然后呢,我们在其他地方要用这个js的时候就是这样调用的:

1
2
var xxx = require("common:widget/xxx.js");//约定好了js模块的id
xxx.init();

然后项目就愉快的运行起来了,而实际上靠的就是mod.js的实现模块化

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
 * file: mod.js
 * ver: 1.0.6
 * update: 2014/1/15
 *
 * https://github.com/zjcqoo/mod
 */
 //全局变量
var require, define;
(function(global) {
    var head = document.getElementsByTagName('head')[0],
        //存储异步加载脚本时存储的异步加载完的回调函数
        loadingMap = {},
        //存储模块js,存储的是整一个模块被包裹的函数执行体
        factoryMap = {},
        //缓存已经require过的模块对象,可直接使用
        modulesMap = {},
        //标识已经引入的脚本
        scriptsMap = {},
        //js的资源对象
        resMap = {},
        //js的合并模块的资源对象
        pkgMap = {};
    //创建脚本:如果脚本在scriptsMap存在,则忽略,否则往head插入脚本    
    function createScript(url, onerror) {
        if (url in scriptsMap) return;
        //标识该脚本已在做引入处理
        scriptsMap[url] = true;
        var script = document.createElement('script');
        //创建失败回调
        if (onerror) {
            var tid = setTimeout(onerror, require.timeout);
            script.onerror = function() {
                clearTimeout(tid);
                onerror();
            };
            script.onreadystatechange = function() {
                if (this.readyState == 'complete') {
                    clearTimeout(tid);
                }
            }
        }
        script.type = 'text/javascript';
        script.src = url;
        head.appendChild(script);
        return script;
    }
    function loadScript(id, callback, onerror) {
        var queue = loadingMap[id] || (loadingMap[id] = []);
        queue.push(callback);
        // resource map query
        //这里的初始化有点问题, 另外resMap应该是一个存储map.json数据的变量
        var res = resMap[id] || {};
        var pkg = res.pkg;
        var url;
        //异步加载模块的时候页面会生成一个传入res(map.json数据)的resourcMap函数执行
        if (pkg) {//如果模块有合并的版本,加载合并模块脚本
            url = pkgMap[pkg].url;
        } else {
            //呃,如果res为{},那url就是一个id?
            url = res.url || id;
        }
        createScript(url, onerror && function() {
            onerror(id);
        });
    }
    //项目代码里对js文件的模块封装
    define = function(id, factory) {
        //将模块代码存进factoryMap
        factoryMap[id] = factory;
        //loadScript时加载脚本后,脚本define函数触发,这里将loadScript时的回调!!!
        var queue = loadingMap[id];
        if (queue) {
            for(var i = 0, n = queue.length; i < n; i++) {
                queue[i]();
            }
            delete loadingMap[id];
        }
    };
    //同步require
    //调用模块js的方法:
    require = function(id) {
        //获得对应规则化的id,这里没规则 = =||
        id = require.alias(id);
        //从modulesMap里查找,如果有直接放回模块导出对象
        var mod = modulesMap[id];
        if (mod) {
            return mod.exports;
        }
        //
        // init module
        // 从factoryMap里获取:factoryMap是 define方法对模块js进行了存储
        var factory = factoryMap[id];
        if (!factory) {//连factoryMap都找不到,你没引入对应的js咯
            throw '[ModJS] Cannot find module `' + id + '`';
        }
        //modulesMap也对这个id进行了保存初始化
        mod = modulesMap[id] = {
            exports: {}
        };
        //
        // factory: function OR value
        //
        var ret = (typeof factory == 'function')
        //这就apply这后面两个参数,对mod进行了赋值处理(原来的mod是初始化的空对象)
                ? factory.apply(mod, [require, mod.exports, mod])
                : factory;
        if (ret) {
        //我去,如果factory,即模块js里面最后return了个变量之类的,
        //分分钟把mod.exports给覆盖掉啊
        //所以,我相信,这里不过是ret初始化的时候,
        //因为factory是一个值才做的处理吧,
        //但是从业务项目来看,这里factory本身注定是一个function了
            mod.exports = ret;
        }
        return mod.exports;
    };
    //
    require.async = function(names, onload, onerror) {
        if (typeof names == 'string') {
            names = [names];
        }
        for(var i = 0, n = names.length; i < n; i++) {
            names[i] = require.alias(names[i]);
        }
        //存储正在加载的模块标识
        var needMap = {};
        //存储加载模块数,用于异步加载最后回调触发用
        var needNum = 0;
        //根据参数里的模块名异步加载脚本
        function findNeed(depArr) {
            for(var i = 0, n = depArr.length; i < n; i++) {
                //
                // skip loading or loaded
                //
                var dep = depArr[i];
                var child = resMap[dep];
                if (child && 'deps' in child) {
                    findNeed(child.deps);
                }
                //如果要加载模块已经存在或者正在加载则跳过
                if (dep in factoryMap || dep in needMap) {
                    continue;
                }
                //needMap表明dep对应的模块正在加载
                needMap[dep] = true;
                //加载数自增
                needNum++;
                //加载脚本,如果加载完成了,
                //被加载的脚本应该是define包装的函数执行,将执行updateNeed
                loadScript(dep, updateNeed, onerror);
            }
        }
        function updateNeed() {
            //当加载数为0,即加载全完成,
            if (0 == needNum--) {
                var args = [];
                //将加载模块依次作为参数传入回调中
                for(var i = 0, n = names.length; i < n; i++) {
                    args[i] = require(names[i]);
                }
                onload && onload.apply(global, args);
            }
        }
        findNeed(names);
        updateNeed();
    };
    //页面会针对async方法生成 该方法的script代码
    require.resourceMap = function(obj) {
        var k, col;
        // merge `res` & `pkg` fields
        col = obj.res;
        for(k in col) {
            if (col.hasOwnProperty(k)) {
                resMap[k] = col[k];
            }
        }
        col = obj.pkg;
        for(k in col) {
            if (col.hasOwnProperty(k)) {
                pkgMap[k] = col[k];
            }
        }
    };
    require.loadJs = function(url) {
        createScript(url);
    };
    require.loadCss = function(cfg) {
        if (cfg.content) {
            var sty = document.createElement('style');
            sty.type = 'text/css';
            if (sty.styleSheet) {       // IE
                sty.styleSheet.cssText = cfg.content;
            } else {
                sty.innerHTML = cfg.content;
            }
            head.appendChild(sty);
        }
        else if (cfg.url) {
            var link = document.createElement('link');
            link.href = cfg.url;
            link.rel = 'stylesheet';
            link.type = 'text/css';
            head.appendChild(link);
        }
    };
    //这个应该是为规范化模块命名做处理,不过这里貌似直接返回了
    require.alias = function(id) {return id};
    require.timeout = 5000;
})(this);

整篇代码注释下来,总结一下就是:

核心:

  1. define : 定义模块,将模块执行体缓存起来

  2. require : 同步加载模块,返回模块执行体里的对象,页面渲染时提前将需要require的模板脚本都引入

  3. require.async : 异步加载模块,即需要用到具体某一个模块时,才去引入模块脚本

这里不管是同步还是异步加载,只要都用的是id式引用,

具体讲:

同步,因为模块的define已经执行了 ,所以按id就能获取到模块对象;

异步,需要页面渲染时调用resourceMap方法,参数为脚本的资源依赖表,用于异步加载时找准真正的脚本资源确保正常加载

反观在开发的业务项目中,极少甚至都没在用require.async,感觉用着mod.js也只用了它源码的三分之一的样子。

2017年06月更新

结合图文

要理解异步加载那部分,需要理解require.resourceMap方法与其实际参数具体案例:

var obj = {
    res: {
        "m-common:static/js/common.js": {
            "uri": "/static/m-common/static/js/common_0d0fed1.js",
            "type": "js",
            "deps": ["m-common:widget/pop/pop.js"],
            "pkg": "m-common:p1"
        }
    },
    pkg: {
        "m-common:p1": {
            "uri": "/static/m-common/static/pkg/common-aio0_e736582.js",
            "type": "js",
            "has": [
                 "m-common:static/mod.js", 
                 "m-common:widget/pop/pop.js",
                 "m-common:static/js/common.js"
                 ],
            "deps": ["m-common:widget/pop/pop.css"]
        }
    }
}

其中res字段的作用是,用于异步加载资源时,搜索出对应的依赖模块deps以及打包模块pkg,前者需要递归调用findNeed函数加载依赖模块(是否缓存或者异步加载脚本),后者则真正加载脚本时得到脚本的实际地址需要(资源打包合并的情况)。

结合代码注释,于是画了一下关系图

image 查看原图

异步部分逻辑看起来有点绕,结合图来看,会清晰一些。

image 查看原图