【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

Node.js 中的关注点分离

  • 2022-11-09
    北京
  • 本文字数:10765 字

    阅读完需:约 35 分钟

Node.js中的关注点分离

关键要点


  • 关注点分离是 Node.js 的一种架构,可以确保代码的可读性、易于重构和良好的代码协作。

  • 通过遵循关注点分离原则,你可以确保最终的系统是稳定和可维护的。

  • 关注点分离的原则确保了组件不会重复,从而使系统更加容易维护和重构。

  • 关注点分离原则认为,业务逻辑应该与控制器分离。这简化了控制器的开发和测试用例的编写。

  • 关注点分离原则有助于提升代码的可重用性。这样可以很容易地找到故障的来源以及如何将其从整个系统中隔离出来,从而降低维护成本和缩短维护时间。

创建好的 Node.js 项目架构


大多数时候,我们在大团队中工作,不同的人处理系统的不同部分,如果事情没有得到妥当安排,就会变得混乱。受疫情影响,越来越多的团队采用了远程工作的方式,拥有清晰和定义良好的代码结构从未像现在这么重要。


从本质上讲,项目结构是一个很重要的主题,因为如何引导应用程序决定了整个项目生命周期的整体开发体验。


Node.js 的惊人之处在于,你可以随心所欲地构造代码,没有所谓的“正确的方法”。你可以选择在一个 app.js 文件中编写所有代码,也可以创建多个文件并将它们放在不同的文件夹中。


然而,大多数开发人员会建议通过将相关数据分组在一起来组织项目结构,而不是将所有东西全部放在一起。当你想要修改模型时,最好可以直接通过浏览模型文件夹来修改,而不是在包含模型、控制器、加载器和服务的单个文件中找来找去。

为什么好的项目架构如此重要


如前所述,好的项目架构非常重要,而混乱的架构可能会造成问题。下面是好的架构的一些好处。


  • 使代码更具可读性和整洁性。

  • 更容易避免重复代码。

  • 更容易扩展和修改。

  • 简化了测试用例的编写。

关注点分离


关注点分离是一种将软件程序划分为多个片段的设计原则。每一个片段都试图解决一个不同的问题,包含了一组对程序代码有影响的细节。


这个概念本质上指的是一种架构模式,程序逻辑与程序内容和表示是分离的。这会让项目变得更加容易维护,并且不容易出现重复。它还简化了团队协作和变更的实现。


Node.js 项目可以有多种组织方式。每种组织方式都有各自的优点和缺点。开发人员的目标是创建可扩展和干净的代码。遵循这种架构模式的项目通常具有这样的结构:


└───app.js        # 应用程序的入口└───api           # 包含控制器、路由和中间件└───config        # 开发和生产环境的应用程序配置└───loaders       # 包含启动进程└───models        # 数据库模型└───services      # 包含我们的业务逻辑└───jobs          # 作业定义(如果你的程序中有cron作业,我们的没有)└───subscribers   # 异步任务的事件处理器└───test          # 测试文件放在这里
复制代码


为了解释文件夹结构和关注点分离的概念,我们将创建一个简单的身份验证 REST API。我们将构建一个可扩展的结构,以便促进团队协作。我们将使用 Node.js、Express.JS 和 MongoDB。请先确保安装了 Node.js MongoDB


我们的示例应用程序是一个简单的用于身份验证的 REST API。当用户注册时,他们的信息被保存在 MongoDB 数据库中。当用户登录时,我们将验证他们的信息,如果验证成功,就返回一个令牌。在构建这个应用程序的过程中,我们将实现一个可扩展的项目结构,并了解实现这个功能需要做些什么。

创建项目文件夹


我们的应用程序将按照以下的方式组织结构。


  • 所有的文件和逻辑都保存在一个叫作 src 的文件夹中。


  • 应用程序的入口和启动在 server.js 和 app.js 中。api 文件夹包含 controllers、middlewares、routes 和 repositories 子文件夹,这些子文件夹主要用于处理数据传输、请求处理和验证等任务。配置文件夹 config 包含与开发环境和生产环境相关的信息。


  • loaders 文件夹包含程序第一次启动时执行的操作,包括数据库加载器(告诉数据库开始启动)和 Express 加载器(执行 Express 应用程序)。


  • models 文件夹包含了用于描述写入数据库或从数据库读取的数据类型的文件。


  • services 文件夹包含可重用的业务逻辑,用于处理数据处理、实现惟一性业务逻辑、调用数据库等任务。


  • utils 文件夹包含辅助工具、验证器、错误处理器、常量等文件。应用程序中的其他文件可以调用它们来执行一些操作。

