JavaScript之Node.js(二):模块化、包、NPM与模块的加载机制

一、模块化

1、模块的基本概念
遵守固定的模块化规则,把一个大文件拆分成相互依赖的小模块。
提高代码的复用性;可维护性;按需加载。
模块化规范,对代码进行规范化拆分时要遵循的规则;好处,降低了沟通成本;方便各模块之间相互调用,利人利己。
 
2、模块的分类
根据来源不同,白龙网认为,模块可分三类:
内置模块:node.js官方提供,fs/path/http模块,在安装node.js时已经安装到电脑上的;
自定义模块:用户创建的JS文件,都是自定义模块;
第三方模块:由第三主开发出来的模块,并不是node.js提供,也不是开发人员开发,需要下载才能使用。
 
3、模块加载
require()方法加载内置模块,用户自定义模块(给个路径),第三方模块(与加载内置模块类同)。
当使用require()方法加载它的模块时,会执行被加载模块中对应的代码。
加载用户自定义模块时,可以省略.js的后缀名。
 
4、模块作用域
在自定义模块中,定义的变量,方法成员,只能在当前模块内被访问,这种模块级另的访问限制,叫做模块作用域。其优点:防止全局变量污染的问题。
 
5、module对象
在每个自定义JS中,都有一个module对象,它存储了和当前模块有关的信息。打印该对象,发现它包含的信息如下,其中exports可以导出内容。
Module {
  id: 'C:\Users\sss\Desktop\node\2.js',
  path: 'C:\Users\sss\Desktop\node',
  exports: {},
  filename: 'C:\Users\sss\Desktop\node\2.js',
  loaded: false,
  children: [],
  paths: [
    'C:\Users\sss\Desktop\node\node_modules',
    'C:\Users\sss\Desktop\node_modules',
    'C:\Users\sss\node_modules',
    'C:\Users\node_modules',
    'C:\node_modules'
  ]
}
①module.exports
默认是空对象,可以用该对象把模块内成员贡献出去,供外界使用。
我们在导入另外一个空模块时,默认显示一个空对象{},这是因为,另外一个空模块的默认值是module.exports = {}。
在外界导入一个自定义模块的时候,得到的成员,就是那个模块中,通过module.exports所指向的对象。如果没有使用module.exports贡献成员,则成员在当前模块中为私有,外界无法访问到。
const my = 'SEO';
module.exports.uname = '白龙网';
module.exports.sayHello = function() {
    console.log('对象内的方法');
}
module.exports.my = my;
注意:
当使用require导入模块时,永远以module.exports指向的对象为准;
 
②exports对象
默认情况下,exports对象与module.exports指向同一个对象,最终共享的结果,还是以module.exports指向的对象为准。
const uname = '白龙网';
exports.uname = uname;
exports.age = 20;
exports.say = function() {
    console.log('Hi,白龙网');
}
 
③两者区别
require()模块时,得到的永远是module.exports指向的对象为准。
因此,建议,不要在同一个模块中同时使用exports、module.exports.
 
6、node.js模块化规范
node.js遵循一commonJS模块化规范,commonJS规定模块的特性和模块之间的相互依赖。commonJS规定:
每个模块内部,都有module变量代表当前的模块;
module变量是一个对象,它的exports属性(module.exports)是对外的接口;
加载某个模块,其实就是加载该模块的module.exports属性,require()方法用于加载模块。

二、npm与包

1、包的概述
包,node.js中,第三方模块,叫包。第三方模块与包是同一个概念。
包是由第三方个人或者第三方开发出来的,免费供所有人使用。
node.js中的包都是免费开源的,不需要付费,免费下载使用。
为什么需要包?包是基于内置模块封装出来的,提供了更高级,更方便的API,极大的提高了开发效率。
包与模块之间的关系,类似于JQ与浏览器内置API之间的关系。
 
2、常用命令
包公司:npm inc.
检索包,www.npmjs.com,全球最大的包共享平台,
下载包:https://registry.npmjs.org
node.js安装的时候,已经把npm包管理工具安装成功了,可以使用npm工具下载包。
在powershell中输入命令:npm -v 显示npm管理工具版本号,说明npm安装成功。
显示npm管理工具版本号:npm -v 
安装最新包命令:npm i moment
安装指定版本包命令:npm i moment@2.22.2
更换不同版本包的时候,不需要删除之前的包,后面的命令会覆盖前面命令的操作。
 
