MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js入门指南:从零开始学习Node.js

2022-05-216.7k 阅读

什么是 Node.js

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境。它允许开发人员在服务器端使用 JavaScript 编写脚本,从而实现网络应用程序的服务器端功能。与传统的服务器端语言(如 Python、Java、PHP 等)不同,Node.js 使用单线程事件驱动架构,这使得它在处理高并发 I/O 操作时表现出色。

Node.js 的核心是事件循环(Event Loop),这是一种处理非阻塞 I/O 操作的机制。当一个 I/O 操作(如读取文件、网络请求等)发起时,Node.js 不会等待操作完成,而是继续执行后续代码。当 I/O 操作完成后,事件循环会将其结果放入队列中,等待主线程处理。这种机制使得 Node.js 能够高效地处理大量并发请求,非常适合构建实时 Web 应用程序、微服务等。

安装 Node.js

在开始学习 Node.js 之前,需要先在本地环境中安装 Node.js。可以从 Node.js 官方网站(https://nodejs.org/)下载适合自己操作系统的安装包。安装过程非常简单,按照安装向导的提示一步步操作即可。

安装完成后,可以在命令行中输入以下命令来检查 Node.js 是否安装成功:

node -v

如果安装成功,会输出 Node.js 的版本号。同时,Node.js 安装包还包含了 npm(Node Package Manager),这是 Node.js 的包管理工具,可以使用以下命令检查 npm 的版本:

npm -v

创建第一个 Node.js 应用程序

创建一个简单的 Node.js 应用程序非常容易。首先,在本地创建一个新的文件夹,例如 my - node - app,然后在该文件夹中创建一个新的 JavaScript 文件,例如 app.js

app.js 文件中,输入以下代码:

console.log('Hello, Node.js!');

保存文件后,打开命令行,进入 my - node - app 文件夹,然后输入以下命令来运行这个 Node.js 应用程序:

node app.js

如果一切正常,命令行中会输出 Hello, Node.js!。这就是一个最简单的 Node.js 应用程序,它使用 console.log() 函数在控制台输出一条信息。

Node.js 模块系统

Node.js 采用了模块化的架构,这意味着可以将代码拆分成多个独立的模块,每个模块都有自己的作用域,并且可以方便地进行复用。Node.js 中的模块分为两种类型:核心模块和用户自定义模块。

核心模块

Node.js 提供了许多内置的核心模块,这些模块可以直接使用。例如,fs 模块用于文件系统操作,http 模块用于创建 HTTP 服务器等。以下是一个使用 fs 模块读取文件内容的示例:

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});

在这个示例中,首先使用 require('fs') 引入了 fs 核心模块,然后使用 fs.readFile() 方法异步读取 example.txt 文件的内容。如果读取过程中发生错误,会在控制台输出错误信息;如果读取成功,会输出文件的内容。

用户自定义模块

除了使用核心模块,还可以创建自己的模块。假设在项目中有一个 math.js 文件,内容如下:

function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

module.exports = {
    add,
    subtract
};

在这个模块中,定义了两个函数 addsubtract,并通过 module.exports 将这两个函数暴露出去。在另一个文件(例如 main.js)中,可以这样使用这个自定义模块:

const math = require('./math');

const result1 = math.add(5, 3);
const result2 = math.subtract(5, 3);

console.log('Addition result:', result1);
console.log('Subtraction result:', result2);

main.js 中,使用 require('./math') 引入了 math.js 模块,然后就可以使用模块中暴露的 addsubtract 函数了。

处理文件系统操作

文件系统操作在服务器端开发中非常常见。Node.js 的 fs 模块提供了丰富的方法来进行文件和目录的操作。

读取文件

前面已经介绍了使用 fs.readFile() 异步读取文件的示例。除了异步方式,fs 模块还提供了同步读取文件的方法 fs.readFileSync()。以下是同步读取文件的示例:

const fs = require('fs');

try {
    const data = fs.readFileSync('example.txt', 'utf8');
    console.log(data);
} catch (err) {
    console.error(err);
}

同步读取文件的优点是代码相对简单,但是它会阻塞主线程,直到文件读取完成。因此,在处理大文件或者需要处理高并发请求时,一般建议使用异步方式。

写入文件

写入文件也有异步和同步两种方式。异步写入文件可以使用 fs.writeFile() 方法,示例如下:

const fs = require('fs');

const content = 'This is some content to write to the file.';
fs.writeFile('output.txt', content, err => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('File written successfully.');
});

