实现一个JavaScript模块化加载器

对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题。尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中面临的越来越复杂的需求。

为什么需要模块化

对开发者而言,有很多理由去将程序拆分为小的代码块。这种模块拆分的过程有助于开发者更清晰的阅读和编写代码,并且能够让编程的过程更多的集中在模块的功能实现上,和算法一样,分而治之的思想有助于提高编程生产率。

在本文中,我们将集中讨论JavaScript的模块化开发,并实现一个简单的module loader。对于模块化的基础知识,可以参考阅读这篇文章

实现模块化

使用函数作为命名空间

在JavaScript中,函数是唯一的可以用来创建新的作用域的途径。考虑到一个最简单的需求,我们通过数字来获得星期值,例如通过数字0得到星期日,通过数字1得到星期一。我们可以编写如下的程序:

var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

function dayName(number) {
    return names[number];
}

console.log(dayName(1));

上面的程序,我们创建了一个函数dayName()来获取星期值。但问题是,names变量被暴露在全局作用域中。更多的时候,我们希望能够构造私有变量,而暴露公共函数作为接口。

对JavaScript中的函数而言,我们可以通过创建立即调用的函数表达式来达到这个效果,我们可以通过如下的方式重构上面的代码,使得私有作用域成为可能:

var dayName = function() {
    var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

    return {
        name: function(number) {
            return names[number];
        },

        number: function(name) {
            return names.indexOf(name);
        }
    };
}();

console.log(dayName.name(3));
console.log(dayName.number("Sunday"));

上面的程序中,我们通过将变量包括在一个函数中,这个函数会立即执行,并返回一个包含两个属性的对象,返回的对象会被赋值给dayName变量。在后面,我们可以通过dayName变量来访问暴露的两个函数接口namenumber

对代码进一步改进,我们可以利用一个exports对象来达到暴露公共接口的目的,这种方法可以通过如下方法实现,代码如下:

var weekDay = {};

(function(exports) {
    var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

    exports.name = function(number) {
        return names[number];
    };

    exports.number = function(name) {
        return names.indexOf(name);
    };

})(weekDay); // outside of a function, this refers to the global scope object

console.log(weekDay.name(weekDay.number("Saturday")));

上面的这种模块构造方式在以浏览器为核心的前端编码中非常常见,通过暴露一个全局变量的方式来将代码包裹在私有的函数作用域中。但这种方法依然会存在问题,在复杂应用中,你依然无法避免同名变量。

从全局作用域中分离,实现require方法

更进一步的,为了实现模块化,我们可以通过构造一个系统,使得一个函数可以require另一个函数的方式来实现模块化编程。所以我们的目标是,实现一个require方法,通过传入模块名来取得该模块的调用。这种实现方式要比前面的方法更为优雅的体现模块化的理念。对require方法而言,我们需要完成两件事。

  1. 我们需要实现一个readFile方法,它能通过给定字符串返回文件的内容。
  2. 我们需要能够将返回的字符串作为代码进行执行。

我们假设已经存在了readFile这个方法,我们更加关注的是如何能够将字符串作为可执行的程序代码。通常我们有好几种方法来实现这个需求,最常见的方法是eval操作符,但我们常常在刚学习JavaScript的时候被告知,使用eval是一个非常不明智的决策,因为使用它会导致潜在的安全问题,因此我们放弃这个方法。

一个更好的方法是使用Function构造器,它需要两个参数:使用逗号分隔的参数列表字符串,和函数体字符串。例如:

var plusOne = new Function("n", "return n+1");
console.log(plusOne(5)); // 6

下面我们可以来实现require方法了:

// module.js
function require(name) {

	// 调用一个模块,首先检查这个模块是否已被调用
    if(name in require.cache) {
        return require.cache[name];
    }

    var code = new Function("exports, module", readFile(name));
    var exports = {},
        module = {exports: exports};
    code(exports, module);

    require.cache[name] = module.exports;
    return module.exports;
}

// 缓存对象,为了应对重复调用的问题
require.cache = Object.create(null);

// todo:
function readFile(fileName) { ... }

在页面中使用require函数:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>demo</title>
    <script src="module.js"></script>