└───src  └───app.js  └───server.js       └───api            └───controllers    └───middlewares    └───routes                            └───config           └───loaders           └───models        └───services            └───utils 
复制代码

utils 文件夹——辅助文件


这些文件为应用程序的其他部分提供支持。它们被几个文件或模块调用,用于验证或修改请求或数据块,因为它们具有可重用的结构。例如,开发一个辅助函数来验证电子邮件的格式是否合法。这个功能可以用来验证用户在注册或登录时输入的电子邮件是否遵循正确的格式。


utils 文件夹包含四个文件:


  • validator.js

  • helpers.js

  • error_handler.js

  • error_response.js

validator.js


这个文件中有一个叫作 signupValidator 的方法,用于验证是否提供了所需的参数,以及参数是否正确。例如,我们验证用户提供了用户名和电子邮件,并且密码是我们想要的格式(至少 8 个字符,并且是字母数字和特殊字符的组合)。


import { celebrate, Joi, Segments } from 'celebrate';export default class Validator {  static signupValidator = celebrate({        [Segments.BODY]: Joi.object().keys({            name: Joi.string().required(),            email: Joi.string().email().required().trim().lowercase(),            password: Joi.string().regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d\w\W]{8,}$/).required().label('Password').messages({                "string.min": "{#label} Must have at least 8 characters",                "string.pattern.base": "{#label} must include at least eight characters, one uppercase and lowercase letter and one number"            })        }),    });}
复制代码

helpers.js


这个文件包含处理 JSON 响应格式、密码散列、随机字符串生成等功能的函数。它包含的函数可以被其他服务使用,因为与其在服务中构建这些函数,不如直接根据需要导入它们,保持代码整洁并加快开发速度。