3、包与npm格式化日期
(1)用包格式化日期
b.js内容:
//格式化日期函数
function dateFormat(dataStr) {
    const dt = new Date();
    const y = dt.getFullYear();
    const m = padZero(dt.getMonth() + 1);
    const d = padZero(dt.getDate());
    const hh = padZero(dt.getHours());
    const mm = padZero(dt.getMinutes());
    const ss = padZero(dt.getSeconds());
    // return '${y}-${m}-${d} ${hh}:${mm}:${ss}'
    return y + '-' + m + '-' + d + ' ' + hh + ':' + mm + ':' + ss;
}
//补0函数
function padZero(n) {
  return  n > 9 ? n : '0' + n;
}
//导出格式化日期
module.exports = {
    dateFormat//此处不能加分号,否则报错
}
a.js内容:
//导入格式化时间
const time = require('./b');
//调用方法,进行时间格式化
const dt = new Date();
// console.log(dt);
const newTime = time.dateFormat(dt);
console.log(newTime);
 
(2)npm包格式化日期
使用命令:npm i moment安装包,i表示安装install,可以写全名。
具体使用方法,可以到www.npmjs.com上搜索查找。
//1.安装moment包:npm i moment
//2.导入moment包
const moment = require('moment')
//3.格式化日期
const newTime = moment().format('YYYY-MM-DD HH:mm:ss')
console.log(newTime)
 
初次安装包后,在项目文件夹下多了一个叫node_modules的文件夹和package-lock.json的配置文件。
node_modules存放所有已经安装的项目中的包,require()导入第三方包时,就是从这个目录中查找并加载包。
package-lock.json配置文件,用来记录node_modules目录下的每个包的下载信息,例如,包的名子,版本号,下载地址等。
上面两个文件内的内容不能修改。
 
4、包的语义化版本规范
包的版本号有3位数字,如2.22.0中,第一位数字表示大版本,底层重构了,会加1;第二位数字是功能版本;第三位数字是BUG修改版本。
版本号的提升规则,只要前面的版本号增长了,则后面的版本为重置为0.
 
5、包管理配置文件
 
在项目的根目录中,必须要有包管理配置文件package.json,用来记录一些项目有关的配置信息。
多人协作的问题,遇到的问题,第三方包的体积过大,不方便团队成员之间共享项目的源代码。解决方案:共享的时候删除node_modules文件夹。
如何记录项目中安装了哪些包,在项目的根目录中,创建一个叫package.json的配置文件,即可用来记录项目中用了哪些包,从而方便删除node_modules目录之后,在团队成员之间共享项目的源代码。需要注意的是,在项目开发中,一定要把node_modules文件夹,添加到.gitgnore忽略文件中。

(1)创建配置文件
 
使用npm init -y,快速创建package.json这个包管理配置文件;路径与名称为英文,且不能有空格;执行npm i 命令后,npm会自动记录相关信息到package.json中。
 
(2)dependencies节点
该节点专门用来记录你使用npm i命令安装了哪些包,该节点位于package.json配置文件中。如果要安装多个包,在npm i 后面添加多个包的名称即可,每个包的名称之间有空格隔开。
 
(3)一次性安装所有包
当拿到一个删除了node_modules的项目之后,需要先把所有包下载到项目中,否则会报错。
执行npm i命令,npm会根据package.json中记录的包信息,重新安装所有依赖包。
 
(4)卸载包
npm uninstall moment命令,卸载包之后,包会从node_modules中删除,并且会从package.json中的dependencies节点中删除包信息。
 
(5)devDependencies节点
如果某些包只在项目开发阶段会乃至,在项目上线之后不会用到,则建议把这些包记录到devDependencies节点。
在www.npmjs.com中查阅webpack包是可以记录在deDependecies节点中的,因此执行命令:npm i webpack -D来实现,必须添加参数:-D。
 
6、解决下载包速度慢的问题
默认情况下,npm是从https://registry.org国外服务器下载包的,因此下载速度慢。
淘宝NPM镜像服务器,淘宝在辆搭建了一个服务器,专门把国外官方服务器上的包同步到辆的服务器,然后在辆提供下载包的服务,从而极大的提高了下载包的速度。
下包镜像源,是下包的服务器地址。
 
查看当前下包地址:npm config get registry
切换国外服务器到淘宝NPM镜像服务器:npm config set registry=https://registry.npm.taobao.org
 
7、nrm工具nrm
方便切换到淘宝镜像NPM服务器,安装这个工具,利用NRM提供了终端命令,快速查看和切换下包的镜像源。
安装包:npm i nrm -g
查看下包服务器:nrm ls
切换包下载服务器:nrm use taobao
 
8、包的分类
使用npm包管理工具,下载的包,分为两大类,分别是:
(1)项目包:安装到node_modules目录中的包,都是项目包,细分为
      A.开发依赖包:被记录到devDependencies节点,只在开发期间会用到;安装命令是:npm i package_name -D
      B.核心依赖包:被记录到dependencies节点,开发期间与项目上线之后都会用到的,安装命令是:npm i package_name。
