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

Node.js前后端分离架构下的数据交互

2022-06-082.0k 阅读

一、前后端分离架构概述

1.1 前后端分离的概念

在传统的 Web 开发模式中,后端不仅负责数据的处理和存储,还负责将数据和页面模板进行渲染后返回给前端。这种模式下,前后端代码耦合度较高,维护和扩展都存在一定困难。随着互联网应用的复杂性增加,前后端分离的架构模式应运而生。

前后端分离架构将前端和后端的职责明确划分。前端专注于用户界面(UI)的开发和用户交互逻辑,负责将数据呈现给用户,并收集用户的输入。后端则主要负责业务逻辑的处理、数据的存储与检索等。前后端之间通过 API 进行数据交互,这种模式使得前后端开发可以并行进行,提高开发效率,同时也便于系统的维护和扩展。

1.2 前后端分离的优势

  1. 提高开发效率:前端和后端开发人员可以专注于各自擅长的领域,并行开发,互不干扰。前端开发人员可以利用各种前端框架快速构建用户界面,后端开发人员则可以专注于业务逻辑和数据处理。
  2. 易于维护和扩展:由于前后端代码分离,修改前端界面或后端业务逻辑时,不会对另一方造成较大影响。例如,如果需要更新前端页面的样式,只需要修改前端代码,而不需要担心影响后端的业务逻辑。
  3. 提升用户体验:前端可以根据用户的操作,通过 API 异步获取数据,动态更新页面,无需重新加载整个页面,从而提高页面的响应速度,给用户带来更好的体验。
  4. 支持多端应用:前后端分离架构下,后端提供的 API 可以被多种前端应用调用,如 Web 应用、移动应用(iOS、Android)等,实现多端数据的统一管理。

二、Node.js 在前后端分离架构中的角色

2.1 Node.js 简介

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它允许开发人员使用 JavaScript 编写服务器端代码。Node.js 采用事件驱动、非阻塞 I/O 模型,使其非常适合构建高性能、可伸缩的网络应用程序。

Node.js 内置了一系列模块,如 HTTP 模块、文件系统模块等,方便开发人员快速搭建服务器。同时,Node.js 还有丰富的第三方模块生态系统,通过 npm(Node Package Manager)可以轻松安装和管理各种模块,极大地提高了开发效率。

2.2 Node.js 在前后端分离中的作用

  1. 作为后端服务器:Node.js 可以作为前后端分离架构中的后端服务器,负责处理前端发送的请求,执行相应的业务逻辑,与数据库进行交互,然后将处理结果返回给前端。例如,前端发送一个获取用户信息的请求,Node.js 服务器接收到请求后,从数据库中查询用户信息,并将其返回给前端。
  2. 提供 API 接口:Node.js 可以构建 RESTful API 接口,前端通过这些接口与后端进行数据交互。RESTful API 是一种基于 HTTP 协议的轻量级 API 设计风格,具有简洁、易扩展等优点。Node.js 可以使用 Express、Koa 等框架快速搭建 RESTful API 服务器。
  3. 数据处理与转换:在前后端数据交互过程中,Node.js 可以对前端发送的数据进行验证、处理和转换,然后再存储到数据库中。同样,从数据库中获取的数据也可以在 Node.js 中进行处理和转换,以满足前端的需求。例如,前端发送一个注册用户的请求,Node.js 服务器可以对用户输入的用户名、密码等数据进行验证,验证通过后将其存储到数据库中。

三、Node.js 与前端的数据交互方式

3.1 HTTP 请求与响应

前后端之间最常用的数据交互方式是通过 HTTP 请求与响应。前端通过浏览器向 Node.js 服务器发送 HTTP 请求,请求可以携带数据(如查询参数、请求体等)。Node.js 服务器接收到请求后,根据请求的 URL 和方法(GET、POST、PUT、DELETE 等)执行相应的业务逻辑,然后将处理结果以 HTTP 响应的形式返回给前端。

以下是一个简单的 Node.js 服务器示例,使用内置的 HTTP 模块处理前端的 GET 请求:

const http = require('http');

const server = http.createServer((req, res) => {
    if (req.method === 'GET' && req.url === '/') {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Hello, Front - End!');
    } else {
        res.statusCode = 404;
        res.end('Not Found');
    }
});

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

在这个示例中,当前端访问根路径(/)时,服务器返回一个简单的文本消息。如果访问其他路径,则返回 404 错误。

3.2 RESTful API

