从零开始,你必须要懂的 Node.js

187 阅读15分钟

我先来列一个提纲吧,要了解 NodeJS,你到底需要了解它的什么!

  1. 基本语法和概念:理解 Node.js 是什么,它的工作原理,以及它与浏览器中的 JavaScript 有何不同。
  2. 异步编程:因为 Node.js 是基于事件驱动的非阻塞 I/O 模型,所以你需要对异步编程有深入的理解。这包括回调函数,Promises,async/await 等。
  3. 核心模块:Node.js 提供了许多内置模块,如 httpfspathquerystringurlstream等,你应该知道如何使用这些模块进行网络编程,文件系统操作等。
  4. 网络编程:理解如何使用 Node.js 创建 HTTP 服务器,处理请求和响应。
  5. 错误处理:理解 Node.js 的错误处理机制,包括同步和异步错误。
  6. 模块系统:理解 Node.js 的模块系统,包括 requireexportsmodule.exports
  7. 使用 NPM:理解如何使用 NPM 来安装和管理 Node.js 的依赖包。
  8. Express.js 或其他框架:熟悉如何使用 Express.js 或其他 Node.js 框架进行 Web 开发。

当然,已上只是基础知识,有精力的话,你还要了解以下内容:

  1. 事件循环:理解 Node.js 的事件循环,微任务和宏任务。
  2. 实时通信:熟悉使用 Node.js 进行实时通信,如 WebSocket,Socket.IO。
  3. 数据库交互:大多数后端应用都需要与数据库进行交互。了解如何使用 Node.js 连接数据库(例如 MySQL,MongoDB 等),进行 CRUD(创建,读取,更新,删除)操作。
  4. 认证与授权:理解如何在 Node.js 应用中实现用户认证和授权,比如使用 Passport.js,JWT (Json Web Tokens) 等。
  5. 部署和监控:理解如何将你的 Node.js 应用部署到生产环境,以及如何监控你的应用的性能和错误。你可能需要了解一些相关的工具和服务,如 PM2,Docker,Kubernetes,AWS, Google Cloud, Azure, New Relic, Sentry 等。
  6. 安全性:理解如何保护你的 Node.js 应用,防止常见的安全威胁,如 SQL 注入,跨站脚本攻击 (XSS),跨站请求伪造 (CSRF) 等。
  7. 性能优化:理解如何优化你的 Node.js 应用的性能,例如理解事件循环,理解如何避免阻塞操作,如何使用缓存等。

基本语法和概念

理解 Node.js 是什么

Node.js 是一个开源的、跨平台的 JavaScript 运行时环境,它允许开发者在服务器端运行 JavaScript。Node.js 不是一种语言。

它不是一个框架,而是一个平台。它的核心运行环境是基于 Google Chrome's V8 JavaScript 引擎。

Node.js 的工作原理是什么

  1. 事件驱动:Node.js 采用事件驱动模型。这意味着 Node.js 不会主动去做任务,而是产生一种反应,当有事件触发的时候(比如,一个 HTTP 请求到达服务器,或者读/写文件操作完成等)。
  2. 非阻塞 I/O:Node.js 采用非阻塞模式进行 I/O 操作,这意味着服务器在对一个请求进行 I/O 操作时,不会因为等待 I/O 完成而阻塞其他请求,能处理更多的并发请求。

在浏览器中,JavaScript 可以利用如 setTimeout,XMLHttpRequest,fetch 等 API 来进行异步操作。当这些异步操作完成时,它们的回调函数会被加入到任务队列中等待执行。而 JavaScript 的主线程在完成当前的同步任务后,会去任务队列中取出任务并执行。这样,即使有一些耗时的异步操作,也不会阻塞 JavaScript 的主线程,从而达到非阻塞的效果。

在 Node.js 中,异步操作主要涉及到 I/O(包括文件 I/O,网络 I/O 等)。Node.js 通过 libuv 库实现了一个高效的事件循环,使得 JavaScript 能够在单线程中处理大量的并发 I/O 操作。当一个异步 I/O 操作被触发时,JavaScript 会继续执行下一个任务,而不用等待这个 I/O 操作的完成。当 I/O 操作完成时,其回调函数会被加入到任务队列中等待执行。这样,Node.js 也达到了非阻塞的效果。

所以,无论是在浏览器中还是在 Node.js 中,JavaScript 都能够实现非阻塞的异步操作,只不过实现的方式有所不同而已。

