[mini-next.js] 开发服务器:HTTP 服务器和请求处理
概述
开发服务器是 Next.js 的核心组件,它负责启动 HTTP 服务器、处理客户端请求、匹配路由并渲染页面。本文将深入分析并实现开发服务器的核心功能。
核心功能分析
1. HTTP 服务器
职责: - 启动 HTTP 服务器 - 监听指定端口 - 处理客户端请求
技术选择:
- 使用 Node.js 原生 http 模块
- 简单、轻量,无需额外依赖
2. 请求处理流程
HTTP 请求
↓
解析 URL
↓
路由匹配
↓
加载组件
↓
渲染 HTML
↓
返回响应
3. 路由处理
职责: - 根据 URL 路径匹配路由 - 处理静态路由和动态路由 - 提取查询参数和动态参数
4. 静态资源服务
职责:
- 服务 public 目录下的静态文件
- 设置正确的 Content-Type
- 处理 404 情况
5. API 路由处理
职责:
- 处理 /api/* 路径
- 执行 API 处理函数
- 返回 JSON 响应
逐步实现
步骤 1:创建服务器类结构
import http from 'http';
import fs from 'fs';
import path from 'path';
import { FileSystemRouter } from '../router/file-system-router.js';
import { ModuleLoader } from '../utils/module-loader.js';
import { renderFullPage } from '../renderer/ssr-renderer.js';
export class DevServer {
constructor(options = {}) {
this.dir = options.dir || process.cwd();
this.port = options.port || 3000;
this.hostname = options.hostname || 'localhost';
this.pagesDir = path.join(this.dir, 'pages');
this.publicDir = path.join(this.dir, 'public');
this.router = new FileSystemRouter(this.pagesDir);
this.moduleLoader = new ModuleLoader();
this.server = null;
}
}
设计说明: - 接受配置选项(目录、端口、主机名) - 初始化路由系统和模块加载器 - 准备目录路径
步骤 2:实现服务器启动
async start() {
console.log(`🚀 启动开发服务器...`);
console.log(`📁 项目目录: ${this.dir}`);
console.log(`📄 Pages 目录: ${this.pagesDir}`);
// 扫描路由
this.router.scanRoutes();
console.log(`✅ 发现 ${this.router.routes.size} 个静态路由`);
console.log(`✅ 发现 ${this.router.dynamicRoutes.size} 个动态路由`);
// 创建 HTTP 服务器
this.server = http.createServer((req, res) => {
this.handleRequest(req, res).catch(err => {
console.error('Request error:', err);
this.sendError(res, 500, err.message);
});
});
// 监听端口
this.server.listen(this.port, this.hostname, () => {
console.log(`\n✨ 服务器运行在 http://${this.hostname}:${this.port}\n`);
});
}
关键点: - 启动前扫描路由 - 创建 HTTP 服务器,绑定请求处理函数 - 错误处理:捕获异步错误
步骤 3:实现请求处理入口
async handleRequest(req, res) {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
// 处理静态资源
if (pathname.startsWith('/_next/static') || pathname.startsWith('/static')) {
return this.serveStatic(req, res, pathname);
}
// 处理 API 路由
if (pathname.startsWith('/api/')) {
return this.handleApiRoute(req, res, pathname);
}
// 处理页面路由
return this.handlePageRoute(req, res, pathname, url);
}
路由优先级:
1. 静态资源(/static/*)
2. API 路由(/api/*)
3. 页面路由(其他路径)
设计说明:
- 使用 URL API 解析请求 URL
- 分离不同类型的请求处理
- 清晰的优先级顺序
步骤 4:实现页面路由处理
async handlePageRoute(req, res, pathname, url) {
// 匹配路由
const route = this.router.matchRoute(pathname);
if (!route) {
return this.send404(res, pathname);
}
try {
// 加载页面组件
const Component = await this.moduleLoader.loadModule(route.filePath);
// 准备 props(包含查询参数和动态路由参数)
const props = {
...route.params,
query: Object.fromEntries(url.searchParams),
};
// 渲染页面
const html = renderFullPage(Component, props);
// 发送响应
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} catch (error) {
console.error(`Error rendering ${pathname}:`, error);
this.sendError(res, 500, error.message);
}
}
处理流程: 1. 路由匹配:使用路由系统匹配 URL 2. 组件加载:动态加载页面组件 3. Props 准备:合并动态参数和查询参数 4. 渲染:服务端渲染组件为 HTML 5. 响应:返回 HTML 响应
关键点:
- 异步处理(async/await)
- 错误处理
- 设置正确的 Content-Type
步骤 5:实现 API 路由处理
async handleApiRoute(req, res, pathname) {
// 将 /api/xxx 转换为 pages/api/xxx.js
const apiPath = pathname.replace('/api/', '');
// 尝试不同的文件扩展名
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
let apiFilePath = null;
for (const ext of extensions) {
const filePath = path.join(this.pagesDir, 'api', `${apiPath}${ext}`);
if (fs.existsSync(filePath)) {
apiFilePath = filePath;
break;
}
}
if (!apiFilePath) {
return this.send404(res, pathname);
}
try {
const handler = await this.moduleLoader.loadModule(apiFilePath);
// 执行 API 处理函数
const handlerFn = typeof handler === 'function' ? handler : handler.default;
if (typeof handlerFn !== 'function') {
throw new Error('API route must export a default function');
}
const result = await handlerFn(req, res);
if (!res.headersSent) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result || { success: true }));
}
} catch (error) {
console.error(`Error handling API ${pathname}:`, error);
if (!res.headersSent) {
this.sendError(res, 500, error.message);
}
}
}
处理流程:
1. 路径转换:/api/hello → pages/api/hello.js
2. 文件查找:尝试多种扩展名
3. 加载处理函数:动态导入 API 模块
4. 执行处理:调用处理函数
5. 返回响应:JSON 格式
关键点: - 支持多种文件扩展名 - 检查响应是否已发送 - 错误处理
步骤 6:实现静态资源服务
serveStatic(req, res, pathname) {
// 简化版:只处理 public 目录
const filePath = path.join(this.publicDir, pathname.replace('/static/', ''));
if (!fs.existsSync(filePath)) {
return this.send404(res, pathname);
}
const ext = path.extname(filePath);
const contentType = this.getContentType(ext);
const content = fs.readFileSync(filePath);
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
}
getContentType(ext) {
const types = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
};
return types[ext] || 'application/octet-stream';
}
功能:
- 从 public 目录读取文件
- 根据文件扩展名设置 Content-Type
- 同步读取文件(简化实现)
步骤 7:实现错误处理
send404(res, pathname) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<!DOCTYPE html>
<html>
<head><title>404 - Page Not Found</title></head>
<body>
<h1>404 - Page Not Found</h1>
<p>无法找到页面: ${pathname}</p>
</body>
</html>
`);
}
sendError(res, statusCode, message) {
res.writeHead(statusCode, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<!DOCTYPE html>
<html>
<head><title>Error ${statusCode}</title></head>
<body>
<h1>Error ${statusCode}</h1>
<pre>${message}</pre>
</body>
</html>
`);
}
设计说明: - 返回友好的错误页面 - 显示错误信息 - 设置正确的状态码
完整实现
import http from 'http';
import fs from 'fs';
import path from 'path';
import { FileSystemRouter } from '../router/file-system-router.js';
import { ModuleLoader } from '../utils/module-loader.js';
import { renderFullPage } from '../renderer/ssr-renderer.js';
export class DevServer {
constructor(options = {}) {
this.dir = options.dir || process.cwd();
this.port = options.port || 3000;
this.hostname = options.hostname || 'localhost';
this.pagesDir = path.join(this.dir, 'pages');
this.publicDir = path.join(this.dir, 'public');
this.router = new FileSystemRouter(this.pagesDir);
this.moduleLoader = new ModuleLoader();
this.server = null;
}
async start() {
console.log(`🚀 启动开发服务器...`);
console.log(`📁 项目目录: ${this.dir}`);
console.log(`📄 Pages 目录: ${this.pagesDir}`);
this.router.scanRoutes();
console.log(`✅ 发现 ${this.router.routes.size} 个静态路由`);
console.log(`✅ 发现 ${this.router.dynamicRoutes.size} 个动态路由`);
this.server = http.createServer((req, res) => {
this.handleRequest(req, res).catch(err => {
console.error('Request error:', err);
this.sendError(res, 500, err.message);
});
});
this.server.listen(this.port, this.hostname, () => {
console.log(`\n✨ 服务器运行在 http://${this.hostname}:${this.port}\n`);
});
}
async handleRequest(req, res) {
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
if (pathname.startsWith('/_next/static') || pathname.startsWith('/static')) {
return this.serveStatic(req, res, pathname);
}
if (pathname.startsWith('/api/')) {
return this.handleApiRoute(req, res, pathname);
}
return this.handlePageRoute(req, res, pathname, url);
}
async handlePageRoute(req, res, pathname, url) {
const route = this.router.matchRoute(pathname);
if (!route) {
return this.send404(res, pathname);
}
try {
const Component = await this.moduleLoader.loadModule(route.filePath);
const props = {
...route.params,
query: Object.fromEntries(url.searchParams),
};
const html = renderFullPage(Component, props);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} catch (error) {
console.error(`Error rendering ${pathname}:`, error);
this.sendError(res, 500, error.message);
}
}
async handleApiRoute(req, res, pathname) {
const apiPath = pathname.replace('/api/', '');
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
let apiFilePath = null;
for (const ext of extensions) {
const filePath = path.join(this.pagesDir, 'api', `${apiPath}${ext}`);
if (fs.existsSync(filePath)) {
apiFilePath = filePath;
break;
}
}
if (!apiFilePath) {
return this.send404(res, pathname);
}
try {
const handler = await this.moduleLoader.loadModule(apiFilePath);
const handlerFn = typeof handler === 'function' ? handler : handler.default;
if (typeof handlerFn !== 'function') {
throw new Error('API route must export a default function');
}
const result = await handlerFn(req, res);
if (!res.headersSent) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result || { success: true }));
}
} catch (error) {
console.error(`Error handling API ${pathname}:`, error);
if (!res.headersSent) {
this.sendError(res, 500, error.message);
}
}
}
serveStatic(req, res, pathname) {
const filePath = path.join(this.publicDir, pathname.replace('/static/', ''));
if (!fs.existsSync(filePath)) {
return this.send404(res, pathname);
}
const ext = path.extname(filePath);
const contentType = this.getContentType(ext);
const content = fs.readFileSync(filePath);
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
}
getContentType(ext) {
const types = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
};
return types[ext] || 'application/octet-stream';
}
send404(res, pathname) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<!DOCTYPE html>
<html>
<head><title>404 - Page Not Found</title></head>
<body>
<h1>404 - Page Not Found</h1>
<p>无法找到页面: ${pathname}</p>
</body>
</html>
`);
}
sendError(res, statusCode, message) {
res.writeHead(statusCode, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<!DOCTYPE html>
<html>
<head><title>Error ${statusCode}</title></head>
<body>
<h1>Error ${statusCode}</h1>
<pre>${message}</pre>
</body>
</html>
`);
}
stop() {
if (this.server) {
this.server.close();
}
}
}
总结
开发服务器的核心是请求路由和响应生成:
- HTTP 服务器:使用 Node.js 原生模块
- 请求处理:根据路径类型分发到不同处理器
- 路由匹配:使用路由系统匹配页面
- 组件加载:动态加载 React 组件
- HTML 渲染:服务端渲染组件(下一篇文章详述)
在下一篇文章中,我们将深入实现服务端渲染系统,这是 Next.js 的核心特性之一。
相关文件:
- src/server/dev-server.js - 完整实现
- src/cli/dev.js - CLI 入口