1. 模块的定义和加载

1.1 模块的定义

一个框架想要能支撑较大的应用,首先要考虑怎么做模块化。有了内核和模块加载系统,外围的模块就可以一个一个增加。不同的JavaScript框架,实现模块化方式各有不同,我们来选择一种比较优雅的方式作个讲解。

先问个问题:我们做模块系统的目的是什么?如果觉得这个问题难以回答,可以从反面来考虑:假如不做模块系统,有什么样的坏处?

我们经历过比较粗放、混乱的前端开发阶段,页面里充满了全局变量,全局函数。那时候要复用js文件,就是把某些js函数放到一个文件里,然后让多个页面都来引用。

考虑到一个页面可以引用多个这样的js,这些js互相又不知道别人里面写了什么,很容易造成命名的冲突,而产生这种冲突的时候,又没有哪里能够提示出来。所以我们要有一种办法,把作用域比较好地隔开。

JavaScript这种语言比较奇怪,奇怪在哪里呢,它的现有版本里没package跟class,要是有,我们也没必要来考虑什么自己做模块化了。那它是要用什么东西来隔绝作用域呢?

在很多传统高级语言里,变量作用域的边界是大括号,在{}里面定义的变量,作用域不会传到外面去,但我们的JavaScript大人不是这样的,他的边界是function。所以我们这段代码,i仍然能打出值:

JavaScript
1
2
3
4

for (var i=0; i<5; i++) {
    //do something
}
alert(i);

那么,我们只能选用function做变量的容器,把每个模块封装到一个function里。现在问题又来了,这个function本身的作用域是全局的,怎么办?我们想不到办法,拔剑四顾心茫然。

我们有没有什么可参照的东西呢?这时候,脑海中一群语言飘过: C语言飘过:“我不是面向对象语言哦~不需要像你这么组织哦~”,“死开!” Java飘过:“我是纯面向对象语言哦,连main都要在类中哦,编译的时候通过装箱清单指定入口哦~”,“死开!” C++飘过:“我也是纯面向对象语言哦”,等等,C++是纯面向对象的语言吗?你的main是什么???main是特例,不在任何类中!

啊,我们发现了什么,既然无法避免全局的作用域,那与其让100个function都全局,不如只让一个来全局,其他的都由它管理。

本来我们打算自己当上帝的,现在只好改行先当个工商局长。你想开店吗?先来注册,不然封杀你!于是良民们纷纷来注册。店名叫什么,从哪进货,卖什么的,一一登记在案,为了方便下面的讨论,我们连进货的过程都让工商局管理起来。

店名,指的就是这里的模块名,从哪里进货,代表它依赖什么其他模块,卖什么,表示它对外提供一些什么特性。

好了,考虑到我们的这个注册管理机构是个全局作用域,我们还得把它挂在window上作为属性,然后再用一个function隔离出来,要不然,别人也定义一个同名的,就把我们覆盖掉了。

JavaScript
1
2
3
4
5
6
7

(function(){
    window.thin={
        define:function(name,dependencies,factory){
            //register a module
        }
    };
})();

在这个module方法内部,应当怎么去实现呢?我们的module应当有一个地方存储,但存储是要在工商局内部的,不是随便什么人都可以看到的,所以,这个存储结构也放在工商局同样的作用域里。

用什么结构去存储呢?工商局备案的时候,店名不能跟已有的重复,所以我们发现这是用map的很好场景,考虑到JavaScript语言层面没有map,我们弄个Object来存。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

(function() {
    var moduleMap = {};
    window.thin = {
        define: function(name, dependencies, factory) {
            if (!moduleMap[name]) {
                var module = {
                    name: name,
                    dependencies: dependencies,
                    factory: factory
                };
                moduleMap[name] = module;
            }
            return moduleMap[name];
        }
    };
})();

现在,模块的存储结构就搞好了。

1.2 模块的使用

存的部分搞好了,我们来看看怎么取。现在来了一个商家,卖木器的,他需要从一个卖钉子的那边进货,卖钉子的已经来注册过了,现在要让这个木器厂能买到钉子。现在的问题是,两个商家处于不同的作用域,也就是说,它们互相不可见,那通过什么方式,我们才能让他们产生调用关系呢?