import bcrypt from 'bcryptjs';
import crypto from 'crypto';
const ENCRYPTION_KEY = "(some_r**n_5_str_$$8276_-yuiuj6]"; // 必须是256位 (32个字符)const IV_LENGTH = 16; // 对于AES来说通常是16
export class JsonResponse { constructor(statusCode = 200) { this.statusCode = statusCode; } error = (res, message, data) => { return res.status(this.statusCode).json({ status: false, message, data }) } success = (res, message, data) => { return res.status(this.statusCode).json({ status: true, message, data }) }}
export const randomString = (length) => { let numbers = "0123456789"; let chars = "acdefhiklmnoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY";
let randomstring = ''; let randomstring2 = '';
for (let x = 0; x < Math.floor(length / 2); x++) { let rnum = Math.floor(Math.random() * chars.length); randomstring += chars.substring(rnum, rnum + 1);
} for (let y = 0; y < Math.floor(length / 2); y++) {
let rnum2 = Math.floor(Math.random() * numbers.length); randomstring2 += numbers.substring(rnum2, rnum2 + 1);
} let finalString = (randomstring + randomstring2).split(''); return shuffle(finalString).join('');}
// 比较散列值export const compareHash = (string, hash) => bcrypt.compare(string, hash);
export const hashString = async function (string) { const salt = await bcrypt.genSalt(10); return await bcrypt.hash(string, salt);}
export const encryptData = data => { let iv = crypto.randomBytes(IV_LENGTH); let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let encrypted = cipher.update(data);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');}
export const decryptData = data => { let textParts = data.split(':'); let iv = Buffer.from(textParts.shift(), 'hex'); let encryptedText = Buffer.from(textParts.join(':'), 'hex'); let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();}
复制代码

error_handler.js


这个文件定义了错误响应结构。例如,当你试图构建一个 try-catch 事件时可以在 catch 部分调用它,并提供必要的参数(如状态、数据和消息)。你可以重用这些定义,而不是在所有地方声明它们。准确显示错误信息是非常重要的,因为这有助于 API 用户和开发人员了解问题的根源。



export default class ErrorResponse extends Error { constructor(message, status) { super(message); this.status = status; }}
复制代码

error_response.js


我们可以从文件名推断,它包含了处理不同错误条件的函数。例如,它提供了处理 404 问题、数据库重复字段和服务器问题的函数。



import ErrorResponse from './error_response';import { isCelebrateError } from 'celebrate';
const errorHandler = (err, req, res, next) => { console.log(err) let error = { ...err }; error.message = err.message;
//Celebrate验证错误 if (isCelebrateError(err)) { if (!err) { error = new ErrorResponse("Unable to process request, try again", 400); } else { const errorBody = err.details.get('body'); if (errorBody) { const { details: [errorDetails] } = errorBody; console.log(errorDetails) const message = errorDetails.message; error = new ErrorResponse(message, 400); } else { error = new ErrorResponse("Invalid payload sent, review and try again", 400); } } }
// mongoose重复错误 if (err.code == 11000) { const message = "Field already exists or duplicate value encountered"; error = new ErrorResponse(message, 400); }
// mongoose验证错误 if (err.name == "CastError") { const message = "Invalid parameter passed"; error = new ErrorResponse(message, 400); }
// mongoose验证错误 if (err.name == "ValidationError") { const message = Object.values(err.errors).map(val => val.message); error = new ErrorResponse(message, 400); }
res.status(error.status || 500).json({ status: false, message: error.message || "Server error! request not completed", data: {} });}
export default errorHandler;
复制代码

config 文件夹——环境管理


大多数时候,我们有不同的环境变量。例如,如果我们使用本地开发环境,那么 MongoDB URI 很可能以 localhost 开头,而在生产环境中可能是一个指向图集数据库的链接。因此,我们需要谨慎处理这些差异。我们的 config 文件夹将包含三个文件——dev.js(用于开发环境)、prod.js(用于生产环境)和 index.js 文件(导入前面两个文件)。此外,index.js 文件有一个开关,根据环境决定应该使用哪个文件。


不要忘记创建一个.env 文件,其中包含所需的所有变量。


dev.js


import '../.env'import dotenv from 'dotenv';dotenv.config()
export const config = { secrets: { jwt: process.env.JWT_SECRET_DEV, jwtExp: '100d' }, dbUrl: process.env.MONGO_URI_DEV,}
复制代码

prod.js



import '../.env'import dotenv from 'dotenv';dotenv.config()
export const config = { secrets: { jwt: process.env.JWT_SECRET, jwtExp: '7d' }, dbUrl: process.env.MONGO_URI,
复制代码

index.js


import { merge } from 'lodash';const env = process.env.NODE_ENV || 'development';const port = process.env.PORT || 4002;
const baseConfig = { env, isDev: env === 'development', port,}
let envConfig = {}
switch (env) { case 'dev': case 'development': envConfig = require('./dev').config break case 'prod': case 'production': envConfig = require('./prod').config break default: envConfig = require('./dev').config}export default merge(baseConfig, envConfig)
复制代码

loaders 文件夹


loaders 文件夹包含特定函数初始化所需的文件。例如,我们有一个 Express 加载器和一个数据库加载器,分别用于启动 Express 应用程序和数据库。


背后的想法是将应用程序的启动过程拆成可测试的组件。各种加载器被导入到 loaders 文件夹的 index.js 文件中,让其他文件可以使用它们。


db-loader.js



import mongoose from 'mongoose';import dotenv from 'dotenv'; import options from '../config';
require('dotenv').config({path: __dirname + '/.env' })
export default (url = options.dbUrl, opts = {}) => { let dbOptions = { ...opts, useNewUrlParser: true, useUnifiedTopology: true }; mongoose.connect(url, dbOptions); const conn = mongoose.connection; return conn;}
复制代码

express-loader.js

import * as fs from 'fs';import morgan from 'morgan';import mongoSanitize from 'express-mongo-sanitize';import rateLimit from 'express-rate-limit';import helmet from 'helmet';import xss from 'xss-clean';import cors from 'cors';import ErrorResponse from '../utils/error_response';import errorHandler from '../utils/error_handler';
// 导入路由import apiRoutes from '../api/routes';
const apiLimiter = rateLimit({ windowMs: 20 * 60 * 1000, // 20 minutes max: 100, // 将IP限制在每`window`(这里是20分钟)100个请求 standardHeaders: true, // 通过`RateLimit-*`标头返回速率限定信息 legacyHeaders: false, // 禁用`X-RateLimit-*`标头 handler: (_request, res, _next) => res.status(429).json({ status: false, message: "Too many requests, please try again later." }),})
export default ({ app, express }) => {
app.disable('x-powered-by');
app.use(express.json()) app.use(express.urlencoded({ extended: true })) // 开发用的日志中间件 if (process.env.NODE_ENV === 'development') { app.use(morgan('dev')); }
app.enable('trust proxy'); app.use(cors())
app.use(mongoSanitize());
// 添加安全标头 app.use(helmet());
app.use(xss()); app.get('/ip', (request, response) => response.send(request.ip)) app.use('/api/v1', apiLimiter, apiRoutes);
app.use(errorHandler);
app.use((_req, _res, next) => next(new ErrorResponse('Route not found', 404))); app.use(errorHandler); return app;};
复制代码

index.js


import dbConnect from './db-loader';import expressLoader from './express-loader';
export default async ({ app, express }) => { const connection = dbConnect(); console.log('MongoDB has been Initialized'); expressLoader({ app, express }); console.log('Express package has been Initialized');}
复制代码

入口文件


我们应用程序的入口点是 app.js。通常的做法是在这里放置大量的代码,但关注点分离要确保所有逻辑是分离的。我们将创建两个入口点,即 server.js 和 app.js。在 server.js 文件中,我们将导入加载器和配置文件,并开始监听 PORT。app.js 文件只导入 server.js。因此,从技术上讲,当服务器试图启动应用程序时,它会读取 app.js 文件并试图启动 server.js 文件中指定的各种函数。

server.js

import express from 'express';import dotenv from 'dotenv';import appLoader from './loaders';import appConfig from './config';export const app = express();require('dotenv').config({path: __dirname + '/.env' })
export const start = async () => { try { await appLoader({ app, express }); app.listen(appConfig.port, () => { console.log(`REST API on http://localhost:${appConfig.port}/api/v1`); console.log(process.env.MONGO_URI_DEV); console.log(appConfig.dbUrl); }); } catch (e) { console.error(e) }}
复制代码

app.js


import { start } from './server'
start()process.on('unhandledRejection', (err, _) => { console.log(`Server error: ${err}`)})
复制代码

到目前为止,在运行我们的应用程序时我们会得到一条消息,说我们的应用程序正运行在首选端口上,Express 服务器已启动,并已成功连接到数据库。

模型


还有一些模型,它们是应用程序和数据库之间的接口。它们用于组织我们在应用程序中传递的数据。因此,我们将在模型文件夹中创建两个文件——user.model.js 和 index.js 文件,我们将把所有模型都导入到 index.js 文件中。

user.model.js



import mongoose from 'mongoose';import bcrypt from 'bcryptjs';import { sign } from 'jsonwebtoken';
import config from '../config';const UserSchema = new mongoose.Schema({ name: { type: String, trim: true, required: [true, "Name is required"] }, email: { type: String, trim: true, unique: true, match: [/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, "Please enter a valid email"], required: [true, "Email is required"] }, password: { type: String, select: false, }, created_at: { type: Date, default: Date.now }})
复制代码

index.js


import User from './user.model';export { User};
复制代码

服务文件

服务文件负责处理数据操作、数据库调用和其他业务逻辑。将应用程序服务与控制器分离是一种关注点分离技术。服务层包含与业务相关的逻辑,与 HTTP 层没有任何关联。这种技术让测试用例变得更容易编写,也更容易重构,控制器也变得更小。服务实现了应用程序的逻辑,并在向控制器返回必要的响应之前与数据库发生通信。我们创建了一个简单的认证服务文件,其中包含我们的登录和注册逻辑。

auth.js

import { User } from '../models';import ErrorResponse from '../utils/error_response';import { randomToken } from '../utils/helpers';

export default class AuthService { //用户注册 async signup(data) { try { const { email, password, name } = data; // 通过电子邮件地址查找用户 let query = { $or: [{ email: { $regex: email, $options: 'i' } }] }; const hasEmail = await User.find(query); console.log(hasEmail); // 如果找不到用户就抛出错误 if (hasEmail.length > 0) { throw new ErrorResponse('Email already exists', 400); } const user = await User.create({ email, password, name }); console.log(user) return user; } catch (e) { throw e; } }
async signin(data) { try { let { email, password } = data; let query = { $or: [ { email: { $regex: email, $options: 'i' } } ] };
// 通过电子邮件地址查找用户 const user = await User.findOne(query).select('+password'); // throw error if user not found if (!user) { throw new ErrorResponse('Invalid credentials', 401); }

// 检查用户密码 const isMatch = await user.comparePassword(password); if (!isMatch) { throw new ErrorResponse('Invalid credentials', 401); } return { user: user.toMap(), token: user.getJwtToken(), }; } catch (e) { throw e; } }}
复制代码

api 文件夹

最后是我们的 api 文件夹,其中包含另外三个重要的子文件夹——controllers、routes 和 middlewares,我们将分别介绍它们。

middlewares


中间件负责处理应用程序中的各种验证或其他一般性检查。我们将创建两个文件,async_handler.js 和 auth_handler.js,来处理 res(响应)和 req(请求)对象,以及用户授权。

async_handler.js

export const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
复制代码

auth_handler.js


import { verify } from 'jsonwebtoken';import ErrorResponse from '../../utils/error_response';import { asyncHandler } from './async_handler';import config from '../../config';
export const userAuth = asyncHandler( async (req, res, next) => { let authHeader = req.headers.authorization; let token = authHeader && authHeader.startsWith('Bearer') && authHeader.split(' ')[1];
if (!token) { return next(new ErrorResponse('Unauthorized access', 401)); } try { const decoded = verify(token, config.secrets.jwt); req.user = decoded.result; next(); } catch (e) { return next(new ErrorResponse('Unauthorized access', 401)); } });
复制代码

controllers


控制器接收请求,调用所需的服务,通过数据访问层与数据库通信,然后将结果发送回服务,服务再将结果发送回控制器,控制器再将结果发送给客户端。我们将在 controllers 文件夹中创建一个叫作 index.js 的文件,其中包含我们的登录和注册控制器。这些控制器使用 handler.js 文件中的 res 和 req 对象向各种服务发送请求。

index.js


import { asyncHandler } from '../middlewares/async_handler';import { JsonResponse } from '../../utils/helpers';import AuthService from '../../services/auth';export default class IndexController {    constructor() {        this.authService = new AuthService();    }    index = asyncHandler(        async (req, res, _) => {            res.json({ status: true, message: "Base API Endpoint." })
}); loginUser = asyncHandler( async (req, res, _) => {
const { user, token } = await this.authService.signin(req.body);
return new JsonResponse().success(res, "User logged in successfully", { user, token });
});
registerUser = asyncHandler( async (req, res, _) => {
await this.authService.signup(req.body);
return new JsonResponse(201).success(res, "User account created successfully", {});
});
}
复制代码

routes


路由定义了我们的应用程序应该如何响应来自客户端的 HTTP 请求。它是程序中与 HTTP 谓词相关的部分。中间件可能会保护这些路由,也可能不会。路由的主要功能是在请求到达时处理请求。


例如,POST 请求创建路由并期望数据被发布或传递。


在 routes 文件夹中,我们创建了一个 index.js 文件,其中包含了访问平台各种服务所需的所有路由。路由接收一个请求,将其转发到控制器,然后控制器将其转发到数据库,并向控制器返回一个报告。

index.js


import { Router } from 'express';import Validator from '../../utils/validator';import IndexController from '../controllers';
const router = Router();// 导入所有的控制器let indexController = new IndexController();
// 注册所有的路由router.get('/', indexController.index);
router.post('/login', indexController.loginUser);router.post('/register', Validator.signupValidator, indexController.registerUser);
//导出基础路由器export default router;
复制代码

结论


每个开发人员都应该努力编写干净的、可读的和可重用的代码,这样可以让重构、协作、测试和少犯错误变得更加容易。设计 API 架构有多种方法,在选择架构时,无论如何确保可伸缩性和可读性都是你的首要考虑因素。


不过我们确实建议采用技术架构分离,因为正如你所看到的,它有许多优点。这项技术已被证明在构建项目时是非常有用的,无论项目的复杂性或团队规模如何。你肯定不希望在生产环境中出现任何错误!


作者简介

Oghenevwede Emeni 是一名拥有超过 6 年经验的软件开发人员,目前在 Bawse(一家音乐科技初创公司)担任全栈工程师,并经营着自己的第三方支付初创公司!


原文链接:https://www.infoq.com/articles/separation-concerns-nodejs/


声明:本文为 InfoQ 翻译,未经许可禁止转载。

2022-11-09 18:494881

评论

发布
暂无评论
发现更多内容

Kubernetes应用管理深度剖析

Bob

云原生 容器化 后端技术 Kubernetes 集群

只分享这一次!阿里软件架构师深入底层手写JDK源码

程序猿阿宇

架构 Java 面试 大厂技能 后端技术 秋招

Moonbeam创始人解读多链新概念Connected Contract

One Block Community

区块链

备战金九银十,两份JAVA面试题2022最新整合版,祝你脱颖而出

王小凡

Java MySQL spring 面试 springboot

HarmonyOS分布式协同演奏技术实现路线(Java)

程序员啊叶

Java 编程 程序员 架构 java面试

4位资深专家多年大厂经验分享出Flink技术内幕架构设计与实现原理

程序员啊叶

Java 编程 程序员 架构 java面试

太牛了,百度这份开发手册几乎涵盖了Spring Cloud所有操作

程序猿阿宇

架构 java程序员 java面试 秋招 程序员‘

京东发布这份SpringBoot实战手册带你从0手动搭建企业级商城项目

程序猿阿宇

程序员 面试 java程序员 Spring Boot CLI 秋招

想用K8s,还得先会Docker吗?其实完全没必要

程序员啊叶

Java 编程 程序员 架构 java面试

上天入地Hadoop——使用高性能分布式技术完成大数据处理任务

程序猿阿宇

大数据 架构 Java 面试 java程序员 编程、

我给大家免费公开五份阿里Java架构师学习手册!助力金九银十

程序猿阿宇

架构 Java 面试 大厂面试 大厂技能 秋招

时序数据库在船舶风险管理领域的应用

CnosDB

IoT 时序数据库 开源社区 CnosDB infra

五面阿里,终拿offer,不明白为什么面试官总喜欢问Java这种问题

程序知音

Java 阿里 后端技术 八股文 Java面试八股文

蓦然回首,“工厂、构造、原型”设计模式,正在灯火阑珊处

掘金安东尼

JavaScript 前端 7月月更

六面蚂蚁金服,唬住了面试官要了30K;其实Java面试也没那么难

程序知音

Java 程序员 后端技术 秋招 Java面试八股文

真香!180页100+题15W+字解析的《Java高级面试指南》,果断收下

程序员啊叶

Java 编程 程序员 架构 java面试

SMI 与 Gateway API 的 GAMMA 倡议意味着什么?

张晓辉

Kubernetes 服务网格 SMI

大厂面试突击必备:“网络编程”高频八连击,扛得住吗?

程序员啊叶

Java 编程 程序员 架构 java面试

阿里内网最新发布“M8”级Java面试笔记,助力金九银十

程序员啊叶

Java 编程 程序员 架构 java面试

浅谈非 EVM 公链的可能性: 兼容多类型虚拟机是否是区块链未来?

One Block Community

区块链

华为云数据治理生产线DataArts,让“数据'慧'说话”

华为云开发者联盟

云计算 华为云

设计消息队列存储消息数据的 MySQL 表格

爱晒太阳的大白

如何写一份高可读性的软件工程设计文档

C++后台开发

数据库 软件工程 后端开发 C/C++后台开发 C/C++开发

腾讯被裁,转头去字节!Java后端核心面试题在手,怎能进不去大厂

程序员啊叶

Java 编程 程序员 架构 java面试

SocialFi 何以成就 Web3 去中心化社交未来

One Block Community

区块链

加密生活,Web3 项目合伙人的一天

TinTinLand

区块链

DTSE Tech Talk丨第2期:1小时深度解读SaaS应用系统设计

华为云开发者联盟

云计算 后端 SaaS

数据中台建设(五):打破企业数据孤岛和提取数据价值

Lansonli

数据中台 7月月更

CMake库搜索函数居然不搜索LD_LIBRARY_PATH

华为云开发者联盟

后端 开发

难道Redis真的变慢了吗?

程序员啊叶

Java 编程 程序员 架构 java面试

首发!阿里技术大牛最新耗时半个月整理出最全MySQL性能优化和高可用架构技术宝典,直接封神!

了不起的程序猿

MySQL 数据库 程序员 性能优化 JAV A

Node.js中的关注点分离_编程语言_Emeni Oghenevwede_InfoQ精选文章