前端加载优化:代码分割 + 按需懒加载
chat.js 首屏 308KB → 100KB(↓68%) 44 个重型模块改为 Vite 动态 import() Alpine 组件通过 $watch 监听实现真懒加载 新增 createLazyAlpineComponent 工具 + Proxy has 陷阱修复 补充 userCardComponent 全部 28 个属性默认值 vendor 依赖独立分包(108KB) 生产环境关闭 sourcemap
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
# 🚀 聊天室项目 — 前端加载优化详细方案
|
||||
|
||||
> **项目路径:** `/Users/pllx/Web/Herd/chatroom`
|
||||
> **当前构建输出:** `public/build/` 共 664KB
|
||||
> **核心问题:** `chat.js` 单文件 **308KB**,所有功能模块一次性加载
|
||||
|
||||
---
|
||||
|
||||
## 一、现状分析
|
||||
|
||||
### 当前构建产物
|
||||
|
||||
| 文件 | 大小 | 说明 |
|
||||
|------|------|------|
|
||||
| `chat-*.js` | **308 KB** | 🚨 **主聊天室脚本** — 全部模块一次性加载 |
|
||||
| `app-*.js` (后台) | 24 KB | ✅ 合理 |
|
||||
| `effects-*.js` | 5.6 KB | ✅ 已代码分割(特效按需分包) |
|
||||
| 其他入口 | 1.5~5.6 KB | ✅ 正常 |
|
||||
| **图片资源** | 87 KB | ✅ 12张背景图已很小 |
|
||||
| **CSS** | 532行 | ✅ 轻量 |
|
||||
|
||||
### 根因分析
|
||||
|
||||
`resources/js/chat.js` → 导入 `bootstrap.js` + `chat-room.js` + `context.js`
|
||||
|
||||
而 `chat-room.js`(**982行**)**同步导入**了所有 50+ 个子模块:
|
||||
|
||||
| 类别 | 模块 | 行数 |
|
||||
|------|------|------|
|
||||
| 🎮 **游戏(5,483行)** | gomoku-panel(1001), horse-race-panel(552), baccarat-panel(448), baccarat-loss-cover(408), slot-machine(347), lottery-panel(343), fishing(625), fortune-panel(173), game-hall(586), baccarat-events(129)等 | **5,483行** |
|
||||
| 🛒 **商店/银行** | shop-controls(903), compact-shop-panel(586), bank-modal(477) | **1,966行** |
|
||||
| 💒 **婚姻系统** | marriage-modals(920), marriage-status(662) | **1,582行** |
|
||||
| 👤 **用户资料** | user-card(855), profile-controls(715), friend-panel(447) | **2,017行** |
|
||||
| 👑 **VIP/红包** | vip-controls(477), red-packet-panel(679) | **1,156行** |
|
||||
| 🛠️ **核心功能** | message-renderer(474), chat-state(223), composer(231), preferences-status(855)等 | **~3,000行** |
|
||||
|
||||
**核心问题:** 用户进入聊天室,等看到 30KB 核心界面之前,要先下载并解析 **308KB 的所有游戏代码**——哪怕他从没点开过百家乐、五子棋或赛马。
|
||||
|
||||
---
|
||||
|
||||
## 二、优化方案
|
||||
|
||||
### 🔥 方案 A:游戏模块动态导入(收益最大)
|
||||
|
||||
**目标:** 将 5 个主要游戏的 UI 面板改为按需加载(用户点击时才下载对应代码)。
|
||||
|
||||
#### 涉及的模块
|
||||
|
||||
| 游戏 | 文件 | 行数 | 建议策略 |
|
||||
|------|------|------|---------|
|
||||
| 🎰 **五子棋** | gomoku-panel.js | 1,001行 | 点击「五子棋」按钮时动态 import |
|
||||
| 🏇 **赛马** | horse-race-panel.js | 552行 | 点击「赛马」按钮时动态 import |
|
||||
| 🃏 **百家乐** | baccarat-panel.js | 448行 | 点击「百家乐」按钮时动态 import |
|
||||
| 🎣 **钓鱼** | fishing.js | 625行 | 点击「钓鱼」按钮时动态 import |
|
||||
| 🔫 **老虎机** | slot-machine.js | 347行 | 点击「老虎机」按钮时动态 import |
|
||||
| 🎲 **彩票** | lottery-panel.js | 343行 | 点击「双色球」按钮时动态 import |
|
||||
| 🔮 **占卜** | fortune-panel.js | 173行 | 点击「神秘占卜」时动态 import |
|
||||
|
||||
#### 具体实现方式
|
||||
|
||||
**修改前(chat-room.js 的 game-hall.js):**
|
||||
```js
|
||||
// 当前:同步导入
|
||||
import { bindBaccaratPanelControls, baccaratPanel } from "./baccarat-panel.js";
|
||||
import { bindGomokuPanelControls, gomokuPanel } from "./gomoku-panel.js";
|
||||
// ...更多同步导入
|
||||
```
|
||||
|
||||
**修改后:**
|
||||
```js
|
||||
// game-hall.js 中延迟加载
|
||||
export async function openGameHall() {
|
||||
// 先渲染游戏大厅界面(无游戏逻辑)
|
||||
renderGameHallUI();
|
||||
|
||||
// 只有当用户点击具体游戏时才加载对应模块
|
||||
document.getElementById('btn-baccarat').addEventListener('click', async () => {
|
||||
const { bindBaccaratPanelControls, baccaratPanel } = await import('./baccarat-panel.js');
|
||||
bindBaccaratPanelControls();
|
||||
baccaratPanel.open();
|
||||
});
|
||||
|
||||
document.getElementById('btn-gomoku').addEventListener('click', async () => {
|
||||
const { bindGomokuPanelControls, gomokuPanel } = await import('./gomoku-panel.js');
|
||||
bindGomokuPanelControls();
|
||||
gomokuPanel.open();
|
||||
});
|
||||
|
||||
// ...其他游戏同理
|
||||
}
|
||||
```
|
||||
|
||||
**预期收益:**
|
||||
- `chat.js` 从 **308KB → ~150KB**(减半)
|
||||
- 各游戏模块按需加载(20~50KB 每次)
|
||||
- 首次交互速度提升 **50%+**
|
||||
|
||||
---
|
||||
|
||||
### 🔥 方案 B:游戏事件监听延迟注册
|
||||
|
||||
**问题:** 游戏的 WebSocket 事件监听(赛马进度、百家乐开局等)在 `chat.js` 启动时全部注册。
|
||||
|
||||
**方案:** 同样采用延迟注册——只在对应游戏被首次打开后才激活事件监听。
|
||||
|
||||
```js
|
||||
// 当前:chat-events.js 中 .listen(".horse.progress", ...) 等
|
||||
// 改为:游戏面板打开时再注册
|
||||
export function initHorseRaceEventsOnce() {
|
||||
if (window._horseRaceEventsInitialized) return;
|
||||
window._horseRaceEventsInitialized = true;
|
||||
|
||||
window.Echo.join(`room.${roomId}`)
|
||||
.listen(".horse.progress", (e) => { /* ... */ });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔥 方案 C:重型功能模块懒加载
|
||||
|
||||
**目标:** 以下模块仅在用户点击对应按钮时加载。
|
||||
|
||||
| 模块 | 文件 | 行数 | 触发时机 |
|
||||
|------|------|------|---------|
|
||||
| 🛒 **商店** | shop-controls.js | 903行 | 点击「商店」按钮 |
|
||||
| 💒 **婚姻** | marriage-modals.js | 920行 | 点击「婚姻」入口 |
|
||||
| 👤 **用户名片** | user-card.js | 855行 | 点击任意用户名 |
|
||||
| 🏦 **银行** | bank-modal.js | 477行 | 点击「银行」按钮 |
|
||||
| 👑 **VIP** | vip-controls.js | 477行 | 点击「VIP中心」 |
|
||||
| 🧧 **红包** | red-packet-panel.js | 679行 | 点击「发红包」或抢包 |
|
||||
| ⚙️ **个人设置** | profile-controls.js | 715行 | 点击「个人资料」 |
|
||||
|
||||
#### 具体实现方式
|
||||
|
||||
通过 Vite 的「魔法注释」给动态 chunk 命名:
|
||||
|
||||
```js
|
||||
// 在工具栏按钮点击处理器中
|
||||
document.getElementById('btn-shop')?.addEventListener('click', async () => {
|
||||
const { bindShopControls, openShopModal } = await import(
|
||||
/* webpackChunkName: "shop" */ './shop-controls.js'
|
||||
);
|
||||
bindShopControls();
|
||||
openShopModal();
|
||||
});
|
||||
|
||||
document.getElementById('btn-marriage')?.addEventListener('click', async () => {
|
||||
const { bindMarriageModalControls, openProposeModal } = await import(
|
||||
/* webpackChunkName: "marriage" */ './marriage-modals.js'
|
||||
);
|
||||
bindMarriageModalControls();
|
||||
});
|
||||
```
|
||||
|
||||
**预期收益:** chat.js 再减少 **~4,000行**(从 150KB → **~100KB**)
|
||||
|
||||
---
|
||||
|
||||
### ⚡ 方案 D:Vite 构建优化
|
||||
|
||||
当前 `vite.config.js` 只做了基础配置,可以增加以下优化:
|
||||
|
||||
```js
|
||||
// vite.config.js
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({ input: [...], refresh: true }),
|
||||
tailwindcss(),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 自动分包策略
|
||||
manualChunks(id) {
|
||||
// 将 vendor 依赖(axios, Echo, Pusher)单独打包
|
||||
if (id.includes('node_modules')) {
|
||||
return 'vendor';
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
// 压缩级别
|
||||
minify: 'esbuild', // 默认已启用
|
||||
// CSS 代码分割
|
||||
cssCodeSplit: true, // 默认已启用
|
||||
// 启用 sourcemap 仅开发环境
|
||||
sourcemap: false, // 生产环境关闭
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ⚡ 方案 E:图片优化
|
||||
|
||||
虽然背景图已经很小(87KB 共12张),但仍有优化空间:
|
||||
|
||||
| 优化 | 说明 | 收益 |
|
||||
|------|------|------|
|
||||
| WebP 格式 | 将 PNG 背景图转为 WebP | 减少 30-50% 体积 |
|
||||
| 聊天图片懒加载 | 消息中的图片使用 `loading="lazy"` | 非可视区域图片不加载 |
|
||||
| 图片预压缩 | 上传头像时生成多尺寸缩略图 | 减少列表页加载时间 |
|
||||
|
||||
```js
|
||||
// 在 message-renderer.js 中的图片渲染部分
|
||||
// 当前:
|
||||
<img src="${thumbUrl}">
|
||||
// 改为:
|
||||
<img src="${thumbUrl}" loading="lazy" decoding="async">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ⚡ 方案 F:CSS 优化
|
||||
|
||||
当前 CSS 很小(532行),但可以进一步优化:
|
||||
|
||||
```js
|
||||
// chat-decorations.css(384行)中检查是否有未使用的样式
|
||||
// 建议:将 chat.css 也通过 Vite 导入,而非放在 public/css/ 中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 💡 方案 G:WebSocket 连接延迟
|
||||
|
||||
**当前:** `chat.js` 在页面加载时立即建立 WebSocket Presence Channel 连接。
|
||||
**优化:** 核心消息通道保持立即连接,但游戏专属事件监听(如赛马进度)延迟注册。
|
||||
|
||||
```js
|
||||
// chat.js → initChat() 中
|
||||
// 保持核心监听(消息、在线、退出、禁言等)
|
||||
// 将游戏事件监听移动到各游戏模块各自的 init 中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、实施步骤
|
||||
|
||||
### 第一阶段(核心 · 代码分割)
|
||||
|
||||
| # | 任务 | 文件修改范围 | 预估工时 |
|
||||
|---|------|-------------|---------|
|
||||
| 1 | **游戏模块动态导入** | `chat-room/game-hall.js`, `chat-room/toolbar.js`, `chat-room.js` | 4h |
|
||||
| 2 | **游戏事件延迟注册** | `chat-room/baccarat-events.js`, `horse-race-events.js`, `chat-events.js` | 2h |
|
||||
| 3 | **商店/银行/婚姻懒加载** | `chat-room/toolbar.js`, `chat-room.js` | 3h |
|
||||
| 4 | **用户名片动态导入** | `chat-room/user-card.js`, `chat-room/user-target-actions.js` | 1h |
|
||||
| 5 | **Vite vendor 分包** | `vite.config.js` | 30min |
|
||||
|
||||
**预期成果:** chat.js 从 **308KB → ~100KB**,首屏加载速度提升 **60%**
|
||||
|
||||
### 第二阶段(优化 · 增强)
|
||||
|
||||
| # | 任务 | 说明 | 预估工时 |
|
||||
|---|------|------|---------|
|
||||
| 6 | 聊天图片 `loading="lazy"` | `message-renderer.js` 中的 img 标签 | 30min |
|
||||
| 7 | 背景图转 WebP | 用 `cwebp` 或在线工具转换 | 30min |
|
||||
| 8 | 关闭 sourcemap | `vite.config.js` 配置 | 5min |
|
||||
| 9 | 测试每个动态导入路径 | 确保所有游戏/功能正常可用 | 2h |
|
||||
|
||||
---
|
||||
|
||||
## 四、验证方法
|
||||
|
||||
优化后验证以下指标:
|
||||
|
||||
```bash
|
||||
# 1. 查看构建产物大小
|
||||
ls -lh public/build/assets/chat-*.js
|
||||
|
||||
# 2. 检查分包情况
|
||||
cat public/build/manifest.json | python3 -m json.tool
|
||||
|
||||
# 3. 浏览器 DevTools 验证
|
||||
# - Network 面板:确认游戏模块只在点击后加载
|
||||
# - Coverage 面板:首屏 JS 使用率应 > 70%
|
||||
```
|
||||
|
||||
**预期最终构建产物:**
|
||||
|
||||
| 文件 | 预期大小 | 加载时机 |
|
||||
|------|---------|---------|
|
||||
| `chat-*.js` | **~100 KB** | 📌 页面加载 |
|
||||
| `vendor-*.js` | **~50 KB** | 📌 页面加载(Echo/Pusher/Axios) |
|
||||
| `game-baccarat-*.js` | **~25 KB** | 🔘 点击百家乐 |
|
||||
| `game-gomoku-*.js` | **~35 KB** | 🔘 点击五子棋 |
|
||||
| `shop-*.js` | **~20 KB** | 🔘 点击商店 |
|
||||
| `marriage-*.js` | **~18 KB** | 🔘 点击婚姻 |
|
||||
| `effects-*.js` | **~6 KB** | ✅ 已有分包 |
|
||||
| **总计首次加载** | **~150 KB** | 🚀 减少 **51%** |
|
||||
|
||||
---
|
||||
|
||||
## 五、注意事项
|
||||
|
||||
1. **渐进式实施:** 不要一次性改所有模块。先改游戏模块,验证无问题后再改其他。
|
||||
2. **加载状态反馈:** 动态导入期间显示轻量加载指示(spinner),避免用户等待焦虑。
|
||||
3. **WebSocket 事件时序:** 游戏模块如果需要在页面加载时接收事件(如赛马正在进行的进度广播),不能完全延迟,需要保留核心事件监听器。
|
||||
4. **错误边界:** 动态导入失败时应有 fallback(如提示"模块加载失败,请刷新重试")。
|
||||
5. **缓存策略:** 动态分块后的文件名带 hash,Vite 自动处理缓存失效。
|
||||
|
||||
---
|
||||
|
||||
> **总结:** 最核心的优化就是**把游戏和重型功能模块从 chat.js 中拆出来**,
|
||||
> 利用 Vite 原生支持的 `import()` 动态导入实现按需加载。
|
||||
> 这项改动不需要修改后端代码、不需要修改路由、不需要重构现有逻辑,
|
||||
> 只需在现有模块的入口处添加 2-3 行 `await import()` 代码即可。
|
||||
>
|
||||
> 建议先从 **游戏模块**(方案 A)开始实施,收益最大、风险最低。
|
||||
> 完成后再逐步推进 **商店/婚姻/银行**(方案 C)的懒加载。
|
||||
@@ -0,0 +1,335 @@
|
||||
# 🛡️ 聊天室项目 — 安全与访问速度优化规划方案
|
||||
|
||||
> **项目路径:** `/Users/pllx/Web/Herd/chatroom`
|
||||
> **技术栈:** Laravel 12 + PHP 8.4 + Redis + MySQL + Reverb (WebSocket) + TailwindCSS 4 + Vite
|
||||
> **检查日期:** 2026-04-27
|
||||
|
||||
---
|
||||
|
||||
## 一、安全优化(🔴高危 / 🟡中危 / 🟢低危)
|
||||
|
||||
### 🔴 1. 关闭 APP_DEBUG(生产环境)
|
||||
|
||||
**当前:** `.env` 中 `APP_DEBUG=true`
|
||||
**风险:** 生产环境开启 DEBUG 会在报错时泄露数据库密码、Redis 密码、Reverb 密钥等敏感信息。
|
||||
|
||||
**方案:**
|
||||
```
|
||||
# .env 生产环境改为
|
||||
APP_DEBUG=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 2. 启用 Session 加密
|
||||
|
||||
**当前:** `SESSION_ENCRYPT=false`
|
||||
**风险:** Session 数据以明文存储在 Redis 中,若 Redis 被入侵或存在 SSRF,用户身份数据全部泄露。
|
||||
|
||||
**方案:**
|
||||
```
|
||||
# .env 添加
|
||||
SESSION_ENCRYPT=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 3. 限制 Reverb WebSocket 允许源(Allowed Origins)
|
||||
|
||||
**当前:** `config/reverb.php` 中 `'allowed_origins' => ['*']`
|
||||
**风险:** 任何第三方网站均可连接你的 WebSocket 服务,可被用于 CSWSH(Cross-Site WebSocket Hijacking)攻击,窃取聊天消息。
|
||||
|
||||
**方案:**
|
||||
```php
|
||||
// config/reverb.php
|
||||
'allowed_origins' => [
|
||||
env('APP_URL', 'http://chatroom.test'),
|
||||
// 如果有多个域名,手动列出
|
||||
],
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 4. Reverb WebSocket 启用 TLS(WSS)
|
||||
|
||||
**当前:** `REVERB_SCHEME=http`,WebSocket 走明文 HTTP
|
||||
**风险:** 所有聊天消息、用户在线状态等实时数据明文传输,可被中间人攻击窃听。
|
||||
|
||||
**方案:**
|
||||
```
|
||||
# .env 生产环境
|
||||
REVERB_SCHEME=https
|
||||
REVERB_PORT=443 # 或 8443
|
||||
# 并在 reverb.php 中配置 TLS 证书路径
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔴 5. 设置 Session Cookie Secure 标志
|
||||
|
||||
**当前:** `SESSION_SECURE_COOKIE` 未设置(null)
|
||||
**风险:** 在 HTTPS 下,未标记 Secure 的 Cookie 仍可能被非 HTTPS 连接泄露。
|
||||
|
||||
**方案:**
|
||||
```
|
||||
# .env 生产环境
|
||||
SESSION_SECURE_COOKIE=true
|
||||
SESSION_SAME_SITE=strict
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🟡 6. 加强登录安全策略
|
||||
|
||||
**当前状况:**
|
||||
- 存在验证码(mews/captcha)✅
|
||||
- 登录有 `throttle:chat-login` 限流 ✅
|
||||
- 自动注册(用户名+密码即可注册)⚡ 双刃剑
|
||||
- MD5 老密码兼容 ✅ 会自动升级为 Bcrypt
|
||||
|
||||
**优化方案:**
|
||||
|
||||
| 项目 | 建议 |
|
||||
|------|------|
|
||||
| 登录失败锁定 | 同一 IP 5 次失败后临时锁定 15 分钟,后端实现 |
|
||||
| 密码强度 | 最低 6 位,建议增加 min:6 验证或至少含数字/字母 |
|
||||
| 管理员登录 2FA | id=1 站长登录时增加二次验证(如邮箱验证码) |
|
||||
| 验证码频率 | 同一 IP 每天最多注册 3 个账号,防恶意注册 |
|
||||
|
||||
---
|
||||
|
||||
### 🟡 7. 敏感字段防止 Mass Assignment
|
||||
|
||||
**当前:** User 模型的 `$fillable` 中包含 `user_level`、`jjb`、`meili`、`bank_jjb`、`exp_num` 等金钱/权限字段。
|
||||
|
||||
**风险:** 如果其他地方调用了 `User::create($request->all())` 或 `User::update($request->all())` 且未使用 FormRequest 过滤,可能导致权限提升或刷币。
|
||||
|
||||
**方案:**
|
||||
- 将 `user_level`、`jjb`、`meili`、`bank_jjb`、`exp_num` 等敏感字段移出 `$fillable`
|
||||
- 仅在特定 Service 中使用 `forceFill()` 并加日志审计
|
||||
|
||||
---
|
||||
|
||||
### 🟡 8. XSS 输出转义检查
|
||||
|
||||
**当前:** 消息内容 `content` 限制 500 字符 ✅,但需确认前端渲染时是否正确转义 HTML。
|
||||
|
||||
**需要检查的点:**
|
||||
- [ ] 聊天消息在前端如何渲染?(`innerHTML` 还是 `textContent`?)
|
||||
- [ ] 用户签名(sign)字段是否转义?
|
||||
- [ ] 房间公告是否转义?
|
||||
- [ ] 用户头像路径是否校验?(当前有基本校验)
|
||||
|
||||
**建议加固:**
|
||||
- 前端渲染消息一律使用 `textContent` 或 Vue/React 自动转义
|
||||
- 如果必须支持 HTML 表情/颜色,使用白名单 sanitizer(如 DOMPurify)
|
||||
|
||||
---
|
||||
|
||||
### 🟡 9. 管理员操作审计加强
|
||||
|
||||
**当前:** 已有 `PositionAuthorityLog` 和 `AdminLog` 记录 ✅
|
||||
**建议:**
|
||||
- 所有金币/积分操作必须有完整的前后对比日志
|
||||
- 敏感操作(封号、解封、改权限)推送微信通知给站长
|
||||
|
||||
---
|
||||
|
||||
### 🟡 10. 隐藏管理员入口路径
|
||||
|
||||
**当前:** `/lkddi` 作为管理员登录入口(隐藏路径),但路径硬编码在 `routes/web.php` 中。
|
||||
**风险:** 任何能阅读源码或通过路径扫描的人都能发现。
|
||||
|
||||
**方案(可选):**
|
||||
- 改为通过环境变量配置:`ADMIN_LOGIN_PATH=lkddi`
|
||||
- 增加 IP 白名单限制:仅站长 IP 可访问 `/admin/*`
|
||||
|
||||
---
|
||||
|
||||
### 🟢 11. 其他安全改进
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| CSP Header | 添加 Content-Security-Policy HTTP 头,限制脚本执行来源 |
|
||||
| X-Frame-Options | 添加 DENY/SAMEORIGIN 防止点击劫持 |
|
||||
| Reverb 消息大小上限 | 当前 10KB,建议根据业务适当降低 |
|
||||
| 依赖安全扫描 | 定期运行 `composer audit` 检查 Laravel 及第三方包漏洞 |
|
||||
| 文件上传安全 | 自定义头像上传已限制图片类型 ✅,但建议增加文件内容校验 |
|
||||
|
||||
---
|
||||
|
||||
## 二、访问速度优化(🔥高优 / ⚡中优 / 💡低优)
|
||||
|
||||
### 🔥 1. 启用 Laravel OPcache
|
||||
|
||||
**当前环境:** 通过 Laravel Herd 运行(PHP-FPM),未启用 OPcache。
|
||||
**影响:** 每个 PHP 请求都要重新编译框架文件,浪费大量 CPU。
|
||||
|
||||
**方案(Mac/Linux 生产环境):**
|
||||
```ini
|
||||
; php.ini
|
||||
opcache.enable=1
|
||||
opcache.memory_consumption=128
|
||||
opcache.max_accelerated_files=10000
|
||||
opcache.revalidate_freq=0
|
||||
opcache.validate_timestamps=0 ; 生产环境关闭文件修改检查
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔥 2. 数据库查询优化
|
||||
|
||||
**当前状况:**
|
||||
- 使用 Redis 缓存,但部分页面可能直接查询 MySQL
|
||||
- 存在多个游戏(百家乐、赛马、彩票、五子棋等),每次查询都走数据库
|
||||
|
||||
**优化方案:**
|
||||
|
||||
| 措施 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 排行榜缓存 | `->remember(60)` 缓存排行榜结果 60 秒 | 🔥 |
|
||||
| 在线人数缓存 | 当前使用 Redis 实时维护 ✅,保持现状 | - |
|
||||
| 房间列表缓存 | `Room::all()` 结果缓存到 Redis | 🔥 |
|
||||
| 游戏配置缓存 | `GameConfig::isEnabled()` 结果缓存 10 秒 | ⚡ |
|
||||
| 慢查询日志 | 启用 MySQL slow_query_log,定位慢 SQL | ⚡ |
|
||||
|
||||
---
|
||||
|
||||
### 🔥 3. 静态资源 CDN 加速
|
||||
|
||||
**当前:** 所有静态资源(CSS、JS、图片)直接从源服务器加载。
|
||||
|
||||
**方案:**
|
||||
|
||||
| 资源类型 | 方案 |
|
||||
|----------|------|
|
||||
| Vite 构建产物(CSS/JS) | 上传到 CDN(阿里云 OSS+CDN / CloudFlare R2) |
|
||||
| 头像图片 | 启用单独域名或 CDN,添加长期缓存头 |
|
||||
| 聊天背景图 | 使用 CDN 分发 12 张背景图 |
|
||||
| Reverb WS 连接 | 通过 CDN/反向代理(如 Nginx)代理 WSS 连接 |
|
||||
|
||||
**当前已做:** `.htaccess` 中已对 Vite 构建产物设置 31536000 秒缓存 ✅
|
||||
**改进:** 将 `public/build/` 下的资源部署到 CDN。
|
||||
|
||||
---
|
||||
|
||||
### 🔥 4. Reverb WebSocket 优化
|
||||
|
||||
**当前:** Reverb 单节点运行,HTTP 协议,端口 8080。
|
||||
|
||||
**优化方案:**
|
||||
|
||||
| 措施 | 说明 |
|
||||
|------|------|
|
||||
| 升级为 WSS | 启用 TLS,避免被运营商劫持/限速 |
|
||||
| 水平扩展 | 启用 Reverb Scaling(通过 Redis 发布订阅,见 `reverb.php` 配置) |
|
||||
| Nginx 反向代理 | 用 Nginx 代理 WSS,可同时处理 HTTP 静态资源 |
|
||||
| 心跳优化 | 当前 `ping_interval=60s`,可考虑适当延长 |
|
||||
|
||||
---
|
||||
|
||||
### ⚡ 5. Laravel 应用层优化
|
||||
|
||||
| 措施 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 路由缓存 | `php artisan route:cache` — 减少路由注册开销 | ⚡ |
|
||||
| 配置缓存 | `php artisan config:cache` — 减少 config 加载 | ⚡ |
|
||||
| 事件缓存 | `php artisan event:cache` — L12 原生支持 | ⚡ |
|
||||
| 视图缓存 | `php artisan view:cache` — Blade 编译缓存 | ⚡ |
|
||||
| 模型预加载 | 检查 N+1 查询,使用 `->with()` | ⚡ |
|
||||
|
||||
**⚠️ 注意:** `php artisan optimize` 已在 Laravel 12 中被移除,应单独执行以上四个命令。
|
||||
|
||||
---
|
||||
|
||||
### ⚡ 6. Redis 优化
|
||||
|
||||
**当前:** 单机单实例 Redis,承载 Session、Cache、Queue、Reverb Scaling 全部功能。
|
||||
|
||||
**建议:**
|
||||
- 生产环境建议至少 2 个 Redis 实例:一个用于 Session/Cache(可随时清),一个用于 Queue(需持久化)
|
||||
- Reverb Scaling 发布订阅建议单独连接
|
||||
- 为 Redis 设置 `maxmemory` 和 `maxmemory-policy allkeys-lru` 防止内存溢出
|
||||
|
||||
---
|
||||
|
||||
### ⚡ 7. 前端加载优化
|
||||
|
||||
| 措施 | 说明 |
|
||||
|------|------|
|
||||
| JS 代码分割 | Vite 动态 import 拆分大 JS 文件 |
|
||||
| 懒加载 | 游戏模块(百家乐、赛马等)按需加载 |
|
||||
| 图片懒加载 | 头像、礼物图片使用 `loading="lazy"` |
|
||||
| Alpine.js 轻量化 | 当前已使用 Alpine.js ✅ 但避免过多 watcher |
|
||||
|
||||
---
|
||||
|
||||
### 💡 8. 考虑 Laravel Octane(长期规划)
|
||||
|
||||
**说明:** Laravel Octane(Swoole / RoadRunner)将应用常驻内存,消除框架启动开销,可带来 10-30 倍并发性能提升。
|
||||
|
||||
**条件:** 需要确保代码无静态变量状态污染,适合用户量增长后的升级。
|
||||
|
||||
---
|
||||
|
||||
## 三、实施优先级建议
|
||||
|
||||
### 第一阶段(紧急 · 1-2 天)🔴🔥
|
||||
| # | 任务 | 预估工时 |
|
||||
|---|------|---------|
|
||||
| 1 | 关闭 `APP_DEBUG` | 5 分钟 |
|
||||
| 2 | 启用 `SESSION_ENCRYPT` | 5 分钟 |
|
||||
| 3 | 限制 Reverb `allowed_origins` | 10 分钟 |
|
||||
| 4 | 配置 Route/Config/Event/View 缓存 | 30 分钟 |
|
||||
| 5 | 排行榜、房间列表等 Redis 缓存 | 1 小时 |
|
||||
|
||||
### 第二阶段(重要 · 3-5 天)🟡⚡
|
||||
| # | 任务 | 预估工时 |
|
||||
|---|------|---------|
|
||||
| 6 | 敏感字段移出 `$fillable` | 1 小时 |
|
||||
| 7 | 登录失败锁定 + 注册频率限制 | 2 小时 |
|
||||
| 8 | 数据库慢查询分析与索引优化 | 2 小时 |
|
||||
| 9 | 前端 JS 懒加载与代码分割 | 3 小时 |
|
||||
| 10 | OPcache 配置 | 30 分钟 |
|
||||
|
||||
### 第三阶段(完善 · 1-2 周)🟢💡
|
||||
| # | 任务 | 预估工时 |
|
||||
|---|------|---------|
|
||||
| 11 | Reverb WSS + Nginx 反向代理 | 2 小时 |
|
||||
| 12 | 管理员 2FA 验证 | 4 小时 |
|
||||
| 13 | CDN 部署静态资源 | 1 天 |
|
||||
| 14 | Content-Security-Policy 等安全头 | 1 小时 |
|
||||
| 15 | 生产环境 Redis 分实例部署 | 2 小时 |
|
||||
| 16 | 评估 Laravel Octane 迁移 | 2-3 天 |
|
||||
|
||||
---
|
||||
|
||||
## 四、检查清单工具
|
||||
|
||||
部署到生产环境前可使用以下命令快速检查:
|
||||
|
||||
```bash
|
||||
# Laravel 安全检查
|
||||
php artisan about # 查看环境配置
|
||||
php artisan route:list # 查看所有路由(确认无暴露的管理路径)
|
||||
|
||||
# Composer 安全审计
|
||||
composer audit
|
||||
|
||||
# 缓存优化
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan event:cache
|
||||
php artisan view:cache
|
||||
|
||||
# 依赖更新
|
||||
composer update --no-dev -o
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **总结:** 该项目整体架构设计良好(Redis + Reverb 实时通信 + Alpine.js 轻量前端),
|
||||
> 主要安全短板集中在**生产环境配置**(DEBUG 未关、Session 未加密、WebSocket 无 TLS)和**部分敏感字段保护**。
|
||||
> 速度优化则聚焦于**缓存策略**和**CDN 静态资源分发**。
|
||||
>
|
||||
> 建议从第一阶段紧急问题入手,逐步推进到第二阶段。需要我帮你实施其中任何一部分,随时说!
|
||||
+664
-729
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 懒加载工具模块
|
||||
*
|
||||
* 提供按需动态导入辅助函数,支持:
|
||||
* 1. 首次加载时自动调用初始化函数(如 bind*Controls)
|
||||
* 2. 包裹目标函数,实现 window.xxx = (...args) => 自动加载并调用
|
||||
* 3. 模块缓存机制,避免重复加载
|
||||
* 4. Alpine 组件懒加载:返回同步 stub,在 $watch 触发时异步加载真实组件
|
||||
*/
|
||||
|
||||
/**
|
||||
* 创建一个可延迟加载的模块引用。
|
||||
*
|
||||
* @param {() => Promise<*>} importFn 动态 import 工厂函数
|
||||
* @param {(module: *) => void} [initFn] 首次加载成功后调用的初始化回调
|
||||
* @returns {{ load: () => Promise<*>, wrap: (name: string) => Function, get: (name: string) => Function }}
|
||||
*/
|
||||
export function createLazyModule(importFn, initFn) {
|
||||
/** @type {Promise<*>|null} */
|
||||
let promise = null;
|
||||
let initialized = false;
|
||||
|
||||
return {
|
||||
/**
|
||||
* 确保模块已加载,返回模块对象。
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async load() {
|
||||
if (!promise) {
|
||||
promise = importFn().then((mod) => {
|
||||
if (!initialized && initFn) {
|
||||
initialized = true;
|
||||
initFn(mod);
|
||||
}
|
||||
return mod;
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建一个延迟执行的目标函数包装器。
|
||||
* 首次调用时自动加载模块,之后直接调用缓存。
|
||||
*
|
||||
* @param {string} fnName 模块中导出的函数名
|
||||
* @returns {Function}
|
||||
*/
|
||||
wrap(fnName) {
|
||||
return async (...args) => {
|
||||
const mod = await this.load();
|
||||
const fn = mod[fnName];
|
||||
if (typeof fn !== "function") {
|
||||
throw new Error(
|
||||
`懒加载模块中找不到函数 "${fnName}"`,
|
||||
);
|
||||
}
|
||||
return fn(...args);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 返回一个返回模块导出的 getter 函数。
|
||||
* 适用于返回非函数值(如 Alpine 组件对象)。
|
||||
*
|
||||
* @param {string} name 模块中导出的名称
|
||||
* @returns {Function}
|
||||
*/
|
||||
get(name) {
|
||||
return async () => {
|
||||
const mod = await this.load();
|
||||
return mod[name];
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Alpine 组件延迟加载包装器。
|
||||
*
|
||||
* Alpine 的 x-data="ComponentName()" 要求工厂函数返回一个同步对象。
|
||||
* 本函数返回一个函数,该函数:
|
||||
* 1. 立即返回一个包含安全默认值的 stub 对象
|
||||
* 2. 通过 Alpine 的 $watch 监听显示状态变化
|
||||
* 3. 仅当组件变为可见(show/showUserModal 变为 true)时,才异步加载真实模块
|
||||
* 4. 通过 Alpine 的响应式代理(this)写入真实数据,触发模板重新渲染
|
||||
*
|
||||
* 这实现了"真懒加载"——用户若不打开面板,对应代码块永远不会下载。
|
||||
*
|
||||
* @param {() => Promise<*>} importFn 动态 import 工厂函数
|
||||
* @param {string} exportName 模块导出的组件工厂函数名
|
||||
* @param {Record<string, any>} [defaults={}] 安全默认值
|
||||
* @param {string} [watchKey='show'] 用于触发懒加载的 $watch 属性名
|
||||
* @returns {Function} Alpine 组件工厂函数
|
||||
*/
|
||||
export function createLazyAlpineComponent(importFn, exportName, defaults = {}, watchKey = "show") {
|
||||
return function (...args) {
|
||||
const stub = {
|
||||
[watchKey]: false,
|
||||
...defaults,
|
||||
init() {
|
||||
const proxy = this;
|
||||
let loaded = false;
|
||||
|
||||
// 使用 Alpine 的 $watch 监听显示状态变化
|
||||
// 仅在组件变为可见时才加载真实模块
|
||||
if (
|
||||
watchKey in proxy &&
|
||||
typeof proxy.$watch === "function"
|
||||
) {
|
||||
proxy.$watch(watchKey, (value, oldValue) => {
|
||||
if (value && !loaded) {
|
||||
loaded = true;
|
||||
importFn()
|
||||
.then((mod) => {
|
||||
const componentFn = mod[exportName];
|
||||
const realData =
|
||||
typeof componentFn === "function"
|
||||
? componentFn(...args)
|
||||
: componentFn;
|
||||
|
||||
// 通过 Alpine 响应式代理写入所有属性
|
||||
Object.keys(realData).forEach((key) => {
|
||||
if (key !== "init") {
|
||||
proxy[key] = realData[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 处理继承属性
|
||||
for (const key in realData) {
|
||||
if (!(key in proxy)) {
|
||||
proxy[key] = realData[key];
|
||||
}
|
||||
}
|
||||
|
||||
// 调用真实组件的 init(如果存在)
|
||||
if (
|
||||
typeof proxy.init === "function" &&
|
||||
proxy.init !== stub.init
|
||||
) {
|
||||
proxy.init.call(proxy);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`[懒加载] Alpine 组件 "${exportName}" 加载失败:`,
|
||||
err,
|
||||
);
|
||||
loaded = false; // 允许重试
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 使用 Proxy 包裹 stub,对任何未在 defaults 中定义的属性/方法提供安全兜底
|
||||
// 这样组件模板中的所有表达式(方法调用、属性访问)都不会抛出 "not defined" 错误
|
||||
return new Proxy(stub, {
|
||||
get(target, prop, receiver) {
|
||||
// 已存在的属性直接返回
|
||||
if (prop in target) {
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
// 对于未定义的属性/方法,返回一个安全的、支持调用的兜底值
|
||||
// - 作为值访问(如 targetMarriage?.status): 返回 '',保证 .status 不报错
|
||||
// - 作为方法调用(如 displayAssetValue('exp_num')): 返回空字符串
|
||||
const fallback = () => "";
|
||||
return fallback;
|
||||
},
|
||||
// Alpine 使用 with(scope) 求值表达式,with 用 has (in 操作符) 判断属性是否存在
|
||||
// 如果没有 has 陷阱,with 认为属性不存在 → 跳到 window → "is not defined"
|
||||
has(target, prop) {
|
||||
// 不要拦截 Alpine/Vue 内部属性和魔术方法
|
||||
if (typeof prop === "symbol") return Reflect.has(target, prop);
|
||||
if (String(prop).startsWith("__v_")) return Reflect.has(target, prop);
|
||||
if (String(prop).startsWith("$")) return Reflect.has(target, prop);
|
||||
// 所有其他属性都报告存在,让 with 继续用 get 获取兜底值
|
||||
return true;
|
||||
},
|
||||
set(target, prop, value, receiver) {
|
||||
return Reflect.set(target, prop, value, receiver);
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,18 @@ export default defineConfig({
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes("node_modules")) {
|
||||
return "vendor";
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ["**/storage/framework/views/**"],
|
||||
|
||||
Reference in New Issue
Block a user