「Node.js系列」深刻浅出Node模块化开发——CommonJS规范

前言

本文将为你们透彻的介绍关于Node的模块化——CommonJS的一切。html

看完本文能够掌握,如下几个方面:前端

  • 什么是模块化,以及没有模块化会带来哪些问题,是如何解决的;
  • JavaScript的设计缺陷;
  • CommonJS规范;
    • 它的规范特性;
    • 如何配合Node完成模块化开发;
    • exports如何导出的;
    • module.exports是如何导出变量的,值类型和引用类型导出间的差别;
    • 从内存角度深度分析module.exportsexports又有怎样的区别和联系;
    • require的细节,以及模块的加载执行顺序;
  • CommonJS的加载过程;
  • CommonJS规范的本质;

PS:本篇文章为「Node.js系列」的第二篇,完全搞懂JS和Node中的模块化机制;

以后会保持每周1~2篇的Node.js文章,欢迎你们和我一块儿学习大前端进阶系列。node

一.什么是模块化?

在不少开发的状况下,咱们都知道要使用模块化开发,那为何要使用它呢?webpack

而事实上,模块化开发最终的目的是将程序划分红一个个小的结构web

  • 在这个结构中编写属于本身的逻辑代码有本身的做用域,不会影响到其余的结构;
  • 这个结构能够将本身但愿暴露的变量函数对象等导出给其结构使用;
  • 也能够经过某种方式,导入另外结构中的变量函数对象等;

上面说提到的结构,就是模块面试

按照这种结构划分开发程序的过程,就是模块化开发的过程;ajax

二.JavaScript设计缺陷

在网页开发的早期,因为JavaScript仅仅做为一种脚本语言,只能作一些简单的表单验证或动画实现等,它仍是具备不少的缺陷问题的,好比:算法

  • var定义的变量做用域问题;
  • JavaScript的面向对象并不能像常规面向对象语言同样使用class;
  • 在早期JavaScript并无模块化的问题,因此也就没有对应的模块化解决方案;

但随着前端和JavaScript的快速发展,JavaScript代码变得愈来愈复杂了;编程

  • ajax的出现,先后端开发分离,意味着后端返回数据后,咱们须要经过JavaScript进行前端页面的渲染
  • SPA的出现,前端页面变得更加复杂:包括前端路由状态管理等等一系列复杂的需求须要经过JavaScript来实现;
  • 包括Node的实现,JavaScript编写复杂的后端程序,没有模块化是致命的硬伤;

因此,模块化已是JavaScript一个很是迫切的需求:后端

  • 可是JavaScript自己,直到ES6(2015)才推出了本身的模块化方案;

  • 在此以前,为了让JavaScript支持模块化,涌现出了不少不一样的模块化规范:AMD、CMD、CommonJS等;

到此,咱们明白了为何要用模块化开发?

那若是没有模块化会带来什么问题呢?

三.没有模块化的问题

当咱们在公司面对一个大型的前端项目时,一般是多人开发的,会把不一样的业务逻辑分步在多个文件夹当中。

3.1 没有模块化给项目带来的弊端

  • 假设有两我的,分别是小豪和小红在开发一个项目
  • 项目的目录结构是这样的

小豪开发的bar.js文件

var name = "小豪";

console.log("bar.js----", name);
复制代码

小豪开发的baz.js文件

console.log("baz.js----", name);
复制代码

小红开发的foo.js文件

var name = "小红";

console.log("foo.js----", name);
复制代码

引用路径以下:

<body>
  <script src="./bar.js"></script>
  <script src="./foo.js"></script>
  <script src="./baz.js"></script>
</body>
复制代码

最后当我去执行的时候,却发现执行结果:

当咱们看到这个结果,有的小伙伴可能就会惊讶,baz.js文件不是小豪写的么?为何会输出小红的名字呢?

究其缘由,咱们才发现,其实JavaScript是没有模块化的概念(至少到如今为止尚未用到ES6规范),换句话说就是每一个.js文件并非一个独立的模块,没有本身的做用域,因此在.js文件中定义的变量,都是能够被其余的地方共享的,因此小豪开发的baz.js里面的name,其实访问的是小红从新声明的。

可是共享也有一点很差就是,项目的其余协做人员也能够随意的改变它们,显然这不是咱们想要的。

3.2 IIFE解决早期的模块化问题

因此,随着前端的发展,模块化变得必不可少,那么在早期是如何解决的呢?

在早期,由于函数是有本身的做用域,因此能够采用当即函数调用表达式(IIFE),也就是自执行函数,把要供外界使用的变量做为函数的返回结果

小豪——bar.js

var moduleBar = (function () {
  var name = "小豪";
  var age = "18";

  console.log("bar.js----", name, age);

  return {
    name,
    age,
  };
})();
复制代码

小豪——baz.js

console.log("baz.js----", moduleBar.name);
console.log("baz.js----", moduleBar.age);
复制代码

小红——foo.js

(function () {
  var name = "小红";
  var age = 20;

  console.log("foo.js----", name, age);
})();
复制代码

