NodeJs模块化

199 阅读10分钟

Nodejs 简介

NodeJs 是一个基于 ChromeV8 引擎的 JavaScript 运行环境

NodeJs 安装

nvm文档手册 - nvm是一个nodejs的版本管理工具 (uihtm.com)

模块化

模块化基本概念

编程领域中的模块化,就是遵守固定的规则,把一个大文件拆分成独立的相互依赖的多个小模块

模块的概念

根据模块的来源,可以分为

  • 内置模块(由 nodejs 官方提供,如 fs、path、http)
  • 自定义模块(用户创建的 js 文件)
  • 第三方模块(第三方提供的模块,需要下载)

加载模块

使用require()可以加载内置、自定义、第三方模块

模块作用域

在模块中定义的变量,方法等,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域,模块的作用域防止了全局变量污染问题

向外共享模块作用域中的成员

在每个模块中,都存在一个module对象,通过该对象可以将模块内的成员导出,如下代码

// 将 sayHello 函数绑定到 exports 对象上
module.exports.sayHello = () => {
    console.log('hello')
}
module.exports.username = 'zhang san'
const model = require('./a') // 导入模块 a
model.sayHello() // 如果 a 中的 module 绑定了成员,那么在当前模块中这些成员可以被共享
console.log(model.username)

除了module.exports,还可以直接使用exports对象来简化代码,二者指向的是同一个地址

console.log(exports === module.exports) // true

需要注意的是,require() 一个模块时,得到的永远是module.exports对象,如果在代码中同时对exportsmodule.exports的引用指向做了更改,最终导入的内容,都以module.exorts为准

为了更好的理解,有如下时间格式化的例子,首先定义时间格式化的模块

function formatDate(date) {
    try {
        if (date instanceof Date) {
            const year = date.getFullYear()
            const month = date.getMonth() + 1
            const day = date.getDate()
            const hours = date.getHours()
            const minute = date.getMinutes()
            const second = date.getSeconds()

            return `${year}-${repairZero(month)}-${repairZero(day)} ${hours}:${repairZero(minute)}:${repairZero(second)}`
        } else {
            throw new TypeError('Type error, expects a parameter of type Date')
        }
    } catch (e) {
        console.error(e)
    }
}

/**
 * 补 0
 */
function repairZero(dateNum) {
    return dateNum > 10 ? dateNum : '0' + dateNum
}

// 导出时间格式化函数
module.exports = {
    formatDate
}

引入时间格式化模块

const dateFormat = require('./dateFormat') // 引入模块
console.log(dateFormat.formatDate(new Date())); // 调用格式化函数

NPM & 包

NodeJs 中的第三方模块又叫做包,这些包由第三方的团队或者个人开源到 npm (npmjs.com) 上供开发人员使用,可以使用 npm 包管理工具来下载和管理这些包,npm 包管理工具在安装 nodejs 时已经被自动安装了,使用npm -v可查看其版本

npm 包安装

如果需要安装指定名称的包,使用如下命令(该命令会将包相关文件存放在当前项目下)

npm install 完整包名称

简写形式如下

npm i 包完整名称

moment为例,首先是安装包,如下

npm i moment

安装包后就可以直接进行导入,导入的名称为安装包时的名称,如下

const moment = require('moment')

导入包之后,需要参照官方文档使用,在搜索引擎中搜索moment即可找到对应的官网Moment.js 中文网 (momentjs.cn),之后即可参照官网使用其提供的 api,如下

const moment = require('moment') // 导入包
console.log(moment().format('YYYY-MM-DD HH:mm:ss')); // 格式化时间

首次安装包完成在项目文件夹下创建node_module文件夹和package-lock.josn配置文件

其中node_module由于存放下载的第三方包,使用require()导入包时,也是从该文件夹下查找和加载相关资源

package-lock.json则用于记录node_module目录下每一个包的相关信息,如包名,版本号,下载地址等

node_module文件夹和package-lock.jsonnpm自动维护,开发人员不建议去修改

