13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

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

前言

10年积累的成都做网站、成都网站制作经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先网站制作后付款的网站建设流程,更有兴安盟乌兰浩特免费网站建设让你可以放心的选择与我们合作。

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

看完本文可以掌握,以下几个方面:

一.什么是模块化?

在很多开发的情况下,我们都知道要使用模块化开发,那为什么要使用它呢?

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

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

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

二.JavaScript设计缺陷

在网页开发的早期,由于JavaScript仅仅作为一种脚本语言,只能做一些简单的表单验证或动画实现等,它还是具有很多的缺陷问题的,比如:

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

所以,模块化已经是JavaScript一个非常迫切的需求:

到此,我们明白了为什么要用模块化开发?

那如果没有模块化会带来什么问题呢?

三.没有模块化的问题

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

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

假设有两个人,分别是小豪和小红在开发一个项目

项目的目录结构是这样的

小豪开发的bar.js文件

 
 
 
 
  1. var name = "小豪"; 
  2.  
  3. console.log("bar.js----", name); 

小豪开发的baz.js文件

 
 
 
 
  1. console.log("baz.js----", name); 

小红开发的foo.js文件

 
 
 
 
  1. var name = "小红"; 
  2.  
  3. console.log("foo.js----", name); 

引用路径如下:

 
 
 
 
  1.  
  2.    
  3.    
  4.    
  5.  

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

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

究其原因,我们才发现,其实JavaScript是没有模块化的概念(至少到现在为止还没有用到ES6规范),换句话说就是每个.js文件并不是一个独立的模块,没有自己的作用域,所以在.js文件中定义的变量,都是可以被其他的地方共享的,所以小豪开发的baz.js里面的name,其实访问的是小红重新声明的。

但是共享也有一点不好就是,项目的其他协作人员也可以随意的改变它们,显然这不是我们想要的。

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

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

在早期,因为函数是有自己的作用域,所以可以采用立即函数调用表达式(IIFE),也就是自执行函数,把要供外界使用的变量作为函数的返回结果。

小豪——bar.js

 
 
 
 
  1. var moduleBar = (function () { 
  2.   var name = "小豪"; 
  3.   var age = "18"; 
  4.  
  5.   console.log("bar.js----", name, age); 
  6.  
  7.   return { 
  8.     name, 
  9.     age, 
  10.   }; 
  11. })(); 

小豪——baz.js

 
 
 
 
  1. console.log("baz.js----", moduleBar.name); 
  2. console.log("baz.js----", moduleBar.age); 

小红——foo.js

 
 
 
 
  1. (function () { 
  2.   var name = "小红"; 
  3.   var age = 20; 
  4.  
  5.   console.log("foo.js----", name, age); 
  6. })(); 

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

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

所以现在急需一个统一的规范,来解决这些缺陷问题,就此CommonJS规范问世了。

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

3.1 CommonJS规范特性

CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为CommonJS规范。

正是因为Node中对CommonJS进行了支持和实现,所以它具备以下几个特点;

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

3.2 CommonJS配合Node模块化开发假设现在有两个文件:

