Skip to content

Node.js 基础

Node.js 是一个基于 Chrome V8 引擎开发的 JS 运行环境,它包含 fs、http 等专有 API,不包含 BOM、DOM 等 WebAPI。

当 JS 代码运行在 Node.js 中时,它作为后端存在。你可以把 Node.js 视为一个专门运行在服务器上的浏览器,只不过不包含 WebAPI,但是实际上更强大了。因为里面内置的新的模块大大强化了 JS 的能力。

为了编写 JS 服务端程序,我们需要先安装 Node.js。

开发时可以多多参考官方文档

运行第一个程序

对于任意的单文件 JS:

js
console.log("Hello, world!");

可在终端中使用:

txt
node 文件名.js

来运行。

fs 模块

fs 模块是 Node.js 中用于操作文件的模块,它提供了一系列方法和属性。

想要使用该模块,需要先导入:

js
const fs = require("fs"); // CommonJS 风格,你也可以使用 ES Module 风格

该模块常用两种方法:

方法名简介
readFile(path[, options], callback)读取指定文件的内容。options 表示编码格式,默认是 utf8callback 表示存放结果的回调函数。这里的回调函数要求接收两个参数为错误时产生的对象成功时得到的字符串
writeFile(path, data[, options], callback)覆写指定内容到指定文件。这里的回调函数要求接收一个参数为错误时产生的对象。若文件不存在,仍可以正常写入;但若路径不存在,则会失败。

TIP

在执行 IO 操作时,Node.js 总是以命令执行的目录与代码中的相对路径拼接。因此我们如果我们不在 JS 文件所在的目录执行其,所有用到相对路径的地方可能都会出错。除非你使用:

js
__dirname; // 变量,表示当前文件所处的目录

来拼接相对路径为动态绝对路径。

案例:复制文件

现有 UTF-8 文本文件 Jiaojiao.txt

txt
我真的会谢

请使用 fs 模块的读写文件方法复制该文件为 Qingtong.txt

js
const fs = require("fs");

fs.readFile(__dirname + "\\Jiaojiao.txt", "utf8", function (err, dataStr) {
  if (!err)
    // 若错误对象为空,则表明读取成功
    fs.writeFile(__dirname + "\\Qingtong.txt", dataStr, "utf8", function (err) {
      if (!err) console.log("复制成功!");
      else console.log("写入阶段出错!" + err);
    });
  else console.log("读取过程出错!" + err);
});

path 模块

path 模块是 Node.js 中用于处理路径的模块,它提供了一系列方法和属性。

使用前需要导入模块:

js
const path = require("path");

该模块常用三种方法:

方法名简介
join([...paths])将多个路径片段拼接为完整的路径字符串。
basename(path[, ext])获取路径中的最后一部分。常用其获取路径中的文件名。ext 表示文件扩展名,若(正确)填写则表示不需要返回文件名的扩展名部分。
extname(path)获取路径中的扩展名部分。

在涉及路径操作的时候,要尽可能使用这些方法,因为它们比原始的字符串处理更可靠。

案例:在父目录创建文件

要求利用 fs 和 path 模块,并有良好的可移植性和容错性来完成需求。

js
const fs = require("fs");
const path = require("path");

const father = path.join(__dirname, "../"); // 回到上一级
fs.writeFile(father + "\\Sign.txt", "Success.", function (err) {
  if (err) console.log("这怎么可能出错?但是真的出错了。");
  else console.log("创建成功!");
});

http 模块

TIP

阅读本节前,需要具备计算机网络技术的知识。

http 模块是 Node.js 中用于创建 Web 服务器的模块,它提供了一系列方法和属性。这使得我们可以抛弃 IIS、Apache 等狭义后端服务器软件。

使用前需要导入模块:

js
const http = require("http");

获取服务器实例:

js
const server = http.createServer();

服务器需要监听来自客户端的请求:

js
server.on("request", (req, res) => {
  // 使用服务器实例的 on() 方法注册“请求”监听
  // 当接收到请求,就会触发...
});

req 包含了来自客户端的属性,如所请求的地址(url)、请求方式(method)......res 包含了服务器的方法,如设置响应头(setHeader())、响应请求(end())。

