Node.js基础

Node.js 简介

Node.js是一个基于 Chrome V8 引擎的 JavaScript 运行时。

Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。Node.js 的包管理器 npm,是全球最大的开源库生态系统。

  • 单线程
  • 非阻塞I/O
  • 事件驱动

三个关键点解释:

  1. 单线程
    • Node.js 是单线程的,这意味着它在任何给定的时间只能执行一个任务。
    • 但是,Node.js 使用了事件循环来实现非阻塞 I/O 操作,因此单线程并不意味着 Node.js 无法处理并发请求。
    • 单线程使得编程模型更加简单,避免了多线程编程中可能出现的竞态条件和死锁等问题。
  2. 非阻塞 I/O
    • Node.js 使用了非阻塞 I/O 模型,这意味着在进行 I/O 操作时,不会阻塞整个进程或线程,而是将 I/O 操作委托给底层操作系统,并通过回调函数来处理操作完成后的结果。
    • 由于 Node.js 是单线程的,因此在等待 I/O 操作完成的同时,可以继续执行其他任务,从而提高了系统的并发能力和性能。
  3. 事件驱动
    • Node.js 的核心是事件驱动的,它基于事件循环机制来处理请求和响应。
    • 当有请求到达时,Node.js 将触发相应的事件,并调用事先注册的回调函数来处理请求。
    • 通过事件驱动的方式,Node.js 可以处理大量的并发请求,而不会因为等待某个操作完成而阻塞整个进程。

这些特点使得 Node.js 成为一种高效、可扩展的服务器端运行环境,特别适用于处理大量并发请求的场景,如 Web 服务器、实时通信应用等。

npm: 包管理器

npm(Node Package Manager)是 Node.js 的默认包管理器,用于管理 JavaScript 项目中的依赖项、安装第三方模块、以及管理项目的版本等。下面对 npm 的一些关键特点进行详细说明:

  1. 包管理
    • npm 提供了一个庞大的开源库生态系统,其中包含了数以百万计的开源 JavaScript 包。开发者可以通过 npm 快速搜索、安装、更新和发布自己的 JavaScript 包。
    • 使用 npm 可以轻松管理项目的依赖项,将项目所需的模块以及它们的版本信息记录在项目的 package.json 文件中,从而确保项目的可重复性和可移植性。
  2. 命令行工具
    • npm 提供了一组命令行工具,用于执行各种与包管理相关的操作。常用的 npm 命令包括:
      • npm install:安装项目的依赖项。
      • npm update:更新项目的依赖项。
      • npm uninstall:卸载项目的依赖项。
      • npm init:初始化一个新的 npm 项目,生成 package.json 文件。
      • npm publish:发布自己的包到 npm 公共仓库。
      • npm search:搜索 npm 仓库中的包。
      • 等等,还有更多命令可以用来管理包和项目。
  3. 本地缓存
    • npm 会将下载的包缓存在本地,避免了重复下载相同的包,提高了包的获取速度。
    • 缓存的位置通常位于用户目录的 .npm 文件夹中。
  4. 版本管理
    • npm 使用语义化版本控制规范(Semantic Versioning),允许开发者指定所需的模块版本范围,以便在安装或更新依赖时自动解析依赖关系并下载符合版本要求的模块。
    • npm 还提供了一些命令和工具,用于管理和控制包的版本,如 npm version 命令和 npm shrinkwrap 命令。
  5. 社区支持
    • npm 拥有一个庞大的社区和活跃的贡献者群体,开发者可以通过 npm 发现新的模块、参与社区讨论、解决问题等。

包和模块

什么是模块

当我们谈论模块时,通常是指一个独立的、可重用的代码单元,它具有特定的功能或提供特定的服务。在 Node.js 中,模块是一种组织和封装 JavaScript 代码的方式,用于促进代码的模块化开发和管理。

