第二节: 模块化Module

NodeJs 采用模块化方式,管理和组织代码,NodeJS的所有功能都存在每个模块中的


1. 模块和模块化开发的了解

1.1 什么是模块

模块: 一个具有特定功能的文件就是一个模块

模块的优点: 有了模块,我们就可以非常方式使用这些模块,因为模块总是完成了特定的功能,如果要修改模块中个功能,那么只需要修改这个自己的模块文件即可,模块独立于每一个文件中,不影响其他模块的代码

模块之间相互独立,如果一个模块引入另一模块,要使用里面的值,我们就需要在被引入的模块中暴露这些值.


1.2 什么是模块化

模块化: 将一个复杂的程序依据一定的规则(规范)封装成几个模块(文件),并进行组合在一起,每个模块内部数据实现私有的, 只是向外部暴露一些接口(方法)与外部其他模块通信

按照模块化思考开发的项目, 也被称为模块化开发.

模块化的进化

// 全局开发模式
// 最早期所有的js代码卸载一个js文件中
function foo(){}

function bar(){}

// 造成的问题,就是代码量过大以后,Global全局被污染,很容易导致命名冲突

后来对代码进行简单的封装

// 简单封装: Namespac 模式, 就是我们俗称的命名空间
var Namespac = {
    foo: function(){},
    bar: function(){}
}
// 减少Global上的变量数目
// 本质就是对象,不太安全

因为简单封装的不安全,又出现了IIFE模式

// 匿名闭包: IIFE模式
var Module = (function(){
    var foo = function(){
        
    }
    
    return {
        foo: foo
    }
})()

Module.foo()

// 函数是JavaScript 的Local Scope

有的时候,我一个模块可能还需要依赖其他的模块,我们就需要在升级一下,将依赖的模块注入

// 增强封装, 引入依赖
var Module = (function($){
    var $body = $(body);
    var foo = function(){
        
    }
    
    return {
        foo:foo
    }
})($)

// 这就是模块模式,也是现代模块实现的基石


1.3 为什么要模块化
  1. 降低复杂度
  2. 提高解耦性
  3. 部署方便


1.4 模块化的好处
  1. 避免命名冲突(减少命名空间的污染)
  2. 更好的分类,按需加载
  3. 更高的复用性
  4. 高可维护性


2. CommonJS 规范

CommonJS规范为JavaScript制定了一个美好的愿景-希望JavaScript能够在任何地方运行


2.1模块规范分类:
  1. CommonJS(NodeJS)

  2. AMD

  3. CMD

  4. ESModule (ES模块化)


2.2. CommenJS 规范了解

CommonJS规范涵盖了模块,二进制,Buffer,字符集编码,I/O流, 进制环境, 文件系统,单元测试,包管理等
NodeJS借鉴CommonJs的规范来实现了一套易用的模块系统.NPM对Package规范的完好支持是的Node应用在开发过程中事半功倍.

CommonJS 规范 :不是一个语言,也不是一个平台,它只是一个标准,主要是对原有的JavaScript标准API进行了增强.

生态系统.jpg


3. CommonJS 模块规范

CommonJS对模块的定义十分简单,主要分为模块的引用, 模块的定义, 模块的标识3个部分


3.1 模块的引用

在CommonJS规范中, 存在require()方法,这个方法接受模块标识, 以此引用一个模块的API到当前上下文中

const math = require('math')

require(moduleId)函数用于在当前模块中加载和使用别的模块,传入一个模块标识,返回一个模块导出对象

'1':{ 
        [Function: require]
        resolve: { [Function: resolve] paths: [Function: paths] },
        main:
          Module {
              id: '.',
              exports: {},
              parent: null,
              filename: 'C:\\Users\\HiWin10\\Desktop\\study\\app.js',
              loaded: false,
              children: [Array],
              paths: [Array] },
              extensions:[Object: null prototype] { 
                  '.js': [Function], 
                  '.json': [Function], 
                  '.node': [Function] 
              },
              cache:[Object: null prototype] {
                 'C:\\Users\\HiWin10\\Desktop\\study\\app.js': [Module],
                 'C:\\Users\\HiWin10\\Desktop\\study\\wu.js': [Module] 
              } 
         },
}