bar.js

 
 
 
 
  1. const name = "时光屋小豪"; 
  2. const age = 18; 
  3.  
  4. function sayHello(name) { 
  5.   console.log("hello" + name); 

main.js

 
 
 
 
  1. console.log(name); 
  2. console.log(age); 

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

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

这点与我们前面看到的明显不同,因为Node中每个js文件都是一个单独的模块。

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

3.3 exports导出

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出。

bar.js文件导出:

 
 
 
 
  1. const name = "时光屋小豪"; 
  2. const age = 18; 
  3.  
  4. function sayHello(name) { 
  5.   console.log("hello" + name); 
  6.  
  7. exports.name = name; 
  8. exports.age = age; 
  9. exports.sayHello = sayHello; 

main.js文件导入:

 
 
 
 
  1. const bar = require('./bar'); 
  2.  
  3. console.log(bar.name);  // 时光屋小豪 
  4. console.log(bar.age);   // 18 

其中要注意的点:

 main.js中的bar变量等于exports对象;

 
 
 
 
  1. bar = exports 

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

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

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

模块之间的引用关系

为了进一步论证,bar和exports是同一个对象:

我们加入定时器看看

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

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

3.5 module.exports又是什么?

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

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

3.6 require细节

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

require的查找规则https://nodejs.org/dist/latest-v14.x/docs/api/modules.html#modules_all_together

3.7 require模块的加载顺序

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

 
 
 
 
  1. // aaa.js 
  2. const name = 'coderwhy'; 
  3.  
  4. console.log("Hello aaa"); 
  5.  
  6. setTimeout(() => { 
  7.   console.log("setTimeout"); 
  8. }, 1000); 
 
 
 
 
  1. // main.js 
  2. const aaa = require('./aaa'); 

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

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

 
 
 
 
  1. // main.js 
  2. const aaa = require('./aaa'); 
  3. const bbb = require('./bbb'); 
 
 
 
 
  1. /// aaa.js 
  2. const ccc = require("./ccc"); 
 
 
 
 
  1. // bbb.js 
  2. const ccc = require("./ccc"); 
 
 
 
 
  1. // ccc.js 
  2. console.log('ccc被加载'); 

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

为什么只会加载运行一次呢?

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

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

多个模块的引入关系

四.module.exports

4.1 真正导出的是module.exports

以下是通过维基百科对CommonJS规范的解析:

但是,为什么exports也可以导出呢?

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

联系:module.exports = exports

进一步论证module.exports = exports

 
 
 
 
  1. // bar.js 
  2. const name = "时光屋小豪"; 
  3.  
  4. exports.name = name; 
  5.  
  6. setTimeout(() => { 
  7.   module.exports.name = "哈哈哈"; 
  8.   console.log("bar.js中1s之后", exports.name); 
  9. }, 1000); 
 
 
 
 
  1. // main.js 
  2. const bar = require("./bar"); 
  3.  
  4. console.log("main.js", bar.name); 
  5.  
  6. setTimeout((_) => { 
  7.   console.log("main.js中1s之后", bar.name); 
  8. }, 2000); 

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

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

区别:有以下两点

那么如果,代码这样修改了:

图解module.exports和exports的区别

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

4.3 关于module.exports的练习题

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

 
 
 
 
  1. // bar.js 
  2. let name = "时光屋小豪"; 
  3.  
  4. setTimeout(() => { 
  5.   name = "123123"; 
  6. }, 1000); 
  7.  
  8. module.exports = { 
  9.   name: name, 
  10.   age: "20", 
  11.   sayHello: function (name) { 
  12.     console.log("你好" + name); 
  13.   }, 
  14. }; 
 
 
 
 
  1. // main.js 
  2. const bar = require("./bar"); 
  3.  
  4. console.log("main.js", bar.name); // main.js 时光屋小豪 
  5.  
  6. setTimeout(() => { 
  7.   console.log("main.js中2s后", bar.name); // main.js中2s后 时光屋小豪 
  8. }, 2000); 

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

 
 
 
 
  1. // bar.js 
  2. let info = { 
  3.   name: "时光屋小豪", 
  4. }; 
  5.  
  6. setTimeout(() => { 
  7.   info.name = "123123"; 
  8. }, 1000); 
  9.  
  10. module.exports = { 
  11.   info: info, 
  12.   age: "20", 
  13.   sayHello: function (name) { 
  14.     console.log("你好" + name); 
  15.   }, 
  16. }; 
 
 
 
 
  1. // main.js 
  2. const bar = require("./bar"); 
  3.  
  4. console.log("main.js", bar.info.name); // main.js 时光屋小豪 
  5.  
  6. setTimeout(() => { 
  7.   console.log("main.js中2s后", bar.info.name); // main.js中2s后 123123 
  8. }, 2000); 

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

五.CommonJS的加载过程

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

 
 
 
 
  1. const flag = true; 
  2.  
  3. if (flag) { 
  4.   const foo = require('./foo'); 
  5.   console.log("等require函数执行完毕后,再输出这句代码"); 

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

六.CommonJS规范的本质

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

后续文章

《JavaScript模块化——ES Module》

在下一篇文章中,

 【编辑推荐】

  1. TIOBE 11月榜单:Python挤掉了Java!
  2. React Concurrent Mode三连:是什么/为什么/怎么做
  3. Linux高性能网络编程之TCP连接的内存使用
  4. Python曾是程序员的“瑞士军刀”,而如今正被慢慢取代
  5. 全球疫情下的网络安全:警惕“趁火打劫”的线上攻击

本文标题:「Node.js系列」深入浅出Node模块化开发——CommonJS规范
转载来源:http://cdbrznjsb.com/article/coggsce.html

其他资讯

让你的专属顾问为你服务