来看一下,解决以后的输出结果,原调用顺序不变;

可是,这又带来了新的问题:

  • 我必须记得每个模块中返回对象的命名,才能在其余模块使用过程当中正确的使用;
  • 代码写起来杂乱无章,每一个文件中的代码都须要包裹在一个匿名函数中来编写;
  • 没有合适的规范状况下,每一个人、每一个公司均可能会任意命名、甚至出现模块名称相同的状况;

因此如今急需一个统一的规范,来解决这些缺陷问题,就此CommonJS规范问世了。

四.Node模块化开发——CommonJS规范

4.1 CommonJS规范特性

CommonJS是一个规范,最初提出来是在浏览器之外的地方使用,而且当时被命名为ServerJS,后来为了体现它的普遍性,修改成CommonJS规范

  • Node是CommonJS在服务器端一个具备表明性的实现;

  • Browserify是CommonJS在浏览器中的一种实现;

  • webpack打包工具具有对CommonJS的支持和转换;

正是由于Node中对CommonJS进行了支持和实现,因此它具有如下几个特色;

  • 在Node中每个js文件都是一个单独的模块
  • 该模块中,包含CommonJS规范的核心变量: exports、module.exports、require;
  • 使用核心变量,进行模块化开发;

无疑,模块化的核心是导出导入,Node中对其进行了实现:

  • exports和module.exports能够负责对模块中的内容进行导出
  • require函数能够帮助咱们导入其余模块(自定义模块、系统模块、第三方库模块)中的内容

4.2 CommonJS配合Node模块化开发

假设如今有两个文件:

bar.js

const name = "时光屋小豪";
const age = 18;

function sayHello(name) {
  console.log("hello" + name);
}
复制代码

main.js

console.log(name);
console.log(age);
复制代码

执行node main.js以后,会看到

这是由于在当前main.js模块内,没有发现name这个变量;

这点与咱们前面看到的明显不一样,由于Node中每一个js文件都是一个单独的模块。

那么若是要在别的文件内访问bar.js变量

  • bar.js须要导出本身想要暴露的变量、函数、对象等等;
  • main.jsbar.js引入想用的变量、函数、对象等等;

4.3 exports导出

exports是一个对象,咱们能够在这个对象中添加不少个属性,添加的属性会导出

bar.js文件导出:

const name = "时光屋小豪";
const age = 18;

function sayHello(name) {
  console.log("hello" + name);
}

exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
复制代码

main.js文件导入:

const bar = require('./bar');

console.log(bar.name);  // 时光屋小豪
console.log(bar.age);   // 18
复制代码

其中要注意的点:

  • main.js中的bar变量等于exports对象;
bar = exports
复制代码
  • 因此咱们经过bar.xxx来使用导出文件内的变量,好比name,age;

  • require实际上是一个函数,返回值是一个对象,值为“导出文件”的exports对象;

4.4 从内存角度分析bar和exports是同一个对象

在Node中,有一个特殊的全局对象,其实exports就是其中之一。

若是在文件内,再也不使用exports.xxx的形式导出某个变量的话,其实exports就是一个空对象。

模块之间的引用关系

  • 当咱们在main.js中require导入的时候,它会去自动查找特殊的全局对象exports,而且把require函数的执行结果赋值给bar
  • barexports指向同一个引用(引用地址相同);
  • 若是发现exports上有变量,则会放到bar对象上,正由于这样咱们才能从bar上读取想用的变量;

为了进一步论证,barexports是同一个对象:

咱们加入定时器看看

因此综上所述,Node中实现CommonJS规范的本质就是对象的引用赋值(浅拷贝本质)。

exports对象的引用赋值bar对象上。

CommonJS规范的本质就是对象的引用赋值

4.5 module.exports又是什么?

可是Node中咱们常用module.exports导出东西,也会遇到这样的面试题:

module.exportsexports有什么关系或者区别呢?

4.6 require细节

require本质就是一个函数,能够帮助咱们引入一个文件(模块)中导入的对象。

require的查找规则nodejs.org/dist/latest…

4.7 require模块的加载顺序

结论一: 模块在被第一次引入时,模块中的js代码会被运行一次

// aaa.js
const name = 'coderwhy';

console.log("Hello aaa");

setTimeout(() => {
  console.log("setTimeout");
}, 1000);
复制代码
// main.js
const aaa = require('./aaa');
复制代码

aaa.js中的代码在引入时会被运行一次

结论二:模块被屡次引入时,会缓存,最终只加载(运行)一次

// main.js
const aaa = require('./aaa');
const bbb = require('./bbb');
复制代码
/// aaa.js
const ccc = require("./ccc");
复制代码
// bbb.js
const ccc = require("./ccc");
复制代码
// ccc.js
console.log('ccc被加载');
复制代码

ccc中的代码只会运行一次。

为何只会加载运行一次呢?

  • 每一个模块对象module都有一个属性:loaded;
    • 为false表示尚未加载;
    • 为true表示已经加载;

结论三:若是有循环引入,那么加载顺序是什么?