Node.js 和 JavaScript 到底有什么不同

  1. 运行环境:Node.js 是服务器端的 JavaScript 运行环境,而浏览器中的 JavaScript 则主要在客户端运行。
  2. 模块系统:Node.js 有一个模块系统,可以使用 require 关键字来导入其他 JavaScript 文件或者 Node.js 的内置模块,而浏览器的 JavaScript 直到 ES6 才开始支持模块,通过 importexport 关键字。
  3. API:Node.js 提供的 API 可以做很多在浏览器中不能做的事情,比如读写文件、监听网络请求等。就好像 Node.js 给了 JavaScript 一个工具箱,让它有了更多的能力。
  4. 同步与异步:JavaScript 本身是单线程的,也就是说一次只能做一件事情。但是 Node.js 利用事件循环和异步 I/O,使得 JavaScript 可以在等待某个长时间操作完成(比如读取文件、请求网络)的时候,先去做其他的事情,然后在操作完成时通过回调函数来获取结果。这就像你在烧水的时候,可以先去做其他的事情,然后等水烧开了再回来泡茶。

异步编程

在 Node.js 中,由于它是基于事件驱动的非阻塞 I/O 模型,因此异步编程是其核心编程思想。

回调函数(Callback)

在 Node.js 中,大多数函数都是异步执行的,也就是说它们不会立即返回结果。

相反,你需要传入一个回调函数,这个函数在异步操作完成时会被调用。回调函数通常作为函数的最后一个参数。

const fs = require('fs');

fs.readFile('/example.txt', 'utf-8', function(err, data) {
  if (err) {
    console.error(err);
  } else {
    console.log(data);
  }
});

Promise

Promise 是一种用于处理异步操作的对象,它代表一个最终可能会完成 (resolved),也可能会失败(rejected) 的操作。

Promise 有三种状态:pending(等待)、fulfilled(完成)、rejected(拒绝)。

const promise = new Promise((resolve, reject) => {
  // 这里写异步操作
});

promise.then(value => {
  // 这个函数在 Promise 完成时被调用
}).catch(reason => {
  // 这个函数在 Promise 失败时被调用
});

async/await

async/await 是基于 Promise 设计的,它简化了异步代码的书写,使其更像同步代码。

async 用于声明一个函数是异步的,await 用于等待一个 Promise 完成。

const fs = require('fs').promises;

