[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:创建服务器类结构
javascript
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:实现服务器启动
javascript
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:实现请求处理入口
javascript
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);
}路由优先级:
- 静态资源(
/static/*) - API 路由(
/api/*) - 页面路由(其他路径)
设计说明:
- 使用
URLAPI 解析请求 URL - 分离不同类型的请求处理
- 清晰的优先级顺序
步骤 4:实现页面路由处理
javascript
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);
}
}处理流程:
- 路由匹配:使用路由系统匹配 URL
- 组件加载:动态加载页面组件
- Props 准备:合并动态参数和查询参数
- 渲染:服务端渲染组件为 HTML
- 响应:返回 HTML 响应
关键点:
- 异步处理(
async/await) - 错误处理
- 设置正确的 Content-Type
步骤 5:实现 API 路由处理
javascript
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);
}
}
}处理流程:
- 路径转换:
/api/hello→pages/api/hello.js - 文件查找:尝试多种扩展名
- 加载处理函数:动态导入 API 模块
- 执行处理:调用处理函数
- 返回响应:JSON 格式
关键点:
- 支持多种文件扩展名
- 检查响应是否已发送
- 错误处理
步骤 6:实现静态资源服务
javascript
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:实现错误处理
javascript
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>
`);
}设计说明:
- 返回友好的错误页面
- 显示错误信息
- 设置正确的状态码
完整实现
javascript
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 入口