RESTful API 是一种遵循 REST(Representational State Transfer)架构风格的 API 设计。在前后端分离架构中,使用 RESTful API 可以使前后端之间的数据交互更加规范和易于理解。

  1. 资源与 URL:RESTful API 将数据看作资源,每个资源都有一个唯一的 URL 来标识。例如,用户资源可以通过 /users URL 来表示,单个用户可以通过 /users/{id} URL 来表示,其中 {id} 是用户的唯一标识符。
  2. HTTP 方法:使用不同的 HTTP 方法来对资源进行操作。常见的 HTTP 方法有:
    • GET:用于获取资源。例如,GET /users 可以获取所有用户的列表,GET /users/1 可以获取 ID 为 1 的用户信息。
    • POST:用于创建新资源。例如,POST /users 可以创建一个新用户,请求体中包含新用户的信息。
    • PUT:用于更新资源。例如,PUT /users/1 可以更新 ID 为 1 的用户信息,请求体中包含更新后的用户信息。
    • DELETE:用于删除资源。例如,DELETE /users/1 可以删除 ID 为 1 的用户。

以下是一个使用 Express 框架构建 RESTful API 的示例:

首先,安装 Express 模块:

npm install express

然后,编写 Node.js 代码:

const express = require('express');
const app = express();
const users = [];

// 创建用户
app.post('/users', (req, res) => {
    const newUser = req.body;
    users.push(newUser);
    res.status(201).json(newUser);
});

// 获取所有用户
app.get('/users', (req, res) => {
    res.json(users);
});

// 获取单个用户
app.get('/users/:id', (req, res) => {
    const userId = parseInt(req.params.id);
    const user = users.find(u => u.id === userId);
    if (user) {
        res.json(user);
    } else {
        res.status(404).send('User not found');
    }
});

// 更新用户
app.put('/users/:id', (req, res) => {
    const userId = parseInt(req.params.id);
    const updatedUser = req.body;
    const index = users.findIndex(u => u.id === userId);
    if (index!== -1) {
        users[index] = {...users[index],...updatedUser };
        res.json(users[index]);
    } else {
        res.status(404).send('User not found');
    }
});

// 删除用户
app.delete('/users/:id', (req, res) => {
    const userId = parseInt(req.params.id);
    const index = users.findIndex(u => u.id === userId);
    if (index!== -1) {
        const deletedUser = users[index];
        users.splice(index, 1);
        res.json(deletedUser);
    } else {
        res.status(404).send('User not found');
    }
});

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

在这个示例中,我们使用 Express 框架创建了一个简单的 RESTful API,用于管理用户资源。前端可以通过发送不同的 HTTP 请求来操作用户数据。

3.3 JSON 数据格式

在前后端数据交互中,JSON(JavaScript Object Notation)是一种常用的数据格式。JSON 具有轻量级、易于阅读和编写、便于解析和生成等优点,非常适合在网络传输中使用。

前端在发送请求时,可以将数据转换为 JSON 格式,通过请求体发送给后端。后端接收到 JSON 数据后,可以进行解析和处理。同样,后端在返回响应时,也可以将数据转换为 JSON 格式返回给前端。

例如,前端使用 fetch API 发送一个 JSON 格式的 POST 请求:

const newUser = {
    name: 'John Doe',
    age: 30
};

fetch('/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(newUser)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

后端使用 Express 框架接收并处理这个 JSON 格式的请求:

app.use(express.json());

app.post('/users', (req, res) => {
    const newUser = req.body;
    // 处理新用户数据
    res.status(201).json(newUser);
});

在这个示例中,前端将 newUser 对象转换为 JSON 字符串发送给后端,后端使用 express.json() 中间件解析 JSON 数据,并进行处理。

四、数据传输过程中的安全性

4.1 数据加密

在前后端数据交互过程中,数据的安全性至关重要。为了防止数据在传输过程中被窃取或篡改,可以对数据进行加密。常见的加密方式有对称加密和非对称加密。

  1. 对称加密:对称加密使用相同的密钥进行加密和解密。例如,使用 AES(Advanced Encryption Standard)算法进行对称加密。在 Node.js 中,可以使用 crypto 模块来实现 AES 加密。

以下是一个简单的 AES 加密和解密示例:

const crypto = require('crypto');

const algorithm = 'aes - 256 - cbc';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);

function encrypt(text) {
    const cipher = crypto.createCipheriv(algorithm, key, iv);
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    return encrypted;
}

function decrypt(encryptedText) {
    const decipher = crypto.createDecipheriv(algorithm, key, iv);
    let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
}

const originalText = 'Hello, World!';
const encryptedText = encrypt(originalText);
const decryptedText = decrypt(encryptedText);

console.log('Original Text:', originalText);
console.log('Encrypted Text:', encryptedText);
console.log('Decrypted Text:', decryptedText);
  1. 非对称加密:非对称加密使用公钥和私钥。公钥用于加密数据,私钥用于解密数据。例如,使用 RSA(Rivest - Shamir - Adleman)算法进行非对称加密。在 Node.js 中,同样可以使用 crypto 模块来实现 RSA 加密。

