浅谈模块化加载的完成道理

试发一弹,本文同步自:http://barretlee.com
略蛋疼的是不支持:

[url reference]

置信许多人都用过 seajs、 requirejs 等这些模块加载器,他们都是非常便利的工程治理工具,简化了代码的构造,更主要的是消除了种种文件依靠和定名争执题目,并运用 AMD / CMD 范例一致了花样。假如你不太邃晓模块化的作用,提议看看玉伯写的一篇文章

为何他们会想到运用模块化加载呢,我以为主如果两点。

  • 一是按需加载,营业越来越大,基本代码也会越来越多,开辟人员能够开辟了一百个小工具,而且都塞在一个叫做 utils.js 的包里,然则一个页面能够只须要三到五个小工具,假如直接去加载这个 utils.js 岂不是很大的糟蹋,PC 端还好,主如果无线端,剩下 1KB 那都是很大的代价啊,所以呢,现在许多框架的开辟都体现出细颗粒度的分化,像百度研讨比较认真的 tangram,阿里放满产品线的 kissy,几乎是细分到了微粒水平,这类细分体式格局也促进了模块化加载手艺的生长,比方为了削减要求数量,kissy 的 config 中开启 combo 就能够兼并多个要求为一个等等。

  • 第二点,应当也是从服务器那里参考而来的,服务器剧本许多都是以文件为单元星散的,假如要运用别的文件的功用,能够易如反掌的 require 或许 include 进来,我没有去研讨这些加载函数的内部完成道理,轻微猜猜应当是把文件写入到缓存,碰到 include 之类的加载函数,停息写入,找到须要 include 的文件地点,把找到的文件接着上面继承写入缓存,以此类推,直到终了,然后编译器举行一致编译。

一、模块化加载的手艺道理

先不斟酌种种模块定义范例,本文目标只是扼要的剖析加载道理, CMD / AMD 范例虽内容然不多,然则要完成起来,工程量照样不小。文章背面会提到。

1. 数据模块的加载

既然是模块化加载,想办法把模块内容拿到当然是重头戏,无论是 script 照样 css 文件的加载,一个 script 或许 link 标签就能够搞定题目,不过我这里采纳的是 ajax,目标是为了拿到 script 的代码,也是为了照应背面要说的 CMD 范例。

var require = function(path){
    var xhr = new XMLHttpRequest(), res;
    xhr.open("GET", path, true);
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4 && xhr.status == 200){
            // 猎取源码
            res = xhr.responseText;
        }
    }
    xhr.send();
};

建立 script 便签加载剧本不会存在跨域题目,不过拿到的剧本会被浏览器立马剖析出来,假如要做同异步的处置惩罚就比较贫苦了。没有跨域的文件我们就经由历程上面的体式格局加载,假如剧本跨域了,再去建立标签,让文档本身去加载。

// 跨域处置惩罚
if(crossDomain){
    var script = document.createElement("script");
    script.src = path;

    (document.getElementsByTagName("head")[0] || document.body).appendChild(script);
}

2. 剖析模块的条理依靠关联

模块之间存在依靠关联是非常平常的,如一个工程的文件构造以下:

project/
├── css/
│   └── main.css
├── js/
│   ├── require.js
│   └── modlues/
│       ├── a.js
│       ├── b.js
│       └── c.js
└── index.html

而这里几个模块的依靠关联是:

            ┌> a.js -> b.js
index.html -|
            └> c.js

// a.js
require("./js/test/b.js");

// b.js
console.log("i am b");

// c.js
console.log("i am c");

我们要从 index.html 中运用 require.js 猎取这一连串的依靠关联,平常采纳的体式格局就是正则婚配。以下:先拿到 function 的代码,然后正则婚配出第一层的依靠关联,接着加载婚配到关联的代码,继承婚配。

// index.html
<script type="text/javascript" src="./js/require.js"></script>
<script type="text/javascript">
    function test(){
        var a = require("./js/modlues/a.js");
        var c = require("./js/modlues/c.js");
    }

    // toString 要领能够拿到 test 函数的 code
    start(test.toString());
</script>

悉数函数的进口是 start,正则表达式为:

var r = /require\((.*)\)/g;

var start = function(str){
    while(match = r.exec(str)) {
        console.log(match[1]);
    }
};

由此我们拿到了第一层的依靠关联,

["./js/modlues/a.js", "./js/modlues/c.js"]

接着要拿到 a.js 和 b.js 的文件条理依靠,之前我们写了一个 require 函数,这个函数能够拿到剧本的代码内容,不过这个 require 函数要轻微修改下,递回去查询和下载代码。

var cache = {};
var start = function(str){
    while(match = r.exec(str)) {
        console.log(match && match[1]);
        // 假如婚配到了内容,下载 path 对应的源码
        match && match[1] && require(match[1]);
    }
};

var require = function(path){
    var xhr = new XMLHttpRequest(), res;
    xhr.open("GET", path, true);
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4 && xhr.status == 200){
            res = xhr.responseText;
            // 缓存文件
            cache[path] = res;
            // 继承递归婚配
            start(res);
        }
    }
    xhr.send();
};

上面的代码已能够很好地拿到文件递归关联了。

3. 增加事宜机制,优化治理代码

然则我们有必要先把 responseText 缓存起来,假如不缓存文件,直接 eval 获得的 responseText 代码,想一想会发作什么题目~ 假如模块之间存在轮回援用,如:

            ┌> a.js -> b.js
index.html -|
            └> b.js -> a.js

