5

前言

希望编写程序能像玩积木一样,首先规划要产出怎样的作品,然后在积木堆中挑选合适的积木块,最后一组合就完工了。

于是JavaScript需要类似这样模块化,每个模块都隐藏内部细节并且对外暴露接口,再处理好模块之间的依赖关系,就可以达到玩积木的效果了。

虽然现有很多模块化框架/工具,但对于新手来说,为什么不自己撸一个简单的模块化工具呢?

希望通过这个工具把自己觉得好用的代码以模块的方式组织起来,渐渐形成自己的JS库,之后可以勇敢地和HR说,自己的小项目用的是自己小JS库,^_^。我觉得,在这个封装的过程中,新手能学习到很多东西。

新手嘛,多造轮子总是有好处的,=_=。

从闭包到模块

以下是《你所不知道的JavaScript(上卷)》中对于闭包的说明。

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

其实,不管怎样,闭包正如其字面意思一样,既能提供一个相对封闭的空间,也能向外界暴露必要的接口。这不就正符合我们模块化的需求吗?

在此,建议参考这篇文章,以加强您对闭包的理解:《假如技术HR问您JavaScript的“闭包”,嘿嘿嘿,举这个例子就够了》

最简单的模块

var test = (function test(){
    function run(){
        console.log("run test");
    }
    return {
        run: run
    };
})();
test.run();

上面的代码有个叫test的函数作为模块创建器,每次调用它都可以创建一个新的模块。这里使用立即执行函数,立即创建了一个test模块。参考闭包的概念,外部可以调用test模块中的run函数,同时test模块又有自己独立的作用域。能达到一个积木块(模块)的要求。

简单的模块这样写没有问题,但是模块间的依赖问题没有解决。

最简单的模块管理工具

模块之间必然会存在依赖关系,而模块管理工具需要能够很好地管理模块间的依赖。下面我们模仿实现了AMD规范的工具requirejs,主要是模仿其define,get的API风格,自己写一个简单的版本。

//模块管理工具,MyModules
var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++){
            //将依赖的名字替换成已经注册了的模块
            deps[i] = modules[deps[i]];
        }
        //将依赖数组展开成参数传入模块的构建函数,生成新模块
        modules[name] = impl.apply(impl, deps);
    }
    function get(name){
        return modules[name];
    }
    return {
        define: define,
        get: get
    }
})();
//定义一个模块,data
MyModules.define("data",[],function(){
    var name = "miku";
    function getName(){
        return name;
    }
    return {
        getName:getName
    }
});
//定义一个模块,app
//该模块依赖data模块
MyModules.define("app", ["data"], function(data){
    function run(){
        console.log(data.getName());
    }
    return {
        run:run
    }
});
//取出app模块
var app = MyModules.get("app");
//调用app模块的run方法
app.run();

可以看出,利用MyModules可以很方便地定义使用模块,管理模块依赖。但是还存在一个问题,MyModules对于模块定义的顺序有要求。以上面的例子来说,就是app模块依赖data模块,那data模块必须在app模块之前被定义。这个限制让我们实际使用中不是很方便。接下来我们将改进它。

改进模块管理工具

我们需要让模块管理工具不需要限制模块的定义顺序,这里我的做法是,使用一个rebuilds数组来保存未成功构建的模块。每次有新模块构建成功的时候就会重新尝试去构建整个rebuilds数组中的模块。具体看下面的代码。

window.mm_modules = (function Manager() {
    var debug = false;
    var modules = {};
    var rebuilds = [];
    function copyArray (array){
        var tempArray = [];
        for(var i=0; i<array.length; i++){
            tempArray.push(array[i]);
        }
        return tempArray;
    }
    function define(name, deps, impl) {
        //判断依赖构建是否成功
        var depsDone = true;
        
        //拷贝一份当前想要构建的模块,当构建失败时使用
        var rebuildCopy = {
            name : name,
            deps : copyArray(deps),
            impl : impl
        };
        
        //循环依赖数组,构建依赖
        for (var i=0; i<deps.length; i++){
            //将依赖的名字替换成已经注册了的模块
            deps[i] = modules[deps[i]];
            //有依赖的模块未定义,所以这个模块构建失败
            if(!deps[i]){
                depsDone = false;
                break;
            }
        }
        
        //如果依赖构建成功,即模块构建成功
        if(depsDone){
            //将依赖数组展开成参数传入模块的构建函数,生成新模块
            modules[name] = impl.apply(impl, deps);
            //从rebuilds数组中移除
            if(rebuilds[name]){
               delete rebuilds[name];
            }
            //循环rebuilds数组,尝试从新构建之前构建失败的模块
            for (key in rebuilds){
                var rebuild = rebuilds[key];
                if(rebuild){
                    //递归调用
                    define(rebuild.name, rebuild.deps, rebuild.impl);
                }
            }
        }
        //模块构建失败,存入rebuilds数组中,等待下一次重新构建
        else{
            rebuilds[name] = rebuildCopy;
        }
        if(debug){
            console.log("[mm_modules debug]need rebuild modules:", rebuilds);
        }
    }
    function get(name){
        return modules[name];
    }
    return {
        define: define,
        get: get
    }
})();

改进后的模块管理工具,能够自动地处理模块依赖,而不需要限制定义顺序了。
那,能不能更进一步呢?试着想一下,我们日常会怎么使用?单文件单模块,然后把这些文件放在不同文件夹里组织好。于是,我就想到使用gulp这样的工具辅助我们。

gulp辅助

请参考下面的目录结构。

├── dist
│   ├── index.html
│   └── js
│       └── mm-modules-build.js
├── gulpfile.js
├── mm-modules
│   ├── queryObject.js
│   ├── request.js
│   ├── template.js
│   ├── test.js
│   └── util.js
├── mm-modules.js

可以在mm-modules下随意地定义模块,如util模块内有各种工具函数,template模块则包含了artTemplate模版引擎。之后利用gulp将mm-modules.js(模块管理工具)与mm-modules下所有的模块文件打包成mm-modules-build.js。项目中只要引入mm-modules-build.js即可。

结尾

到此,我们自己构建了一个很实用的JavaScript模块化工具,项目的源码在这里:https://github.com/MIKUScallion/mm-modules,喜欢的话,给个✨。

再回顾一下前言的话。

希望通过这个工具把自己觉得好用的代码以模块的方式组织起来,渐渐形成自己的JS库,之后可以勇敢地和HR说,自己的小项目用的是自己小JS库,^_^。我觉得,在这个封装的过程中,新手能学习到很多东西。

新手嘛,多造轮子总是有好处的,=_=。

参考

  • 《你所不知道的JavaScript(上卷)》


Myou_Aki
415 声望35 粉丝