./.表示当前目录,模块文件路径的js后缀可以省略

var foo = require('./foo'); // 当前目录下面的foo.js文件
var foo2 = require('./foo.js')

导入如果不加后缀名 ,会先找js文件,如果没有js文件 会找json文件

注意:

  1. 引入模块文件有语法错误时会报错
  2. 引入模块不存在时会报错
  3. 重复引入模块只会执行一次(返回值会被缓存起来)
  4. 所有的模块如果没有返回值,导入的模块中将返回空对象
  5. 导入自定义模块必须加'./' 因为在node.js中查找 模块默认是在node_modules目录中查找的
  6. 如果引入第三方模块,直接写模块的名字就可以了require("jquery")


3.1.1 主模块main

require有一个属性main来确定当前模块是不是主模块,应该应用程序中, 通过node启动的模块就是应用模块,也叫主模块

console.log(module == require.main)  // true表示当前模块是主模块, false表示当前模块不是主模块


3.1.2 require.resolve() 方法获取绝对路径

require.resolve()方法 传入一个相对路径参数,返回拼接后的绝对路径

console.log(require.resolve("./module"))


3.1.3 require.cache 属性

require.cache属性值是一个对象,缓存了所有已经被加载的模块

console.log(require.cache)


3.1.4 require.extensions属性

require.extensions属性值是一个对象,用于引入路径省略后缀名时,按照extension标识的后缀查找

console.log(require.extensions)


3.2 模块的定义

在模块上下文提供了exports对象用于导出当前模块的方法或者变量, 并且它是唯一导出出口, 在模块中还存在一个module对象, 它代表模块自身.而exports是module的属性.

3.2.1 查看导出对象

在模块中通过打印当前模块的信息来查看module对象与exports对象

console.log(arguments)
console.log(arguments.callee.toString); // 打印函数体

打印的arguments对象

[Arguments] {
    '0': {},
    '1':{ 
        [Function: require]
        resolve: { [Function: resolve] paths: [Function: paths] },
        main:
          Module {
              id: '.',
              exports: {},
              parent: null,
              filename: 'C:\\Users\\HiWin10\\Desktop\\study\\app.js',
              loaded: false,
              children: [Array],
              paths: [Array] },
              extensions:[Object: null prototype] { 
                  '.js': [Function], 
                  '.json': [Function], 
                  '.node': [Function] 
              },
              cache:[Object: null prototype] {
                 'C:\\Users\\HiWin10\\Desktop\\study\\app.js': [Module],
                 'C:\\Users\\HiWin10\\Desktop\\study\\wu.js': [Module] 
              } 
         },
    '2':
        Module {
            id: '.',
            exports: {},
            parent: null,
            filename: 'C:\\Users\\HiWin10\\Desktop\\study\\app.js',
            loaded: false,
            children: [ [Module] ],
            paths:[
                'C:\\Users\\HiWin10\\Desktop\\study\\node_modules',
                'C:\\Users\\HiWin10\\Desktop\\node_modules',
                'C:\\Users\\HiWin10\\node_modules',
                'C:\\Users\\node_modules',
                'C:\\node_modules' 
            ] 
        },
                                                          
      '3': 'C:\\Users\\HiWin10\\Desktop\\study\\app.js',
      '4': 'C:\\Users\\HiWin10\\Desktop\\study' 
}

打印的函数体

function (exports, require, module, __filename, __dirname) {    
    console.log(arguments.callee.toString())
}

所有用户编写的代码都自动封装一个函数中,函数有五个参数,我们就可以在函数内部使用五个实参


模块中的五个参数

  1. exports 暴露对象,可以将模块中的数据暴露给引入的地方
  2. require 引入模块的函数,用于在一个模块中引入另外一个模块,并且将子模块暴露的数据 赋值给变量
  3. module 模块对象包含了模块的所有信息(当前模块信息)
  4. __filename 当前模块的文件名
  5. __dirname 当前模块所在 的路径


