欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

回头再看JS模块化编程

程序员文章站 2022-04-25 14:30:32
...

什么是模块?我们可能要从需求上出发进行理解,当web应用的规模变得越来越大,业务变得越来越复杂时,我们需要将一些函数分门别类,在分类的基础上对函数进行封装,这就形成了模块。下面看一下js模块的一些形式。

对象模块

假如我们有多个函数,想作为一个模块使用,最原始的做法就是把这几个函数全部放在一个js文件,通过文件的形式来对js进行划分模块。

// my_module.js

function add(a, b) {
    return a + b;
}
function multiply(a, b) {
    return a * b;
}

然而这样的做法会污染全局环境,引用这个js后,window对象就会多了两个方法。那么如何减少这种对全局环境的污染呢?想到的最简单的一个办法就是,把这几个函数都放在一个对象中,只暴露一个对象。

// my_module.js

var MyModule = {
    add: function (a, b) {
        return a + b;
    },
    multiply: function (a, b) {
        return a * b;
    }
}

IIFE

IIFE(immediately invoked function expression),也就是立即执行函数表达式。假设有这样一个场景,你的模块需要定义默认参数,而你又希望这个默认参数不被外界所改变,那么使用对象模块的方式就没有办法做到了,因为这个对象已经暴露在全局环境中。那么如何能隔离作用域呢?聪明的你已经想到了函数,对的,函数可以做到。我们通过IIFE为window挂载了setColor方法。

(function(global) {
    var default_option = {
        color: 'blue'
    }
    global.setColor = function(id, colorValue) {
        document.getElementById(id).style.color = colorValue || default_option.color
    }
})(this)

这里的default_option就不会暴露在全局环境中,你可以尝试一下在控制台console.log(window.default_option),得到的就是undefined。

CommonJS

引用百度给出的定义

CommonJS API定义很多普通应用程序(主要指非浏览器的应用)使用的API,从而填补了这个空白。它的终极目标是提供一个类似Python,Ruby和Java标准库。

CommonJS提供的模块方案认为,一个js就是一个模块,我们经常用到的变量和函数有global,module,exports,require。global是nodejs环境的全局对象,类似与浏览器环境的window,也是根对象,任何在全局环境下定义的变量或函数都是global的属性或方法,global涉及很多东西,这里不再赘述。而module是模块对象,exports包含该模块要导出的变量或函数,require是导入模块的方法。我们首先写两个简单的js来认识它们。

// a.js
module.exports.add = function (a, b) {
    return a + b;
}

// b.js
var a = require('./a.js');

var result = a.add(1, 2);
console.log(result); // 输出3

这是最简单的模块写法,a.js通过exports导出add函数,而b.js通过require导入a模块,便可以调用a模块的add函数。

module

那么我们先来看看module这个对象。在b.js中我们console.log(module),则会打印出模块b的信息。

回头再看JS模块化编程

可以看到,模块b的children里有模块a,说明模块b引用了模块a。我们再观察一下模块a,修改a.js的代码如下,再运行b.js

console.log(module)

module.exports.add = function (a, b) {
    return a + b;
}

回头再看JS模块化编程

可以看到,模块a的parent指向模块b,是因为执行的是b.js,而b.js引用了模块a。注意此时模块a的loaded属性值仍是false,因为此时模块还没加载完成。如果我们在add方法中打日志,而b.js调用a.js的add函数,则会发现此时模块a的loaded已经变成了true

从上面可以了解到,module对象下面有以下属性

  • id:模块id,一般默认是模块的路径
  • exports:模块对外导出对象,包含了对外导出的函数和属性
  • parent:指向首次加载本模块的模块(为什么说是首次呢?假设b.js引用了模块a,c.js引用了模块a和模块b,此时运行c.js,模块a的parent指向的是模块b)
  • filename:模块的绝对路径
  • loaded:模块是否已经加载完成
  • children:当前模块引用的其他模块
  • paths:对于加载模块时没给出./ ../ /…/时,加载模块的搜索路径。依次从第一个路径搜索到最后一个路径。

exports

接下来我们说说exports,在这里要了解module.exports与exports的区别。Node.js 在初始化时执行了 exports = module.exports , 所以 exports 与 module.exports 指向了相同的内存。当不改变两者的指向时,两者还是全等的。因此,我前面的写法 exports.add 只是给 exports 指向的对象上添加了add方法,并未改变其指向。这之后exports与module.exports仍是一致的。到这里大家应该明白了什么情况两者会不相等了。

// 如果采用这种改变指向的写法,那么之后exports与module.exports就不一样了。
module.exports = {
    add: function (a, b) {
        return a + b
    }
}

通过exports导出的函数和属性可以被其他模块调用,这一点想必大家都清楚了。

require

这里先说一下模块的分类,NodeJS中模块分为核心模块和文件模块。核心模块是被编译成二进制代码,引用的时候只需require表示符即可,如require(‘fs’),不需要加路径的。而引用文件模块时需要加上路径,表示对文件的引用。假如你加载一个自定义的test.js模块时,没有指定路径,那么它会首先从当前目录的node_modules子目录下寻找test.js,如果没有,则查找上一级目录的node_modules子目录,一直查到盘符的根目录为止。也就是前面提到的module.paths的查找顺序。

说到这里,我们再来回顾一下我们在开发时,npm install 安装的一些依赖包。它们的package.json一般都包含了main字段,用来标识入口js文件。

回头再看JS模块化编程

如果没有指定main字段,那么nodejs会默认去加载index.js或者index.node文件。例如:

回头再看JS模块化编程

看到这里,是不是突然有点懂了node_modules哪些依赖包的写法了。好的,接着往下看。

我们在c.js中打出日志,观察require方法的结构。

console.log(require)

回头再看JS模块化编程

可以知道,require函数包含了以下属性和方法。

  • require.resolve():将模块名解析,得到该模块的绝对路径
  • require.main:指向当前执行的主模块
  • require.cache:指向所有缓存的模块
  • require.extensions:根据文件的后缀名,调用不同的执行函数

我们再细致看一下,主要看看resolve和extensions

var a = require('./a.js');
var b = require('./b.js');

console.log('resolve测试')
console.log(require.resolve('./a.js'))

console.log('extensions测试')
console.log(require.extensions['.js'].toString())

console.log(require.extensions['.json'].toString())

console.log(require.extensions['.node'].toString())

得到的结果如下图所示:

回头再看JS模块化编程

写到这里,算是对模块有一点初步的认识。接下来我们还需要了解AMD,CMD,UMD的概念。由于篇幅太长,接下来我将分篇叙述这些概念,请阅读后续系列文章!以上观点源于自己的一些理解,如有描述不对的地方,请您指正!