简介:这是一套基于Node.js全栈技术实现的猫眼电影平台源码,面向Web开发初学者与全栈进阶学习者,帮助其掌握Express框架路由设计、MySQL数据建模、前后端交互及静态资源组织等核心能力。资源共37个文件,含28张PNG(涵盖电影海报、界面图标、UI组件等)、5个HTML(构成首页、列表页、详情页等核心视图)、1个CSS(定义全局样式)、1个JS(处理前端交互逻辑)、1个ICO(网站favicon)及1份readme.txt说明文档,压缩包仅266KB,轻量易读。已有246人学习下载,适合快速部署调试与代码拆解。读者可完整复现一个具备电影展示、搜索筛选、用户界面渲染功能的轻量级购票平台,深入理解static资源目录划分、HTML模板嵌套结构、MySQL表关系设计(如电影/用户/订单),并获得可直接运行的server.js服务入口与清晰的项目启动指引。
1. 这不是静态页面,而是一个可运行的电影平台最小可行后端——Node+Express+MySQL 三件套跑通即用
你下载的这个upload.zip,表面看是37个零散文件:一堆PNG图标、5个HTML、1个CSS、1个JS、1个README,甚至还有favicon.ico和V3.png这类细节图。但真正让它“活起来”的,是那个不起眼的server.js——它才是整个猫眼电影平台的中枢神经。这不是一个纯前端演示站,而是一个完整闭环的轻量级服务端应用:用户在cinema.html点击电影,请求发到server.js,Express 路由解析后查 MySQL 表(比如movies或tickets),再把 JSON 或渲染后的 HTML 返回浏览器。整个链路不依赖任何云服务或第三方 API,本地装好 Node、启动 MySQL、执行node server.js,端口3000就能打开真实交互界面。适合刚学完 Express 基础路由、想立刻验证“数据库连上了没”“POST 请求能不能写入”的开发者;也适合需要快速搭建内部影讯管理后台的运维或产品同学——删掉购票逻辑、保留电影列表和搜索,20 分钟就能改成部门团建排片系统。
2. 从server.js入手:解析 Express 路由与 MySQL 查询的耦合逻辑
2.1server.js的核心结构:四层职责清晰分离
打开server.js,你会发现它没有用require('mysql2')而是require('mysql'),这是关键线索。当前主流推荐mysql2(支持 Promise 和预编译语句),但本项目用原生mysql模块,意味着所有数据库操作都基于回调函数。典型结构如下:
const express = require('express'); const mysql = require('mysql'); const app = express(); // 1. 数据库连接池配置(注意:密码为空,host 为 localhost) const db = mysql.createPool({ host: 'localhost', user: 'root', password: '', database: 'maoyan_db', port: 3306, waitForConnections: true, connectionLimit: 10 }); // 2. 静态资源托管(对应 static/ 目录) app.use(express.static('static')); // 3. 解析 POST 请求体(关键!否则表单提交拿不到数据) app.use(express.urlencoded({ extended: true })); app.use(express.json()); // 4. 核心路由:/api/movies 返回全部电影,/api/movie/:id 查单部 app.get('/api/movies', (req, res) => { db.query('SELECT * FROM movies ORDER BY score DESC', (err, results) => { if (err) return res.status(500).json({ error: err.message }); res.json(results); }); });提示:
express.urlencoded({ extended: true })必须放在路由定义之前,否则req.body为空对象。这是新手最常踩的坑——页面表单提交后console.log(req.body)显示{},实际是中间件加载顺序错了。
2.2 MySQL 表结构逆向还原:从 SQL 查询语句反推建表语句
项目没提供.sql导出文件,但server.js里的查询语句暴露了表名和字段。例如:
SELECT * FROM movies ORDER BY score DESC→ 表名movies,含score字段INSERT INTO tickets (user_id, movie_id, seat, time) VALUES (?, ?, ?, ?)→ 表名tickets,字段user_id,movie_id,seat,timeSELECT * FROM users WHERE phone = ?→ 表名users,含phone字段
据此可还原基础建表语句(直接复制执行):
-- 创建数据库(若不存在) CREATE DATABASE IF NOT EXISTS maoyan_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换数据库 USE maoyan_db; -- 电影表 CREATE TABLE movies ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, director VARCHAR(50), actors TEXT, score DECIMAL(2,1) DEFAULT 0.0, duration INT COMMENT '单位:分钟', release_date DATE, poster_url VARCHAR(200) COMMENT '对应 static/images/ 下的 PNG 文件名' ); -- 用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL, phone VARCHAR(11) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL COMMENT '应为 SHA256 加密存储' ); -- 订票记录表 CREATE TABLE tickets ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, seat VARCHAR(10) NOT NULL COMMENT '如 A12, B05', time DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (movie_id) REFERENCES movies(id) );注意:
poster_url字段存的是相对路径(如images/yan.png),前端cinema.html中<img src="static/{{poster_url}}">才能正确加载。若你把图片放在static/外,路径会 404。
2.3 Express 中间件链:为什么/list.html能直接访问,而/api/movies却返回 JSON?
项目中list.html是纯静态页,通过app.use(express.static('static'))自动映射到http://localhost:3000/list.html。但server.js里还定义了显式路由:
app.get('/list.html', (req, res) => { res.sendFile(__dirname + '/list.html'); });这看似冗余,实则关键:express.static默认只服务static/目录下的文件,而list.html在根目录,必须显式sendFile。同理,cinema.html、movie.html都需此处理。而/api/movies这类路径未被static拦截,自然落到 Express 路由处理器,返回 JSON 数据供前端 AJAX 调用。
| 路径 | 处理方式 | 返回内容 | 前端调用场景 |
|---|---|---|---|
/list.html | res.sendFile() | HTML 文本 | 页面首次加载 |
/static/images/yan.png | express.static('static') | PNG 二进制 | <img>标签自动请求 |
/api/movies | 自定义路由 +db.query() | JSON 数组 | fetch('/api/movies').then(data => renderList(data)) |
3. 前端交互实战:用cinema.html+maoyan.css+index.js实现动态电影列表
3.1cinema.html的 DOM 结构与数据绑定模式
cinema.html是主入口页,其核心是<div id="movie-list"></div>容器。它不包含任何硬编码电影数据,而是依赖 JavaScript 动态渲染。查看index.js(注意:项目正文提到只有1个 JS 文件,即此文件):
// index.js document.addEventListener('DOMContentLoaded', () => { loadMovies(); // 页面加载完立即拉取数据 }); function loadMovies() { fetch('/api/movies') .then(res => res.json()) .then(movies => { const container = document.getElementById('movie-list'); container.innerHTML = movies.map(movie => ` <div class="movie-card"> <img src="static/${movie.poster_url}" alt="${movie.title}"> <h3>${movie.title}</h3> <p>导演:${movie.director || '未知'}</p> <p>评分:<span class="score">${movie.score}</span></p> <button onclick="bookTicket(${movie.id})">购票</button> </div> `).join(''); }) .catch(err => console.error('加载电影失败:', err)); } function bookTicket(movieId) { // 弹出购票表单(简化版) const seat = prompt('请输入座位号(如 A12):'); if (!seat) return; fetch('/api/book', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ movie_id: movieId, seat: seat }) }) .then(res => res.json()) .then(data => alert(data.success ? '购票成功!' : data.error)); }逻辑说明:
fetch('/api/movies')对应后端app.get('/api/movies');movie.poster_url直接拼接成static/images/yan.png,与static/目录结构严格匹配。若你把yan.png放错位置,这里就会 404。
3.2maoyan.css的响应式布局技巧:适配移动端电影海报栅格
maoyan.css采用传统 float 布局而非 Flex/Grid(因项目较老),但对电影卡片做了精准控制:
.movie-card { width: 200px; margin: 10px; float: left; text-align: center; border: 1px solid #eee; border-radius: 4px; overflow: hidden; } .movie-card img { width: 100%; height: 260px; /* 固定高度,避免海报比例不一导致错位 */ object-fit: cover; /* 关键:裁剪居中显示 */ } /* 移动端适配:小屏下每行只显示2张 */ @media (max-width: 768px) { .movie-card { width: 140px; margin: 5px; } }参数说明:
object-fit: cover确保不同尺寸的 PNG 海报(如yan.png400×600,mengban.png300×400)都能填满卡片且不拉伸变形。若去掉此属性,矮胖海报会被强行拉高,人物脸变宽。
3.3readme.txt的隐藏配置项:启动前必须修改的三个参数
readme.txt内容虽短,但包含三个必须手动修改的配置点(原文未明说,但从代码可推断):
- MySQL 密码:
server.js中password: '',生产环境必须改为强密码 - 数据库名:
database: 'maoyan_db',需确保该库已按 2.2 节创建 - 端口号:
app.listen(3000),若 3000 被占用,需改为app.listen(3001)并同步更新前端 AJAX 地址
验证是否生效:启动后访问http://localhost:3000/api/movies,应返回 JSON 数组;若返回Cannot GET /api/movies,检查server.js是否在根目录、Node 版本是否 ≥ 14(本项目兼容 Node 12+)。
4. 排查常见启动失败:从端口冲突到 MySQL 连接拒绝的逐层诊断
4.1 启动报错Error: connect ECONNREFUSED 127.0.0.1:3306的根因与修复
这是最典型的错误,表示 Express 找不到 MySQL 服务。不要先怀疑代码,按以下顺序排查:
确认 MySQL 服务已运行:
- Windows:任务管理器 → 服务 → 找到
MySQL80或mysqld,状态应为“正在运行” - macOS/Linux:终端执行
brew services list | grep mysql(Homebrew)或sudo systemctl status mysql
- Windows:任务管理器 → 服务 → 找到
验证 MySQL 登录凭据:
mysql -u root -p # 输入密码(若为空则直接回车) # 成功进入 MySQL 命令行后,执行: SHOW DATABASES; # 应看到 maoyan_db检查
server.js中的 host 是否为127.0.0.1:
某些 MySQL 安装默认只监听::1(IPv6),需在 MySQL 配置文件my.cnf中添加:[mysqld] bind-address = 0.0.0.0然后重启 MySQL 服务。
提示:若你用 Docker 运行 MySQL,
host应改为容器名(如mysql-container),而非localhost——因为 Node 进程在宿主机,localhost指向宿主机而非容器。
4.2 页面空白或图片 404:静态资源路径的双重陷阱
cinema.html中图片显示为叉号?检查两个层面:
第一层:文件物理路径
server.js的app.use(express.static('static'))要求所有静态资源(CSS、JS、图片)必须放在static/目录下。但项目文件列表显示images/是static的子目录,所以yan.png的真实路径是static/images/yan.png。若你解压时把images放到项目根目录,<img src="static/images/yan.png">就会 404。第二层:HTML 中的引用路径
cinema.html里写的是<img src="static/images/yan.png">,这没问题;但list.html若写成<img src="images/yan.png">(漏掉static/),就会失败。统一原则:所有src和href必须以static/开头。
4.3 表单提交后无反应:req.body为空的中间件缺失诊断
当点击“购票”按钮,bookTicket()发送 POST 请求,但后端req.body为空,movie_id取不到。原因必然是:
// ❌ 错误:中间件放在路由之后 app.get('/api/movies', ...); app.use(express.urlencoded({ extended: true })); // 位置错了! // ✅ 正确:必须在所有路由之前 app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.get('/api/movies', ...);验证方法:在路由处理器中加一行console.log('Body:', req.body),启动服务后提交表单,观察终端输出。若输出{},就是中间件顺序问题。
5. 进阶技巧:用mysql2替换原生mysql模块并启用 Promise 风格查询
5.1 为什么必须升级?原生mysql的回调地狱与安全风险
原项目用mysql模块,每个查询都要嵌套回调:
db.query('SELECT * FROM movies WHERE id = ?', [id], (err, movie) => { if (err) throw err; db.query('SELECT * FROM tickets WHERE movie_id = ?', [movie.id], (err, tickets) => { // 第二层嵌套... }); });这导致三层以上关联查询时代码难以维护,且易出现callback called multiple times错误。更严重的是,原生mysql对 SQL 注入防护较弱,?占位符虽基础防护,但复杂条件拼接仍可能漏掉。
5.2 三步完成mysql2迁移:安装、连接池重构、Promise 化查询
第一步:安装新模块
npm uninstall mysql npm install mysql2第二步:重写数据库连接(server.js开头)
const mysql = require('mysql2/promise'); // 注意:导入 promise 版本 const db = mysql.createPool({ host: 'localhost', user: 'root', password: 'your_secure_password', database: 'maoyan_db', waitForConnections: true, connectionLimit: 10, queueLimit: 0 });第三步:将回调查询改为 await/async
// 原回调写法(删除) // app.get('/api/movie/:id', (req, res) => { // db.query('SELECT * FROM movies WHERE id = ?', [req.params.id], (err, results) => { // res.json(results[0]); // }); // }); // 新 Promise 写法(替换) app.get('/api/movie/:id', async (req, res) => { try { const [rows] = await db.execute('SELECT * FROM movies WHERE id = ?', [req.params.id]); res.json(rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } });参数说明:
mysql2/promise的execute()返回[rows, fields]元组,[rows]解构直接获取结果数组;await让异步代码线性书写,避免回调嵌套。同时,mysql2默认启用typeCast类型转换,数字字段不再返回字符串,省去parseInt()步骤。
5.3 安全加固:为用户登录增加 bcrypt 密码哈希
当前users表password字段明文存储(从readme.txt提示可推断),必须加密。在server.js中加入:
npm install bcryptjsconst bcrypt = require('bcryptjs'); // 用户注册时(假设 POST /api/register) app.post('/api/register', async (req, res) => { const { username, phone, password } = req.body; const hashedPassword = await bcrypt.hash(password, 10); // 10 轮 salt db.execute( 'INSERT INTO users (username, phone, password) VALUES (?, ?, ?)', [username, phone, hashedPassword] ); }); // 登录验证时 app.post('/api/login', async (req, res) => { const { phone, password } = req.body; const [users] = await db.execute('SELECT * FROM users WHERE phone = ?', [phone]); if (users.length && await bcrypt.compare(password, users[0].password)) { res.json({ success: true, user: { id: users[0].id, username: users[0].username } }); } else { res.status(401).json({ error: '手机号或密码错误' }); } });至此,这个猫眼电影平台源码就从“能跑”升级为“可交付”。你不仅掌握了 Express 路由与 MySQL 的基础集成,更获得了排查真实线上问题的能力——端口、路径、中间件顺序,这些细节决定项目生死。下一步,把tickets表加上唯一索引防止重复购票:ALTER TABLE tickets ADD UNIQUE KEY unique_seat_time (movie_id, seat, time);
本文还有配套的精品资源,点击获取