个人解决不了的问题还是得靠政府,有困难要坚决克服,没有困难就制造困难来克服。现在困难有了,该克服了。商家说,我能不能给你我的进货名单,你帮我查一下它们在哪家店,然后告诉我?这么简单的要求当然一口答应下来,但是采用什么方式传递给你呢?这可犯难了。

我们参考AngularJS框架,写了一个类似的代码:

JavaScript
1
2
3
4
5
6
7
8

thin.define("A",[],function(){
    //module A
});
thin.define("B",["A"],function(A){
    //module B
    vara=newA();
});

看这段代码特别在哪里呢?模块A的定义,毫无特别之处,主要看模块B。它在依赖关系里写了一个字符串的A,然后在工厂方法的形参写了一个真真切切的A类型。嗯?这个有些奇怪啊,你的A类型要怎么传递过来呢?其实是很简单的,因为我们声明了依赖项的数组,所以可以从依赖项,挨个得到对应的工厂方法,然后创建实例,传进来。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

use: function(name) {
    var module = moduleMap[name];
    if (!module.entity) {
        var args = [];
        for (var i=0; i<module.dependencies.length; i++) {
            if (moduleMap[module.dependencies[i]].entity) {
                args.push(moduleMap[module.dependencies[i]].entity);
            }
            else {
                args.push(this.use(module.dependencies[i]));
            }
        }
        module.entity = module.factory.apply(noop, args);
    }
    return module.entity;
}

我们可以看到,这里面递归获取了依赖项,然后当作参数,用这个模块的工厂方法来实例化了一下。这里我们多做了一个判断,如果模块工厂已经执行过,就缓存在entity属性上,不需要每次都创建。以此类推,假如一个模块有多个依赖项,也可以用类似的方式写,毫无压力:

JavaScript
1
2
3
4
5
6

thin.define("D",["A","B","C"],function(A,B,C){
    //module D
    vara=newA();
    varb=newB();
    varc=newC();
});

注意了,D模块的工厂,实参的名称未必就要是跟依赖项一致,比如,以后我们代码较多,可以给依赖项和模块名称加命名空间,可能变成这样:

JavaScript
1
2
3
4
5
6

thin.define("foo.D", ["foo.A", "foo.B", "foo.C"], function(A, B, C) {
    //module D
    var a = new A();
    var b = new B();
    var c = new C();
});

这段代码仍然可以正常运行。我们来做另外一个测试,改变形参的顺序:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

thin.define("A",[],function(){
    return"a";
});
thin.define("B",[],function(){
    return"b";
});
thin.define("C",[],function(){
    return"c";
});
thin.define("D",["A","B","C"],function(B,A,C){
    returnB+A+C;
});
varD=thin.use("D");
alert(D);

试试看,我们的D打出什么结果呢?结果是”abc”,所以说,模块工厂的实参只跟依赖项的定义有关,跟形参的顺序无关。我们看到,在AngularJS里面,并非如此,实参的顺序是跟形参一致的,这是怎么做到的呢?

我们先离开代码,思考这么一个问题:如何得知函数的形参名数组?对,我们是可以用func.length得到形参个数,但无法得到每个形参的变量名,那怎么办呢?

AngularJS使用了一种比较极端的办法,分析了函数的字面量。众所周知,在JavaScript中,任何对象都隐含了toString方法,对于一个函数来说,它的toString就是自己的实现代码,包含函数签名和注释。下面我贴一下AngularJS里面的这部分代码:

JavaScript
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

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)$$/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function annotate(fn) {
  var $inject,
      fnText,
      argDecl,
      last;
  if (typeof fn == 'function') {
    if (!($inject = fn.$inject)) {
      $inject = [];
      fnText = fn.toString().replace(STRIP_COMMENTS, '');
      argDecl = fnText.match(FN_ARGS);
      forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
        arg.replace(FN_ARG, function(all, underscore, name){
          $inject.push(name);
        });
      });
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) {
    last = fn.length - 1;
    assertArgFn(fn[last], 'fn');
    $inject = fn.slice(0, last);
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject;
}

可以看到,这个代码也不长,重点是类型为function的那段,首先去除了注释,然后获取了形参列表字符串,这段正则能获取到两个结果,第一个是全函数的实现,第二个才是真正的形参列表,取第二个出来split,就得到了形参的字符串列表了,然后按照这个顺序再去加载依赖模块,就可以让形参列表不对应于依赖项数组了。

