@
目录一、模块化
1. 什么是模块化
① JavaScript开发弊端
JavaScript在使用时存在两大问题,文件依赖
和命名冲突
。
比如,js中文件的依赖是不确定的,如下
graph TD A[a.js] B[b.js] C[c.js] D[d.js] D --> C C --> B B --> A上述流程中,b.js 依赖 a.js,可 c.js 却不一定依赖 b.js ; 所以在项目开发中,我们引入一个js功能脚本,脚本功能却不起作用,我们第一时间能想到该脚本缺少依赖,可我们却不能很明显的发现这个依赖。
又比如,js中的变量覆盖问题,如下
graph TD A[a.js num = 10] B[b.js] C[c.js num = 10] D[d.js] D --> C C --> B B --> A上述流程中,在文件依赖不确定的情况下,我们也能知道 c.js 中的num会覆盖 a.js 中的 num,导致程序的不确定性
② 软件中的模块化开发
模块化 是指解决一个 复杂问题 时,自顶向下逐层 把系统划分成若干模块的过程 。对于整个系统来说,模块是可组合、分解和更换的单元。
一个功能就是一个模块,多个模块可以组成完整应用,抽离一个模块不会影响其他功能的运行。
比如:
graph TD A[app.js] --> B[user.js] A --> C[post.js] A --> D[goods.js] B --> E[addUser.js] B --> F[deleteUser.js] D --> G[findGoods.js] D --> H[addGoods.js]上述流程中,主模块 app 下分为三个子模块分分别伪 user 用户管理功能、post文章管理功能、goods商品管理功能,user下又分为addUser添加用户功能、deleteUser删除用户功能;像这样多个单一模块形成一个功能强大的整体,有序而不乱,各自相互不影响,这样就是软件的模块化开发。
总之,编程领域中的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。
把代码进行模块化拆分的好处:
- 提高了代码的复用性
- 提高了代码的可维护性
- 可以实现按需加载
③ 模块化规范
模块化规范 就是对代码进行模块化的拆分与组合时,需要遵守的那些规则。
例如:
⚫ 使用什么样的语法格式来引用模块
⚫ 在模块中使用什么样的语法格式向外暴露成员
模块化规范的好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用,利人利己。
二、Node.js中模块化
Node.js 模块系统 | 菜鸟教程 (runoob.com)
1. Node.js 中模块的分类
Node.js 中根据模块来源的不同,将模块分为了 3 大类,分别是:
- 内置模块(内置模块是由 Node.js 官方提供的,例如 fs、path、http 等)
- 自定义模块(用户创建的每个 .js 文件,都是自定义模块)
- 第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)
2. Node.js 中的模块作用域
① 什么是模块作用域
和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,外部模块无法访问到,这种模块级别的访问限制,叫做 模块作用域。
② 模块作用域的好处
防止了全局变量污染的问题
3. 模块成员的共享
① 模块成员导出
module 对象
在每个 自定义模块中都有一个 module 对象,它里面存储了和当前模块有关的信息,打印如下:
module.exports 对象
在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用。
外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指向的对象。
//01.custom.js
const username = '张三';
function sayHello(){
console.log('你好,我是: '+ username);
}
module.exports.username = username;
module.exports.sayHello = sayHello;
//02.test.js
const custom = require('./01.custom');
console.log(module);
console.log(custom);
global.exports
Node 提供了 exports 对象。默认情况下,exports
是module.exports
的别名(地址引用关系),默认下它们指向同一个空间地址。
//01.custom.js
const username = '张三';
function sayHello(){
console.log('你好,我是: '+ username);
}
const sex = '男';
exports.username = username;
exports.sayHello = sayHello;
module.exports.sex = sex;
//02.test.js
const custom = require('./01.custom');
console.log(module);
console.log(custom);
② 模块成员的导入(模块加载)
global.require()
使用require() **可以加载需要的 内置模块、用户自定义模块、第三方模块 进行使用
// 1. 加载内置 fs模块
const fs = require('fs');
// 2. 加载自定义模块
const custom = require('./custom.js')
// 3. 加载第三方模块
const moment = require('monment');
③ 共享成员时的注意点
require() 模块时,得到的最终结果是 module.exports
指向的对象
-
exports.username = 'zs' module.exports = { gender: '男', age: '22' }
-
module.exports.username = 'zs' exports = { gender: '男', age: '22' }
-
exports.username = 'zs', module.exports.gender = '男'
-
exports = { username: 'zs', gender: '男' } module.exports = exports module.exports.age = '22'
三、npm与包
1.包
① 包的简介
Node.js 中的 第三方模块 又叫做 包。
不同于 Node.js 中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用。
注意:Node.js 中的包都是免费且开源的,不需要付费即可免费下载使用。
由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发的时,效率很低。
包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大的提高了开发效率。
包和内置模块之间的关系,类似于 jQuery 和 浏览器内置 API 之间的关系。
② 包的下载
包的下载网址 : https://www.npmjs.com/
, 该网址由国外公司npm, lnc.所有, 该网址为: 全球最大的包共享平台。
同时供了一个地址为 https://registry.npmjs.org/
的服务器,来对外共享所有的包,可以从这个服务器上下载自己所需要的包
想要下载使用包,需要使用由 npm,lnc 提供的包管理工具(Node Package Manager,简称 → npm包管理工具) ,该工具是直接从 https://registry.npmjs.org/
服务器 把需要的包下载到本地
npm包管理工具不用单独下载,它集成在Node.js中。
终端中执行 npm -v
命令,来查看自己电脑上所安装的 npm 包管理工具的版本号:
③ 包的 “语义化版本规范”
包也会更新迭代,所以包会有对应的版本号,例如:2.24.0
;
如上,包的版本号是以“点分十进制”形式进行定义的,总共有三位数字;
其中每一位数字所代表的的含义如下:
- 第1位数字:大版本
- 第2位数字:功能版本
- 第3位数字:Bug修复版本
版本号提升的规则:只要前面的版本号增长了,则后面的版本号归零。
④ 包的分类
项目包
那些被安装到项目的 node_modules 目录中的包,都是项目包。
项目包又分为两类,分别是:
- 开发依赖包(被记录到 devDependencies 节点中的包,只在开发期间会用到)
- 核心依赖包(被记录到 dependencies 节点中的包,在开发期间和项目上线之后都会用到)
全局包
在执行 npm install 命令时,如果提供了-g
参数,则会把包安装为全局包。
全局包会被安装到 C:\Users\用户目录\AppData\Roaming\npm\node_modules
目录下。
修改全局安装默认目录过程
-
修改全局安装目录
方式一:通过命令行
npm config
配置npm config set prefix “D:\...\nodejs\node_modules\node_global_modules”
npm config set cache “D:\...\nodejs\node_modules\node_cache”
方式二:通过配置文件
.npmrc
配置在nodejs的安装目录中找到
node_modules\npm\.npmrc
文件修改如下即可:
prefix = D:\...\nodejs\node_modules\node_global cache = D:\...\nodejs\node_modules\node_cache
-
配置环境变量,通知电脑系统第三方下载的全局模块路径改变
首先在“系统变量”中新建,新建一个名为NODE_PATH的变量,变量值为:之前新建的用于存放全局模块的文件夹中的node_modules文件夹的绝对路径
D:\...\nodejs\node_modules\node_global_modules\node_modules
在系统变量下的
path
变量添加第三方全局模块安装目录D:\...\nodejs\node_modules\node_global_modules
❕注意:
- 只有工具性质的包,才有全局安装的必要性。因为它们提供了好用的终端命令。
- 判断某个包是否需要全局安装后才能使用,可以参考官方提供的使用说明即可。
⑤ 规范的包结构
一个规范的包,它的组成结构,必须符合以下 3 点要求:
- 包必须以单独的目录而存在
- 包的*目录下要必须包含 package.json 这个包管理配置文件
- package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。
注意:以上 3 点要求是一个规范的包结构必须遵守的格式,关于更多的约束,可以参考如下网址:https://yarnpkg.com/zh-Hans/docs/package-json
2. npm的使用
① 在项目中安装包的命令
如果想在项目中安装指定名称的包,需要运行如下的命令npm install 包的完整名称
可简写命令 npm install → npm i
默认情况下,使用 npm install
命令安装包的时候,会自动安装最新版本的包。
如果需要安装指定版本的包,可以在包名之后,通过@
符号指定具体的版本,例如:npm i moment@2.22.2
② node_modules / package-lock.json
初次装包完成后,在项目根目录自动下新增了 node_modules
的文件夹和 package-lock.json
的配置文件。
node_modules
在项目的开发阶段和线上运营阶段,都需要依赖的第三方包,称为项目依赖,默认这些依赖都下载到项目根目录下 node_modules 文件夹下。
- 项目中
node_modules
一般加载了很多依赖,里面的文件夹以及文件过多过碎,当我们将项目整体拷贝给对方的时候,,传输速度会很慢很慢。 - 复杂的模块依赖关系需要被记录,确保模块的版本和当前保持一致,否则会导致当前项目运行报错
package-lock.json 配置文件
用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。
作用:
- 锁定包的版本,确保再次下载时不会因为包版本不同而产生问题
- 加快下载速度,因为该文件中已经记录了项目所依赖第三方包的树状结构和包的下载地址,重新安装时只需下载即可,不需要做额外的工作
//package-lock.json
{
"name": "description",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {...} //此字段记录了模块依赖关系
}
注意:开发者无需手动修改 node_modules文件夹 和 package-lock.json文件,它们由 npm包管理工具 自动管理维护。
③ package.json
文件简介
npm 规定,在 项目根目录 中,必须提供一个叫做package.json
包管理配置文件。用来记录与项目有关的一些配置信息。例如:
{
//项目名称
"name": "blog",
//项目版本
"version": "1.0.0",
//项目描述
"description": "",
//项目主入口文件
"main": "index.js",
//该对象存储 命令 的别名
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
//项目描述关键字
"keywords": [],
//项目作者
"author": "",
//项目遵循协议, ISC:开放源代码协议
"license": "ISC",
//此字段存储项目开发依赖
"devDependencies": {
"gulp": "^3.9.1",
},
//此字段存储项目的开发与部署阶段需要用到的包
"dependencies": {
"express": "^4.16.4",
"express-art-template": "^1.0.1",
}
}
文件创建
npm 包管理工具提供了一个快捷命令npm init
,可以在执行命令时所处的目录中创建 package.json 这个包管理配置文件,除了包的名称和版本号,其他文件内部参数值需要单独指定,
也可以通过命令npm init -y
快速创建 package.json 这个包管理配置文件,文件内部参数值都为默认值
❕注意:
上述命令只能在 英文 的目录下成功运行!所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不能出现空格。
文件作用
-
项目依赖缺失
项目中的node_modules
第三方包的体积过大!
从而导致,不方便团队成员之间共享项目源代码。共享时剔除node_modules,可这会导致一个问题,当他人获取到你所共享源代码时会发现,项目缺少依赖无法运行!,比如:
此时若项目中没有package.json
文件,就只能去源码中寻找所需要的依赖包,这会相当麻烦
通过项目的package.json
文件能重新找回依赖。
在项目缺失依赖,而缺失的依赖相关信息已被记录到项目的 package.json 中的情况下,根据项目情况:
-
使用
npm install
命令将项目缺失的核心依赖包重新下载。执行 npm install 命令时, npm 包管理工具会先读取 package.json 中的 dependencies 节点, 读取到记录的所有依赖包名之后,npm 会把这些包一次性下载到项目中
-
使用
npm install --production
命令重新下载项目开发包
devDependencies 节点
开发依赖包:某些包只在项目开发阶段会用到,在项目上线之后不会用到,
建议通过命令npm i 包名 --save-dev
把包安装到项目依赖文件夹下,并把包记录到package.json
配置文件的 devDependencies
节点中。
dependencies 节点
核心依赖包:某些包在开发和项目上线之后都需要用到,则建议把这些包记录到 dependencies 节点中,npm i 包名
安装的包就被记录到该节点中。
scripts节点 的用法
- 建立别名替代复杂命令
//package.json
{
//其他信息省略
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
// "别名": "被替代的命令"
"build": "nodemon app.js"
},
}
//app.js
console.log('app.js文件被执行了');
- 通过
npm run 别名
执行被替代命令
D:\...\test\gulp_item\description>npm run build
> description@1.0.0 build D:\...\test\gulp_item\description
> nodemon app.js
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
app.js文件被执行了
[nodemon] clean exit - waiting for changes before restart
③ 包的卸载
可以运行 npm uninstall
命令,来卸载指定的包:
④ 解决下包速度慢的问题
为什么下包速度慢
在使用 npm 下包的时候,默认从国外的 https://registry.npmjs.org/ 服务器进行下载,此时,网络数据的传输需要经过漫长的海底光缆,因此下包速度会很慢。
淘宝 NPM 镜像服务器
淘宝在国内搭建了一个服务器,专门把国外官方服务器上的包同步到国内的服务器,然后在国内提供下包的服务。从而极大的提高了下包的速度。
镜像(Mirroring):是一种文件存储形式,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本即为镜像
切换 npm 的下包镜像源
下包的镜像源,指的就是 下包的服务器地址。
使用 nrm 第三方模块快速切换镜像源
https://blog.csdn.net/weixin_45941499/article/details/120837237#nrm_49
3. 开发属于自己的包
① 明确功能需求
举例实现如下三种功能
// 导入自定义包
const utils = require('gh-utils');
// 功能1:实现日期格式化
const dt = utils.dateFormat(new Date());
console.log(dt);// 输出 2021-01-20 10:09:45
//功能2: 转义 html 中的特殊字符
const htmlStr = "<h1 style='color: red;'>你好!©<span>小黄!</span></h1>"
const str = utils.htmlEscape(htmlStr);
// &It;h1 style="color: red;">你好! &copy;&It;span>小黄!>/span>&It;/h1>
console.log(str);
//功能3: 还原 html 中的特殊字符
const rawHTML = utils.htmlUnEscape(str);
// 输出 <h1 style='color: red;'>你好!©<span>小黄!</span></h1>
console.log(rawHTML);
② 初始化包的基本结构
- 新建项目文件,作为包的根目录
- 在项目文件中,新建如下三个文件:
⚫package.json (包管理配置文件)
⚫index.js (包的入口文件)
⚫README.md (包的说明文档)
③ 初始化 package.json
{
"name": "gh-utils",
"version": "1.0.0",
"main": "index.js",
"description": "该包提供了若干功能",
"keywords": [],
"license": "ISC"
}
关于更多 license 许可协议相关的内容,可参考 https://www.jianshu.com/p/86251523e898
③ 在 index.js中编写功能函数实现功能
function dateFormat (dateStr){/* 省略 */}
function htmlEscape(htmlStr) {/* 省略 */}
function htmlUnEscape(str) {/* 省略 */}
module.exports = {
dateFormat
}
④ 将不同的功能进行模块化拆分
- 将格式化时间的功能,拆分到 src -> dateFormat.js 中
- 将处理 HTML 字符串的功能,拆分到 src -> htmlEscape.js 中
- 在 index.js 中,导入两个模块,得到需要向外共享的方法
- 在 index.js 中,使用 module.exports 把对应的方法共享出去
⑤ 编写包的说明文档
包根目录中的README.md
文件,是包的使用说明文档。通过它,我们可以事先把包的使用说明,以 markdown 的格式写出来,方便用户参考。
README 文件中具体写什么内容,没有强制性的要求;只要能够清晰地把包的作用、用法、注意事项等描述清楚即可。
我们所创建的这个包的 README.md 文档中,会包含以下 6 项内容:
- 安装方式
- 导入方式
- 格式化时间
- 转义 HTML 中的特殊字符
- 还原 HTML 中的特殊字符
- 开源协议
⑥ 发布包
- 注册 npm 账号
- 登录 npm 账号
npm 账号注册完成后,可以在终端中执行npm login
命令,依次输入用户名、密码、邮箱后,即可登录成功。
注意:在运行 npm login 命令之前,必须先把下包的服务器地址切换为 npm 的官方服务器。否则会导致发布包失败! - 把包发布到 npm 上
将终端切换到包的根目录之后,运行npm publish
命令,即可将包发布到 npm 上(注意:包名不能雷同)。
- 删除已发布的包
运行npm unpublish 包名 --force
命令,即可从 npm 删除已发布的包。
注意:
① npm unpublish 命令只能删除 72 小时以内发布的包
② npm unpublish 删除的包,在 24 小时内不允许重复发布
③ 发布包的时候要慎重,尽量不要往 npm 上发布没有意义的包
四、模块加载机制
1. 优先从缓存中加载
模块在第一次加载后会被缓存。 这也意味着多次调用 require()
不会导致模块的代码被执行多次。
注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率。
2. 内置模块的加载机制
内置模块是由 Node.js 官方提供的模块,内置模块的加载优先级最高。
例如,require('fs') 始终返回内置的 fs 模块,即使在 node_modules 目录下有名字相同的包也叫做 fs。
3. 自定义模块的加载机制
使用 require() 加载自定义模块时,必须指定以 ./ 或 ../ 开头的路径标识符。在加载自定义模块时,如果没有指定 ./ 或 ../
这样的路径标识符,则 node 会把它当作内置模块或第三方模块进行加载。
同时,在使用 require() 导入自定义模块时,如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下的文件:
① 按照确切的文件名进行加载
② 补全 .js 扩展名进行加载
③ 补全 .json 扩展名进行加载
④ 补全 .node 扩展名进行加载
⑤ 加载失败,终端报错
4. 第三方模块的加载机制
如果传递给 require() 的模块标识符不是一个内置模块,也没有以 ‘./’ 或 ‘../’ 开头,则 Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在 'C:\Users\test\project\foo.js' 文件里调用了 require('tools'),则 Node.js 会按以下顺序查找:
①C:\Users\test\project\node_modules\tools
②C:\Users\test\node_modules\tools
③C:\Users\node_modules\tools
④C:\node_modules\tools
5. 目录作为模块
当把目录作为模块标识符,传递给 require() 进行加载的时候,有三种加载方式:
① 在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口
② 如果目录里没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件。
③ 如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error: Cannot find module 'xxx'
6. 当模块有路径且有后缀时
require('./find.js');
require('./find');
- require方法根据模块路径查找模块,如果是完整路径,直接引入模块。
- 如果模块后缀省略,先找同名JS文件再找同名JS文件夹
- 如果找到了同名文件夹,找文件夹中的index.js
- 如果文件夹中没有index.js就会去当前文件夹中的package.json文件中查找main选项中的入口文件
- 如果找指定的入口文件不存在或者没有指定入口文件就会报错,模块没有被找到
7. 当模块没有路径且没有后缀时
require('find');
- Node.js会假设它是系统模块
- Node.js会去node_modules文件夹中
- 首先看是否有该名字的JS文件
- 再看是否有该名字的文件夹
- 如果是文件夹看里面是否有index.js
- 如果没有index.js查看该文件夹中的package.json中的main选项确定模块入口文件
- 否则找不到报错
require('find');