安装指定版本的包

npm install会自动安装最新的包,如果需要指定版本号,则需要通过@指定具体的版本,如下

npm i moment@2.22.2

如果已经存在对应的包,那么重新执行npm install时会替换对应的包

包的语义化版本规范

包的版本以点分十进制的形式定义,总共三位数字,如2.22.4,其中每一位代表的含义如下

  • 第一位数字:大版本(例如发生底层重构时,则大版本号发生改变)
  • 第二位数字:功能版本(功能上的变动,则功能版本发生改变)
  • 第三位数字:bug 修复版本(修复bug后,bug 修复版本发生改变)

只要前面的版本号增长了,则后面的版本号归0,例如2.22.4的大版本发生改变,则更改后的版本为3.0.0,以此类推

包管理配置文件

npm 规定,在项目的根目录中,必须提供一个package.json的包管理配置文件,用于记录一些项目相关的配置,如

  • 项目名称,版本号,描述等
  • 项目中用到了那些包
  • 那些包会在开发期间用到
  • 那些包会在开发和部署时都需要用到

通过package.json文件最主要的是能够提供对项目的包依赖管理,其他用户在拿到项目时,只需要通过package.json文件就可以自行下载当前项目中的所有依赖包,npm提供了为当前项目快速生成package.json的指令,如下

npm init -y # 该命令只能在英文目录下成功运行,不要出现中文和空格

当项目中存在package.json文件时,之后执行npm install命令时,都会将对应的包名称和版本号记录到package.json文件中

dependencies 节点

首次创建package.json时,如果项目中没有依赖,那么dependencies节点不存在,该节点专门用于记录npm install的包名和版本号,如下

{
  "name": "nodejs-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "moment": "^2.22.2" // 包名为 moment,版本号为 2.22.2
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

一次性安装所有的包

当项目中存在package.json文件时,直接运行npm installnpm i即可一次性安装所有的包依赖

卸载包

使用npm uninstall 包名命令来卸载指定的包,同时对应的package.json中的对应包也会被删除

devDependencies 节点

如果某些包只会在开发阶段用上,那么可以将这些包挂载到devDependencies节点中,否则还是应该挂载到dependencies 节点中,想要挂载到devDependencies中,需要执行如下包安装命令

npm i 包名 -D

包的分类

npm 安装的包主要分为两类,包括项目包全局包,项目包主要在当前项目中使用,而如果在执行npm install命令时,提供了-g参数,如下

npm install 包名 -g

那么该包可以称之为全局包,如果要卸载全局包,则使用如下命令

npm uninstall 包名 -g

注意,只有工具性质的包才需要全局安装

规范的包结构

  • 包必须以单独的目录存在
  • 包的顶级目录下必须包含package.json文件
  • package.json 中必须包含name versino main三个属性,分别包含包名 版本号 包的主入口,其中main中规定的主入口会在require()导入包时被加载

根据以上规范,有如下自定义模块的例子

首先新建目录my-tools,在该目录下初始化一个package.json文件,如下

npm init -y

根据package.json文件中的main属性,在根目录下新建index.js文件

根目录下再新建src目录,该目录用于存放项目代码的 js 文件,如下

/**
 * 时间格式化
 * @param date
 * @return {string}
 */
function formatDate(date) {
    try {
        if (date instanceof Date) {
            const year = date.getFullYear()
            const month = date.getMonth() + 1
            const day = date.getDate()
            const hours = date.getHours()
            const minute = date.getMinutes()
            const second = date.getSeconds()

            return `${year}-${repairZero(month)}-${repairZero(day)} ${hours}:${repairZero(minute)}:${repairZero(second)}`
        } else {
            throw new TypeError('Type error, expects a parameter of type Date')
        }
    } catch (e) {
        console.error(e)
    }
}

/**
 * 补 0
 */
function repairZero(dateNum) {
    return dateNum > 10 ? dateNum : '0' + dateNum
}

module.exports = {
    formatDate
}
/**
 * html 标签替换为转义字符
 * @param htmlStr
 * @return {*}
 */
function htmlEscape(htmlStr) {
    return htmlStr.replace(/<|>|"|&/g, (match) => {
        switch (match) {
            case '<':
                return '&lt;'
            case '>':
                return '&gt;'
            case '"':
                return '&quot;'
            case '&':
                return '&amp;'
        }
    })
}