3.2.2 exports 导出对象

exports 对象是当前模块的导出对象,用于导出模块共有的方法和属性,别的模块在通过require函数导入使用当前模块时就会获得当前模块的exports 对象

ps:例子

exports.hello = function(){
    console.log("hello World")
}

注意事项:

  1. exports 是module.exports对象的引用
  2. exports 不能改指向,只能添加属性和方法

如果希望更改暴露指向,那么我 们就需要使用modeule.exports进行暴露

var aa = function(){
    console.log(11)
}
module.exports = aa;


3.2.3 module 模块对象(重中之重)
'2':
    Module {
        id: '.',    // 模块的id ,模块的名称,  .代表主模块(其他模块,id就是模块的文件路径)
        exports: {},    // 当前模块导出的内容(重点,其他了解)
        parent: null,   // 引入当前模块的父模块,
        path: '' ,   // 当前文件夹的例子
        filename: 'C:\\Users\\HiWin10\\Desktop\\study\\app.js', //当前模块文件路径
        loaded: false, 
        children: [ [Module] ],   // 子模块
        paths:[      // 默认模块查找路径,当前目录找不到,就当上一层文件夹找
            'C:\\Users\\HiWin10\\Desktop\\study\\node_modules',
            'C:\\Users\\HiWin10\\Desktop\\node_modules',
            'C:\\Users\\HiWin10\\node_modules',
            'C:\\Users\\node_modules',
            'C:\\node_modules' 
        ] 
    },

module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块导出的对象

module.exports = {}

例如默认的导出对象是一个对象,我们可以改变,让导出对象为普通的函数或其他数据类型的值

module.exports = function(){
    console.log('hello world')
}

module.exports 真正的暴露对象,exports只是对module.exports的引用


3.3 模块标识

模块标识其实就是传递给require()方法的参数.它必须是符合小驼峰命名的字符串, 或者以.,.. 开头的相对路径,或者绝对路径


4. CommonJS模块分类

4.1 模块的分类
  1. 系统模块
    NodeJS开发 团队已经开发了很多功能模块,不需要安装,直接 引入就可以使用
  2. 第三方模块
    第三模块必须先安装在使用
    使用包管理工具npm进行安装
    第三方模块在npmjs.org中搜素
  3. 自定义模块
    自己写的一个js文件就是一个模块



也有将模块分成两种的

  1. 核心模块(系统模块)
  2. 文件模块,(包括第三方模块和自定义模块)


还有按照模块功能按照关系划分为以下两类

  1. 主模块
    主模块就是被node执行的入口模块,通常喜欢命名app.js main.js index.js
    一个项目只能有一个主模块(也叫入口文件,就是整个项目的启动模块)
  2. 依赖模块


4.2 内置模块

NodeJs中的内置了很多模块,可以直接使用require来进行引用.国际惯例,你接受的名字最好和模块的名字一样

const http = reqire('http')

如果特别长可以简写

const qs = require('querystring')

内置模块的引用是无条件的,无路径的


4.3 自定义模块

自定义模块就是自己写的一个模块文件

每一个js文件都是一个模块,Node.js使用commonjs规范

定义a模块

// a.js
var str = '这个是一个字符串'
exports.str = str;   // exports 是导出(也可以叫暴露)对象

定义b模块导入a模块

// b.js
var aa = require('./a.js');
// 此时aa是a模块中暴露对象,如果要使用暴露的字符串str,
console.log(aa.str)

然后通过node执行b.js

node b.js

会发现require()谁,就会执行谁,会让a.js文件执行

注意点:

  1. 会发现require()谁,就会执行谁,会让a.js文件执行
  2. require()引入的模块如果有异步语句,不会死等,会将所有的同步执行完毕后在执行异步语句
  3. 如果是多层引用会先把引入文件里的引入执行干净了
  4. 如果循环引用.A引用B,B引入A,Node会很智能的组织好第二次引入