在这个示例中,fs.writeFile() 方法将 content 写入到 output.txt 文件中。如果写入过程中发生错误,会在控制台输出错误信息;如果写入成功,会输出提示信息。

同步写入文件可以使用 fs.writeFileSync() 方法,示例如下:

const fs = require('fs');

const content = 'This is some content to write to the file (synchronous).';
try {
    fs.writeFileSync('output - sync.txt', content);
    console.log('File written synchronously successfully.');
} catch (err) {
    console.error(err);
}

同样,同步写入会阻塞主线程,在实际应用中要谨慎使用。

创建和删除目录

创建目录可以使用 fs.mkdir() 方法,示例如下:

const fs = require('fs');

fs.mkdir('new - directory', err => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Directory created successfully.');
});

删除目录可以使用 fs.rmdir() 方法,示例如下:

const fs = require('fs');

fs.rmdir('new - directory', err => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Directory deleted successfully.');
});

需要注意的是,fs.rmdir() 方法只能删除空目录,如果要删除非空目录,需要先删除目录中的所有文件和子目录。

构建 HTTP 服务器

Node.js 的 http 模块可以用来构建 HTTP 服务器。以下是一个简单的 HTTP 服务器示例:

const http = require('http');

const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content - Type', 'text/plain');
    res.end('Hello, World!');
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,首先使用 http.createServer() 创建了一个 HTTP 服务器实例。createServer() 方法接受一个回调函数,这个回调函数会在每次收到 HTTP 请求时被调用。回调函数的两个参数 req(请求对象)和 res(响应对象)分别用于处理请求和生成响应。

在回调函数中,设置了响应状态码为 200,表示请求成功,设置响应头 Content - Typetext/plain,表示响应内容是纯文本格式,最后使用 res.end() 方法结束响应并发送 Hello, World! 字符串。

最后,使用 server.listen() 方法启动服务器,并指定监听的端口为 3000。当服务器成功启动后,会在控制台输出提示信息。

可以在浏览器中访问 http://localhost:3000,就可以看到 Hello, World! 的响应内容。

处理 HTTP 请求

在实际应用中,HTTP 服务器需要能够处理不同类型的请求和请求参数。

获取请求方法和 URL

在前面的 HTTP 服务器示例中,可以通过 req 对象获取请求的方法和 URL。以下是一个修改后的示例:

const http = require('http');

