模块加载器

模块加载器

最近在做新项目的时候自己利用一点业余时间写了一个简单的js模块加载器。后来因为用了webpack就没有考虑把它放到项目里面去,也没有继续更新它了。模块加载器开源的有很多,一般来说seaJS和reqiureJS都能满足基本需求。本篇博文主要分享一下卤煮写这个加载器的一些想法和思路,作为学习的记录。

js模块化加载已经不是一个新鲜概念了,很多人都一再强调,大型项目要使用模块化开发,因为一旦随着项目的增大,管理和组织代码的难度会越来越难,使得我们对代码的管理变得重要起来。当然,在后端模块化已经相当成熟,而作为前端的模块化概念,是很久之后才提出来的。模块化好处是使得代码结构更加清晰,高的内聚,功能独立,复用等等。在服务端,随着nodejs 的兴起,js模块化被越来越多地引起人们的注意。但是对于后端和前端来说,最大的区别就是同步和异步加载的问题,因为服务器上获取模块是不需要花费很多的,模块加载进来的时间就操作系统文件的时间,这个过程可以看成是同步的。而在浏览器的前端却需要发送请求到服务器来获取文件,这导致了一个异步延迟的问题,针对这个问题,以AMD规范的异步模块加载器requireJS应运而生。

加载原理

以上简单介绍了一下前端模块化的历程,下面主要介绍一下模块加载主要原理:

1. createElement(‘script‘)和appendChild(script) 动态创建脚本,添加到head元素中。

