服务端框架✅
本主题涵盖Node.js服务端开发中的主流框架,包括Express、Koa等框架的核心概念和使用方法。
Express中间件(Middleware)的工作原理是什么?
答案
核心概念
Express中间件是处理HTTP请求的函数,它们按照定义的顺序依次执行,形成一个处理链。中间件可以执行代码、修改请求和响应对象、结束请求-响应循环,或调用下一个中间件。
工作原理
Express中间件基于**责任链模式(Chain of Responsibility Pattern)**实现:
1. 中间件函数签名
function middleware(req, res, next) {
// 执行业务逻辑
// 修改req或res对象
next(); // 调用下一个中间件
}
2. 执行流程
- 请求进入→第一个中间件→执行逻辑→调用next()→下一个中间件→...→响应返回
- 如果某个中间件不调用next(),请求处理链就会中断
- 错误处理中间件有4个参数:
(err, req, res, next)
3. 中间件类型
- 应用级中间件:
app.use() - 路由级中间件:
router.use() - 错误处理中间件: 4参数函数
- 内置中间件:
express.static(),express.json() - 第三方中间件:
morgan,cors等
示例代码:
- 中间件工作原理
- 执行顺序说明
// Express 中间件工作原理演示 const express = require('express'); const app = express(); // 模拟数据库 const users = [ { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' }, { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' }, { id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'user' } ]; /** * 1. 基础中间件示例 */ function basicLoggingMiddleware(req, res, next) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); // 必须调用next()传递控制权 } /** * 2. 认证中间件 - 责任链模式实现 */ function authenticate(req, res, next) { const token = req.headers.authorization; if (!token) { return res.status(401).json({ error: 'No token provided' }); } // 模拟token验证 if (token === 'Bearer valid-token') { req.user = { id: 1, name: 'Alice', role: 'admin' }; next(); // 验证通过,继续下一个中间件 } else { res.status(401).json({ error: 'Invalid token' }); } } /** * 3. 权限检查中间件工厂函数 */ function authorize(requiredRole) { return function(req, res, next) { if (!req.user) { return res.status(401).json({ error: 'Not authenticated' }); } if (req.user.role !== requiredRole && req.user.role !== 'admin') { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; } /** * 4. 错误处理中间件 */ function errorHandler(err, req, res, next) { console.error('Error occurred:', err); // 如果响应已经发送,将错误传递给默认的Express错误处理器 if (res.headersSent) { return next(err); } // 根据错误类型返回不同的状态码 let statusCode = 500; let message = 'Internal Server Error'; if (err.name === 'ValidationError') { statusCode = 400; message = err.message; } else if (err.name === 'UnauthorizedError') { statusCode = 401; message = 'Unauthorized'; } res.status(statusCode).json({ error: message, timestamp: new Date().toISOString(), path: req.path }); } /** * 5. 请求验证中间件 */ function validateUserInput(req, res, next) { const { name, email } = req.body; if (!name || typeof name !== 'string' || name.trim().length === 0) { const error = new Error('Name is required and must be a non-empty string'); error.name = 'ValidationError'; return next(error); } if (!email || !email.includes('@')) { const error = new Error('Valid email is required'); error.name = 'ValidationError'; return next(error); } // 验证通过,继续执行 next(); } /** * 6. 响应时间统计中间件 */ function responseTime(req, res, next) { const start = Date.now(); // 监听响应完成事件 res.on('finish', () => { const duration = Date.now() - start; console.log(`Request to ${req.path} took ${duration}ms`); }); next(); } /** * 7. CORS中间件实现 */ function corsMiddleware(options = {}) { const { origin = '*', methods = 'GET,HEAD,PUT,PATCH,POST,DELETE', allowedHeaders = 'Content-Type,Authorization', credentials = false } = options; return function(req, res, next) { // 设置CORS头 res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Methods', methods); res.header('Access-Control-Allow-Headers', allowedHeaders); if (credentials) { res.header('Access-Control-Allow-Credentials', 'true'); } // 处理预检请求 if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }; } /** * 8. 缓存中间件 */ const cache = new Map(); function cacheMiddleware(duration = 300000) { // 默认5分钟 return function(req, res, next) { // 只缓存GET请求 if (req.method !== 'GET') { return next(); } const key = req.originalUrl; const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < duration) { console.log(`Cache hit for ${key}`); return res.json(cached.data); } // 保存原始的res.json方法 const originalJson = res.json; // 重写res.json方法以缓存响应 res.json = function(data) { cache.set(key, { data: data, timestamp: Date.now() }); console.log(`Cached response for ${key}`); return originalJson.call(this, data); }; next(); }; } /** * 9. 中间件执行顺序演示 */ function middlewareOrderDemo() { console.log('=== Express 中间件执行顺序演示 ==='); // 全局中间件 - 按顺序执行 app.use(corsMiddleware()); app.use(responseTime); app.use(basicLoggingMiddleware); app.use(express.json()); // 解析JSON请求体 // 路由级中间件 app.get('/users', cacheMiddleware(60000), // 缓存1分钟 (req, res) => { res.json(users); } ); // 需要认证的路由 app.get('/profile', authenticate, (req, res) => { res.json(req.user); } ); // 需要管理员权限的路由 app.delete('/users/:id', authenticate, authorize('admin'), (req, res) => { const userId = parseInt(req.params.id); const userIndex = users.findIndex(u => u.id === userId); if (userIndex === -1) { return res.status(404).json({ error: 'User not found' }); } users.splice(userIndex, 1); res.json({ message: 'User deleted successfully' }); } ); // 带验证的POST路由 app.post('/users', authenticate, authorize('admin'), validateUserInput, (req, res) => { const newUser = { id: users.length + 1, name: req.body.name, email: req.body.email, role: req.body.role || 'user' }; users.push(newUser); res.status(201).json(newUser); } ); // 触发错误的路由 app.get('/error', (req, res, next) => { const error = new Error('This is a test error'); error.name = 'TestError'; next(error); // 传递错误给错误处理中间件 }); // 错误处理中间件必须放在最后 app.use(errorHandler); // 404处理 app.use('*', (req, res) => { res.status(404).json({ error: 'Route not found', path: req.originalUrl }); }); } /** * 10. 自定义中间件类 - 更高级的封装 */ class MiddlewareManager { constructor() { this.middlewares = []; } use(middleware) { this.middlewares.push(middleware); return this; } // 创建一个处理函数,按顺序执行所有中间件 getHandler() { return (req, res) => { let index = 0; const next = (error) => { if (error) { // 查找错误处理中间件 const errorMiddleware = this.middlewares.find(mw => mw.length === 4); if (errorMiddleware) { return errorMiddleware(error, req, res, () => {}); } throw error; } if (index >= this.middlewares.length) { return; // 所有中间件执行完毕 } const middleware = this.middlewares[index++]; try { if (middleware.length === 4) { // 跳过错误处理中间件 next(); } else { middleware(req, res, next); } } catch (err) { next(err); } }; next(); }; } } /** * 演示自定义中间件管理器 */ function demonstrateMiddlewareManager() { const manager = new MiddlewareManager(); manager .use((req, res, next) => { console.log('Middleware 1: Request received'); next(); }) .use((req, res, next) => { console.log('Middleware 2: Processing request'); req.processed = true; next(); }) .use((req, res, next) => { console.log('Middleware 3: Finalizing'); res.json({ processed: req.processed }); }); return manager.getHandler(); } // 启动演示服务器 function startServer() { middlewareOrderDemo(); // 自定义路由使用中间件管理器 const customHandler = demonstrateMiddlewareManager(); app.get('/custom', customHandler); const PORT = process.env.PORT || 3000; const server = app.listen(PORT, () => { console.log(`🚀 Express middleware demo server started on port ${PORT}`); console.log('\n📝 可以测试的路由:'); console.log(` GET http://localhost:${PORT}/users`); console.log(` GET http://localhost:${PORT}/profile (需要 Authorization: Bearer valid-token)`); console.log(` POST http://localhost:${PORT}/users (需要认证和管理员权限)`); console.log(` DELETE http://localhost:${PORT}/users/1 (需要认证和管理员权限)`); console.log(` GET http://localhost:${PORT}/error (测试错误处理)`); console.log(` GET http://localhost:${PORT}/custom (自定义中间件管理器)`); console.log('\n💡 测试命令示例:'); console.log(' curl http://localhost:3000/users'); console.log(' curl -H "Authorization: Bearer valid-token" http://localhost:3000/profile'); // 演示完成后自动关闭服务器 setTimeout(() => { console.log('\n✅ 演示完成,关闭服务器'); server.close(); }, 30000); }); return server; } // 模拟中间件测试 function simulateMiddlewareFlow() { console.log('\n=== 中间件执行流程模拟 ==='); // 模拟请求对象 const mockReq = { method: 'GET', url: '/users', headers: { 'authorization': 'Bearer valid-token' }, user: null }; // 模拟响应对象 const mockRes = { status: (code) => { mockRes.statusCode = code; return mockRes; }, json: (data) => { console.log(`响应 [${mockRes.statusCode || 200}]:`, JSON.stringify(data, null, 2)); return mockRes; } }; // 执行中间件链 console.log('1. 开始执行中间件链...'); basicLoggingMiddleware(mockReq, mockRes, () => { console.log('2. 日志中间件执行完成'); authenticate(mockReq, mockRes, () => { console.log('3. 认证中间件执行完成,用户:', mockReq.user); // 模拟路由处理器 console.log('4. 执行路由处理器'); mockRes.json({ message: '中间件链执行成功', user: mockReq.user }); }); }); } // 如果直接运行此文件 if (require.main === module) { console.log('Express 中间件工作原理演示'); console.log('================================'); // 首先运行模拟演示 simulateMiddlewareFlow(); // 然后启动实际服务器 setTimeout(() => { startServer(); }, 2000); } module.exports = { basicLoggingMiddleware, authenticate, authorize, errorHandler, validateUserInput, responseTime, corsMiddleware, cacheMiddleware, MiddlewareManager, startServer, simulateMiddlewareFlow };
中间件执行顺序:
请求 → 日志中间件 → 认证中间件 → 路由处理 → 错误处理 → 响应
↓ ↓ ↓ ↓ ↓
req对象 验证token 处理业务 捕获异常 返回结果
关键概念:
- next(): 传递控制权给下一个中间件
- next(error): 跳转到错误处理中间件
- 中间件顺序: 定义顺序决定执行顺序
- 错误处理: 必须放在所有中间件之后
最佳实践:
- 全局中间件放在路由之前
- 错误处理中间件放在最后
- 使用工厂函数创建可配置中间件
- 合理使用异步中间件避免阻塞
面试官视角:
该题考察候选人对Express核心机制的理解:
- 要点清单: 理解责任链模式;掌握中间件执行顺序;了解错误处理机制;能自定义中间件
- 加分项: 有实际项目经验;了解性能优化;能设计复用性强的中间件;理解异步中间件
- 常见失误: 不理解next()作用;搞错中间件顺序;不了解错误处理;缺乏实践经验
延伸阅读:
- Express官方文档 — 中间件使用指南
- 《Express实战》 — Express深度实践
Koa洋葱模型的原理和实现是什么?
答案
核心概念
Koa的洋葱模型是指中间件的执行流程像剥洋葱一样,从外层到内层,再从内层回到外层。这种模式让异步流程控制变得更加直观和可预测。
洋葱模型特点
1. 执行顺序
中间件1进入 → 中间件2进入 → 中间件3进入 → 中间件3退出 → 中间件2退出 → 中间件1退出
2. async/await支持
- 天然支持异步操作
- 可以等待下游中间件执行完成
- 错误会自动向上冒泡
3. 上下文共享
- 所有中间件共享ctx对象
- 可以在ctx.state中存储状态
- 支持请求和响应的统一处理
实现原理:
- 洋葱模型演示
- koa-compose原理
// Koa 洋葱模型和异常处理演示 const Koa = require('koa'); const compose = require('koa-compose'); /** * 1. 洋葱模型基础演示 */ function demonstrateOnionModel() { console.log('=== Koa 洋葱模型演示 ==='); const app = new Koa(); // 中间件1 - 外层 app.use(async (ctx, next) => { console.log('中间件1 - 进入'); const start = Date.now(); await next(); // 等待下一个中间件执行完成 const duration = Date.now() - start; console.log('中间件1 - 退出'); console.log(`请求处理时间: ${duration}ms`); // 在响应头中添加处理时间 ctx.set('X-Response-Time', `${duration}ms`); }); // 中间件2 - 中层 app.use(async (ctx, next) => { console.log('中间件2 - 进入'); // 添加请求日志信息 ctx.state.requestInfo = { method: ctx.method, url: ctx.url, timestamp: new Date().toISOString() }; await next(); console.log('中间件2 - 退出'); console.log('请求信息:', ctx.state.requestInfo); }); // 中间件3 - 内层 app.use(async (ctx, next) => { console.log('中间件3 - 进入'); // 模拟异步操作 await new Promise(resolve => setTimeout(resolve, 100)); await next(); console.log('中间件3 - 退出'); }); // 路由处理器 - 最内层 app.use(async (ctx) => { console.log('路由处理器 - 处理请求'); ctx.body = { message: '洋葱模型演示', path: ctx.path, method: ctx.method, requestInfo: ctx.state.requestInfo }; console.log('路由处理器 - 响应已设置'); }); return app; } /** * 2. 不使用async/await的洋葱模型实现 (Generator版本) */ function demonstrateGeneratorOnionModel() { console.log('\n=== Generator版本洋葱模型 ==='); // 简化的Koa实现,使用Generator class SimpleKoa { constructor() { this.middlewares = []; } use(middleware) { this.middlewares.push(middleware); return this; } // 使用Generator实现的compose函数 compose(middlewares) { return function* (next) { let index = -1; function* dispatch(i) { if (i <= index) { throw new Error('next() called multiple times'); } index = i; const middleware = middlewares[i]; if (!middleware) { if (next) yield* next; return; } yield* middleware(dispatch.bind(null, i + 1)); } yield* dispatch(0); }; } // 模拟请求处理 handleRequest(ctx) { const composed = this.compose(this.middlewares); const generator = composed.call(ctx); // 手动执行generator function runGenerator(gen) { const result = gen.next(); if (!result.done) { return runGenerator(gen); } return result.value; } return runGenerator(generator); } } const app = new SimpleKoa(); // Generator中间件 app.use(function* (next) { console.log('Generator中间件1 - 进入'); yield* next; console.log('Generator中间件1 - 退出'); }); app.use(function* (next) { console.log('Generator中间件2 - 进入'); yield* next; console.log('Generator中间件2 - 退出'); }); app.use(function* () { console.log('Generator路由处理器'); this.body = 'Generator洋葱模型演示'; }); // 模拟请求 const ctx = { method: 'GET', url: '/' }; app.handleRequest(ctx); console.log('响应:', ctx.body); return app; } /** * 3. Koa异常处理演示 */ function demonstrateErrorHandling() { console.log('\n=== Koa 异常处理演示 ==='); const app = new Koa(); // 全局错误处理中间件 app.use(async (ctx, next) => { try { await next(); } catch (error) { console.error('捕获到错误:', error.message); // 设置错误响应 ctx.status = error.status || 500; ctx.body = { error: error.message, status: ctx.status, timestamp: new Date().toISOString() }; // 触发应用级错误事件 ctx.app.emit('error', error, ctx); } }); // 请求日志中间件 app.use(async (ctx, next) => { console.log(`收到请求: ${ctx.method} ${ctx.url}`); try { await next(); console.log(`响应状态: ${ctx.status}`); } catch (error) { console.log(`请求处理失败: ${error.message}`); throw error; // 重新抛出错误让上层处理 } }); // 业务中间件 - 可能抛出错误 app.use(async (ctx, next) => { if (ctx.path === '/error') { // 模拟业务错误 const error = new Error('这是一个业务错误'); error.status = 400; throw error; } if (ctx.path === '/server-error') { // 模拟服务器错误 throw new Error('服务器内部错误'); } await next(); }); // 正常路由 app.use(async (ctx) => { if (ctx.path === '/') { ctx.body = { message: '正常响应' }; } else { ctx.status = 404; ctx.body = { error: '路由不存在' }; } }); // 监听应用级错误事件 app.on('error', (error, ctx) => { console.log('应用级错误处理:', { error: error.message, url: ctx?.url, method: ctx?.method, status: ctx?.status }); }); return app; } /** * 4. Koa中间件组合和复用 */ function demonstrateMiddlewareComposition() { console.log('\n=== Koa 中间件组合演示 ==='); // 创建可复用的中间件组 const authMiddleware = async (ctx, next) => { const token = ctx.headers.authorization; if (!token) { ctx.status = 401; ctx.body = { error: 'No authorization token' }; return; } if (token === 'Bearer valid-token') { ctx.state.user = { id: 1, name: 'Alice' }; await next(); } else { ctx.status = 401; ctx.body = { error: 'Invalid token' }; } }; const validationMiddleware = (schema) => { return async (ctx, next) => { const { error, value } = schema.validate(ctx.request.body); if (error) { ctx.status = 400; ctx.body = { error: error.details[0].message }; return; } ctx.state.validatedData = value; await next(); }; }; const loggingMiddleware = async (ctx, next) => { const start = Date.now(); console.log(`-> ${ctx.method} ${ctx.url}`); await next(); const duration = Date.now() - start; console.log(`<- ${ctx.status} (${duration}ms)`); }; // 组合中间件 const apiMiddlewares = compose([ loggingMiddleware, authMiddleware, async (ctx, next) => { console.log('API中间件执行'); await next(); } ]); const app = new Koa(); // 基础路由 app.use(async (ctx, next) => { if (ctx.path === '/public') { ctx.body = { message: '公开接口' }; return; } await next(); }); // 需要认证的API路由 app.use(async (ctx, next) => { if (ctx.path.startsWith('/api')) { await apiMiddlewares(ctx, next); } else { await next(); } }); // API路由处理 app.use(async (ctx) => { if (ctx.path === '/api/user') { ctx.body = { message: '认证成功', user: ctx.state.user }; } else { ctx.status = 404; ctx.body = { error: 'Route not found' }; } }); return app; } /** * 5. 手动实现简版的koa-compose */ function createCompose() { return function compose(middleware) { if (!Array.isArray(middleware)) { throw new TypeError('Middleware stack must be an array!'); } for (const fn of middleware) { if (typeof fn !== 'function') { throw new TypeError('Middleware must be composed of functions!'); } } return function composedMiddleware(context, next) { let index = -1; function dispatch(i) { if (i <= index) { return Promise.reject(new Error('next() called multiple times')); } index = i; let fn = middleware[i]; if (i === middleware.length) { fn = next; } if (!fn) { return Promise.resolve(); } try { return Promise.resolve(fn(context, () => dispatch(i + 1))); } catch (err) { return Promise.reject(err); } } return dispatch(0); }; }; } /** * 6. 演示自定义compose的使用 */ function demonstrateCustomCompose() { console.log('\n=== 自定义Compose演示 ==='); const compose = createCompose(); const middleware1 = async (ctx, next) => { console.log('自定义中间件1 - 开始'); ctx.count = (ctx.count || 0) + 1; await next(); console.log('自定义中间件1 - 结束'); }; const middleware2 = async (ctx, next) => { console.log('自定义中间件2 - 开始'); ctx.count = (ctx.count || 0) + 1; await next(); console.log('自定义中间件2 - 结束'); }; const middleware3 = async (ctx, next) => { console.log('自定义中间件3 - 开始'); ctx.count = (ctx.count || 0) + 1; console.log(`中间件执行次数: ${ctx.count}`); console.log('自定义中间件3 - 结束'); }; const composedFn = compose([middleware1, middleware2, middleware3]); // 执行组合后的中间件 const ctx = {}; composedFn(ctx).then(() => { console.log('所有中间件执行完成'); console.log('最终ctx:', ctx); }); } /** * 7. 完整的Koa应用演示 */ async function runCompleteDemo() { console.log('\n=== 完整Koa应用演示 ==='); const app = demonstrateOnionModel(); const errorApp = demonstrateErrorHandling(); const compositionApp = demonstrateMiddlewareComposition(); // 模拟请求处理 console.log('\n1. 模拟正常请求:'); await simulateRequest(app, { method: 'GET', url: '/' }); console.log('\n2. 模拟错误请求:'); await simulateRequest(errorApp, { method: 'GET', url: '/error' }); console.log('\n3. 模拟认证请求:'); await simulateRequest(compositionApp, { method: 'GET', url: '/api/user', headers: { authorization: 'Bearer valid-token' } }); } /** * 模拟请求处理函数 */ async function simulateRequest(app, request) { const ctx = { method: request.method, url: request.url, path: new URL(request.url, 'http://localhost').pathname, headers: request.headers || {}, set: function(key, value) { this.headers[key.toLowerCase()] = value; }, state: {}, status: 200, body: null }; try { // 获取应用的中间件并执行 const composed = app.middleware.length > 0 ? compose(app.middleware) : async (ctx) => { ctx.body = 'No middleware'; }; await composed(ctx); console.log(`响应 [${ctx.status}]:`, JSON.stringify(ctx.body, null, 2)); } catch (error) { console.log(`请求失败:`, error.message); } } // 主演示函数 async function main() { console.log('Koa洋葱模型和异常处理演示'); console.log('==============================='); // 1. 基础洋葱模型 demonstrateOnionModel(); // 2. Generator版本 setTimeout(() => { demonstrateGeneratorOnionModel(); }, 1000); // 3. 异常处理 setTimeout(() => { demonstrateErrorHandling(); }, 2000); // 4. 中间件组合 setTimeout(() => { demonstrateMiddlewareComposition(); }, 3000); // 5. 自定义compose setTimeout(() => { demonstrateCustomCompose(); }, 4000); // 6. 完整演示 setTimeout(() => { runCompleteDemo(); }, 5000); } // 如果直接运行此文件 if (require.main === module) { main().catch(console.error); } module.exports = { demonstrateOnionModel, demonstrateGeneratorOnionModel, demonstrateErrorHandling, demonstrateMiddlewareComposition, createCompose, demonstrateCustomCompose, simulateRequest };
核心实现 - koa-compose:
function compose(middleware) {
return function(context, next) {
let index = -1;
function dispatch(i) {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
与Generator的关系:
- Koa v1使用Generator函数实现
- Koa v2改为async/await,原理相同
- compose函数是核心,将多个中间件组合成一个函数
异常处理:
Koa的异常处理基于Promise,错误会自动向上传播:
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = err.message;
ctx.app.emit('error', err, ctx);
}
});
与Express对比:
| 特性 | Express | Koa |
|---|---|---|
| 异步处理 | 回调/Promise | 原生async/await |
| 错误处理 | 手动传递 | 自动冒泡 |
| 中间件模式 | 直线型 | 洋葱型 |
| 体积大小 | 较大(内置功能多) | 轻量(核心功能) |
面试官视角:
该题考察候选人对Koa架构的深度理解:
- 要点清单: 理解洋葱模型概念;掌握async/await异步流程;了解koa-compose原理;能处理异步异常
- 加分项: 了解与Express的区别;有Koa项目经验;理解Generator实现;能自定义compose函数
- 常见失误: 不理解执行顺序;不了解异步等待;混淆与Express的区别;缺乏实践经验
延伸阅读:
- Koa官方文档 — Koa框架官方指南
- 《Koa与Node.js开发实战》 — Koa深度应用
什么是SSR,如何实现服务端渲染?
答案
核心概念
SSR(Server-Side Rendering)是指在服务器端将应用渲染成HTML字符串,然后发送给客户端。相比CSR(Client-Side Rendering),SSR能提供更好的首屏加载速度和SEO支持。
SSR优势
1. 性能优势
- 更快的首屏加载时间(FCP)
- 更好的用户体验,特别是慢网络环境
- 减少客户端JavaScript执行负担
2. SEO优势
- 搜索引擎可以直接抓取完整HTML
- 更好的搜索排名和社交媒体分享
- 支持无JavaScript环境的客户端
3. 可访问性
- 内容在JavaScript加载前就可见
- 更好的可访问性支持
- 渐进增强的用户体验
实现方式:
- SSR实现演示
- 渲染模式对比
// Server-Side Rendering (SSR) 演示 const http = require('http'); const fs = require('fs').promises; const path = require('path'); /** * 1. 基础的HTML模板渲染 */ class TemplateRenderer { constructor() { this.templates = new Map(); } // 加载模板 async loadTemplate(name, filePath) { try { const template = await fs.readFile(filePath, 'utf8'); this.templates.set(name, template); return template; } catch (error) { console.error(`加载模板失败: ${filePath}`, error); throw error; } } // 渲染模板 render(templateName, data = {}) { const template = this.templates.get(templateName); if (!template) { throw new Error(`模板不存在: ${templateName}`); } // 简单的模板替换 return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { return data[key] !== undefined ? String(data[key]) : ''; }); } // 支持条件渲染和循环的高级模板引擎 renderAdvanced(templateName, data = {}) { let template = this.templates.get(templateName); if (!template) { throw new Error(`模板不存在: ${templateName}`); } // 处理条件渲染 {{#if condition}}...{{/if}} template = template.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, condition, content) => { return data[condition] ? content : ''; }); // 处理循环 {{#each items}}...{{/each}} template = template.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, arrayKey, itemTemplate) => { const array = data[arrayKey]; if (!Array.isArray(array)) { return ''; } return array.map(item => { return itemTemplate.replace(/\{\{(\w+)\}\}/g, (match, key) => { return item[key] !== undefined ? String(item[key]) : ''; }); }).join(''); }); // 处理普通变量替换 template = template.replace(/\{\{(\w+)\}\}/g, (match, key) => { return data[key] !== undefined ? String(data[key]) : ''; }); return template; } } /** * 2. React风格的虚拟DOM SSR演示 */ class VirtualDOM { static createElement(tag, props = {}, ...children) { return { tag, props, children: children.flat() }; } static renderToString(vdom) { if (typeof vdom === 'string' || typeof vdom === 'number') { return String(vdom); } if (!vdom || !vdom.tag) { return ''; } const { tag, props = {}, children = [] } = vdom; // 构建属性字符串 const propsString = Object.entries(props) .filter(([key, value]) => value !== null && value !== undefined) .map(([key, value]) => { // 处理特殊属性 if (key === 'className') { key = 'class'; } return `${key}="${String(value)}"`; }) .join(' '); const propsAttr = propsString ? ` ${propsString}` : ''; // 自闭合标签 const voidTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']; if (voidTags.includes(tag)) { return `<${tag}${propsAttr} />`; } // 渲染子元素 const childrenString = children .map(child => VirtualDOM.renderToString(child)) .join(''); return `<${tag}${propsAttr}>${childrenString}</${tag}>`; } } // 便捷函数 const h = VirtualDOM.createElement; /** * 3. 组件化的SSR实现 */ class Component { constructor(props = {}) { this.props = props; this.state = {}; } render() { throw new Error('render method must be implemented'); } static renderToString(ComponentClass, props = {}) { const instance = new ComponentClass(props); const vdom = instance.render(); return VirtualDOM.renderToString(vdom); } } // 示例组件:用户卡片 class UserCard extends Component { render() { const { user } = this.props; return h('div', { className: 'user-card' }, h('img', { src: user.avatar || '/default-avatar.png', alt: `${user.name}'s avatar`, className: 'user-avatar' }), h('div', { className: 'user-info' }, h('h3', { className: 'user-name' }, user.name), h('p', { className: 'user-email' }, user.email), user.bio && h('p', { className: 'user-bio' }, user.bio) ) ); } } // 用户列表组件 class UserList extends Component { render() { const { users, title } = this.props; return h('div', { className: 'user-list' }, h('h2', {}, title || '用户列表'), h('div', { className: 'users' }, ...users.map(user => Component.renderToString(UserCard, { user }) ) ) ); } } // 页面布局组件 class Layout extends Component { render() { const { title, children } = this.props; return h('html', { lang: 'zh-CN' }, h('head', {}, h('meta', { charset: 'utf-8' }), h('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }), h('title', {}, title), h('style', {}, ` body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } .user-card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 12px; } .user-avatar { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; } .user-name { margin: 0 0 8px 0; color: #333; } .user-email { margin: 0 0 4px 0; color: #666; font-size: 14px; } .user-bio { margin: 0; color: #888; font-size: 12px; } .header { background: #f5f5f5; padding: 20px; margin-bottom: 20px; } .nav { display: flex; gap: 20px; } .nav a { text-decoration: none; color: #0066cc; } .nav a:hover { text-decoration: underline; } `) ), h('body', {}, h('div', { className: 'header' }, h('h1', {}, 'SSR 演示应用'), h('nav', { className: 'nav' }, h('a', { href: '/' }, '首页'), h('a', { href: '/users' }, '用户列表'), h('a', { href: '/about' }, '关于') ) ), h('main', {}, children) ) ); } } /** * 4. 路由系统 */ class Router { constructor() { this.routes = new Map(); } route(path, handler) { this.routes.set(path, handler); return this; } async handle(request, response) { const url = new URL(request.url, 'http://localhost'); const path = url.pathname; const handler = this.routes.get(path); if (handler) { try { await handler(request, response, url); } catch (error) { console.error('路由处理错误:', error); response.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); response.end('<h1>500 - 服务器内部错误</h1>'); } } else { response.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' }); response.end('<h1>404 - 页面不存在</h1>'); } } } /** * 5. 数据获取模拟 */ class DataService { static async getUsers() { // 模拟异步数据获取 await new Promise(resolve => setTimeout(resolve, 100)); return [ { id: 1, name: '张三', email: 'zhangsan@example.com', avatar: 'https://via.placeholder.com/60', bio: '全栈开发工程师,热爱技术分享' }, { id: 2, name: '李四', email: 'lisi@example.com', avatar: 'https://via.placeholder.com/60', bio: 'Frontend专家,React和Vue都很熟练' }, { id: 3, name: '王五', email: 'wangwu@example.com', avatar: 'https://via.placeholder.com/60', bio: '后端架构师,专注于高并发系统设计' } ]; } static async getUserById(id) { const users = await this.getUsers(); return users.find(user => user.id === parseInt(id)); } } /** * 6. SSR应用主类 */ class SSRApp { constructor() { this.router = new Router(); this.templateRenderer = new TemplateRenderer(); this.setupRoutes(); } setupRoutes() { // 首页路由 this.router.route('/', async (req, res) => { const html = Component.renderToString(Layout, { title: 'SSR演示 - 首页', children: h('div', {}, h('h2', {}, '欢迎来到SSR演示应用'), h('p', {}, '这是一个使用Node.js实现的服务端渲染演示。'), h('ul', {}, h('li', {}, '⚡ 快速的首屏加载'), h('li', {}, '🔍 SEO友好'), h('li', {}, '♿ 更好的可访问性'), h('li', {}, '📱 支持无JavaScript的客户端') ), h('a', { href: '/users', style: 'display: inline-block; background: #0066cc; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; margin-top: 20px;' }, '查看用户列表') ) }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<!DOCTYPE html>' + html); }); // 用户列表路由 this.router.route('/users', async (req, res) => { try { // 服务端数据获取 const users = await DataService.getUsers(); const userListHtml = Component.renderToString(UserList, { users, title: '用户列表' }); const html = Component.renderToString(Layout, { title: 'SSR演示 - 用户列表', children: userListHtml }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<!DOCTYPE html>' + html); } catch (error) { console.error('获取用户数据失败:', error); res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<h1>数据加载失败</h1>'); } }); // 关于页面路由 this.router.route('/about', async (req, res) => { const html = Component.renderToString(Layout, { title: 'SSR演示 - 关于', children: h('div', {}, h('h2', {}, '关于本演示'), h('p', {}, '这是一个Node.js服务端渲染(SSR)的完整示例,展示了:'), h('ol', {}, h('li', {}, '虚拟DOM的服务端渲染'), h('li', {}, '组件化的页面结构'), h('li', {}, '路由系统'), h('li', {}, '数据获取和注入'), h('li', {}, 'SEO优化的HTML输出') ), h('h3', {}, '技术特性'), h('ul', {}, h('li', {}, '🚀 零客户端JavaScript依赖'), h('li', {}, '📦 轻量级虚拟DOM实现'), h('li', {}, '🎨 CSS-in-JS样式支持'), h('li', {}, '🔧 模块化的组件系统') ) ) }); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<!DOCTYPE html>' + html); }); // API路由 - 返回JSON数据 this.router.route('/api/users', async (req, res) => { try { const users = await DataService.getUsers(); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(users, null, 2)); } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ error: 'Internal Server Error' })); } }); } // 启动服务器 listen(port = 3000) { const server = http.createServer((req, res) => { console.log(`${req.method} ${req.url}`); this.router.handle(req, res); }); server.listen(port, () => { console.log(`🚀 SSR服务器启动成功!`); console.log(`📡 访问地址: http://localhost:${port}`); console.log(`\n📋 可用路由:`); console.log(` 🏠 http://localhost:${port}/`); console.log(` 👥 http://localhost:${port}/users`); console.log(` ℹ️ http://localhost:${port}/about`); console.log(` 📊 http://localhost:${port}/api/users`); console.log(`\n💡 这些页面都是在服务端渲染的,查看源代码可以看到完整的HTML!`); }); return server; } } /** * 7. 性能监控中间件 */ function performanceMiddleware() { return (req, res, next) => { const start = process.hrtime.bigint(); // 重写res.end方法来记录响应时间 const originalEnd = res.end; res.end = function(...args) { const duration = process.hrtime.bigint() - start; const ms = Number(duration) / 1000000; // 转换为毫秒 console.log(`⏱️ ${req.method} ${req.url} - ${res.statusCode} - ${ms.toFixed(2)}ms`); return originalEnd.apply(this, args); }; if (next) next(); }; } /** * 8. 演示不同渲染模式的对比 */ function demonstrateRenderingComparison() { console.log('=== 渲染模式对比演示 ===\n'); const userData = [ { id: 1, name: '张三', email: 'zhangsan@example.com' }, { id: 2, name: '李四', email: 'lisi@example.com' } ]; // 1. 客户端渲染(CSR)模拟 console.log('1. 客户端渲染(CSR)输出:'); const csrHtml = ` <!DOCTYPE html> <html> <head> <title>用户列表 - CSR</title> </head> <body> <div id="root">Loading...</div> <script> // 客户端获取数据并渲染 fetch('/api/users') .then(res => res.json()) .then(users => { const html = users.map(user => \`<div class="user">\${user.name} - \${user.email}</div>\` ).join(''); document.getElementById('root').innerHTML = html; }); </script> </body> </html>`; console.log(csrHtml.trim()); // 2. 服务端渲染(SSR)输出 console.log('\n2. 服务端渲染(SSR)输出:'); const ssrUserList = userData.map(user => `<div class="user">${user.name} - ${user.email}</div>` ).join(''); const ssrHtml = ` <!DOCTYPE html> <html> <head> <title>用户列表 - SSR</title> </head> <body> <div id="root"> <h1>用户列表</h1> ${ssrUserList} </div> <script> // 可选:客户端激活(hydration) console.log('页面已经渲染完成,可以添加交互功能'); </script> </body> </html>`; console.log(ssrHtml.trim()); // 3. 静态生成(SSG)说明 console.log('\n3. 静态生成(SSG):'); console.log('在构建时预先生成HTML文件,类似于SSR但在构建阶段完成'); console.log('优点: 更快的响应速度,可以使用CDN缓存'); console.log('缺点: 动态数据需要额外处理\n'); // 4. 渲染性能对比 console.log('4. 渲染模式性能对比:'); const comparison = [ ['指标', 'CSR', 'SSR', 'SSG'], ['首屏加载', '慢(需要JS)', '快(立即显示)', '最快(预生成)'], ['SEO友好', '差(需爬虫支持)', '好(完整HTML)', '最好(静态HTML)'], ['服务器负载', '低', '高', '最低'], ['实时数据', '好', '好', '需要额外处理'], ['开发复杂度', '低', '中', '中'] ]; console.table(comparison.slice(1), comparison[0]); } // 主演示函数 function main() { console.log('Node.js SSR (服务端渲染) 演示'); console.log('================================\n'); // 1. 基础演示 demonstrateRenderingComparison(); // 2. 启动SSR应用 console.log('启动SSR演示应用...\n'); const app = new SSRApp(); const server = app.listen(3000); // 30秒后自动关闭 setTimeout(() => { console.log('\n🔄 演示结束,关闭服务器'); server.close(); }, 30000); return server; } // 如果直接运行此文件 if (require.main === module) { main(); } module.exports = { TemplateRenderer, VirtualDOM, Component, UserCard, UserList, Layout, Router, DataService, SSRApp, performanceMiddleware, demonstrateRenderingComparison };
CSR vs SSR vs SSG:
| 特性 | CSR | SSR | SSG |
|---|---|---|---|
| 首屏速度 | 慢 | 快 | 最快 |
| SEO友好 | 差 | 好 | 最好 |
| 服务器负载 | 低 | 高 | 最低 |
| 实时数据 | 好 | 好 | 需额外处理 |
| 缓存策略 | 客户端 | 复杂 | 简单 |
技术选择:
- CSR: 适合后台管理系统、交互密集应用
- SSR: 适合内容网站、电商平台
- SSG: 适合博客、文档站点
- 混合模式: 根据页面类型选择渲染方式
现代SSR框架:
Next.js (React生态)
// pages/users.js
export async function getServerSideProps(context) {
const users = await fetchUsers();
return { props: { users } };
}
export default function Users({ users }) {
return (
<div>
{users.map(user => <div key={user.id}>{user.name}</div>)}
</div>
);
}
Nuxt.js (Vue生态)
// pages/users.vue
export default {
async asyncData() {
const users = await $http.$get('/api/users');
return { users };
}
}
技术挑战:
1. 数据获取时机
- 服务端需要预先获取数据
- 避免客户端二次请求
- 处理数据获取失败的情况
2. 状态管理
- 服务端状态需要序列化传递给客户端
- 避免客户端与服务端状态不一致
- 处理敏感数据的过滤
3. 性能优化
- 服务端渲染缓存策略
- 代码分割和懒加载
- 静态资源的预加载
面试官视角
该题考察候选人对现代Web架构的理解:
- 要点清单: 理解SSR概念和优势;掌握基本实现方法;了解与CSR的区别;知道相关框架
- 加分项: 有SSR项目经验;了解性能优化;掌握同构应用开发;理解SEO和可访问性
- 常见失误: 只知道概念不了解实现;不理解技术权衡;缺乏实际经验;混淆不同渲染模式
延伸阅读
- React SSR指南 — React服务端渲染
- 《现代前端架构》 — 全栈开发架构设计