(2)全局包:npm i package_name -g,提供了-g参数,就是全局包,安装目录在:C:Users白龙网AppDataRoaming pm ode_modules下。卸载全局包的命令是:npm uninstall package_name -g。
只有工具性质的包,才有全局安装的必要性;参考官方使用说明确认某包是否为全局包。
 
(3)i5ting_toc包
可以把md文件转换为html文件。
安装包:npm i i5ting_toc -g
把md文件转换为html并打开:i5ting_toc -f .day1.md -o
 
9、规范的包结构
以单独的目录而存在;表的顶级目录下包含package.json这个配置文件,文件中必须包含name/version/main这三个属性,分别代表包的名称、版本号、包入口。
 
10、开发属于自己的包
①定义与调用
A.新建一个bailong文件夹,作为包的根目录;
B.在bailong文件夹中新建package.json、index.js、README.md;
C.在package.json文件中输入一个对象包含六个属性:
    {
        "name": "bailong",
        "version": "1.0.0",
        "main": "index.js",
        "description": "格式化日期时间,HTML eSCAPE",
        "keywords": ["bailong","long","bailong"],
        "license": "ISC"
    }
 
D.在index.js中定义封装一个日期格式化的函数+补0函数+导出日期函数
    function dateFormat(dataeStr) {
        const dt = new Date(dataeStr)
        const y = dt.getFullYear()
        const m = padZero(dt.getMonth() + 1)
        const d = padZero(dt.getDate())
        const hh = padZero(dt.getHours())
        const mm = padZero(dt.getMinutes())
        const ss = padZero(dt.getSeconds())
        return y + '-' + m + '-' + d  + ' ' + hh + ':'+ mm + ':'+ ss
    }
    function padZero(n) {
      return  n > 9 ? n : '0' + n
    }
    //定义转义HTML的方法
    function htmlEscape(htmlstr) {
      return htmlstr.replace(/<|>|"|&/g,(match) => {
        switch(match) {
          case '<':
            return '&lt;'
          case '>':
            return '&gt;'
          case '"':
            return '&quot;'
          case '&':
            return '&amp;'
        }
      })
    }
    //定义还原HMTL的方法
    function htmlUnescape(str) {
    return str.replace(/&lt;|&gt;|&quot;|&amp;/g,(match) => {
      switch(match) {
        case '&lt;':
          return '<'
        case '&gt;':
          return '>'
        case '&quot;':
          return '"'
        case '&amp;':
          return '&'
      }
    })
    }
    //导出变量
    module.exports = {
        dateFormat,
        htmlEscape,
        htmlUnescape
    }
 
E.新建文件2.js并导入上述自定义的包,
    const bailong = require('./index')//引号之内,可以是当前目录,也可以是当前目录下的index.js文件;因为package.json中的main属性指定的index.js
    //格式化日期
    const time = bailong.dateFormat(new Date())
    console.log(time);
    console.log('-------------------------------')
    //字符串转义
    const htmlstr = '<h1 title = "白龙网">这是一个H1标签<span>&nbsp</span></h1>'
    const str = bailong.htmlEscape(htmlstr)
    console.log(str)
    console.log('-------------------------------')
    //还原转义字符串
    const str2 = bailong.htmlUnescape(str)
    console.log(str2);
打开终端,执行node 2.js,日期格式化成功
 
②模块化拆分与
 
A.把index.js中的格式化日期方法拆分到scr->dateFormat.js,并暴露该文件
    function dateFormat(dataeStr) {
        const dt = new Date(dataeStr)
        const y = dt.getFullYear()
        const m = padZero(dt.getMonth() + 1)
        const d = padZero(dt.getDate())
        const hh = padZero(dt.getHours())
        const mm = padZero(dt.getMinutes())
        const ss = padZero(dt.getSeconds())
        return y + '-' + m + '-' + d  + ' ' + hh + ':'+ mm + ':'+ ss
    }
    function padZero(n) {
      return  n > 9 ? n : '0' + n
    }
    module.exports = {
        dateFormat
    }
B.把index.js文件中的2个字符串处理方法拆分到src->htmlEscape.js,并暴露该文件
    //定义转义HTML的方法
    function htmlEscape(htmlstr) {
        return htmlstr.replace(/<|>|"|&/g,(match) => {
          switch(match) {
            case '<':
              return '&lt;'
            case '>':
              return '&gt;'
            case '"':
              return '&quot;'
            case '&':
              return '&amp;'
          }
        })
      }
      //定义还原HMTL的方法
      function htmlUnescape(str) {
      return str.replace(/&lt;|&gt;|&quot;|&amp;/g,(match) => {
        switch(match) {
          case '&lt;':
            return '<'
          case '&gt;':
            return '>'
          case '&quot;':
            return '"'
          case '&amp;':
            return '&'
        }
      })
      }
      module.exports = {
        htmlEscape,
        htmlUnescape
      }
