组件和模块
概述
Node.js 应用程序使用模块构建——封装功能的可重用代码片段。本章涵盖如何有效地创建、组织和使用模块,包括 CommonJS 模块、ES6 模块和 npm 包。
模块系统
CommonJS 模块(默认)
CommonJS 是 Node.js 中的传统模块系统:
javascript
// math-utils.js - Exporting functions
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// Different export patterns
module.exports = {
add,
subtract,
multiply,
divide
};
// Alternative: individual exports
// exports.add = add;
// exports.subtract = subtract;javascript
// calculator.js - Exporting a class
class Calculator {
constructor() {
this.history = [];
this.memory = 0;
}
calculate(operation, a, b) {
let result;
switch (operation) {
case 'add':
result = a + b;
break;
case 'subtract':
result = a - b;
break;
case 'multiply':
result = a * b;
break;
case 'divide':
if (b === 0) throw new Error('Division by zero');
result = a / b;
break;
default:
throw new Error('Unknown operation');
}
this.history.push({ operation, a, b, result, timestamp: new Date() });
return result;
}
getHistory() {
return [...this.history];
}
clearHistory() {
this.history = [];
}
memoryStore(value) {
this.memory = value;
}
memoryRecall() {
return this.memory;
}
memoryClear() {
this.memory = 0;
}
}
module.exports = Calculator;javascript
// app.js - Using modules
const mathUtils = require('./math-utils');
const Calculator = require('./calculator');
// Using function exports
console.log('Addition:', mathUtils.add(5, 3));
console.log('Subtraction:', mathUtils.subtract(10, 4));
// Using class export
const calc = new Calculator();
console.log('Calculator result:', calc.calculate('multiply', 6, 7));
console.log('History:', calc.getHistory());ES6 Modules (ESM)
Enable ES6 modules by adding "type": "module" to package.json:
javascript
// math-utils.mjs - Named exports
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
export class MathHelper {
static square(n) {
return n * n;
}
static cube(n) {
return n * n * n;
}
}
// Default export
export default function multiply(a, b) {
return a * b;
}javascript
// calculator.mjs - Default and named exports
export default class Calculator {
constructor() {
this.value = 0;
}
add(n) {
this.value += n;
return this;
}
subtract(n) {
this.value -= n;
return this;
}
multiply(n) {
this.value *= n;
return this;
}
divide(n) {
if (n === 0) throw new Error('Division by zero');
this.value /= n;
return this;
}
getValue() {
return this.value;
}
reset() {
this.value = 0;
return this;
}
}
export const CONSTANTS = {
PI: 3.14159,
E: 2.71828
};
export function formatResult(value, decimals = 2) {
return parseFloat(value.toFixed(decimals));
}javascript
// app.mjs - Using ES6 modules
import multiply, { add, subtract, PI, MathHelper } from './math-utils.mjs';
import Calculator, { CONSTANTS, formatResult } from './calculator.mjs';
// Using named imports
console.log('Addition:', add(5, 3));
console.log('PI value:', PI);
console.log('Square:', MathHelper.square(4));
// Using default import
console.log('Multiplication:', multiply(6, 7));
// Using class
const calc = new Calculator();
const result = calc.add(10).multiply(2).subtract(5).getValue();
console.log('Chained calculation:', formatResult(result));Creating Reusable Components
Database Connection Module
javascript
// database/connection.js
const mongoose = require('mongoose');
const config = require('../config');
class DatabaseConnection {
constructor() {
this.connection = null;
this.isConnected = false;
}
async connect() {
try {
if (this.isConnected) {
return this.connection;
}
this.connection = await mongoose.connect(config.database.url, {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: config.database.poolSize
});
this.isConnected = true;
console.log('Database connected successfully');
// Handle connection events
mongoose.connection.on('error', (error) => {
console.error('Database connection error:', error);
this.isConnected = false;
});
mongoose.connection.on('disconnected', () => {
console.log('Database disconnected');
this.isConnected = false;
});
return this.connection;
} catch (error) {
console.error('Database connection failed:', error);
throw error;
}
}
async disconnect() {
if (this.connection) {
await mongoose.disconnect();
this.isConnected = false;
console.log('Database disconnected');
}
}
getConnection() {
return this.connection;
}
isConnectionActive() {
return this.isConnected;
}
}
// Singleton pattern
const dbConnection = new DatabaseConnection();
module.exports = dbConnection;Logger Module
javascript
// utils/logger.js
const winston = require('winston');
const path = require('path');
const config = require('../config');
class Logger {
constructor() {
this.logger = this.createLogger();
}
createLogger() {
const logFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
);
const transports = [
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 5
})
];
// Add console transport in development
if (config.app.env !== 'production') {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
);
}
return winston.createLogger({
level: config.logging.level,
format: logFormat,
transports
});
}
info(message, meta = {}) {
this.logger.info(message, meta);
}
error(message, meta = {}) {
this.logger.error(message, meta);
}
warn(message, meta = {}) {
this.logger.warn(message, meta);
}
debug(message, meta = {}) {
this.logger.debug(message, meta);
}
// HTTP request logging
logRequest(req, res, responseTime) {
const logData = {
method: req.method,
url: req.url,
statusCode: res.statusCode,
responseTime: `${responseTime}ms`,
userAgent: req.get('User-Agent'),
ip: req.ip
};
if (res.statusCode >= 400) {
this.error('HTTP Request Error', logData);
} else {
this.info('HTTP Request', logData);
}
}
// Database operation logging
logDatabaseOperation(operation, collection, duration, error = null) {
const logData = {
operation,
collection,
duration: `${duration}ms`
};
if (error) {
this.error('Database Operation Failed', { ...logData, error: error.message });
} else {
this.debug('Database Operation', logData);
}
}
}
// Singleton pattern
const logger = new Logger();
module.exports = logger;Email Service Module
javascript
// services/email-service.js
const nodemailer = require('nodemailer');
const fs = require('fs').promises;
const path = require('path');
const config = require('../config');
const logger = require('../utils/logger');
class EmailService {
constructor() {
this.transporter = null;
this.templates = new Map();
this.init();
}
async init() {
try {
// Create transporter
this.transporter = nodemailer.createTransporter({
host: config.email.smtp.host,
port: config.email.smtp.port,
secure: config.email.smtp.secure,
auth: config.email.smtp.auth
});
// Verify connection
await this.transporter.verify();
logger.info('Email service initialized successfully');
// Load email templates
await this.loadTemplates();
} catch (error) {
logger.error('Email service initialization failed', { error: error.message });
throw error;
}
}
async loadTemplates() {
const templatesDir = path.join(__dirname, '../templates/email');
try {
const templateFiles = await fs.readdir(templatesDir);
for (const file of templateFiles) {
if (file.endsWith('.html')) {
const templateName = path.basename(file, '.html');
const templatePath = path.join(templatesDir, file);
const templateContent = await fs.readFile(templatePath, 'utf8');
this.templates.set(templateName, templateContent);
}
}
logger.info(`Loaded ${this.templates.size} email templates`);
} catch (error) {
logger.warn('Failed to load email templates', { error: error.message });
}
}
async sendEmail(to, subject, template, data = {}) {
try {
let html = this.templates.get(template);
if (!html) {
throw new Error(`Template '${template}' not found`);
}
// Simple template variable replacement
html = this.replaceTemplateVariables(html, data);
const mailOptions = {
from: `${config.email.from.name} <${config.email.from.address}>`,
to,
subject,
html
};
const result = await this.transporter.sendMail(mailOptions);
logger.info('Email sent successfully', {
to,
subject,
template,
messageId: result.messageId
});
return result;
} catch (error) {
logger.error('Failed to send email', {
to,
subject,
template,
error: error.message
});
throw error;
}
}
replaceTemplateVariables(template, data) {
let result = template;
for (const [key, value] of Object.entries(data)) {
const regex = new RegExp(`{{${key}}}`, 'g');
result = result.replace(regex, value);
}
return result;
}
async sendWelcomeEmail(userEmail, userName) {
return this.sendEmail(
userEmail,
'Welcome to Our Platform!',
'welcome',
{
userName,
loginUrl: `${config.app.url}/login`,
supportEmail: config.email.from.address
}
);
}
async sendPasswordResetEmail(userEmail, resetToken) {
const resetUrl = `${config.app.url}/reset-password?token=${resetToken}`;
return this.sendEmail(
userEmail,
'Password Reset Request',
'reset-password',
{
resetUrl,
expiryTime: '1 hour'
}
);
}
}
module.exports = EmailService;Cache Module
javascript
// services/cache-service.js
const redis = require('redis');
const config = require('../config');
const logger = require('../utils/logger');
class CacheService {
constructor() {
this.client = null;
this.isConnected = false;
this.defaultTTL = config.redis.ttl;
}
async connect() {
try {
this.client = redis.createClient({
host: config.redis.host,
port: config.redis.port,
password: config.redis.password,
db: config.redis.db
});
this.client.on('error', (error) => {
logger.error('Redis connection error', { error: error.message });
this.isConnected = false;
});
this.client.on('connect', () => {
logger.info('Redis connected');
this.isConnected = true;
});
this.client.on('disconnect', () => {
logger.warn('Redis disconnected');
this.isConnected = false;
});
await this.client.connect();
return this.client;
} catch (error) {
logger.error('Failed to connect to Redis', { error: error.message });
throw error;
}
}
async disconnect() {
if (this.client) {
await this.client.disconnect();
this.isConnected = false;
}
}
generateKey(prefix, identifier) {
return `${config.redis.keyPrefix}${prefix}:${identifier}`;
}
async get(key) {
try {
if (!this.isConnected) {
logger.warn('Cache not available, skipping get operation');
return null;
}
const value = await this.client.get(key);
if (value) {
logger.debug('Cache hit', { key });
return JSON.parse(value);
}
logger.debug('Cache miss', { key });
return null;
} catch (error) {
logger.error('Cache get error', { key, error: error.message });
return null;
}
}
async set(key, value, ttl = this.defaultTTL) {
try {
if (!this.isConnected) {
logger.warn('Cache not available, skipping set operation');
return false;
}
const serializedValue = JSON.stringify(value);
await this.client.setEx(key, ttl, serializedValue);
logger.debug('Cache set', { key, ttl });
return true;
} catch (error) {
logger.error('Cache set error', { key, error: error.message });
return false;
}
}
async delete(key) {
try {
if (!this.isConnected) {
return false;
}
const result = await this.client.del(key);
logger.debug('Cache delete', { key, deleted: result > 0 });
return result > 0;
} catch (error) {
logger.error('Cache delete error', { key, error: error.message });
return false;
}
}
async exists(key) {
try {
if (!this.isConnected) {
return false;
}
const result = await this.client.exists(key);
return result === 1;
} catch (error) {
logger.error('Cache exists error', { key, error: error.message });
return false;
}
}
async flush() {
try {
if (!this.isConnected) {
return false;
}
await this.client.flushDb();
logger.info('Cache flushed');
return true;
} catch (error) {
logger.error('Cache flush error', { error: error.message });
return false;
}
}
// Higher-level caching methods
async cacheFunction(key, fn, ttl = this.defaultTTL) {
const cachedResult = await this.get(key);
if (cachedResult !== null) {
return cachedResult;
}
const result = await fn();
await this.set(key, result, ttl);
return result;
}
async invalidatePattern(pattern) {
try {
if (!this.isConnected) {
return false;
}
const keys = await this.client.keys(pattern);
if (keys.length > 0) {
await this.client.del(keys);
logger.info('Cache pattern invalidated', { pattern, keysDeleted: keys.length });
}
return true;
} catch (error) {
logger.error('Cache pattern invalidation error', { pattern, error: error.message });
return false;
}
}
}
module.exports = CacheService;Module Organization Patterns
Repository Pattern
javascript
// repositories/base-repository.js
class BaseRepository {
constructor(model) {
this.model = model;
}
async findById(id) {
return await this.model.findById(id);
}
async findAll(options = {}) {
const { page = 1, limit = 10, sort = { createdAt: -1 } } = options;
const skip = (page - 1) * limit;
const [items, total] = await Promise.all([
this.model.find().sort(sort).skip(skip).limit(limit),
this.model.countDocuments()
]);
return {
items,
total,
page,
limit,
pages: Math.ceil(total / limit)
};
}
async create(data) {
const item = new this.model(data);
return await item.save();
}
async update(id, data) {
return await this.model.findByIdAndUpdate(id, data, { new: true });
}
async delete(id) {
return await this.model.findByIdAndDelete(id);
}
async findByField(field, value) {
return await this.model.find({ [field]: value });
}
async exists(id) {
const count = await this.model.countDocuments({ _id: id });
return count > 0;
}
}
module.exports = BaseRepository;javascript
// repositories/user-repository.js
const BaseRepository = require('./base-repository');
const User = require('../models/User');
class UserRepository extends BaseRepository {
constructor() {
super(User);
}
async findByEmail(email) {
return await this.model.findOne({ email });
}
async findActiveUsers() {
return await this.model.find({ isActive: true });
}
async updateLastLogin(userId) {
return await this.model.findByIdAndUpdate(
userId,
{ lastLoginAt: new Date() },
{ new: true }
);
}
async searchUsers(query, options = {}) {
const { page = 1, limit = 10 } = options;
const skip = (page - 1) * limit;
const searchRegex = new RegExp(query, 'i');
const filter = {
$or: [
{ name: searchRegex },
{ email: searchRegex }
]
};
const [users, total] = await Promise.all([
this.model.find(filter).skip(skip).limit(limit),
this.model.countDocuments(filter)
]);
return {
users,
total,
page,
limit,
pages: Math.ceil(total / limit)
};
}
}
module.exports = UserRepository;Service Layer Pattern
javascript
// services/user-service.js
const UserRepository = require('../repositories/user-repository');
const EmailService = require('./email-service');
const CacheService = require('./cache-service');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const config = require('../config');
const logger = require('../utils/logger');
class UserService {
constructor() {
this.userRepository = new UserRepository();
this.emailService = new EmailService();
this.cacheService = new CacheService();
}
async createUser(userData) {
try {
// Check if user already exists
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new Error('User with this email already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(userData.password, config.security.bcryptRounds);
// Create user
const user = await this.userRepository.create({
...userData,
password: hashedPassword,
isActive: true,
createdAt: new Date()
});
// Send welcome email
await this.emailService.sendWelcomeEmail(user.email, user.name);
// Cache user data
const cacheKey = this.cacheService.generateKey('user', user._id);
await this.cacheService.set(cacheKey, this.sanitizeUser(user));
logger.info('User created successfully', { userId: user._id, email: user.email });
return this.sanitizeUser(user);
} catch (error) {
logger.error('Failed to create user', { error: error.message, userData: { email: userData.email } });
throw error;
}
}
async getUserById(userId) {
try {
// Try cache first
const cacheKey = this.cacheService.generateKey('user', userId);
const cachedUser = await this.cacheService.get(cacheKey);
if (cachedUser) {
return cachedUser;
}
// Fetch from database
const user = await this.userRepository.findById(userId);
if (!user) {
return null;
}
const sanitizedUser = this.sanitizeUser(user);
// Cache the result
await this.cacheService.set(cacheKey, sanitizedUser);
return sanitizedUser;
} catch (error) {
logger.error('Failed to get user by ID', { userId, error: error.message });
throw error;
}
}
async authenticateUser(email, password) {
try {
const user = await this.userRepository.findByEmail(email);
if (!user) {
throw new Error('Invalid credentials');
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new Error('Invalid credentials');
}
if (!user.isActive) {
throw new Error('Account is deactivated');
}
// Update last login
await this.userRepository.updateLastLogin(user._id);
// Generate JWT token
const token = jwt.sign(
{ userId: user._id, email: user.email },
config.jwt.secret,
{ expiresIn: config.jwt.expiresIn }
);
logger.info('User authenticated successfully', { userId: user._id, email });
return {
user: this.sanitizeUser(user),
token
};
} catch (error) {
logger.error('Authentication failed', { email, error: error.message });
throw error;
}
}
async updateUser(userId, updateData) {
try {
// Remove sensitive fields
const { password, ...safeUpdateData } = updateData;
const updatedUser = await this.userRepository.update(userId, safeUpdateData);
if (!updatedUser) {
throw new Error('User not found');
}
// Invalidate cache
const cacheKey = this.cacheService.generateKey('user', userId);
await this.cacheService.delete(cacheKey);
logger.info('User updated successfully', { userId });
return this.sanitizeUser(updatedUser);
} catch (error) {
logger.error('Failed to update user', { userId, error: error.message });
throw error;
}
}
async searchUsers(query, options) {
try {
const result = await this.userRepository.searchUsers(query, options);
return {
...result,
users: result.users.map(user => this.sanitizeUser(user))
};
} catch (error) {
logger.error('Failed to search users', { query, error: error.message });
throw error;
}
}
sanitizeUser(user) {
const { password, ...sanitizedUser } = user.toObject ? user.toObject() : user;
return sanitizedUser;
}
}
module.exports = UserService;NPM Package Creation
Creating a Reusable Package
javascript
// package.json for a utility package
{
"name": "@mycompany/node-utils",
"version": "1.0.0",
"description": "Utility functions for Node.js applications",
"main": "index.js",
"module": "index.mjs",
"types": "index.d.ts",
"files": [
"lib/",
"index.js",
"index.mjs",
"index.d.ts",
"README.md"
],
"scripts": {
"build": "babel src --out-dir lib",
"test": "jest",
"lint": "eslint src/",
"prepublishOnly": "npm run build && npm test"
},
"keywords": ["nodejs", "utilities", "helpers"],
"author": "Your Name",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}javascript
// index.js - Main entry point
const { formatDate, formatCurrency } = require('./lib/formatters');
const { validateEmail, validatePhone } = require('./lib/validators');
const { generateId, generateSlug } = require('./lib/generators');
const { asyncRetry, asyncTimeout } = require('./lib/async-utils');
module.exports = {
formatters: {
formatDate,
formatCurrency
},
validators: {
validateEmail,
validatePhone
},
generators: {
generateId,
generateSlug
},
asyncUtils: {
asyncRetry,
asyncTimeout
}
};下一步
在下一章中,我们将探索 Node.js 应用程序中的路由和导航,包括 Express.js 路由模式。
实践练习
- 创建一个模块化身份验证系统,包含 JWT、OAuth 和本地身份验证的独立模块
- 构建一个允许动态加载模块的插件系统
- 为通用实用函数创建一个 npm 包
- 实现一个带依赖注入的服务层
关键要点
- 模块是 Node.js 应用程序的构建块
- CommonJS 和 ES6 模块提供了不同的代码组织方法
- 仓库模式分离数据访问逻辑
- 服务层封装业务逻辑
- 适当的模块组织提高可维护性和可测试性
- 创建可重用的包促进跨项目的代码共享
- 依赖注入使模块更具可测试性和灵活性