AngularJS的这段代码很强大,但是要损耗一些性能,考虑到我们的框架首要原则是简单,甚至可以为此牺牲一些灵活性,我们不做这么复杂的事情了。

1.3 模块的加载

到目前为止,我们可以把多个模块都定义在一个文件中,然后手动引入这个js文件,但是如果一个页面要引用很多个模块,引入工作就变得比较麻烦,比如说,单页应用程序(SPA)一般比较复杂,往往包含数以万计行数的js代码,这些代码至少分布在几十个甚至成百上千的模块中,如果我们也在主界面就加载它们,载入时间会非常难以接受。但我们可以这样看:主界面加载的时候,并不是用到了所有这些功能,能否先加载那些必须的,而把剩下的放在需要用的时候再去加载?

所以我们可以考虑万能的AJAX,从服务端获取一个js的内容,然后……,怎么办,你当然说不能eval了,因为据说eval很evil啦,但是它evil在哪里呢?主要是破坏全局作用域啦,怎么怎么,但是如果这些文件里面都是按照我们规定的模块格式写,好像也没有什么在全局作用域的……,好吧。

算了,我们还是用最简单的方式了,就是动态创建script标签,然后设置src,添加到document.head里,然后监听它们的完成事件,做后续操作。真的很简单,因为我们的框架不需要考虑那么多种情况,不需要AMD,不需要require那么麻烦,用这框架的人必须按照这里的原则写。

所以,说真的我们这里没那么复杂啦,要是你们想看更详细原理的不如去看这个,解释得比我好哎:http://coolshell.cn/articles/9749.html#jtss-tsina

[补一段,@Franky 大神指出了这篇文章中一些不符合现状的地方,我把它也贴在这里,供读者参考]

很多观点都是 史蒂夫那本老书上的观点. 和那时候同期产生的一些数据和资料…所以显得不少东西说的太想当然了… 譬如script标签的加载和执行会阻塞后面资源的加载和执行之类的.说的过于肯定了. 比如chrome7+就开始逐渐改进的 预加载机制 就分 head 里的资源, body里的资源 .两个资源是否跨界三种情形. 不提这些浏览器. 我们看看ie10也同样改进了 死循环10秒 这后面的图片能被提前加载. 就更不用说其他A级浏览器的丰富的优化策略了. 所以还是建议博主, 别拿几年前的老资料作为依据.尤其这些数据是用来说明更新速度像在赛跑一样的各个浏览器了.

关于 defer , 似乎史蒂夫的老书上是这么说的么? 显然没有测试全非ie浏览器的各个版本.或者是他测试数据的时候ff某大版本的几个beta子版本还没出现?

其次是就你的加载器提到的预加载策略. 你有测过所有浏览器用object预加载可能涉及到的问题么(比如chrome,8,9的预加载的会话级别的资源类型缓存bug). 抛开这个问题不谈,假设你预加载到一半,用户再次触发了加载.你觉得这种情况如果频繁发生.是否合适? 你的预加载策略连script.onload状态都无法测知,进一步优化的可能性就消失了. 考虑下为什么seajs 的 umd要设计成那个样子?

最后吐槽下你的代码. 有注意到你用 document.body.appendChild 来像dom 中插入脚本. 我的建议是 永远不要这样做.除非你可以无视ie6用户.以及ie7缺失某些补丁的子版本.

你可以选择body 可以.但请用insertBefore. 但在某些极端情况下.这仍然会发生问题. 最佳实践是 head.insertBefore 向其第一个子节点插入.(你甚至无需检测是否存在子节点. 这个api会在没有子节点的时候,行为同appendChild). 而更加稳妥的情况是. 如果注入script. 发现document.head还没有被构建时. 可以自己造一个. 这才是一个通用加载器要做到的程度…

我也偷懒了,只是贴一下代码,顺便解释一下,界面把所依赖的js文件路径放在数组里,然后挨个创建script标签,src设置为路径,添加到head中,监听它们的完成事件。在这个完成时间里,我们要做这么一些事情:在fileMap里记录当前js文件的路径,防止以后重复加载,检查列表中所有文件,看看是否全部加载完了,如果全加载好了,就执行回调。

JavaScript
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