Node.js 的模块系统基于 CommonJS 规范,它允许开发者将代码拆分成多个文件,并通过 require() 函数将这些文件组织起来,使其在程序中可以相互引用和调用。下面详细解释一下模块的概念和 Node.js 中模块的加载规则:

  1. 模块的定义
    • 在 Node.js 中,一个模块通常可以是一个 JavaScript 文件,或者是一个包含了 package.json 文件的文件夹(也称为包)。每个模块都有自己的作用域,避免了变量污染和命名冲突的问题。
  2. 模块的加载
    • 当使用 require() 函数加载一个模块时,Node.js 会根据传入的参数查找对应的模块文件或包。
    • 如果传入的是一个文件路径,则 Node.js 会尝试加载对应的 JavaScript 文件。如果路径指向一个文件夹,则 Node.js 会尝试加载该文件夹下的 index.jsindex.node 文件。
    • 如果传入的是一个模块名,而非文件路径,则 Node.js 会依次查找当前目录下的 node_modules 文件夹和父级目录中的 node_modules 文件夹,直到找到对应的模块文件或包。
  3. package.json 文件
    • 如果一个模块是一个文件夹,并且该文件夹下包含了 package.json 文件,那么 Node.js 会根据 package.json 文件中的 main 字段来确定模块的入口文件。
    • 如果 main 字段未指定或无法解析,则 Node.js 将尝试加载该文件夹下的 index.jsindex.node 文件。
  4. 模块的导出
    • 模块可以通过 module.exportsexports 对象导出变量、函数、类等,使其在其他模块中可以引用和使用。
    • 在模块内部,使用 module.exportsexports 对象导出内容;在其他模块中,使用 require() 函数引入模块,并使用导出的内容。

示例 1:导出变量

1
2
3
// 在 myModule.js 中定义一个变量并导出
var message = "Hello, world!";
module.exports = message;
1
2
3
// 在 main.js 中引入并使用导出的变量
var msg = require('./myModule');
console.log(msg); // 输出:Hello, world!

示例 2:导出函数

1
2
3
4
5
// 在 myModule.js 中定义一个函数并导出
function greet(name) {
return "Hello, " + name + "!";
}
module.exports = greet;
1
2
3
// 在 main.js 中引入并使用导出的函数
var greet = require('./myModule');
console.log(greet("John")); // 输出:Hello, John!

示例 3:导出对象

1
2
3
4
5
6
// 在 myModule.js 中定义一个对象并导出
var person = {
name: "Alice",
age: 30
};
module.exports = person;
1
2
3
4
// 在 main.js 中引入并使用导出的对象
var person = require('./myModule');
console.log(person.name); // 输出:Alice
console.log(person.age); // 输出:30

示例 4:导出类

1
2
3
4
5
6
7
8
9
10
11
// 在 myModule.js 中定义一个类并导出
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return "Hello, " + this.name + "!";
}
}
module.exports = Person;
1
2
3
4
// 在 main.js 中引入并使用导出的类
var Person = require('./myModule');
var person = new Person("Bob", 25);
console.log(person.greet()); // 输出:Hello, Bob!

什么是包

简单的讲,有package.json文件的模块就是一个包。

  • 谈论 Node.js 中的包时,通常是指具有以下特征之一的模块:

    1. 包含 package.json 文件的文件夹
      • 如果一个文件夹中包含了 package.json 文件,那么这个文件夹就可以被视为一个包。
      • package.json 文件描述了包的元数据,包括名称、版本、依赖关系、入口文件等信息。
    2. 压缩文件
      • 如果一个包被打包成了压缩文件(通常是 .tar.gz.zip 格式),那么这个压缩文件也可以被视为一个包。
      • 压缩文件中必须包含一个有效的 package.json 文件,以描述包的元数据。
    3. URL
      • 如果一个 URL 可以解析为一个有效的压缩文件,而且该压缩文件包含了有效的 package.json 文件,那么这个 URL 也可以被视为一个包。
    4. npm 仓库中的包
      • 在 npm 仓库中发布的包,可以通过 <name>@<version><name>@<tag> 的形式访问。
      • <name> 是包的名称,<version> 是包的版本号,<tag> 是包的标签,如 latest
    5. Git URL
      • 如果一个包使用 Git 进行版本控制,并且 Git 仓库中包含了有效的 package.json 文件,那么这个 Git URL 也可以被视为一个包。
      • 通常,从 Git URL 克隆或拉取代码时,会得到一个包含有效 package.json 的文件夹,从而视为一个包。

    一个包就是一个可以被加载、引用和使用的模块单元,它具有清晰的元数据描述,便于管理和发布。

