NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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:494883

评论

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

火山引擎 MARS X 今日头条 | 大型App高效协同开发,实现研发流程自动化

字节跳动终端技术

今日头条 字节跳动 研发 火山引擎MARS

2022年中国茶饮产业洞察

易观分析

茶饮 产业洞察

LSM-Tree - LevelDb了解和实现

懒时小窝

LSM树 LSM-Tree

DDD实战(8):冲刺1战术之聚合设计

深清秋

DDD 软件架构 生鲜电商系统 4月月更

鲸智WhaleBI 平民化数据消费还业务以真正的“数据自由”

鲸品堂

方法论 数据 数据治理

人人都可以学会的产品手册制作方法

小炮

产品宣传手册

龙蜥大讲堂:如何利用硬件SIMD指令提升Java程序的性能?|第14期

OpenAnolis小助手

Java simd arm sig 龙蜥大讲堂

组合式应用新利器?SaaS新时代事件网格如何解决集成标准化问题

华为云开发者联盟

Serverless 无服务器 事件网格 组合式应用

TASKCTL 变量的函数表达式运算

TASKCTL

批量任务 调度引擎 ETL 自动化运维 调度任务

大咖说·智篆商业|颜杰华:数字经济时代,不确定性中的确定性

大咖说

阿里巴巴 数字经济数字时代 数智化 存量时代

什么时候需要使用CRM系统?

低代码小观

CRM 客户关系管理 低代码开发 CRM系统 客户关系管理系统

《数字经济全景白皮书》Z世代用户洞察篇(2)重磅发布!

易观分析

Z世代

MySQL事务并发带来的问题以及其解决方案分析

乌龟哥哥

4月月更

java高级用法之:JNA中的Structure

程序那些事

Java 程序那些事 JNA 4月月更

GPU底层技术、全球市场格局分析(中)

Finovy Cloud

人工智能 云计算 云服务器 GPU服务器 GPU算力

增长270%!PWA 在 Chrome 平台迎来井喷式增长

鼎道智联

桌面运维工程师需要会哪些技能?主要是做什么的?

行云管家

云计算 运维 运维工程师 IT运维 云运维

一起来试玩!在线可编程交互的实时音视频 Web SDK 入门教程

声网

音视频 教程

小程序运行时+SAAS级服务,提升研发降本增效作用

Speedoooo

flutter APP开发 SaaS平台 小程序容器 小程序运行时

CrashSight异常崩溃管理解决方案

WeTest

NFT数字藏品交易平台系统开发搭建

薇電13242772558

NFT 数字藏品

项目中如何优雅的消除if-else

Rubble

4月日更 4月月更

基于LAXCUS开发分布式视频转码应用

LAXCUS分布式操作系统

分布式计算 分布式应用 视频转码

轻量级兼顾本地体验,PWA应用到底有多卷?

鼎道智联

当 API 成为服务,我们将连接一切!

鼎道智联

关于 WordPress 你了解多少?

海拥(haiyong.site)

4月月更

华为云物联网高级攻城狮的4年配置中心实践分享

华为云开发者联盟

开源 DevOps 配置中心 Apollo 业务配置

TASKCTL 调度设计器作业属性的应用

TASKCTL

分布式 元数据区 ETL 自动化运维 任务调度器

Flink整合ElasticSearch详细指南及踩坑记录

五分钟学大数据

4月月更

【云管平台】三大云管平台厂商详细介绍

行云管家

云计算 云管平台 云堡垒机 云厂商

TiDB 在连锁快餐企业丨海量交易与实时分析的应用探索

TiDB 社区干货传送门

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