C.清空index.js后,分别导入dateFormat.js、htmlEscape.js文件,同时展开运算符date、escape
    const date = require('./src/dateFormat')
    const escape = require('./src/htmlEscpae')
    module.exports = {
      ...date,
      ...escape
    }
 
E.调用拆分后的自定义义
const bailong = require('./index')//引号之内,可以是当前目录,也可以是当前目录下的index.js文件;因为package.json中的main属性指定的index.js
//格式化日期
const time = bailong.dateFormat(new Date())
console.log(time);
console.log('-------------------------------')
//字符串转义
const htmlstr = '<h1 title = "白龙网">这是一个H1标签<span>&nbsp</span></h1>'
const str = bailong.htmlEscape(htmlstr)
console.log(str)
console.log('-------------------------------')
//还原转义字符串
const str2 = bailong.htmlUnescape(str)
console.log(str2);
 
③编写包的使用说明文档
 
一般包括包的安装、功能、协议等等部分,按照markdown的格式布局即可。#数量可以是1-6个,分别代表类似h1-h6的功能。代码块注释以```(tab上面的那个的键)开头,以```结尾。
    # 安装
    ```js
    npm install bailong
    ```
    # 格式化时间
    ``` 定义等待格式化的日期
    const time = bailong.dateFormat(new Date())
    ```
    ``` 显示格式化日期
    console.log(time);
    ```
    # 字符串转义
    ``` 定义一个字符串
    const htmlstr = '<h1 title = "白龙网">这是一个H1标签<span>&nbsp</span></h1>'
    ```
    ``` 转义字符串
    const str = bailong.htmlEscape(htmlstr)
    ```
    ``` 打印字符串
    console.log(str)
    ```
    # 还原转义字符串
    ``` 还原字符串
    const str2 = bailong.htmlUnescape(str)
    ```
    ``` 打印还原的字符串
    console.log(str2);
    ```
    # 开源协议
    ISC 111111
 
11、发布包的流程
 
①在npm官网注册一个帐号,牢记用户名、密码、邮箱。
②通过终端登陆npm帐号,需要输入用户名、密码、邮箱;登陆之前要先把帐号切换到npm官网;并且把路径切换到包所有的根目录。
③然后使用命令npm publish发布包。
同一个包24小时只能发布一次;如果发布错误,可以使用命令npm version patch补丁,然后再运行命令npm publish,即可逐个以补丁的形式添加相应文件。
执行npm unpublish bailong -force命令,可删除72小时发布的包。不要发布无意义的包。所有涉及到的命令如下:
PS C:UsersailongDesktopailong> nrm ls
PS C:UsersailongDesktopailong> nrm use npm
PS C:UsersailongDesktopailong> npm login
PS C:UsersailongDesktopailong> npm publish
PS C:UsersailongDesktopailong> npm unpublish bailong --force

三、模块的加载机制

 
模块会优先从缓存中加载
 
模块在第一次加载后会被缓存,因此,这也意味着多次调用require()不会导致模块的代码被执行多次。
内置模块,自定义模块,第三方模块,都会优先从缓存中加载,从而提升模块的加载效率。
 
1、内置模块的加载机制
 
内置模块是由NODE.JS官方提供的,他的优先级最高。例如,require('fs)始终返回的是fs模块,即使在node_modules目录下有名子相同的包也叫做fs。使用require()加载时,指定模块名即可:const str = require('moment)。
 
2、自定义模块的加载机制
 
使用require()加载自定义的模块时,必须指定./或者../开头的路径标识符,否则就会报错。在加载自定义的模块时,如果没有指定./或者../这样的路径标签符,则node会把它当作内置模块或者第三方模块进行加载,找不到时,就会报错。
 
如果在使用require()导入自定义模块时,省略了文件扩展名,则node.js会按顺序分别尝试加载以下的文件:
A.按照确切的文件名进行加载
B.补全.js扩展名进行加载;
C.补全.json扩展名进行加载;
D.补全.node扩展名进行加载;
E.加载失败,终端报错。
 
3、第三方模块加载机制
 
如果传递给require()的模块不是一个内置模块,也没有以./ ../开头,则node.js会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到上一层父目录,进行加载,直到文件系统的根目录。
 
4、目录作为模块
 
当目录作为模块标识符,传递给require()进行加载的时候,有三种方式:
A.在被加载的目录下查找一个叫做package.json的文件,并寻找main属性,作为require()的入口;
B.如果目录没有package.json文件,或者main入口不存在无法解析,则node.js针将会试图加载目录下的index.js文件;
C.如果上述两步都失败了,则node.js会在终端打印错误消息,报告模块缺失:error: can not find modules 'xxx'。