const http = require('http') const fs = require('fs') const path = require('path') const { URL } = require('url') const projectRoot = __dirname const defaultWebRoot = path.join(projectRoot, 'unpackage', 'dist', 'build', 'web') const defaultHost = '127.0.0.1' const defaultPort = 5500 const mimeTypes = { '.css': 'text/css; charset=utf-8', '.gif': 'image/gif', '.html': 'text/html; charset=utf-8', '.ico': 'image/x-icon', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.png': 'image/png', '.svg': 'image/svg+xml', '.ttf': 'font/ttf', '.txt': 'text/plain; charset=utf-8', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2' } function getOption(name, fallback) { const prefix = `${name}=` const argument = process.argv.find((item) => item.startsWith(prefix)) return argument ? argument.slice(prefix.length) : fallback } function getPort() { const value = getOption('--port', process.env.PORT || String(defaultPort)) const port = Number(value) if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`端口无效:${value}`) } return port } function getWebRoot() { const configuredRoot = getOption('--dir', defaultWebRoot) return path.resolve(projectRoot, configuredRoot) } function isInsideRoot(root, target) { const relativePath = path.relative(root, target) return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) } function sendText(response, statusCode, message) { response.writeHead(statusCode, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' }) response.end(message) } function sendFile(response, filePath) { const extension = path.extname(filePath).toLowerCase() const contentType = mimeTypes[extension] || 'application/octet-stream' response.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': extension === '.html' ? 'no-cache' : 'public, max-age=3600' }) fs.createReadStream(filePath).pipe(response) } function tryServeFile(response, webRoot, pathname) { const decodedPath = decodeURIComponent(pathname) const relativePath = decodedPath === '/' ? 'index.html' : decodedPath.slice(1) const filePath = path.resolve(webRoot, relativePath) if (!isInsideRoot(webRoot, filePath)) { sendText(response, 403, 'Forbidden') return true } if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { return false } sendFile(response, filePath) return true } function createServer(webRoot) { return http.createServer((request, response) => { if (request.method !== 'GET' && request.method !== 'HEAD') { sendText(response, 405, 'Method Not Allowed') return } let requestUrl try { requestUrl = new URL(request.url, 'http://localhost') } catch { sendText(response, 400, 'Bad Request') return } try { if (tryServeFile(response, webRoot, requestUrl.pathname)) { return } // Web 发行项目使用前端路由时,未知路径回退到入口页面。 if (!path.extname(requestUrl.pathname) && tryServeFile(response, webRoot, '/index.html')) { return } sendText(response, 404, 'Not Found') } catch (error) { console.error(`[serve-web] ${error.message}`) sendText(response, 500, 'Internal Server Error') } }) } function printUsage() { console.log('用法:node serve-web.js [--port=5500] [--host=127.0.0.1] [--dir=unpackage/dist/build/web]') } function start() { if (process.argv.includes('--help') || process.argv.includes('-h')) { printUsage() return } const webRoot = getWebRoot() const host = getOption('--host', process.env.HOST || defaultHost) const port = getPort() if (!fs.existsSync(webRoot)) { console.error(`未找到 Web 文件目录:${webRoot}`) console.error('请先在 HBuilderX 中发行 Web,或使用 --dir 指定已打包的目录。') process.exitCode = 1 return } const server = createServer(webRoot) server.listen(port, host, () => { console.log(`Web 文件目录:${webRoot}`) console.log(`本机访问:http://localhost:${port}`) console.log(`局域网访问:http://<本机IP>:${port}`) console.log('按 Ctrl+C 停止服务器') }) } start()