</head>
<body>
    <script>
        var weekDay = require("weekDay");
        var today = require("today");

        console.log(weekDay.name(today.dayNumber()));
    </script>
</body>
</html>

通过这种方式实现的模块化系统通常被称为是CommonJS模块风格的,Node.js正式使用了这种风格的模块化系统。这里只是提供了一个最简单的实现方法,在真实应用中会有更加精致的实现方法。

慢载入模块和AMD

对浏览器编程而言,通常不会使用CommonJS风格的模块化系统,因为对于Web而言,加载一个资源远没有在服务端来的快,这收到网络性能的影响,尤其一个模块如果过大的话,可能会中断方法的执行。Browserify是解决这个问题的一个比较出名的模块化方案。

这个过程大概是这样的:首先检查模块中是否存在require其他模块的语句,如果有,就解析所有相关的模块,然后组装为一个大模块。网站本身为简单的加载这个组装后的大模块。

模块化的另一个解决方案是使用AMD,即异步模块定义,AMD允许通过异步的方式加载模块内容,这种就不会阻塞代码的执行。关于AMD的相关背景知识,可以参考这篇文章

我们想要实现的功能大概是这个样子的:

// index.html 中的部分代码
define(["weekDay.js", "today.js"], function (weekDay, today) {
        console.log(weekDay.name(today.dayNumber()));
        document.write(weekDay.name(today.dayNumber()));
    });

问题的核心是实现define方法,它的第一个参数是定义该模块说需要的依赖列表,参数而是该模块的具体工作函数。一旦所依赖的模块都被加载后,define便会执行参数2所定义的工作函数。weekDay模块的内容大概是下面的内容:

// weekDay.js
define([], function() {
    var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

    return {
        name: function(number) { return names[number]},
        number: function(name) { return names.indexOf(name)}
    }
});

下面我们来关注如何实现define()方法。为了实现这个方法,我们需要定义一个backgroundReadFile()方法来异步的获取文件内容。此外我们需要能够监视模块的加载状态,当模块加载完后能够告诉函数去执行具体的工作函数(回调)。

// 通过Ajax来异步加载模块
function backgroundReadFile(url, callback) {
    var req = new XMLHttpRequest();
    req.open("GET", url, true);
    req.addEventListener("load", function () {
        if (req.status < 400)
            callback(req.responseText);
    });
    req.send(null);
}

通过实现一个getModule函数,通过给定的模块名进行模块的调度运行工作。同样,我们需要通过缓存的方式避免同一个模块被重复的载入。实现代码如下:

// module.js 的部分内容
var defineCache = Object.create(null);
var currentMod = null;

function getModule(name) {
    if (name in defineCache) {
        return defineCache[name];
    }

    var module = {
        exports: null,
        loaded: false,
        onLoad: []
    };

    defineCache[name] = module;
    backgroundReadFile(name, function(code) {
        currentMod = module;
        new Function("", code)();
    });
    return module;
}

有了getModule()了函数之后,define方法可以借助该方法来为当前模块的依赖列表获取或创建模块对象。define方法的简单实现如下:

// module.js 的部分内容
function define(depNames, moduleFunction) {
    var myMod = currentMod;
    var deps = depNames.map(getModule);

    deps.forEach(function(mod) {
        if(!mod.loaded) {
            mod.onLoad.push(whenDepsLoaded);
        }
    });

	// 用于检查是否所有的依赖模块都被成功加载了
    function whenDepsLoaded() {
        if(!deps.every(function(m) { return m.loaded; })) {
            return;
        }

        var args = deps.map(function(m) { return m.exports; });

        var exports = moduleFunction.apply(null, args);
        if (myMod) {
            myMod.exports = exports;
            myMod.loaded = true;
            myMod.onLoad.forEach(function(f) { f(); });
        }
    }

    whenDepsLoaded();
}

关于AMD的更加常见的实现是RequireJS,它提供了AMD风格的更加流行的实现方式。

小结

