读懂CommonJS的模块加载

栏目: Node.js · 发布时间: 5年前

内容简介:Common这个英文单词的意思,相信大家都认识,我记得有一个词组common knowledge是常识的意思,那么CommonJS是不是也是类似于常识性的,大家都理解的意思呢?很明显不是,这个常识一点都不常识。我最初认为commonJS是一个开源的JS库,就是那种非常方便用的库,里面都是一些常用的前端方法,然而我错得离谱,CommonJS不仅不是一个库,还是一个看不见摸不着的东西,他只是一个规范!就像校纪校规一样,用来规范JS编程,束缚住前端们。就和Promise一样是一个规范,虽然有许多实现这些规范的开

Common这个英文单词的意思,相信大家都认识,我记得有一个词组common knowledge是常识的意思,那么CommonJS是不是也是类似于常识性的,大家都理解的意思呢?很明显不是,这个常识一点都不常识。我最初认为commonJS是一个开源的JS库,就是那种非常方便用的库,里面都是一些常用的前端方法,然而我错得离谱,CommonJS不仅不是一个库,还是一个看不见摸不着的东西,他只是一个规范!就像校纪校规一样,用来规范JS编程,束缚住前端们。就和Promise一样是一个规范,虽然有许多实现这些规范的开源库,但是这个规范也是可以依靠我们的JS能力实现的。

CommonJs规范

那么CommonJS规范了些什么呢?要解释这个规范,就要从JS的特性说起了。JS是一种直译式脚本语言,也就是一边编译一边运行,所以没有模块的概念。因此CommonJS是为了完善JS在这方面的缺失而存在的一种规范。

CommonJS定义了两个主要概念:

require
module.exports

然而这两个关键字,浏览器都不支持,所以我认为这是为什么浏览器不支持CommonJS的原因。如果一定腰在浏览器上使用CommonJs,那么就需要一些编译库,比如browserify来帮助哦我们将CommonJs编译成浏览器支持的语法,其实就是实现require和exports。

那么CommonJS可以用于那些方面呢?虽然CommonJS不能再浏览器中直接使用,但是nodejs可以基于CommonJS规范而实现的,亲儿子的感觉。在nodejs中我们就可以直接使用require和exports这两个关键词来实现模块的导入和导出。

Nodejs中CommomJS模块的实现

require

导入,代码很简单, let {count,addCount}=require("./utils") 就可以了。那么在导入的时候发生了些什么呢??首先肯定是解析路径,系统给我们解析出一个绝对路径,我们写的相对对路径是给我们看的,绝对路径是给系统看的,毕竟绝对路径辣么长,看着很费力,尤其是当我们的的项目在N个文件夹之下的时候。所以 requir e第一件事就是解析路径。我们可以写的很简洁,只需要写出相对路径和文件名即可,连后缀都可以省略,让 require 帮我们去匹配去寻找。也就是说 require 的第一步是解析路径获取到模块内容:

  • 如果是核心模块,比如 fs ,就直接返回模块
  • 如果是带有路径的如 / , ./ 等等,则拼接出一个绝对路径,然后先读取缓存 require.cache 再读取文件。如果没有加后缀,则自动加后缀然后一一识别。
    .js
    .json
    .node
    
  • 首次加载后的模块会缓存在 require.cache 之中,所以多次加载 require ,得到的对象是同一个。
  • 在执行模块代码的时候,会将模块包装成如下模式,以便于作用域在模块范围之内。
(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});
复制代码

nodejs官方给出的解释,大家可以参考下

module

说完了require做了些什么事,那么 require 触发的 module 做了些什么呢?我们看看用法,先写一个简单的导出模块,写好了模块之后,只需要把需要导出的参数,加入 module.exports 就可以了。

let count=0
function addCount(){
    count++
}
module.exports={count,addCount}
复制代码

然后根据require执行代码时需要加上的,那么实际上我们的代码长成这样:

(function(exports, require, module, __filename, __dirname) {
    let count=0
    function addCount(){
        count++
    }
    module.exports={count,addCount}
});
复制代码

require 的时候究竟 module 发生了什么,我们可以在vscode打断点:

读懂CommonJS的模块加载

根据这个断点,我们可以整理出:

黄色圈出来的时 require ,也就是我们调用的方法

红色圈出来的时 Module 的工作内容

Module._compile
Module.extesions..js
Module.load
tryMouduleLoad
Module._load
Module.runMain
复制代码

蓝色圈出来的是nodejs干的事,也就是 NativeModule ,用于执行 module 对象的。

我们都知道在JS中,函数的调用时栈stack的方式,也就是先近后出,也就是说require这个函数触发之后,图中的运行时从下到上运行的。也就是蓝色框最先运行。我把他的部分代码扒出来,研究研究。

NativeModule 原生代码关键代码,这一块用于封装模块的。

NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];
复制代码

NativeModule 触发 Module.runMain 之后,我们的模块加载开始了,我们按照从下至上的顺序来解读吧。

  • Module._load ,就是新建一个 module 对象,然后将这个新对象放入 Module 缓存之中。
    var module = new Module(filename, parent);
    Module._cache[filename] = module;
    复制代码
  • tryMouduleLoad ,然后就是新建的 module 对象开始解析导入的模块内容
    module.load(filename);
    复制代码
  • 新建的 module 对象继承了Module.load,这个方法就是解析文件的类型,然后分门别类地执行
  • Module.extesions..js 这就干了两件事,读取文件,然后准备编译
  • Module._compile 终于到了编译的环节,那么JS怎么运行文本?将文本变成可执行对象,js有3种方法:
    • eval方法 eval("console.log('aaa')")

    • new Function() 模板引擎

      let str="console.log(a)"
      new Function("aaa",str)
      复制代码
    • node执行字符串,我们用高级的 vm

      let vm=require("vm")
      let a='console.log("a")'
      vm.runInThisContext(a)
      复制代码

      这里Module用vm的方式编译,首先是封装一下,然后再执行,最后返回给require,我们就可以获得执行的结果了。

      var wrapper = Module.wrap(content);
      var compiledWrapper = vm.runInThisContext(wrapper, {
          filename: filename,
          lineOffset: 0,
          displayErrors: true
      });
      复制代码