async function readExampleFile() {
  try {
    const data = await fs.readFile('/example.txt', 'utf-8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

readExampleFile();

核心模块

Node.js 有一些内置的核心模块,它们提供了很多有用的功能,

我们来看看比较常用也比较重要的吧

http

例如,你正在构建一个 Web 服务,它需要接收来自用户的请求,然后根据请求参数提供相应的响应。

在这种情况下,你会使用 http 模块来创建服务器,并设置相应的请求处理函数。

const http = require('http');

const server = http.createServer((req, res) => {
  res.end('Hello, world!');
});

server.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

fs:这个模块用于处理文件系统操作,如读取文件、写入文件、删除文件、创建目录等。

文件系统模块是处理本地文件非常强大的工具。

例如,如果你正在开发一个网站,用户可以上传文件,你需要将这些文件保存在服务器上。

在这种情况下,你会使用 fs 模块的 writeFile 或者 writeStream 函数。另一个常见的场景是读取文件内容,比如读取配置文件,模板文件等。

const fs = require('fs');

fs.readFile('./test.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);
});

path:这个模块提供了一些工具函数,用于处理文件和目录的路径。

当你处理文件路径时,可能需要使用 path 模块。

例如,你可能需要构建一个在不同操作系统中都能正确工作的文件路径。在这种情况下,你可以使用 path.join 或 path.resolve 函数,它们可以自动处理不同系统的路径分隔符。

const path = require('path');

const filename = path.join(__dirname, 'test.txt');
console.log(filename);

os:这个模块提供了一些方法,用于获取和操作操作系统相关的信息和功能。

例如,你可能需要获取当前的操作系统类型,或者当前的系统时间,或者当前的用户信息。在这种情况下,你可以使用 os 模块提供的 os.type,os.uptime,os.userInfo 等函数。

const os = require('os');

console.log('OS platform:', os.platform());
console.log('Free memory:', os.freemem());

events:Node.js 是基于事件的,这个模块提供了 EventEmitter 类,你可以使用这个类创建和处理自定义事件。

在 Node.js 中,许多对象都会发出事件,例如,HTTP 服务器对象会发出 'request' 事件,fs 读取文件的流会发出 'data' 事件等。events 模块提供了 EventEmitter 类,你可以使用它创建自己的事件发送者。

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();

myEmitter.on('event', () => {
  console.log('an event occurred!');
});

myEmitter.emit('event');

stream:这个模块用于处理 Node.js 中的流操作。流是 Node.js 中处理大量数据的一种方法。

例如,你可能正在下载一个大文件,你不希望一次性加载所有的内容到内存,而是希望一边下载一边处理数据。在这种情况下,你可以使用 stream 模块,创建一个可读流,并通过监听 'data' 事件来处理数据。

const fs = require('fs');
const readStream = fs.createReadStream('./test.txt');

readStream.on('data', (chunk) => {
  console.log(`Received ${chunk.length} bytes of data.`);
});

util:这个模块提供了一些实用的工具函数。

例如,你可能需要将一个函数转化为 promise 形式,或者你需要深度复制一个对象。

在这种情况下,你可以使用 util 模块的 util.promisify 和 util.deepCopy 函数。

const util = require('util');

const text = util.format('Hello %s', 'world!');
console.log(text);

buffer:这个模块用于处理二进制数据。

例如,你可能正在读取一个图片文件,或者你正在处理一个网络请求的二进制数据。在这种情况下,你可以使用 buffer 模块来创建和操作这些二进制数据。

const buf = Buffer.from('hello world', 'ascii');

console.log(buf.toString('hex'));

child_process:这个模块用于创建和管理子进程。你可以使用这个模块执行 shell 命令,运行其他程序等,或者调用其他语言(如 Python,Perl)的脚本。

const { exec } = require('child_process');

exec('ls', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }

  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

querystring:这个模块用于解析和格式化 URL 查询字符串。

例如,你可能需要从 URL 中获取查询参数,或者将一个对象转化为查询字符串。在这种情况下,你可以使用 querystring 模块的 parse 和 stringify 函数。

const querystring = require('querystring');

const query = querystring.parse('name=John&age=30');
console.log(query);

网络编程

Node.js 的内置 http 模块提供了创建 HTTP 服务器的功能。创建一个基础的 HTTP 服务器可以这样做:

创建 HTTP 服务器

const http = require('http');

const server = http.createServer((request, response) => {
    response.end('Hello, world!');
});

server.listen(3000, () => {
    console.log('Server is running on http://localhost:3000');
});

我们首先导入 http 模块,然后调用其 createServer 方法创建一个新的 HTTP 服务器。createServer 方法接收一个回调函数,这个函数在每次有新的请求到达时被调用。

回调函数有两个参数:request 和 response,分别代表进来的请求和将要发出的响应。最后,我们调用 server.listen 方法来启动服务器,让它开始监听 3000 端口的请求。

处理请求

request 对象包含了关于客户端请求的所有信息,如请求头(headers)、请求方法(method)、请求的 URL 等。例如,你可以这样获取请求方法和 URL:

const method = request.method;
const url = request.url;

如果客户端发送了 POST 请求并包含了请求体(request body),你可以这样获取它:

let body = '';
request.on('data', chunk => {
    body += chunk.toString(); // 将 Buffer 转化为字符串
});
request.on('end', () => {
    console.log(body); // 请求体已完全接收
});

这里我们监听了 request 对象的 'data' 和 'end' 事件,当请求体的数据块(chunk)到达时,我们将其加到 body 字符串上;当全部数据块都已到达时,我们可以处理整个请求体了。

发送响应

在回调函数中,response 对象代表服务器要发送的响应。

你可以设置响应头(response headers),设置状态码,以及写入响应体(response body)。例如:

response.statusCode = 200;
response.setHeader('Content-Type', 'text/plain');
response.end('Hello, world!');

这里我们设置了 200 状态码和 Content-Type 响应头,然后写入响应体并结束响应。

注意,end 方法不仅会写入响应体,还会结束响应,所以一定要在所有的响应头都设置完成、所有的响应体都写入完成后再调用。

错误处理

在处理错误时,我们主要考虑两种类型的错误:同步错误和异步错误。

同步错误

通常是编程错误或预料之外的条件,例如引用未定义的变量或者尝试读取一个不可读的属性。

这些错误通常在代码执行时立即抛出,可以通过使用 try...catch 语句进行捕获和处理。

try {
    // 一些可能抛出错误的代码
    let a = undefinedVariable; // ReferenceError: undefinedVariable is not defined
} catch (error) {
    console.error(error); // 错误会被捕获并在此处处理
}

异步错误

通常发生在处理异步操作(如文件 I/O、网络请求等)时。

在 Node.js 中,异步函数通常会接受一个回调函数作为它的最后一个参数,错误对象作为回调函数的第一个参数。

如果异步操作成功,错误参数会是 nullundefined;如果操作失败,错误参数会包含错误信息。

const fs = require('fs');

fs.readFile('/some/nonexistent/file', (err, data) => {
  if (err) {
    console.error('There was an error reading the file:', err);
    return;
  }
  // 对读取的数据进行处理
});

如果使用 Promise 或 async/await(这是异步操作的另一种处理方式),你可以使用 .catch() 方法或者 try...catch 语句来捕获和处理错误:

const fs = require('fs').promises;

fs.readFile('/some/nonexistent/file')
  .then(data => {
    // 对读取的数据进行处理
  })
  .catch(err => {
    console.error('There was an error reading the file:', err);
  });

或者使用 async/await:

const fs = require('fs').promises;

async function readData() {
  try {
    let data = await fs.readFile('/some/nonexistent/file');
    // 对读取的数据进行处理
  } catch (err) {
    console.error('There was an error reading the file:', err);
  }
}

值得注意的是,未捕获的异步错误可能会导致 Node.js 进程终止,所以你应该确保你的代码能够正确地捕获和处理所有可能的异步错误。

你还可以添加 'unhandledRejection' 和 'uncaughtException' 事件处理器来捕获那些你的代码没有处理的错误。

模块系统

这也是 NodeJs 的核心特性之一,它允许你将代码拆分成可重用的独立片段,并且可以在其他地方轻松地使用这些片段。

Node.js 使用 CommonJS 模块规范,主要涉及到以下几个关键概念:

require:这是 Node.js 的内置函数,用于在当前模块中导入其他模块。例如,你可以使用 require 函数来导入 Node.js 的内置模块,或者你自己定义的模块:

const fs = require('fs'); // 导入 Node.js 的内置文件系统模块
const myModule = require('./myModule'); // 导入同一目录下的 myModule.js 文件

exports:这是 Node.js 提供的一个对象,你可以将你想要公开的函数或变量添加到这个对象上,使它们可以被其他模块访问:

exports.myFunction = function() {
  // ...
};

const myModule = require('./myModule');
myModule.myFunction(); // 调用 myFunction 函数

与 CommonJS 不同,ES6 模块的导入导出语法是静态的。这就意味着你不能在运行时改变一个模块的导入或导出。这个特性使得 ES6 模块更易于静态分析和优化。

此外,ES6 模块中的 export default 语句使得模块可以有一个默认导出。这意味着导入模块时可以不用知道模块的具体内容,只需要导入模块的默认导出就可以了。这是 CommonJS 中没有的特性。

module.exports:这与 exports 对象类似,但更为强大。你可以将 module.exports 设定为任何值(例如函数、对象、字符串、数值等),并且当你 require 一个模块时,你将得到的就是 module.exports 的值:

module.exports = function() {
  // ...
};

const myFunction = require('./myModule');
myFunction(); // 调用 myFunction 函数

注意:如果你同时使用了 exportsmodule.exportsrequire 会返回 module.exports 的值。

Express 框架

Express.js 是 Node.js 的一个非常流行的轻量级框架,它提供了许多有用的特性来简化 Node.js 应用程序的开发。

为什么要使用 Express.js:

  1. 简化路由:Express 提供了一个简单的 API 来定义各种 HTTP 路由和处理 HTTP 请求。
  2. 中间件支持:Express 支持中间件,这是一个强大的功能,允许你为请求处理管道中的任何点添加额外的处理逻辑。
  3. 模板引擎支持:Express 支持多种模板引擎,使得动态生成 HTML 变得容易。
  4. 错误处理:Express 提供了一个简单的错误处理机制,使得捕获和处理应用中的错误变得容易。
  5. 集成性和可扩展性:Express 很好地与许多其他 Node.js 库和框架集成,可以轻松添加更多功能,比如添加用户认证、安全性等。

让我们通过具体的代码来对比一下 Express.js 与原生 Node.js 在这些方面的差异:

简化路由

原生 Node.js:

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.write('Hello World');
    res.end();
  } else if (req.url === '/api/courses') {
    res.write(JSON.stringify([1, 2, 3]));
    res.end();
  } else {
    res.statusCode = 404;
    res.end();
  }
});