服务器在 80 端口上启动:

js
server.listen(80, () => {
  // 启动后要做的事...
});

案例:网站服务器设计

仿照 Apache Tomcat 设计简单的网站服务器,向外部提供 Web 服务。

js
const fs = require("fs");
const path = require("path");
const http = require("http");

const server = http.createServer();
server.on("request", (req, res) => {
  const goalFile = req.url == "/" ? "/index.html" : req.url;
  const goalPath = path.join(__dirname, goalFile);
  fs.readFile(goalPath, "utf-8", (err, dataStr) => {
    console.log(
      new Date().toLocaleString() +
        " " +
        req.headers["host"] +
        " tries to visit " +
        goalFile.slice(1),
    );
    res.setHeader("Content-Type", "text/html; charset=utf-8"); // 告知浏览器编码格式
    if (err) return res.end("404 not found!");
    res.end(dataStr);
  });
});
server.listen(80, () => {
  console.log("Server is running on http://127.0.0.1/");
});

模块管理

Node.js 内置了许多模块,我们可以直接导入。我们编写的的每一个 JS 文件,也被视为模块,可用相仿的办法导入:

js
const userModule = require("./user-module.js");

若我们的模块文件名不与已有内置模块冲突,则可以不用加扩展名:

js
const um = require("./user-module");

模块内有许多成员,各个模块间的成员不互相流通,这样避免了全局变量污染。如果想要共享当前模块的部分成员,则需要设置向外暴露export):

TIP

CommonJS 中,每个模块都含一个隐式对象 module,它记载了模块本身的一些属性。我们可以使用 module.exports 共享模块内的指定成员。外界导入该模块时,得到的就是 module.exports 所指向的成员。

js
module.exports.demoAttr1 = "abcd"; // 暴露的属性
module.exports.demoFunc1 = function () {
  // 暴露的函数
  console.log("你干嘛哎哟");
};
const demoAttr2 = 1234;
module.exports.demoAttr2 = demoAttr2; // 暴露的已有属性

也可以让 module.exports 直接指向一个匿名对象,但这会覆盖掉之前所有的挂载操作:

js
module.exports = {
  demoAttr3: "efgh",
  demoFunc2() {
    console.log("Amagi");
  },
  demoAttr4: 5678,
};

TIP

Node.js 提供了隐式对象 exports,它直接指向 module.exports。因此若使用 exports,只能依次挂载成员,不能使其直接指向匿名对象,否则将覆盖掉映射。

以上这些规范是 CommonJS 的大致内容。CommonJS 是在 ESM 尚未出世前,社区通用的一套模块标准。虽然现在正在逐渐被“正规军”替代,但仍有必要了解。下表演示了 CJS 与 ESM 的对比:

CJSESM
编译同步异步
模块导出值的情况拷贝引用
导入模块const fs = require('fs')import fs from 'fs'
导出成员module.exports.demoAttr = 'abc'

module.exports = {
demoAttr: 'abc'
}
export const demoAttr = 'abc'

为了追随主流,之后的内容统一采用 ESM 写法。

包与 NPM

除了 Node.js 内置了许多模块、我们自行编写的模块,还有很多第三方模块,它们被称为。包是对内置模块的封装,一般提供了更高级、更方便的 API。

我们可以在 NPM(Node Package Manager)上获得各种各样的包(因为人人都可以上传包到上面),它在我们安装 Node.js 时就一同被部署了。

在工程目录下执行命令来部署包:

txt
npm install 包名[@版本]

txt
npm i 包名[@版本]

后面可以添加参数 -S(全过程,默认)或 -D(仅开发时)来决定模块的生存周期。部署完成后就可以像调用内置模块那样来操作了。

如果我们需要发布自己的包,要注意版本号的规范:

  • 第一部分数字:大版本。当底层被重构时可以+1。
  • 第二部分数字:功能版本。当新功能被添加时可以+1。
  • 第三部分数字:错误修复版本。当有错误被修复时可以+1。
  • 当靠前的版本号自增了,靠后的版本号要归零。

遵从 CC BY-NC-SA 4.0