那 start 和 require 将会堕入死轮回,不停的加载代码。所以我们须要先拿到依靠关联,然后解构关联,剖析出我们须要加载哪些模块。值得注意的是,我们必需根据加载的递次去 eval 代码,假如 a 依靠 b,先去实行 a 的话,一定会报错!

有两个题目我纠结了半天,上面的要求体式格局,何时会终了?用什么体式格局去纪录文件依靠关联?

末了照样决定将 start 和 require 两个函数的互相递归修改成一个函数的递归。用一个对象,提议要求时把 URL 作为 key,在这个对象里保留 XHR 对象,XHR 对象要求完成后,把抓取到的新要求再用一样的体式格局放入这个对象中,同时从这个对象中把本身删撤除,然后推断这个对象上是不是存在 key, 假如存在申明另有 XHR 对象没完成。

var r = /require\(\s*"(.*)"\s*\)/g;
var cache = {};    // 文件缓存
var relation = []; // 依靠历程掌握
var obj = {};      // xhr 治理对象

//辅佐函数,猎取键值数组
Object.keys = Object.keys || function(obj){
    var a = [];
    for(a[a.length] in obj);
    return a ;
};

// 进口函数
function start(str){
    while(match = r.exec(str)){
        obj[match[1]] = new XMLHttpRequest();
        require(obj[match[1]], match[1]);
    }
}

// 递归要求
var require = function(xhr, path){
    //纪录依靠历程
    relation.push(path);

    xhr.open("GET", path, true);
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4 && xhr.status == 200){
            var res = xhr.responseText;
            // 缓存文件
            cache[path] = res;
            // 从xhr对象治理器中删除已加载终了的函数
            delete obj[path];

            // 假如obj为空则触发 allLoad 事宜
            Object.keys(obj).length == 0 ? Event.trigger("allLoad") : void 0;
            //递归前提
            while(match = r.exec(res)){
                obj[match[1]] = new XMLHttpRequest();
                require(obj[match[1]], match[1]);
            }
        }
    }
    xhr.send();
};

上面的代码已基本完成了文件依靠剖析,文件的加载和缓存事情了,我写了一个,有兴致能够看一看。这个demo的文件构造为:

project/
├── js/
│   ├── require.js
│   └── test/
│       ├── a.js
│       ├── b.js
│       ├── c.js
│       ├── d.js
│       └── e.js
└── index.html

//文件依靠关联为
                       ┌> c.js
            ┌> a.js ->-|
index.html -|          └> d.js
            └> b.js -> e.js

戳我 → Demo

4. CMD 范例的引见

上面写了一大堆内容,也完成了模块加载器的原型,然则放在现实运用中,他就是个成品,回到最最先,我们为何要运用模块化加载。目标是为了不去运用贫苦的定名空间,把庞杂的模块依靠交给 require 这个函数去治理,但现实上呢,上面拿到的一切模块都是暴露在全局变量中的,也就是说,假如 a.js 和 b.js 中存在定名雷同的变量,后者将会掩盖前者,这是我们不愿意看到的。为了处置惩罚此类题目,我们有必要把一切的模块都放到一个闭包中,这样一来,只需不运用 window.vars 定名,闭包之间的变量是不会互相影响的。我们能够运用本身的体式格局去治理代码,不过有人已研讨处置惩罚一套规范,而且是环球一致,那就拿着用吧~

关于 CMD 范例,我这里就不多说了,能够去看看草案,玉伯也翻译了一份,。每一模块有且唯一一个对外公然的接口 exports,如:

define(function(require, exports) {

  // 对外供应 foo 属性
  exports.foo = 'bar';

  // 对外供应 doSomething 要领
  exports.doSomething = function() {};

});

剩下的事情就是针对 CMD 范例写一套相符规范的代码接口,这个比较噜苏,就不写了。

二、分外的话题

上面的代码中提到了关于 Event 的事宜治理。在模块悉数加在终了以后,须要有个东西通知你,所以随手写了一个 Event 的事宜治理器。

// Event
var Event = {};
Event.events = [];
Event.on = function(evt, func){
    for(var i = 0; i < Event.events.length; i++){
        if(Event.events[i].evt == evt){
            Event.events[i].func.push(func);
            return;
        }
    }

    Event.events.push({
        evt: evt,
        func: [func]
    });
};
Event.trigger = function(evt){
    for(var i = 0; i < Event.events.length; i++){
        if(Event.events[i].evt == evt){
            for(var j = 0; j < Event.events[i].func.length; j++){
                Event.events[i].func[j]();
            }
            return;
        }
    }
};
Event.off = function(evt){
    for(var i = 0; i < Event.events.length; i++){
        Event.events.splice(i, 1);
    }       
};

我以为 seajs 是一个很不错的模块加载器,假如感兴致,能够去看看他的源码完成,代码不长,只要一千多行。模块的加载它采纳的是建立文本节点,让文档去加载模块,及时检察状况为 interactive 的 script 标签,假如处于交互状况就拿到他的代码,接着删除节点。当节点数量为 0 的时刻,加载事情完成。

本文没有斟酌 css 文件的加载题目,我们能够把它当作一个没有 require 关键词的 js 文件,或许把它婚配出来以后另作处置惩罚,由于他是不能够存在模块依靠关联的。

然后就是许多许多细节,本文的目标并非写一个相似 seajs 的模块治理工具,只是轻微说几句本身对这玩艺儿的意见,假如说的有错,请多多吐槽!

三、参考资料

    原文作者:小胡子哥
    原文地址: https://segmentfault.com/a/1190000000400756
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