server.listen(3000);

使用 Express.js:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.get('/api/courses', (req, res) => {
  res.send([1, 2, 3]);
});

app.use((req, res) => {
  res.status(404).send('Not found');
});

app.listen(3000);

中间件支持

原生 Node.js 中没有中间件概念,要实现类似的功能就需要在每个请求的处理函数中手动实现。

使用 Express.js:

const express = require('express');
const app = express();

// 中间件:记录请求日志
app.use((req, res, next) => {
  console.log(`Request received: ${req.url}`);
  next();
});

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.get('/api/courses', (req, res) => {
  res.send([1, 2, 3]);
});

app.listen(3000);

模板引擎支持

原生 Node.js 没有内置模板引擎支持,需要自己引入第三方模板引擎。

const express = require('express');
const app = express();

app.set('view engine', 'pug');

app.get('/', (req, res) => {
  res.render('index', { title: 'My App', message: 'Hello World' });
});

app.listen(3000);

错误处理

原生 Node.js:

const http = require('http');

const server = http.createServer((req, res) => {
  try {
    // ... 
  } catch (error) {
    console.error(error);
    res.statusCode = 500;
    res.end('Internal server error');
  }
});

server.listen(3000);

使用 Express.js:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  throw new Error('Oops!');
});

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).send('Internal server error');
});

app.listen(3000);