• 什么是模块化?


    模块化

    本文主要包含以下知识点:

    • 使用模块的好处
    • 模块加载规则
    • 模块缓存
    • CommonJS 规范
    • 模块原理
    使用模块化的好处
    模块化是一种设计思想,利用模块化可以把一个非常复杂的系统结构细化到具体的功能点,每个功能看
    作一个模块,然后通过某种规则把这些⼩的模块组合到一起,构成模块化系统。
    在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易
    维护。
    为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就
    相对较少,很多编程语言都采用这种组织代码的方式。在 Node.js 环境中,一个 JavaScript 文件就称之
    为一个模块。
    从生产⻆度来看,模块化开发有如下 2 个特点:
    1. 生产效率高
    • 灵活架构,焦点分离
    • 多人协作,互不干扰
    • 方便模块间组合,分解
    2. 维护成本低
    • 可分的单元测试
    • 方便单个模块功能的调试和升级
    模块加载规则
    在 Node.js 中,使用 require() 来进行模块的加载。但是加载模块有一定的加载规则,在介绍具体的
    加载规则之前,我们先来看一下模块的分类。主要可以分为 3 大类:文件模块核心模块以及第三方模
    文件模块
    使用 require() 函数加载文件模块时,需要使用两种模块标识:
    • 以 / 开头的模块标识,指向当前文件所属盘符的跟路径。
    • 以 ./ 或者 ../ 开头的相对路径模块标识。
    加载文件模块的语法如下:
    require("路径.扩展名");
    例如,加载不同路径下的.js文件,其语法如下:
    require("/example.js"); // 如果当前文件在 C 盘,将加载 C:example.js
    require("./example.js"); // 当前目录下的 example.js
    require("../example.js"); // 上一级目录下的 example.js
    在上面的代码中,可以省略文件的扩展名 .js ,写作 require("/example") ,Node.js 会尝试为文件
    名添加 .js , .json , .node 来进行查找。
    核心模块
    核心模块可以看作是 Node.js 的心脏,它由一些精简而高效的库组成,为 Node.js 提供了基本的 API。
    常用的核心模块有:
    • 全局对象
    • 常用工具
    • 事件机制
    • 文件系统访问
    • HTTP 服务器与客户端
    由于 Node.js 的模块机制,这些 Node.js 中内置的核心模块被编译成二进制文件,保存在 Node.js 源码
    的 lib 文件夹下,在本质上也是文件模块,但是在加载方式上与文件模块有所区别。
    核心模块是唯一的,所以在加载核心模块的时候,不需要书写 ./ , ../ 或者 / 这些开头,直接书写模
    块名即可,如下:
    require("模块名");
    例如,Node.js 模块中提供了一个 OS 核心模块,在该模块中提供了一些与操作系统相关的 API,如
    下:
    // 核心模块就是一个固定标识
    // 如果写错就无法加载
    const os = require('os');
    // 输出 CPU 信息
    console.log(os.cpus());
    效果:
    NPM_test node index
    [ { model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
     speed: 2700,
     times:
     { user: 7304340, nice: 0, sys: 6596550, idle: 85524090, irq: 0 } },
    { model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
    speed: 2700,
    times:
    { user: 3011400, nice: 0, sys: 1714130, idle: 94697450, irq: 0 } },
    { model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
    speed: 2700,
    times:
    { user: 7244960, nice: 0, sys: 4380050, idle: 87797960, irq: 0 } },
    { model: 'Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz',
    speed: 2700,
    times:
    { user: 2767370, nice: 0, sys: 1446820, idle: 95208790, irq: 0 } } ]
    第三方模块
    社区或第三方开发的功能模块,这种模块在 Node.js 里面本身没有,需要通过 NPM 的方式下载之后再
    引入。比如要操作 mysql 数据库,则需要引入 mysql 这个模块。
     
    例如:
    const express = require('mysql');
    加载规则
    在了解了模块的分类以后,我们就可以来具体地看一下模块的加载规则了。使用 require() 进⾏模块
    加载时,需要经历 3 个步骤:
    • 路径分析
    • 文件定位
    • 编译执行
    先来看路径分析。
     
    当require() 当中的参数字符串以 ./ 或 ../ 开头,表示按照相对路径,从当前文件所在的文件夹开始
    寻找要载入的模块文件。当 require() 当中的参数字符串以 / 开头,则表示从系统根目录开始寻找该
    模块文件。不能直接写文件名。若在参数字符串当中直接写文件名,则代表载入的是一个模块包,模块
    包必须放在一个特定名字的文件夹当中,即 node_modules。
    使用 require() 来加载文件时可以省略扩展名。比如 require('./module') ,此时会作出如下的匹
    配操作:
    • 按 js 文件来执行(先找对应路径当中的 module.js 文件来加载)
    • 按 json 文件来解析(若上面的 js 文件找不到时,则找对应路径当中的 module.json 文件来加载)
    • 按照预编译好的c++模块来执行(寻找对应路径当中的module.node文件来加载)
    • 若参数字符串为一个目录(文件夹)的路径,则自动先查找该文件夹下的 package.json 文件,然后再加载该文件当中 main 字段所指定的入口文件。
    注:若 package.json 文件当中没有 main 字段,或者根本没有 package.json 文件,则再默认查
    找该文件夹下的 index.js 文件作为模块来载入。

    上面所介绍的是加载文件的路径分析。如果参数字符串不以 ./ , ../ 或 / 开头,说明要加载的不是一个文件,而是一个默认提供的核心模

    块。此时则先在 Node.js 平台所提供的核心模块当中找,然后再寻找 NPM 模块(即第三方模块包,或自己写的模块包)。在寻找 NPM 模块包时,
    会从当前目录出发,向上搜索各级当中的 node_modules 文件夹当中的文件,但若有两个同名文件,则遵循就近原则。
    路径分析完毕后,就会根据路径去定位文件,然后进行编译执行。但是不同类型的模块也存在一个优先
    级的问题。其中 Node.js 的系统模块的优先级最高,一旦有第三方模块包与系统模块重名,则以系统模
    块为准。总的来讲,其优先顺序从上往下依次为:
    • 核心模块,如 http、fs、path
    • 以 . 或 .. 开始的相对路径文件模块
    • 以 / 开始的绝对路径文件模块
    • 非路径形式的文件模块
    模块缓存
    在模块加载的过程中,对于多次加载同一个模块的情况,Node.js 只会加载一次。这是由于第一次加载
    某模块时,Node.js 会缓存该模块,再次加载时将从缓存中获取。所有缓存的模块保存
    在 require.cache 中,可以自动删除模块缓存。下面我们来演示模块的缓存,如下:
    首先在项目根目录下创建 2.js ,代码如下:
    console.log("模块被加载了");
    接下来在 index.js 中使用 require() 方法来引入5次 2.js 文件:
     
    require("./2.js");
    require("./2.js");
    require("./2.js");
    require("./2.js");
    require("./2.js");
    效果:
    模块被加载了
    可以看到,在上述代码中,虽然加载了 5 次 2.js 模块,但是只打印一次"模块被加载了",这就说
    明 2.js 模块只被加载了一次。
    我们可以在 REPL 模式下输入 require 来查看当前的模块缓存情况,如下:
    NPM_test node
    > require
    { [Function: require]
     resolve: { [Function: resolve] paths: [Function: paths] },
     main: undefined,
     extensions:
     { '.js': [Function], '.json': [Function], '.node': [Function] },
     cache: {} }
    此时会返回一个对象,该对象的具体信息如下
     
    require(): 加载外部模块
    require.resolve(): 将模块名解析到一个绝对路径
    require.main: 指向主模块
    require.cache: 指向所有缓存的模块
    require.extensions: 根据文件的后缀名,调用不同的执行函数
    在实际开发中,有些时候开发者并不希望加载的模块被缓存,这个时候可以选择删除缓存操作,在被加
    载的模块下面添加如下的代码:
    //删除指定模块的缓存
    delete require.cache[module.filename];
    // or
    // 删除所有模块的缓存
    Object.keys(require.cache).forEach(function(key) {
     delete require.cache[key];
    })
    这⾥我们来示例一个就好了。例如这里的 2.js 是被加载的模块,所以在该模块的下面添加如下的代
    码:
    console.log("foo模块被加载了");
    delete require.cache[module.filename];
    之后我们再次访问 index.js ,结果如下:
    模块被加载了
    模块被加载了
    模块被加载了
    模块被加载了
    模块被加载了
    可以看到,加载了 2.js 模块后,模块并没有被缓存,所以输出了 5 次"模块被加载了",这说明缓存成
    功被清除了。
    CommonJS 规范
    上面我们所介绍的模块加载机制属于 CommonJS 规范。这⾥简单介绍一下什么是 CommonJS 规范。
    Node.js 并不是第一个尝试使 JavaScript 运行在浏览器之外的项目。追根溯源,在 JavaScript 诞生之
    初,网景公司就实现了服务端的 JavaScript,但由于需要支付一大笔授权费用才能使用,服务端
    JavaScript 在当年并没有像客户端 JavaScript 一样流行开来。真正使大多数人见识到 JavaScript 在服务
    器开发威力的,是微软的 ASP。
    2000年左右,也就是 ASP 蒸蒸日上的年代,很多开发者开始学习 JScript。然而 JScript 在当时并不是很
    受欢迎,一方面是早期的 JScript 和 JavaScript 兼容较差,另一方面微软大力推广的是 VBScript,而不
    是 JScript。随着后来 LAMP 的兴起,以及 Web 2.0 时代的到来,Ajax 等一系列概念的提出,JavaScript
    成了前端开发的代名词,同时服务端 JavaScript 也逐渐被人遗忘。
    直至几年前,JavaScript 的种种优势才被重新提起,JavaScript 又具备了在服务端流行的条件,Node.js
    应运而生。与此同时,RingoJS 也基于 Rhino 实现了类似的服务端 JavaScript 平台,还有像CouchDB、
    MongoDB 等新型非关系型数据库也开始用 JavaScript 和 JSON 作为其数据操纵语言,基于 JavaScript
    的服务端实现开始遍地开花。
    CommonJS 规范与实现
    正如当年为了统一 JavaScript 语言标准,人们制定了 ECMAScript 规范一样,如今为了统一JavaScript
    在浏览器之外的实现,CommonJS 诞生了。CommonJS 试图定义一套普通应用程序使用的 API,从而
    填补 JavaScript 标准库过于简单的不足。
    CommonJS 的终极目标是制定一个像 C++ 标准库一样的规范,使得基于 CommonJS API 的应用程序可
    以在不同的环境下运行,就像用 C++ 编写的应用程序可以使用不同的编译器和运行时函数库一样。为了
    保持中立,CommonJS 不参与标准库实现,其实现交给像 Node.js 之类的项目来完成。
     
    CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、
    控制台(console)、编码(encodings)、⽂件系统(filesystems)、套接字(sockets)、单元测试
    (unit testing)等部分。
    目前大部分标准都在拟定和讨论之中,已经发布的标准有 Modules/1.0、Modules/1.1、
    Modules/1.1.1、Packages/1.0、System/1.0。
    Node.js 是目前 CommonJS 规范最热门的一个实现,它基于 CommonJS 的 Modules/1.0 规范实现了
    Node.js 的模块,同时随着 CommonJS 规范的更新,Node.js 也在不断跟进。由于目前 CommonJS 大
    部分规范还在起草阶段,Node.js 已经率先实现了一些功能,并将其反馈给 CommonJS 规范制定组
    织,但 Node.js 并不完全遵循 CommonJS 规范。这是所有规范制定者都会遇到的尴尬局面,因为规范
    的制定总是滞后于技术的发展。
    模块原理
    Node.js 应⽤是由模块组成的,遵循了 CommonJS 的模块规范,来隔离每个模块的作用域,使每个模
    块在它自身的命名空间中执行。
    CommonJS 规范的主要内容:
    模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当
    前模块作用域中。
    CommonJS 模块的特点:
    • 所有代码运行在当前模块作用域中,不会污染全局作用域
    • 模块同步加载,根据代码中出现的顺序依次加载
    • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,
    • 就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
    一个简单的例子:
    我们在 2.js 中导出一些属性和方法,如下
    module.exports.name = 'Aphasia';
    module.exports.sayHello = function(){
     console.log('Hello World');
    };
    接下来我们就可以在 index.js 中引用该模块,如下:
    const test = require('./2.js');
    console.log(test.name); // Aphasia
    test.sayHello(); // Hello World
    module 对象
     
    根据 CommonJS 规范,每一个文件就是一个模块,在每个模块中,都会有一个 module 对象,这个对
    象就指向当前的模块。 module 对象具有以下属性:
    • id:当前模块的id
    • exports:表示当前模块暴露给外部的值
    • parent: 是一个对象,表示调用当前模块的模块
    • children:是一个对象,表示当前模块调用的模块
    • filename:模块的绝对路径
    • paths:从当前文件目录开始查找node_modules目录;然后依次进入父目录,查找父目录下的
    • node_modules目录;依次迭代,直到根目录下的node_modules目录
    • loaded:一个布尔值,表示当前模块是否已经被完全加载
    下面我们可以在导出的模块和引入的模块中分别打印这个 module 对象,如下:
    2.js 作为导出的模块
    module.exports.name = 'Aphasia';
    module.exports.sayHello = function(){
     console.log('Hello World');
    };
    console.log(module);
    // 打印结果如下
    NPM_test node 2
    Module {
     id: '.',
     exports: { name: 'Aphasia', sayHello: [Function] },
     parent: null,
     filename: '/Users/Desktop/NPM_test/2.js',
     loaded: false,
     children: [],
     paths:
     [ '/Users/Desktop/NPM_test/node_modules',
     '/Users/Desktop/node_modules',
     '/Users/node_modules','/node_modules' ] }
    index.js 引入了 2.js 这个模块,当然自己本身也会存在 module 对象:
    const test = require('./2.js');
    console.log(module);
    // 打印结果如下
    Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/Desktop/NPM_test/index.js',
     loaded: false,
     children:
     [ Module {
     id: '/Users/Desktop/NPM_test/2.js',
     exports: [Object],
    parent: [Circular],
     filename: '/Users/Desktop/NPM_test/2.js',
     loaded: true,
     children: [],
     paths: [Array] } ],
     paths:
     [ '/Users/Desktop/NPM_test/node_modules',
     '/Users/Desktop/node_modules',
     '/User/node_modules','/node_modules' ] }
    从上面的例子我们也能看到, module 对象具有一个 exports 属性,该属性就是用来对外暴露变量、方
    法或整个模块的。当其他的文件 require 进来该模块的时候,实际上就是读取了该模块 module 对象
    的 exports 属性。
    exports 对象
    为了让开发者使用起来更方便,Node.js 还提供了一个 exports 对象。它是一个指向的module.exports 的引用, module.exports 的初始值为一个空对象 {} ,所以 exports 的初始值也是
    一个 {} 。
    我们在 2.js 中打印这个 exports :
    // 通过 module.exports 的方式导出属性
    module.exports.name = 'Aphasia';
    // 直接通过 exports 的方式导出方法
    exports.sayHello = function(){
     console.log('Hello World');
    };
    console.log('module:',module);
    console.log('exports:',exports);
    // 打印结果如下:
    module: Module {
     id: '.',
     exports: { name: 'Aphasia', sayHello: [Function] },
     parent: null,
     filename: '/Users/Jie/Desktop/NPM_test/2.js',
     loaded: false,
     children: [],
     paths:
     [ '/Users/Desktop/NPM_test/node_modules',
     '/Users/Desktop/node_modules',
     '/Users/node_modules','/node_modules' ] }
    exports: { name: 'Aphasia', sayHello: [Function] }
    可以看到该对象就是指向了 module 对象的 exports 属性。
    虽然这 2 种方式都可以对外暴露变量、方法或整个模块的。但是其实两者还是有细微的区别。
    module.exports 可以单独定义,返回数据类型,而exports 只能是返回一个 object 对象。
    例如,我们在 2.js 中将 module.exports 单独定义成一个数组:
    // 现在导出的整个模块不再是一个对象,而是一个数组
    module.exports = ['zhangsan','lisi','wangwu'];
    然后在 index.js 中引入该模块时,也就变成了数组,如下:
    const test = require('./2.js');
    console.log(test); // ['zhangsan','lisi','wangwu']
    但是 exports 就不能单独定义,因为它只能返回一个 object 对象,例如我现在在 2.js 中将 exports
    也单独定义成一个数组:
    // exports 的指向已经被改变,已经切断了 exports 与 module.exports 的联系
    exports = ['zhangsan','lisi','wangwu'];
    然后在 index.js 中引入该模块时,因为始终引入的是 module.exports ,所以仍然为空,如下:
    const test = require('./2.js');
    console.log(test); // {}
  • 相关阅读:
    sqlserver导入cvs文件
    记录搭建redis集群以及使用过程中踩过的坑
    Linux网络管理工具之mtr
    Linux文本处理三剑客之awk学习笔记12:实战演练
    Linux文本处理三剑客之awk学习笔记11:选项、内置变量和内置函数
    Linux文本处理三剑客之awk学习笔记10:函数
    Linux文本处理三剑客之awk学习笔记09:ARGC和ARGV等
    Linux文本处理三剑客之awk学习笔记08:数组
    Linux文本处理三剑客之awk学习笔记06:输出操作
    Linux文本处理三剑客之awk学习笔记05:getline用法详解
  • 原文地址:https://www.cnblogs.com/Yangyecool/p/13337853.html
Copyright © 2020-2023  润新知