以下是一个简单的 RSA 加密和解密示例:

const crypto = require('crypto');

// 生成密钥对
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: 2048,
    publicKeyEncoding: {
        type: 'pkcs1',
        format: 'pem'
    },
    privateKeyEncoding: {
        type: 'pkcs1',
        format: 'pem'
    }
});

function encryptWithPublicKey(text) {
    const buffer = Buffer.from(text, 'utf8');
    const encrypted = crypto.publicEncrypt(publicKey, buffer);
    return encrypted.toString('base64');
}

function decryptWithPrivateKey(encryptedText) {
    const buffer = Buffer.from(encryptedText, 'base64');
    const decrypted = crypto.privateDecrypt(privateKey, buffer);
    return decrypted.toString('utf8');
}

const originalText = 'Hello, Secure World!';
const encryptedText = encryptWithPublicKey(originalText);
const decryptedText = decryptWithPrivateKey(encryptedText);

console.log('Original Text:', originalText);
console.log('Encrypted Text:', encryptedText);
console.log('Decrypted Text:', decryptedText);

4.2 身份验证与授权

  1. 身份验证:身份验证用于确认请求方的身份。常见的身份验证方式有用户名和密码验证、Token 验证等。
    • 用户名和密码验证:前端用户输入用户名和密码,发送到后端服务器进行验证。后端服务器查询数据库,验证用户名和密码是否匹配。如果匹配,则验证通过,否则验证失败。
    • Token 验证:用户登录成功后,后端服务器生成一个 Token,通常是一个包含用户信息的 JSON Web Token(JWT)。后端将 Token 返回给前端,前端在后续的请求中,将 Token 放在请求头或其他合适的位置发送给后端。后端接收到请求后,验证 Token 的有效性,如果有效,则确认用户身份,否则拒绝请求。

以下是一个使用 Express 和 jsonwebtoken 实现 JWT 验证的示例:

首先,安装 jsonwebtoken 模块:

npm install jsonwebtoken

然后,编写 Node.js 代码:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = 'your - secret - key';

// 模拟用户数据
const users = [
    { id: 1, username: 'admin', password: 'password' }
];

// 用户登录
app.post('/login', (req, res) => {
    const { username, password } = req.body;
    const user = users.find(u => u.username === username && u.password === password);
    if (user) {
        const token = jwt.sign({ userId: user.id }, SECRET_KEY, { expiresIn: '1h' });
        res.json({ token });
    } else {
        res.status(401).send('Invalid credentials');
    }
});