注意: 每一个模块中的js代码尽在模块第一次使用时执行一次,并在执行过程中初始化模块的导出对象,之后,会将导出对象缓存起来,被重复利用


5. NodeJS 作用域

5.1. 作用域

作用域: 规定一个变量和函数可以使用的范围,作用域分为两种,全局作用域和局部作用域

在Node中,每一个js文件都是单独的作用域,因为node的模块化会将每一个文件中的代码封装在一个函数内部,这和浏览器开发不同,浏览器中,var a,此时a会自动成为window的属性,此时js文件和js文件共享作用域,但是Node.js中,每个js文件是独立的作用域不共享,所以Node中每一个文件中的变量都是私有的.

比如我们编写一下代码

var num = 10

NodeJS会在执行之前,编译这个模块为

function (exports,require,module,__filename,__dirname){
    var num = 10
}


5.2. 暴露(导出)

所以,如果让其他作用域使用本作用域中的值呢.我们采用暴露的方式将值抛出去: 也就是暴露共享的数据

暴露数据

module.exports = { num: 10}

使用exports暴露

// test.js
var a = 100;
exports.a = a;
// exports是一个对象,给这个对象添加了一个属性a,a的值就是本作用域的变量值

此时当我们通过require引入test.js的时候,就可以拿到这个属性啊的值

// b.js
var test = require('./test.js');
// 此时text是引入的对象,就是exports对象,所以如果要获取a的值应该用
// test.a
console.log(test.a)

注意,模块叫test文件,定义接受的变量名也叫test.其他名字不会报错,但是我们不会这么做

使用module.exports 暴露数据

刚才我们使用了exports像外暴露一些值,但是很不方便,这个东西还必须是exports的属性,require()导入的时候,返回的是一个对象我们还得去对象里那属性和方法,如果仅仅只能暴露一个东西,我希望向外暴露一个类,此时就很不方便

// 到处类 Person.js
function Person(name,age,sex){
  this.name = name;
  this.age = age;
  this.sex = sex;
}
exports.Person = Person;

导入

var Person = require(./Person.js);
// 此时的Person是exports对象,如果想使用这个类,得Person.Person()使用
var xiaoming = new Person.Person('小明',12,'男')

此时就会发现使用很不方便

如果js里仅仅只暴露一个东西,我们可以使用module.exports来暴露

这样module.export暴露的是什么,那么require()导入返回的就是什么

module.exports暴露一个东西,exports暴露多个东西

// 到处类 Person.js
function Person(name,age,sex){
  this.name = name;
  this.age = age;
  this.sex = sex;
}
module.exports = Person;

导入

var Person = require(./Person.js);
// 如果采用抵module.exports暴露的,这里Person就是暴露的那个类,所以饿哦们可以直接使用
var xiaoming = new Person('小明',12,'男')

这样在使用的时候就方便很多


5.3. 定义为全局变量

在模块中通过global全局对象定义全局数据

var username = 'wuwei
global.name = username

使用时可以不写global

    console.log(name)


6. 模块的共性

所有的模块化都有一个共性,模块的功能都是把代码放到一个独立的函数中

  1. 模块中使用的var 定义变量都是局部变量
  2. 模块中定义的函数也是局部的
  3. 模块有一个模块对象,包含moduleName(模块名),exports(导出对象)
  4. 如果模块中需要 暴露方法或者属性给外部使用,那么就是像exports对象上添加
  5. 导入一个模块使用require('moduleName'),改方法返回的是模块对象的exports对象
var aa = require('./a.js')


7. 关于路径与后缀名情况

/ 表示绝对路径

./ 表示当前路径

如果在引入模块是不传入后缀名, 会按照.js, .json, .node后缀名顺序查找, 都找不到就报错

如果不写路径则认为是内置模块或各级node_modules文件夹中的第三方模块

你可能感兴趣的:(第二节: 模块化Module)