因为所有的模块都是封装之后再执行的,也就说导入的这个模块,我们只能根据 module.exports 这一个对外接口来访问内容。

总结一下

这些代码看的人真的很晕,其实主要流程就是 require 之后解析路径,然后触发 Module 这一个类,然后 Module_load 的方法就是在当前模块中创建一个 新module 的缓存,以保证下一次再 require 的时候可以直接返回而不用再次执行。然后就是这个 新module的load 方法载入并通过VM执行代码返回对象给 require

正因为是这样编译运行之后赋值给的缓存,所以如果export的值是一个参数,而不是函数,那么如果当前参数的数值改变并不会引起export的改变,因为这个赋予export的参数是静态的,并不会引起二次运行。

CommonJs模块和ES6模块的区别

使用场景

CommonJS因为关键字的局限性,因此大多用于服务器端。而ES6的模块加载,已经有浏览器支持了这个特性,因此ES6可以用于浏览器,如果遇到不支持ES6语法的浏览器,可以选择转译成ES5。

语法差异

ES6也是一种JavaScript的规范,它和CommonJs模块的区别,显而易见,首先代码就不一样,ES6的导入导出很直观 importexport

commonJS ES6
支持的关键字 arguments,require,module,exports,__filename,__dirname import,export
导入 const path=require("path") import path from "path"
导出 module.exports = APP; export default APP
导入的对象 随意修改 不能随意修改
导入次数 可以随意 require ,但是除了第一次,之后都是从模块缓存中取得 在头部导入

** 大家注意了!划重点!nodejs是CommonJS的亲儿子,所以有些ES6的特性并不支持,比如ES6对于模块的关键字 importexport ,如果大家在nodejs环境下运行,就等着大红的报错吧~**

加载差异

除了语法上的差异,他们引用的模块性质是不一样的。虽然都是模块,但是这模块的结构差异很大。

在ES6中,如果大家想要在浏览器中测试,可以用以下代码:

//utils.js
const x = 1;
export default x
复制代码
<script type="module">
    import x from './utils.js';
    console.log(x);
    export default x
</script>
复制代码

首先要给 script 一个 type="module" 表明这里面是ES6的模块,而且这个标签默认是异步加载,也就是页面全部加载完成之后再执行,没有这个标签的话代码不然无法运行哦。然后就可以直接写import和export了。

ES6模块导入的几个问题:

  • 相同的模块只能引入一次,比如 x 已经导入了,就不能再从utils中导入 x
  • 不同的模块引入相同的模块,这个模块只会在首次 import 中执行。
  • 引入的模块就是一个值的引用,并且是动态的,改变之后其他的相关值也会变化
  • 引入的对象不可随意斩断链接,比如我引入的 count 我就不能修改他的值,因为这个是导入进来的,想要修改只能在 count 所在的模块修改。但是如果 count 是一个对象,那么可以改变对象的属性,比如 count.one=1 ,但是不可以 count={one:1}

大家可以看这个例子,我写了一个改变object值的小测试,大家会发现 utils.js 中的 count 初始值应该是 0 ,但是运行了 addCount 所以 count 的值动态变化了,因此 count 的值变成了 2

let count=0
function addCount(){
    count=count+2
}
export {count,addCount}
复制代码
<script type="module">
    import {count,addCount} from './utils.js';
    //count=4//不可修改,会报错
    addCount()
    console.log(count);
</script>
复制代码

与之对比的是commonJS的模块引用,他的特性是:

  • 上一节已经解释了,模块导出的固定值就是固定值,不会因为后期的修改而改变,除非不导出静态值,而改成函数,每次调用都去动态调用,那么每次值都是最新的了。
  • 导入的对象可以随意修改,相当于只是导入模块中的一个副本。

如果想要深入研究,大家可以参考下阮老师的 ES6入门——Module 的加载实现

CommonJS模块总结

CommonJS模块只能运行再支持此规范的环境之中,nodejs是基于CommonJS规范开发的,因此可以很完美地运行CommonJS模块,然后nodejs不支持ES6的模块规范,所以nodejs的服务器开发大家一般使用CommonJS规范来写。

CommonJS模块导入用 require ,导出用 module.exports 。导出的对象需注意,如果是静态值,而且非常量,后期可能会有所改动的,请使用函数动态获取,否则无法获取修改值。导入的参数,是可以随意改动的,所以大家使用时要小心。


以上所述就是小编给大家介绍的《读懂CommonJS的模块加载》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

精通正则表达式

精通正则表达式

Jeffrey E. F. Friedl / 余晟 / 电子工业出版社 / 2007 / 75

随着互联网的迅速发展,几乎所有工具软件和程序语言都支持的正则表达式也变得越来越强大和易于使用。本书是讲解正则表达式的经典之作。本书主要讲解了正则表达式的特性和流派、匹配原理、优化原则、实用诀窍以及调校措施,并详细介绍了正则表达式在Perl、Java、.NET、PHP中的用法。 本书自第1 版开始着力于教会读者“以正则表达式来思考”,来让读者真正“精通”正则表达式。该版对PHP的相关内容、Ja......一起来看看 《精通正则表达式》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具