require:function(pathArr,callback){
    for(vari=0;i<pathArr.length;i++){
        varpath=pathArr[i];
        if(!fileMap[path]){
            varhead=document.getElementsByTagName('head')[0];
            varnode=document.createElement('script');
            node.type='text/javascript';
            node.async='true';
            node.src=path+'.js';
            node.onload=function(){
                fileMap[path]=true;
                head.removeChild(node);
                checkAllFiles();
            };
            head.appendChild(node);
        }
    }
    functioncheckAllFiles(){
        varallLoaded=true;
        for(vari=0;i<pathArr.length;i++){
            if(!fileMap[pathArr[i]]){
                allLoaded=false;
                break;
            }
        }
        if(allLoaded){
            callback();
        }
    }
}

1.4 小结

到此为止,我们的简易框架的模块定义系统就完成了。完整的代码如下:

JavaScript
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

(function () {
    var moduleMap = {};
    var fileMap = {};
    var noop = function () {
    };
    var thin = {
        define: function(name, dependencies, factory) {
            if (!moduleMap[name]) {
                var module = {
                    name: name,
                    dependencies: dependencies,
                    factory: factory
                };
                moduleMap[name] = module;
            }
            return moduleMap[name];
        },
        use: function(name) {
            var module = moduleMap[name];
            if (!module.entity) {
                var args = [];
                for (var i=0; i<module.dependencies.length; i++) {
                    if (moduleMap[module.dependencies[i]].entity) {
                        args.push(moduleMap[module.dependencies[i]].entity);
                    }
                    else {
                        args.push(this.use(module.dependencies[i]));
                    }
                }
                module.entity = module.factory.apply(noop, args);
            }
            return module.entity;
        },
        require: function (pathArr, callback) {
            for (var i = 0; i < pathArr.length; i++) {
                var path = pathArr[i];
                if (!fileMap[path]) {
                    var head = document.getElementsByTagName('head')[0];
                    var node = document.createElement('script');
                    node.type = 'text/javascript';
                    node.async = 'true';
                    node.src = path + '.js';
                    node.onload = function () {
                        fileMap[path] = true;
                        head.removeChild(node);
                        checkAllFiles();
                    };
                    head.appendChild(node);
                }
            }
            function checkAllFiles() {
                var allLoaded = true;
                for (var i = 0; i < pathArr.length; i++) {
                    if (!fileMap[pathArr[i]]) {
                        allLoaded = false;
                        break;
                    }
                }
                if (allLoaded) {
                    callback();
                }
            }
        }
    };
    window.thin = thin;
})();

测试代码如下:

JavaScript
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

thin.define("constant.PI",[],function(){
    return3.14159;
});
thin.define("shape.Circle",["constant.PI"],function(pi){
    varCircle=function(r){
        this.r=r;
    };
    Circle.prototype={
        area:function(){
            returnpi*this.r*this.r;
        }
    }
    returnCircle;
});
thin.define("shape.Rectangle",[],function(){
    varRectangle=function(l,w){
        this.l=l;
        this.w=w;
    };
    Rectangle.prototype={
        area:function(){
            returnthis.l*this.w;
        }
    };
    returnRectangle;
});
thin.define("ShapeTypes",["shape.Circle","shape.Rectangle"],function(Circle,Rectangle){
    return{
        CIRCLE:Circle,
        RECTANGLE:Rectangle
    };
});
thin.define("ShapeFactory",["ShapeTypes"],function(ShapeTypes){
    return{
        getShape:function(type){
            varshape;
            switch(type){
                case"CIRCLE":{
                    shape=newShapeTypes[type](arguments[1]);
                    break;
                }
                case"RECTANGLE":  {
                    shape=newShapeTypes[type](arguments[1],arguments[2]);
                    break;
                }
            }
            returnshape;
        }
    };
});
varShapeFactory=thin.use("ShapeFactory");
alert(ShapeFactory.getShape("CIRCLE",5).area());
alert(ShapeFactory.getShape("RECTANGLE",3,4).area());

在这个例子里定义了四个模块,每个模块只需要定义自己所直接依赖的模块,其他的可以不必定义。也可以来这里看测试链接:http://xufei.github.io/thin/demo/demo.0.1.html