const server = http.createServer((req, res) => {
    const method = req.method;
    const url = req.url;

    res.statusCode = 200;
    res.setHeader('Content - Type', 'text/plain');
    res.end(`Method: ${method}, URL: ${url}`);
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,通过 req.method 获取请求方法(如 GETPOST 等),通过 req.url 获取请求的 URL。然后将这些信息作为响应内容返回。

解析 URL 参数

当使用 GET 方法发送请求时,参数通常包含在 URL 中。可以使用 querystring 模块来解析 URL 参数。以下是一个示例:

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
    if (req.method === 'GET' && req.url.startsWith('/search')) {
        const urlParts = req.url.split('?');
        if (urlParts.length === 2) {
            const query = urlParts[1];
            const params = querystring.parse(query);
            const keyword = params.keyword;

            res.statusCode = 200;
            res.setHeader('Content - Type', 'text/plain');
            res.end(`Search keyword: ${keyword}`);
        } else {
            res.statusCode = 400;
            res.setHeader('Content - Type', 'text/plain');
            res.end('Invalid request');
        }
    } else {
        res.statusCode = 404;
        res.setHeader('Content - Type', 'text/plain');
        res.end('Not found');
    }
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,首先检查请求方法是否为 GET 且 URL 是否以 /search 开头。如果满足条件,将 URL 分割成两部分,取第二部分作为查询字符串,然后使用 querystring.parse() 方法将查询字符串解析成参数对象。最后从参数对象中获取 keyword 参数,并将其作为响应内容返回。

处理 POST 请求

处理 POST 请求相对复杂一些,因为请求体的数据需要通过流的方式读取。以下是一个处理 POST 请求的示例:

const http = require('http');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
    if (req.method === 'POST' && req.url === '/submit') {
        let data = '';
        req.on('data', chunk => {
            data += chunk;
        });
        req.on('end', () => {
            const params = querystring.parse(data);
            const username = params.username;
            const password = params.password;

            res.statusCode = 200;
            res.setHeader('Content - Type', 'text/plain');
            res.end(`Username: ${username}, Password: ${password}`);
        });
    } else {
        res.statusCode = 404;
        res.setHeader('Content - Type', 'text/plain');
        res.end('Not found');
    }
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,当检测到 POST 请求且 URL 为 /submit 时,通过 req.on('data') 事件来监听请求体数据的到来,并将数据拼接起来。当 req.on('end') 事件触发时,表示数据接收完毕,此时使用 querystring.parse() 方法解析数据,并获取 usernamepassword 参数,最后将这些参数作为响应内容返回。

使用 Express 框架

虽然使用 Node.js 的原生 http 模块可以构建 HTTP 服务器,但是在实际项目中,使用框架可以大大提高开发效率。Express 是 Node.js 中最流行的 Web 应用框架之一。

安装 Express

首先需要使用 npm 安装 Express。在项目目录下打开命令行,输入以下命令:

npm install express

安装完成后,在项目的 package.json 文件中会看到 express 被列为依赖项。

创建 Express 应用

以下是一个简单的 Express 应用示例:

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

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

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,首先引入了 Express 模块,然后使用 express() 创建了一个 Express 应用实例。通过 app.get() 方法定义了一个处理 GET 请求的路由,当访问根路径 '/' 时,会返回 Hello, Express!。最后使用 app.listen() 方法启动服务器并监听指定端口。

定义路由

Express 提供了丰富的路由定义方法,除了 app.get() 处理 GET 请求,还可以使用 app.post() 处理 POST 请求,app.put() 处理 PUT 请求等。以下是一个包含多个路由的示例:

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

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

app.get('/about', (req, res) => {
    res.send('About page');
});

app.post('/submit', (req, res) => {
    res.send('Form submitted');
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,定义了三个路由:根路径 '/''/about''/submit',分别处理不同的请求。

处理请求参数

Express 处理请求参数也非常方便。对于 GET 请求的参数,可以通过 req.query 获取;对于 POST 请求的参数,需要先使用中间件 express.urlencoded()express.json() 来解析请求体。以下是一个示例:

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

app.use(express.urlencoded({ extended: true }));

app.get('/search', (req, res) => {
    const keyword = req.query.keyword;
    res.send(`Search keyword: ${keyword}`);
});

app.post('/submit', (req, res) => {
    const username = req.body.username;
    const password = req.body.password;
    res.send(`Username: ${username}, Password: ${password}`);
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,通过 app.use(express.urlencoded({ extended: true })) 使用了 express.urlencoded() 中间件来解析 POST 请求体中的 URL 编码格式数据。对于 GET 请求的 search 路由,通过 req.query.keyword 获取 keyword 参数;对于 POST 请求的 submit 路由,通过 req.body.usernamereq.body.password 获取表单提交的 usernamepassword 参数。

连接数据库

在大多数 Web 应用中,需要与数据库进行交互。Node.js 可以与多种数据库进行集成,这里以 MySQL 和 MongoDB 为例进行介绍。

连接 MySQL 数据库

首先需要安装 mysql 模块。在项目目录下打开命令行,输入以下命令:

npm install mysql

以下是一个连接 MySQL 数据库并执行查询的示例:

const mysql = require('mysql');

const connection = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'test'
});

connection.connect(err => {
    if (err) {
        console.error('Error connecting to database:', err);
        return;
    }
    console.log('Connected to database');

    const sql = 'SELECT * FROM users';
    connection.query(sql, (err, results, fields) => {
        if (err) {
            console.error('Error executing query:', err);
            return;
        }
        console.log('Query results:', results);
    });

    connection.end();
});

在这个示例中,首先使用 mysql.createConnection() 创建了一个数据库连接对象,配置了数据库的主机、用户名、密码和数据库名。然后使用 connection.connect() 方法连接数据库,如果连接成功,会输出连接成功的信息,并执行一个查询语句 SELECT * FROM users。查询结果会在回调函数中返回,如果查询过程中发生错误,会输出错误信息。最后使用 connection.end() 方法关闭数据库连接。

连接 MongoDB 数据库

对于 MongoDB,需要安装 mongodb 模块。在项目目录下打开命令行,输入以下命令:

npm install mongodb

以下是一个连接 MongoDB 数据库并插入文档的示例:

const { MongoClient } = require('mongodb');

const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri);

async function run() {
    try {
        await client.connect();
        console.log('Connected to MongoDB');

        const database = client.db('test');
        const collection = database.collection('users');

        const newUser = { name: 'John', age: 30 };
        const result = await collection.insertOne(newUser);
        console.log('Inserted document:', result.insertedId);
    } catch (e) {
        console.error('Error connecting to MongoDB or inserting document:', e);
    } finally {
        await client.close();
    }
}

run().catch(console.dir);

在这个示例中,首先引入了 MongoClient 类,然后创建了一个 MongoClient 实例并指定连接字符串。通过 client.connect() 方法异步连接到 MongoDB 数据库。连接成功后,获取数据库和集合对象,然后插入一个新的用户文档。如果插入成功,会输出插入文档的 _id。最后在 finally 块中关闭数据库连接。

错误处理

在 Node.js 开发中,错误处理非常重要。无论是文件系统操作、HTTP 请求处理还是数据库连接等,都可能发生错误。

同步操作的错误处理

对于同步操作,错误通常通过 try...catch 块来捕获。例如,在同步读取文件时:

const fs = require('fs');

try {
    const data = fs.readFileSync('nonexistent.txt', 'utf8');
    console.log(data);
} catch (err) {
    console.error('Error reading file:', err);
}

在这个示例中,如果文件 nonexistent.txt 不存在,fs.readFileSync() 会抛出一个错误,这个错误会被 catch 块捕获并在控制台输出错误信息。

异步操作的错误处理

对于异步操作,错误处理方式有所不同。在前面的文件系统异步操作示例中,错误是通过回调函数的第一个参数来传递的。例如:

const fs = require('fs');

fs.readFile('nonexistent.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('Error reading file:', err);
        return;
    }
    console.log(data);
});

在这个异步读取文件的示例中,当文件不存在时,fs.readFile() 的回调函数的第一个参数 err 会包含错误信息,在回调函数中可以对错误进行处理。

Express 中的错误处理

在 Express 应用中,可以使用全局错误处理中间件来处理路由和其他中间件中抛出的错误。以下是一个示例:

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

app.get('/error', (req, res) => {
    throw new Error('This is an error');
});

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something went wrong!');
});

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,定义了一个 /error 路由,在这个路由中抛出了一个错误。然后使用 app.use() 定义了一个全局错误处理中间件,这个中间件接受四个参数(errreqresnext),在中间件中,将错误堆栈信息输出到控制台,并返回一个状态码为 500 的响应,表示服务器内部错误。