2. fn.toString().match(/\.require\((\"|\‘)[^\)]*(\"|\‘)\)/g) 将模块转换为字符串,然后通过正则表达式,匹配每个模块中的的依赖文件。

3. 建立脚本加载队列。

4.递归加载,分析完依赖之后,我们需要按照依赖出现的位置,将它们加载到客户端。

5.为每一个命名的模块建立缓存,即 module[name] = callback;

6.currentScript : 对于匿名模块,通过currentScript 来获取文件名,存入到缓存中。

下面贴出对应主要的代码:

一、动态创建脚本

创建脚本较为简单,主要是用createElement方法和appendChild。在创建脚本函数中,我们需要为该脚本绑定一个onload事件,这个事件是为了通知加载脚本队列执行的时间,告诉它什么时候可以加载下一个js文件了。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

function _createScript(url) {

    //创建script

    var script = doc.createElement(‘script‘);

    var me = this;

    //设置属性为异步加载

    script.async = true;

    script.src = url + ‘.js‘;

    //为脚本添加加载完成事件

    if (‘onload‘ in script) {

        script.onload = function(event) {

            return _scriptLoaded.call(me, script);

        };

    else {

        script.onreadystatechange = function() {

            if (/loaded|complete/.test(node.readyState)) {

                me.next();

                _scriptLoaded(script);

            }

        };

    }

    //加入script

    head.appendChild(script);

}

二、分析依赖建立

分析依赖是模块加载器中最重要的环节之一。每个模块可能会依赖不同的模块,我们需要理清楚这些模块之间的依赖关系,然后分别将它们加载进来。为了分析依赖关系,我们使用toString的方法,将模块转化为一个string,然后去其中寻找依赖。


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

function _analyseDepend(func) {

    //匹配依赖,所有在.reqiure()括号内的依赖都会被匹配出来。

    var firstReg = /\.require\((\"|\‘)[^\)]*(\"|\‘)\)/g,

        secondReg = /\((\"|\‘)[^\)]*(\"|\‘)\)/g,

        lastReplaceRge = /\((\"|\‘)|(\"|\‘)\)/g;

    //将模块字符串化

    var string = func.toString();

    var allFiles = string.match(firstReg);

    var newArr = [];

    if (!allFiles) {

        return ‘‘;

    }

    //将依赖的文件名存入一个堆栈内

    allFiles.map(function(v) {<br>  //对文件名做处理

        var m = v.match(secondReg)[0].replace(lastReplaceRge, ‘‘);

        //只有在异步加载的情况下需要 返回解析依赖

        if(!modules[_analyseName(m)]) {

            newArr.push(m);

        }

    });

    if(newArr.length > 0) {

        return newArr;

    }else{

        return ‘

    }

}

三、建立脚本加载队列

分析完依赖之后,我们可以得到一个脚本名称的栈,我们从其中获取脚本名称,依次按照顺序地加载它们。因为每个脚本加载过程都是异步的,所以,我们需要有一个异步加载机制。在这里,我们使用了设计模式中的职责链条模式来完成整个异步加载过程。通过在onload事件通知队列加载的完成情况。下面是职责链模式的实现代码


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

function _Chain() {

    this.cache = [];

}

/**

 * add function to order stack

 * @param func (func)

 * @returns {_Chain}

 */

_Chain.prototype.after = function(fn) {

        this.cache.push(fn);

        this.cur = 0;

        return this;

    }

    /**

     * To pass the authority to next function excute

     * @param

     * @returns

     */

_Chain.prototype.passRequest = function() {

        var result = ‘continue‘;

        while (this.cur < this.cache.length && result === ‘continue‘) {

            result = this.cache[this.cur++].apply(this, arguments);

            if (this.cur === this.cache.length) {

                this.clear();

            }

        }

    }

    /**

     * an api to excute func in stack

     * @param

     * @returns

     */

_Chain.prototype.next = function() {

        this.excute();

    }

    /**

     * let use to excute those function

     * @param

     * @returns

     */

_Chain.prototype.excute = function() {

    this.passRequest.apply(this, arguments)

}

/**

 * to clear stack all function

 * @param

 * @returns

 */

_Chain.prototype.clear = function() {

    this.cache = [];

    this.cur = 0;

}

var excuteChain = new _Chain();

每个脚本加载完毕后调用next函数,可以通知职责链中的下一个函数继续执行,这样解决了异步加载问题。这里将模式的实现代码放到模块加载器中是不太合适的,一般情况下我们可以将它独立出来,放入公共模块当中,为其他的模块共同使用。但这里纯粹是一个单文件的项目,所以就暂时将它放入此处。

四、递归加载

根据模块中的依赖出现的次序,依次加载各个模块。


1

2

3

4

5

6

7

8

9

10

11

12

function _excuteRequire(depends) {

    if (depends.length === 0) {

        var u = excuteStack.length;

        while (u--) {

            var params = excuteStack[u]();

            if (u === 0) {

                Events.trigger(‘excute‘, params);

                excuteStack = [];

            }

        }

    }

}

五、为模块建立缓存对象


1

2

//在文件加载完毕后将模块存入缓存

return modules[string] = func();

六、currentScript

currentScript主要是用来解决获取那些未命名的模块的js文件名,如 define(function(){})这样的模块是匿名的,我们通过这个方法可以获取正在执行的脚本文件名,从而为其建立缓存。


1

2

3

4

5

6

7

function _getCurrentScript() {

        //取得正在解析的script节点

        if (doc.currentScript) {

            //firefox 4+

            return doc.currentScript;

        }

    }

七、定义module

最后我们需要做的事给出定义模块的方法,一般情况下定义方法主要分以下几种:

1.define(‘a‘, function(){})

2.define(function(){})

第一种是命名的模块,第二种是未命名的模块,我们需要对它们分别处理。用typeof方法分析参数,建立以string方法为基础的加载模式:


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

function define() {

    var arg = Array.prototype.slice.call(arguments);

    var paramType = Object.prototype.toString.call(arg[0]).split(‘ ‘)[1].replace(/\]/, ‘‘);

    defineParamObj[paramType].apply(null, arg);

    // Chain.excute();

}

function _String(string, func) {

    string = _analyseName(string);

    //分析依赖

    var depends = _analyseDepend(func) || [];

    // 将加载好的模块存入缓存

    excuteStack.push(function() {

        return modules[string] = func();

    });

    //执行加载依赖函数

    _excuteRequire(depends);

    for (var i = 0, l = depends.length; i < l; i++) {

        (function(i) {

            excuteChain.after(function() {

                var c = require(depends[i]);

                if(c) {

                    this.next();

                };

            });

        })(i);

    }

}

function _Function(func) {

    var name = _analyseName(_getCurrentScript().src);

    _String(name, func);

}

结束

以上就是一个实现模块加载器的主要原理,卤煮写完发现也只有四百行的代码,实现了最基本的模块加载功能。当然,其中还有很多细节没有实现,比起大而全的requireJs来说,只是一个小儿科而已。但是明白了主要这几项后,对于我们来说就足够理解一个模块加载器的实现方式了。代码存入github上: https://github.com/constantince/require

时间: 2024-07-29 22:18:42

模块加载器的相关文章

【模块化编程】理解requireJS-实现一个简单的模块加载器

在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ...... 为了深入了解加载器,中间阅读过一点requireJS的源码,但对于很多同学来说,对加载器的实现依旧不太清楚 事实上不通过代码实现,单单凭阅读想理解一个库或者框架只能达到一知半解的地步,所以今天便来实现一个简单的加载器 加载器原理分析 分与合 事实上,一个程序运行需要完整的模块,以下代码为例: 1

JavaScript AMD 模块加载器原理与实现

关于前端模块化,玉伯在其博文 前端模块化开发的价值 中有论述,有兴趣的同学可以去阅读一下. 1. 模块加载器 模块加载器目前比较流行的有 Requirejs 和 Seajs.前者遵循 AMD规范,后者遵循 CMD规范.前者的规范产出比较适合于浏览器异步环境的习惯,后者的规范产出对于写过 nodejs 的同学来说是比较爽的.关于两者的比较,有兴趣的同学请参看玉伯在知乎的回答 AMD和CMD的区别有哪些.本文希望能按照 AMD 规范来简单实现自己的一个模块加载器,以此来搞清楚模块加载器的工作原理.

JS模块加载器加载原理是怎么样的?

路人一: 原理一:id即路径 原则.通常我们的入口是这样的: require( [ 'a', 'b' ], callback ) .这里的 'a'.'b' 都是 ModuleId.通过 id 和路径的对应原则,加载器才能知道需要加载的 js 的路径.在这个例子里,就是 baseUrl + 'a.js' 和 baseUrl + 'b.js'. 但 id 和 path 的对应关系并不是永远那么简单,比如在 AMD 规范里就可以通过配置 Paths 来给特定的 id 指配 path. 原理二:crea

js模块化/js模块加载器/js模块打包器

之前对这几个概念一直记得很模糊,也无法用自己的语言表达出来,今天看了大神的文章,尝试根据自己的理解总结一下,算是一篇读后感. 大神的文章:http://www.css88.com/archives/7628(大神的文章写的很详细,建议先看完大神的文章) 一.js模块化 什么是js模块化,我们从历史说起. 1.一开始我们怎么写脚本?就是在html文件中用<script></script>写代码 这种方式的缺点:代码复用靠复制,基本是全局变量. 2.后来我们用js文件写代码,用<

Sea.js 是一个模块加载器

1 模块定义define define(function(require,exports,module){ //require 引入需要的模块如jquery等 //var $ = require('./jquery'); //exports可以把方法或属性暴露给外部 exports.name = 'hi'; exports.hi = function(){ alert('hello'); } //module提供了模块信息 }); 2 使用定义好的模块seajs.use <!doctype ht

SeaJS:一个适用于 Web 浏览器端的模块加载器

什么是SeaJS?SeaJS是一款适用于Web浏览器端的模块加载器,它同时又与Node兼容.在SeaJS的世界里,一个文件就是一个模块,所有模块都遵循CMD(Common Module Definition)规范.SeaJS本身遵循(Keep it Simple, Stupid)理念开发,API仅有几个. 为什么用SeaJS?假如我们要开发一个web应用App,在这个应用中需要使用jquery框架.App的首页index.html会用到module1.js,module1.js依赖module2

实现简单的 JS 模块加载器

实现简单的 JS 模块加载器 按需加载是前端性能优化的一个重要手段,按需加载的本质是从远程服务器加载一段JS代码(这里主要讨论JS,CSS或者其他资源大同小异),该JS代码就是一个模块的定义,如果您之前有去思考过按需加载的原理,那你可能已经知道按需加载需要依赖一个模块加载器.它可以加载所有的静态资源文件,比如: JS 脚本 CSS? 脚本 图片 资源 如果你了解 webpack,那您可以发现在 webpack 内部,它实现了一个模块加载器.模块加载器本身需要遵循一个规范,当然您可以自定义规范,大

自研模块加载器(一) 模块系统概述与自定义模块规范书写规定

模块系统概述 CommonJs/AMD/CMD/ES6 Modules 什么是模块化? 模块化就是把系统分离成独立的功能的方法,需要什么功能,就加载什么功能 当一个系统越来越复杂时候,我们会遇到这些问题 1. 命名冲突 2. 文件依赖 使用模块化开发可以避免以上问题,并提升开发效率 1. 可维护性 2. 可复用性 在生产角度,模块化是一种生产方式,这种生产方式效率高,维护成本低. 模块化开发演变 1. 全局函数 早期开发中,将重复的代码封装成函数,将多个函数放在一个文件中. 缺点: 污染全局变量

自研模块加载器(二) 加载器结构与设计导论

结构导论 模块部分 数据初始化: 加载器中设计来一个名为Module的构造函数,每个模块都是此构造函数实例对象.构造函数中给实例对象扩展了"未来"所需用到的属性和方法. 模块存储: 加载器中设计来一个名为cache的缓存对象,每个文件(模块)都会存储在cache对象中. 具体存储方式: { "当前模块的绝对路径" : new Module()}, 注意: 当前模块的绝对路径是通过资源部分,资源定位方法实现的. 每个模块创建都先初始化数据,存储在缓存对象中. 资源部分