npm常用命令

  • npm init:创建一个package.json文件,可用于设置新的或现有的 npm 包。

  • npm install:安装一个包。此命令安装一个包和它所依赖的任何包。未指定包名时将安装以下文件中的包(有优先级顺序):

    • npm-shrinkwrap.json
    • package-lock.json
    • yarn.lock

    可选参数:

    • -g, --global: 使用该参数则全局安装,默认是只在当前目录安装。
    • -P, --save-prod: 包名将出现在你的 dependencies 里。 这是默认的,除非使用了 -D-O 参数。
    • -D, --save-dev: 包名 将出现在你的 devDependencies
    • -O, --save-optional: 包名将出现在你的 optionalDependencies
    • --no-save: 阻止包名出现在 dependencies
  • npm uninstall: 删除包。

  • npm run <command>: 运行任意包脚本。简单的讲就是运行写在scripts下的<command>对应的命令。如果没有指定<command>,将列出所有可用脚本命令。

以下是一些常见的 npm run 命令的示例:

  1. 运行预定义脚本
1
2
3
4
5
6
7
8
// package.json
{
"scripts": {
"start": "node server.js",
"build": "webpack --config webpack.config.js",
"test": "jest"
}
}
1
2
3
4
5
6
7
8
# 运行 start 脚本
npm run start

# 运行 build 脚本
npm run build

# 运行 test 脚本
npm run test
  1. 自定义脚本
1
2
3
4
5
6
7
// package.json
{
"scripts": {
"lint": "eslint .",
"format": "prettier --write ."
}
}
1
2
3
4
5
# 运行 lint 脚本进行代码风格检查
npm run lint

# 运行 format 脚本格式化代码
npm run format
  1. 串行执行多个脚本
1
2
3
4
5
6
// package.json
{
"scripts": {
"build": "npm run lint && npm run format && npm run compile"
}
}

这将依次执行 lint、format 和 compile 脚本。

  1. 并行执行多个脚本
1
2
3
4
5
6
// package.json
{
"scripts": {
"build": "npm-run-all --parallel lint format compile"
}
}

这将同时执行 lint、format 和 compile 脚本,而不是依次执行。需要使用 npm-run-all 或类似工具。

CommonJS

CommonJS 是一种 JavaScript 模块规范,旨在解决 JavaScript 在不同环境下模块化开发的问题。它提供了一种标准的模块定义和导入导出方式,使得 JavaScript 代码可以更加模块化、可维护和可重用。Node.js 是 CommonJS 规范的一个主要实现者,它使用了 CommonJS 规范来组织和管理模块。

CommonJS 的特点:

  1. 模块定义:使用 module.exports 导出模块,使用 require() 函数导入模块。
  2. 同步加载:模块在引入时是同步加载的,即在代码执行前会先加载所有依赖的模块。
  3. 模块缓存:Node.js 会缓存已加载的模块,避免重复加载,提高性能。

示例:

假设有两个模块 math.jsapp.js,分别表示数学库和应用程序:

  1. math.js
1
2
3
4
5
6
7
8
// math.js
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

module.exports = {
add,
subtract
};
  1. app.js
1
2
3
4
5
// app.js
const math = require('./math');

console.log(math.add(2, 3)); // 输出:5
console.log(math.subtract(5, 2)); // 输出:3

在这个示例中,math.js 导出了两个函数 addsubtract,而 app.js 使用 require() 函数导入 math.js 模块,并调用了其中的函数。这种模块导入导出方式就是 CommonJS 规范的体现。