若是出现下面模块的引用关系,那么加载顺序是什么呢?

  • 这个实际上是一种数据结构:图结构;
  • 图结构在遍历的过程当中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
  • Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb;

多个模块的引入关系

五.module.exports

5.1 真正导出的是module.exports

如下是经过维基百科对CommonJS规范的解析:

  • CommonJS中是没有module.exports的概念的;

  • 可是为了实现模块的导出,Node中使用的是Module的类,每个模块都是Module的一个实例module

  • 因此在Node中真正用于导出的其实根本不是exports,而是module.exports

  • exports只是module上的一个对象

可是,为何exports也能够导出呢?

  • 这是由于module对象的exports属性是exports对象的一个引用;

  • 等价于module.exports = exports = main中的bar(CommonJS内部封装);

5.2 module.exports和exports有什么关系或者区别呢?

联系module.exports = exports

进一步论证module.exports = exports

// bar.js
const name = "时光屋小豪";

exports.name = name;

setTimeout(() => {
  module.exports.name = "哈哈哈";
  console.log("bar.js中1s以后", exports.name);
}, 1000);
复制代码
// main.js
const bar = require("./bar");

console.log("main.js", bar.name);

setTimeout((_) => {
  console.log("main.js中1s以后", bar.name);
}, 2000);
复制代码

在上面代码中,只要在bar.js中修改exports对象里的属性,导出的结果都会变,由于即便真正导出的是 module.exports,而module.exportsexports是都是相同的引用地址,改变了其中一个的属性,另外一个也会跟着改变。

注意:真正导出的模块内容的核心实际上是module.exports,只是为了实现CommonJS的规范,恰好module.exports对exports对象使用的是同一个引用而已

图解module.exports和exports联系

区别:有如下两点

那么若是,代码这样修改了:

  • module.exports 也就和 exports没有任何关系了;

    • 不管exports怎么改,都不会影响最终的导出结果;
  • 由于module.exports = { xxx }这样的形式,会在堆内存中新开辟出一块内存空间,会生成一个新的对象,用它取代以前的exports对象的导出

    • 那么也就意味着require导入的对象是新的对象;

图解module.exports和exports的区别

讲完它们两个的区别,来看下面这两个例子,看看本身是否真正掌握了module.exports的用法

5.3 关于module.exports的练习题

练习1:导出的变量为值类型

// bar.js
let name = "时光屋小豪";

setTimeout(() => {
  name = "123123";
}, 1000);

module.exports = {
  name: name,
  age: "20",
  sayHello: function (name) {
    console.log("你好" + name);
  },
};
复制代码
// main.js
const bar = require("./bar");

console.log("main.js", bar.name); // main.js 时光屋小豪

setTimeout(() => {
  console.log("main.js中2s后", bar.name); // main.js中2s后 时光屋小豪
}, 2000);
复制代码

练习2:导出的变量为引用类型

// bar.js
let info = {
  name: "时光屋小豪",
};

setTimeout(() => {
  info.name = "123123";
}, 1000);

module.exports = {
  info: info,
  age: "20",
  sayHello: function (name) {
    console.log("你好" + name);
  },
};
复制代码
// main.js
const bar = require("./bar");

console.log("main.js", bar.info.name); // main.js 时光屋小豪

setTimeout(() => {
  console.log("main.js中2s后", bar.info.name); // main.js中2s后 123123
}, 2000);
复制代码

main.js输出结果来看,定时器修改的name变量的结果,并无影响main.js中导入的结果。

  • 由于name为值类型,基本类型,一旦定义以后,就把其属性值,放到了module.exports的内存里(练1)
  • 由于info为引用类型,因此module.exports里存放的是info的引用地址,因此由定时器更改的变量,会影响main.js导入的结果(练2)

六.CommonJS的加载过程

CommonJS模块加载js文件的过程是运行时加载的,而且是同步的:

  • 运行时加载意味着是js引擎在执行js代码的过程当中加载模块;
  • 同步的就意味着一个文件没有加载结束以前,后面的代码都不会执行;
const flag = true;

if (flag) {
  const foo = require('./foo');
  console.log("等require函数执行完毕后,再输出这句代码");
}
复制代码

CommonJS经过module.exports导出的是一个对象:

  • 导出的是一个对象意味着能够将这个对象的引用在其余模块中赋值给其余变量;
  • 可是最终他们指向的都是同一个对象,那么一个变量修改了对象的属性,全部的地方都会被修改;

七.CommonJS规范的本质

CommonJS规范的本质就是对象的引用赋值

后续文章

《JavaScript模块化——ES Module》

在下一篇文章中,

  • 会重点讲解ES Module规范的一切;
  • CommonJS和ES Module是如何交互的;
  • 类比CommonJS和ES Module优缺点,如何完美的回答这道面试题;

感谢你们💙

若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
  2. 关注公众号「前端时光屋」,持续为你推送精选好文
  3. 以为不错的话,也能够阅读时光屋小豪近期梳理的文章(感谢掘友的鼓励与支持🌹🌹🌹)
相关文章
相关标签/搜索