模块通过将代码分离为不同的文件和命名空间,为大型程序提供了清晰的结构。通过构建良好的接口可以使得开发者更加建议的阅读、使用、扩展代码。尤其对JavaScript语言而言,由于天生的缺陷,使得模块化更加有助于程序的组织。在JavaScript的世界,有两种流行的模块化实现方式,一种称为CommonJS,通常是服务端的模块化实现方案,另一种称为AMD,通常针对浏览器环境。其他关于模块化的知识,你可以参考这篇文章

References

  1. Eloquent JavaScript, chapter 10, Modules
  2. Browserify运行原理分析
  3. Why AMD?
  4. JavaScript模块化知识点小结
时间: 2024-10-12 14:09:31

实现一个JavaScript模块化加载器的相关文章

如何构建一个微型的CMD模块化加载器

前言 前端模块化是一个老生常谈的话题,模块化的好处是不言而喻,比如易于代码复用.易于维护.易于团队开发d等云云.对于前端模块加载器,以前仅仅止步于会用的阶段,为了加深对前端模块化的理解,大概花了一周的时间来学习.调研并尝试自己实现一个简易版的符合CMD规范的加载器. 设计 加载器是按照CMD规范进行设计的,具体的CMD规范就不列出了,详情请见CMD规范. 入口函数 use(ids, callback) 模块定义函数 define(factory) 模块加载函数 require(id) 取得模块接

该如何理解AMD ,CMD,CommonJS规范--javascript模块化加载学习总结

是一篇关于javascript模块化AMD,CMD,CommonJS的学习总结,作为记录也给同样对三种方式有疑问的童鞋们,有不对或者偏差之处,望各位大神指出,不胜感激. 本篇默认读者大概知道require,seajs的用法(AMD,CMD用法),所以没有加入使用语法. 1.为何而生: 这三个规范都是为javascript模块化加载而生的,都是在用到或者预计要用到某些模块时候加载该模块,使得大量的系统巨大的庞杂的代码得以很好的组织和管理.模块化使得我们在使用和管理代码的时候不那么混乱,而且也方便了

JavaScript文件加载器LABjs API详解

在<高性能JavaScript>一书中提到了LABjs这个用来加载JavaScript文件的类库,LABjs是Loading And Blocking JavaScript的缩写,顾名思义,加载和阻塞JavaScript,而它的API script()和wait()则优雅地实现了这两个功能,我在高性能JavaScript 加载和执行一文中也简单讲解了这两个核心API的用法.当然,LABjs还有更多的API,本文用实例讲解下LABjs其他API. $LAB.setGlobalDefaults()

解密javascript模块加载器require.js

require.config require.config设置require.js模板加载选项 // 定义config req.config = function (config) { return req(config); }; // 加载config配置项 req = requirejs = function (deps, callback, errback, optional) { var context, config, contextName = defContextName; //

利用require.js实现javascript模块化加载

这种引入很痛苦吧, <script src="1.js"></script> <script src="2.js"></script> <script src="3.js"></script> <script src="4.js"></script> <script src="5.js"></s

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

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

JS模块化编程之加载器原理

世面上有好多JavaScript的加载器,比如 sea.js, require.js, yui loader, labJs...., 加载器的使用范围是一些比较大的项目, 个人感觉如果是小项目的话可以不用,  我用过seaJS和requireJS, 在项目中用过requireJS, requireJS是符合AMD,全称是(Asynchronous Module Definition)即异步模块加载机制 , seaJS是符合CMD规范的加载器. AMD__和__CMD AMD规范是依赖前置, CM

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

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

AMD加载器实现笔记(一)

之前研究过AMD,也写过一篇关于AMD的文章<以代码爱好者角度来看AMD与CMD>.代码我是有看过的,基本的原理也都明白,但实际动手去实现却是没有的.因为今年计划的dojo教程<静静的dojo>中,有一章节来专门讲解AMD,不免要把对AMD的研究回炉一下.时隔多日,再回头探索AMD实现原理时,竟抓耳挠腮,苦苦思索不得要领.作为开发人员,深感惭愧.故有此文,记录我在实现一个AMD加载器时的思考总结. requireJS是所有AMD加载器中,最广为人知的一个.目前的版本更凝聚了几位大牛