从零开始写JavaScript框架(一)相关推荐

  1. 从零开始写JavaScript框架

    有一定Web前端开发经验的人,很多都会有这么个想法:那些写框架的人好厉害,什么时候我才能写一个自己的框架呢?有时候看看别人的框架代码,又觉得很复杂,不知道从何看起,只有很少的人突破了这个界限,领悟到了 ...

  2. 从零开始写一个框架的详细步骤

    定位 所谓定位就是回答几个问题,我出于什么目的要写一个框架,我的这个框架是干什么的,有什么特性适用于什么场景,我的这个框架的用户对象是谁,他们会怎么使用,框架由谁维护将来怎么发展等等. 如果你打算写框 ...

  3. 源码分析系列 | 从零开始写MVC框架

    1. 前言 2. 为什么要自己手写框架 3. 简单MVC框架设计思路 4. 课程目标 5. 编码实战 5.1 配置阶段 web.xml配置 config.properties 自定义注解 5.2 初始 ...

  4. web中间件_HTTP中间件机制实现与原理 - 从零开始写GO-API框架

    大家好,很高兴您能阅读这篇文章. 最近在投稿公众号时发现从未做过自我介绍,首先请允许我介绍一下自己. 我叫张晓亮,就职于新浪微博,Golang的忠实粉丝,平时的爱好看看书.撸撸码,典型的程序员性格,最 ...

  5. 从零开始写javaweb框架 pdf_大学写的一个 Java Web 框架

    简介 today-web是一个基于Servlet的高性能轻量级Web框架. 安装 <dependency><groupId>cn.taketoday</groupId&g ...

  6. 从零开始写javaweb框架笔记2-搭建web项目框架

    在前面我们已经创建了一个maven项目,但是pox.xml中还没有任何的maven依赖,随后会添加一些java web所需的依赖.在添加java web的依赖之前,我们需要把maven项目转换为jav ...

  7. 如何搭建python框架_从零开始:写一个简单的Python框架

    原标题:从零开始:写一个简单的Python框架 Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 你为什么想搭建一个Web框架?我想有下面几个原因: 有一个 ...

  8. 写一个 JavaScript 框架:比 setTimeout 更棒的定时执行

    这个系列是关于一个开源的客户端框架,叫做 NX.在这个系列里,我主要解释一下写该框架不得不克服的主要困难.如果你对 NX 感兴趣可以参观我们的 主页. 这个系列包含以下几个章节: 项目结构 定时执行 ...

  9. 从零开始写一个RPC框架的详细步骤

    http://blog.csdn.net/liu88010988/article/details/51547592 定位 所谓定位就是回答几个问题,我出于什么目的要写一个框架,我的这个框架是干什么的, ...

最新文章

  1. mysql 二进制日志后缀数字最大为多少
  2. 【福利】IT学习视频免费送:思科/华为、Liunx、ORACLE、VMware等等
  3. 新版数采仪问题解决全记录-升级失败问题
  4. 水晶易表调用C#的WebService,返回数据集合
  5. git web框架搭建_Git,Python Web框架,AI,机器学习,Android,Linux和更多必读内容
  6. 基于JAVA+SpringMVC+MYSQL的企业员工管理系统
  7. 2018年计算机二级知识点,2018年计算机二级考试公共基础知识点:栈及其基本运算...
  8. Robolectric 探索之路
  9. webpack 样式表抽离成专门的单独文件并且设置版本号
  10. 如何用HTML语言设计进度条,html进度条代码_html5如何实现简单进度条效果
  11. EPLAN教程——导出CAD如何快捷配置
  12. MFC 树形控件的使用
  13. Kotlin 只读变量
  14. python 典型相关分析_Canonical Correlation Analysis 典型相关分析
  15. 使用Qt实现一个必应壁纸客户端
  16. CTex listings宏包出错undefined control sequence,换成verbatim解决问题
  17. 【C语言】计算日期差
  18. MySQL--常见业务/笔试题
  19. Unity中实现四舍五入
  20. [DDR]2 - Initialization, Training and Calibration

热门文章

  1. netcat网工必备工具
  2. 当AI对话系统像自动驾驶一样分级,谁能率先跑出L5?
  3. 正态分布离群值检验——偏度与峰度方法
  4. linux 生成内核patch,谢宝友: 手把手教你给Linux内核发patch
  5. PDF可以通过OCR图文识别软件转换为JPEG图像吗
  6. 【组成原理-处理器】微程序控制器
  7. C语言实验——用*号输出字母C的图案
  8. 【Nodejs】Http模块01
  9. linux下删除oracle数据库实例
  10. pdf工具类之添加页码