// 需要身份验证的路由
app.get('/protected', (req, res) => {
    const token = req.headers['authorization'];
    if (!token) {
        return res.status(401).send('Access denied. No token provided.');
    }
    try {
        const decoded = jwt.verify(token.replace('Bearer ', ''), SECRET_KEY);
        res.json({ message: 'This is a protected route', user: decoded });
    } catch (error) {
        res.status(400).send('Invalid token');
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
  1. 授权:授权用于确定经过身份验证的用户是否有权限执行特定的操作。例如,只有管理员用户才能删除其他用户的数据。在 Node.js 应用中,可以在身份验证通过后,根据用户的角色或权限信息,判断用户是否有权限执行请求的操作。

五、处理跨域问题

5.1 跨域的概念

当一个前端页面的 URL 与后端 API 的 URL 不在同一个域(包括协议、域名、端口)时,就会出现跨域问题。浏览器出于安全考虑,默认会阻止跨域的 HTTP 请求。例如,前端页面在 http://localhost:8080 运行,而后端 API 在 http://localhost:3000 提供服务,这种情况下前端向后端发送请求就会遇到跨域问题。

5.2 解决跨域的方法

  1. CORS(Cross - Origin Resource Sharing):CORS 是一种在服务器端进行配置,允许跨域请求的机制。在 Node.js 中,可以使用 cors 模块来实现 CORS。

首先,安装 cors 模块:

npm install cors

然后,在 Express 应用中使用:

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

app.use(cors());

// 其他路由和中间件

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

在这个示例中,app.use(cors()) 允许所有来源的跨域请求。也可以对 cors 进行更细粒度的配置,例如只允许特定来源的请求:

const corsOptions = {
    origin: 'http://localhost:8080',
    optionsSuccessStatus: 200
};

app.use(cors(corsOptions));
  1. JSONP(JSON with Padding):JSONP 是一种利用 <script> 标签不受跨域限制的特性来实现跨域数据请求的方法。前端通过动态创建 <script> 标签,将请求的 URL 作为 src 属性值,后端返回一个包裹在函数调用中的 JSON 数据。

以下是一个简单的 JSONP 示例:

前端代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device - width, initial - scale = 1.0">
    <title>JSONP Example</title>
</head>

<body>
    <script>
        function handleResponse(data) {
            console.log(data);
        }
        const script = document.createElement('script');
        script.src = 'http://localhost:3000/jsonp?callback=handleResponse';
        document.body.appendChild(script);
    </script>
</body>

</html>

后端代码(使用 Express):

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

app.get('/jsonp', (req, res) => {
    const callback = req.query.callback;
    const data = { message: 'This is JSONP data' };
    res.send(`${callback}(${JSON.stringify(data)})`);
});

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

在这个示例中,前端通过 <script> 标签发起 JSONP 请求,后端返回包裹在指定回调函数中的 JSON 数据。

  1. 代理:可以在前端开发服务器(如 Webpack Dev Server)中设置代理,将跨域请求转发到后端服务器。这样,对于浏览器来说,请求的是同一个域,从而避免跨域问题。

例如,在 Webpack Dev Server 的配置文件(webpack.config.js)中设置代理:

module.exports = {
    // 其他配置项
    devServer: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000',
                changeOrigin: true,
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
};

在这个配置中,前端请求 /api 开头的 URL 时,Webpack Dev Server 会将请求转发到 http://localhost:3000,并去掉 /api 前缀,这样就解决了跨域问题。

六、性能优化

6.1 缓存策略

  1. 前端缓存:前端可以使用浏览器的缓存机制来提高性能。例如,使用 localStoragesessionStorage 缓存一些不经常变化的数据,如用户设置等。当页面再次加载时,可以直接从缓存中读取数据,减少对后端的请求。
// 存储数据到 localStorage
localStorage.setItem('userSettings', JSON.stringify({ theme: 'dark' }));

// 从 localStorage 读取数据
const userSettings = JSON.parse(localStorage.getItem('userSettings'));
  1. 后端缓存:在 Node.js 服务器端,可以使用缓存来减少数据库查询等操作的次数。例如,使用 node - cache 模块来实现简单的内存缓存。

首先,安装 node - cache 模块:

npm install node - cache

然后,在 Node.js 代码中使用:

const NodeCache = require('node - cache');
const myCache = new NodeCache();

app.get('/users', (req, res) => {
    const cachedUsers = myCache.get('users');
    if (cachedUsers) {
        res.json(cachedUsers);
    } else {
        // 从数据库查询用户数据
        const users = []; // 假设从数据库查询到的用户数据
        myCache.set('users', users);
        res.json(users);
    }
});

6.2 压缩与合并

  1. 前端资源压缩与合并:在前端开发中,可以对 CSS、JavaScript 和图片等资源进行压缩和合并。例如,使用工具如 UglifyJS 压缩 JavaScript 文件,使用 cssnano 压缩 CSS 文件,使用 imagemin 压缩图片。同时,将多个 CSS 和 JavaScript 文件合并为一个文件,可以减少浏览器请求的次数。

  2. 后端响应数据压缩:在 Node.js 服务器端,可以对返回给前端的响应数据进行压缩,如使用 compression 模块对响应数据进行 gzip 压缩。

首先,安装 compression 模块:

npm install compression

然后,在 Express 应用中使用:

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

app.use(compression());

// 其他路由和中间件

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

这样,服务器在返回响应数据时,会先对数据进行 gzip 压缩,从而减少数据传输量,提高响应速度。

6.3 异步处理与并发控制

  1. 异步处理:Node.js 天生支持异步编程,通过使用 async/await 或 Promises 可以更方便地处理异步操作,避免阻塞事件循环。例如,在处理数据库查询或文件读取等操作时,使用异步方式可以提高服务器的性能和响应能力。
const fs = require('fs').promises;

app.get('/file', async (req, res) => {
    try {
        const data = await fs.readFile('example.txt', 'utf8');
        res.send(data);
    } catch (error) {
        res.status(500).send('Error reading file');
    }
});
  1. 并发控制:在处理多个异步任务时,需要进行并发控制,以避免过多的并发请求导致服务器资源耗尽。可以使用 async - parallelasync - waterfall 等模块来控制异步任务的并发数量和执行顺序。

例如,使用 async - parallel 模块并行执行多个异步任务:

npm install async - parallel
const asyncParallel = require('async - parallel');

app.get('/tasks', (req, res) => {
    asyncParallel([
        (callback) => {
            setTimeout(() => {
                callback(null, 'Task 1 completed');
            }, 1000);
        },
        (callback) => {
            setTimeout(() => {
                callback(null, 'Task 2 completed');
            }, 1500);
        }
    ], (error, results) => {
        if (error) {
            res.status(500).send(error);
        } else {
            res.json(results);
        }
    });
});

在这个示例中,两个异步任务并行执行,当所有任务完成后,将结果返回给前端。通过合理控制并发数量,可以提高服务器的性能和稳定性。