性能优化

在开发 Node.js 应用时,性能优化是非常重要的。以下是一些常见的性能优化方法:

合理使用缓存

在处理频繁读取的数据时,可以使用缓存来减少数据库或文件系统的 I/O 操作。例如,对于一些配置文件或不经常变化的数据,可以在内存中缓存起来。在 Node.js 中,可以使用简单的对象来实现缓存,也可以使用专门的缓存模块,如 node - cache

以下是一个简单的缓存示例:

const cache = {};

function getValue(key) {
    if (cache[key]) {
        return cache[key];
    }
    // 如果缓存中没有,从其他数据源获取数据
    const value = getValueFromDataSource(key);
    cache[key] = value;
    return value;
}

优化 I/O 操作

由于 Node.js 是基于事件循环的单线程模型,I/O 操作的性能对整体性能影响很大。尽量使用异步 I/O 操作,避免阻塞主线程。同时,可以使用连接池来管理数据库连接,减少连接的创建和销毁开销。

例如,在连接 MySQL 数据库时,可以使用连接池:

const mysql = require('mysql');

const pool = mysql.createPool({
    host: 'localhost',
    user: 'root',
    password: 'password',
    database: 'test',
    connectionLimit: 10
});

pool.query('SELECT * FROM users', (err, results, fields) => {
    if (err) {
        console.error('Error executing query:', err);
        return;
    }
    console.log('Query results:', results);
});

在这个示例中,使用 mysql.createPool() 创建了一个连接池,connectionLimit 设置了最大连接数为 10。通过连接池执行查询操作,可以提高数据库操作的性能。

代码优化

优化代码结构,避免不必要的计算和内存消耗。例如,避免在循环中创建大量的临时对象,合理使用变量作用域等。

以下是一个简单的代码优化示例:

// 优化前
function calculateSum() {
    let sum = 0;
    for (let i = 0; i < 1000; i++) {
        let temp = i * 2;
        sum += temp;
    }
    return sum;
}

// 优化后
function calculateSum() {
    let sum = 0;
    for (let i = 0; i < 1000; i++) {
        sum += i * 2;
    }
    return sum;
}

在这个示例中,优化前在循环中创建了一个临时变量 temp,而优化后直接在计算中使用 i * 2,减少了不必要的变量创建,提高了代码的执行效率。

通过以上这些方法,可以有效地提高 Node.js 应用的性能,使其能够更好地应对高并发的业务场景。在实际开发中,需要根据具体的业务需求和应用场景,综合运用这些优化技巧,打造高效稳定的 Node.js 应用程序。