/**
 * 还原被替换后的 html
 * @param htmlEscapeStr
 * @return {*}
 */
function unEscape(htmlEscapeStr) {
    return htmlEscapeStr.replace(/&lt;|&gt;|&quot;|&amp;/g, (match) => {
        switch (match) {
            case '&lt;':
                return '<'
            case '&gt;':
                return '>'
            case '&quot;':
                return '"'
            case '&amp;':
                return '&'
        }
    })
}

module.exports = {
    htmlEscape,
    unEscape
}

index.js文件中,引入src目录下的相关文件,因为package.json中的main属性规定了index.js作为包的入口,那么在入口中,就可以将需要对外暴露的接口使用module对象导出,代码如下

const dateFormat = require('./src/dateFormat') // 引入自定义的时间格式化js文件
const htmlEscape = require('./src/htmlEscape') // 引入自定义的 html 转移js文件
module.exports = {
    // 导出相关函数
    ...dateFormat, ...htmlEscape
}

发布包

以上完成了一个基本的包结构搭建,现在可以将该包发布到npm,首先需要注册一个npm账号;之后在本地的命令行中执行npm login命令完成登录,注意,登录之前需要将 npm 源设置为官方源,如下

# 设置为官方源
npm config set registry https://registry.npmjs.org/

# 设置为淘宝源
npm config set registry http://registry.npm.taobao.org/

命令行登录时,还需要到邮箱验证一次验证码,登录成功后,会出现Logged in as dhj4612 on https://registry.npmjs.org/.字样

切换到需要发布包项目的根目录下,执行如下命令发布包

npm publish

注意:在这之前,需要检查npm上是否有同名的包,如果有包名冲突的情况,会有如下错误提示

Package name too similar to existing package my_tools; try renaming your package to '@dhj4612/my-tools' and publishing with 'npm publish --access=public' instead

删除以发布的包

如果想要删除已发布的包,可以执行如下命令从(只能删除 72 小时内发布的包删除的包不能在 24 小时内重新发布

npm unpublish 包名 --force

模块的加载机制

优先从缓存中加载,模块在第一次加载后,会被缓存,这也意味着多次调用require()不会导致代码被重复执行多次,不论是内置模块,用户模块或则是第三方模块,都会优先从缓存中加载

内置模块加载的优先级最高,如果非内置模块中同样存在fs模块,那么在加载时,优先加载内置模块中的fs

使用require()加载自定义模块时,必须指定以./../等开头的路径标识符,否则的话,NodeJs 会将其当作内置模块进行加载,例如require('自定义模块')就会被当作内置模块加载

在导入自定义模块时,如果省略了文件的扩展名,NodeJs会按照顺序尝试加载以下文件

  • 按照确切文件名进行加载
  • 补全 .js 的扩展名进行加载
  • 补全 .json 的扩展名进行加载
  • 补全 .node 扩展名进行加载
  • 最终加载失败,终端报错

如果传递给require()的模块标识符没有以./../开头,也不是一个内置模块,那么 NodeJs 会从当前模块的父目录开始,尝试从 /node_module文件夹进行加载,如果依然没有,那么继续移动到上一级目录,直至到达当前项目的根目录

require()中传入的模块标识符为一个目录时,有如下加载方式

  • 在目录中查找package.json文件并寻找main属性作为require()加载的入口
  • 如果没有package.json或者main属性指向的入口文件不存在,则 NodeJs 会试图加载根目录下的index.js文件
  • 以上都失败,则在终端打印错误信息:Error Cannot find module xxx